MarcSkovMadsen commited on
Commit
42f5456
1 Parent(s): 2c150e9
Files changed (4) hide show
  1. .gitignore +2 -1
  2. app.py +11 -315
  3. components.py +316 -0
  4. utils.py +3 -0
.gitignore CHANGED
@@ -103,4 +103,5 @@ venv.bak/
103
  # mypy
104
  .mypy_cache/
105
 
106
- data/
 
 
103
  # mypy
104
  .mypy_cache/
105
 
106
+ data/
107
+ script.py
app.py CHANGED
@@ -1,320 +1,16 @@
1
- import dask.dataframe as dd
2
- import holoviews as hv
3
- import numpy as np
4
- import pandas as pd
5
  import panel as pn
6
- import param
7
- from holoviews.operation.datashader import dynspread, rasterize
8
 
9
- from utils import (
10
- DATASETS,
11
- DATASHADER_LOGO,
12
- DATASHADER_URL,
13
- DESCRIPTION,
14
- ESA_EASTING,
15
- ESA_NORTHING,
16
- MAJOR_TOM_LOGO,
17
- MAJOR_TOM_LYRICS,
18
- MAJOR_TOM_PICTURE,
19
- MAJOR_TOM_REF_URL,
20
- META_DATA_COLUMNS,
21
- PANEL_LOGO,
22
- PANEL_URL,
23
- get_closest_rows,
24
- get_image,
25
- get_meta_data,
26
- )
27
 
 
 
28
 
29
- class DatasetInput(pn.viewable.Viewer):
30
- value = param.Selector(objects=DATASETS, allow_None=False, label="Dataset")
31
 
32
- data = param.DataFrame(allow_None=False)
33
-
34
- def __panel__(self):
35
- return pn.widgets.RadioButtonGroup.from_param(
36
- self.param.value, button_style="outline"
37
- )
38
-
39
- @pn.depends("value", watch=True, on_init=True)
40
- def _update_data(self):
41
- self.data = pn.cache(get_meta_data)(dataset=self.value)
42
-
43
-
44
- class MapInput(pn.viewable.Viewer):
45
- data = param.DataFrame(allow_refs=True, allow_None=False)
46
-
47
- data_in_view = param.DataFrame(allow_None=False)
48
- data_selected = param.DataFrame(allow_None=False)
49
-
50
- _plot = param.Parameter(allow_None=False)
51
- _pointer_x = param.Parameter(allow_None=False)
52
- _pointer_y = param.Parameter(allow_None=False)
53
- _range_xy = param.Parameter(allow_None=False)
54
- _tap = param.Parameter(allow_None=False)
55
-
56
- updating = param.Boolean()
57
-
58
- def __panel__(self):
59
- return pn.Column(
60
- pn.pane.HoloViews(
61
- self._plot, height=550, width=800, loading=self.param.updating
62
- ),
63
- self._description,
64
- )
65
-
66
- @param.depends("data", watch=True, on_init=True)
67
- def _handle_data_dask_change(self):
68
- with self.param.update(updating=True):
69
- data_dask = dd.from_pandas(self.data).persist()
70
- points = hv.Points(
71
- data_dask, kdims=["centre_easting", "centre_northing"], vdims=[]
72
- )
73
-
74
- rangexy = hv.streams.RangeXY(source=points)
75
- tap = hv.streams.Tap(source=points, x=ESA_EASTING, y=ESA_NORTHING)
76
-
77
- agg = rasterize(
78
- points, link_inputs=True, x_sampling=0.0001, y_sampling=0.0001
79
- )
80
- dyn = dynspread(agg)
81
- dyn.opts(cmap="kr_r", colorbar=True)
82
-
83
- pointerx = hv.streams.PointerX(x=ESA_EASTING, source=points)
84
- pointery = hv.streams.PointerY(y=ESA_NORTHING, source=points)
85
- vline = hv.DynamicMap(lambda x: hv.VLine(x), streams=[pointerx])
86
- hline = hv.DynamicMap(lambda y: hv.HLine(y), streams=[pointery])
87
- tiles = hv.Tiles(
88
- "https://tile.openstreetmap.org/{Z}/{X}/{Y}.png", name="OSM"
89
- ).opts(xlabel="Longitude", ylabel="Latitude")
90
-
91
- self.param.update(
92
- _plot=tiles * agg * dyn * hline * vline,
93
- _pointer_x=pointerx,
94
- _pointer_y=pointery,
95
- _range_xy=rangexy,
96
- _tap=tap,
97
- )
98
-
99
- update_viewed = pn.bind(
100
- self._update_data_in_view,
101
- rangexy.param.x_range,
102
- rangexy.param.y_range,
103
- watch=True,
104
- )
105
- update_viewed()
106
-
107
- update_selected = pn.bind(
108
- self._update_data_selected, tap.param.x, tap.param.y, watch=True
109
- )
110
- update_selected()
111
-
112
- def _update_data_in_view(self, x_range, y_range):
113
- if not x_range or not y_range:
114
- self.data_in_view = self.data
115
- return
116
-
117
- data = self.data
118
- data = data[
119
- (data.centre_easting.between(*x_range))
120
- & (data.centre_northing.between(*y_range))
121
- ]
122
- self.data_in_view = data.reset_index(drop=True)
123
-
124
- def _update_data_selected(self, tap_x, tap_y):
125
- self.data_selected = get_closest_rows(self.data, tap_x, tap_y)
126
-
127
- @pn.depends("data_in_view")
128
- def _description(self):
129
- return f"Rows: {len(self.data_in_view):,}"
130
-
131
-
132
- class ImageInput(pn.viewable.Viewer):
133
- data = param.DataFrame(allow_refs=True, allow_None=False)
134
- column_name = param.Selector(
135
- default="Thumbnail", objects=list(META_DATA_COLUMNS), label="Image Type"
136
- )
137
- updating = param.Boolean()
138
-
139
- meta_data = param.DataFrame()
140
- image = param.Parameter()
141
- plot = param.Parameter()
142
-
143
- _timestamp = param.Selector(label="Timestamp", objects=[None])
144
-
145
- def __panel__(self):
146
- return pn.Column(
147
- pn.Row(
148
- pn.widgets.RadioButtonGroup.from_param(
149
- self.param._timestamp,
150
- button_style="outline",
151
- align="end",
152
- ),
153
- pn.widgets.Select.from_param(
154
- self.param.column_name, disabled=self.param.updating
155
- ),
156
- ),
157
- pn.Tabs(
158
- pn.pane.HoloViews(
159
- self.param.plot,
160
- loading=self.param.updating,
161
- height=800,
162
- width=800,
163
- name="Interactive Image",
164
- ),
165
- pn.pane.Image(
166
- self.param.image,
167
- name="Static Image",
168
- loading=self.param.updating,
169
- width=800,
170
- ),
171
- pn.widgets.Tabulator(
172
- self.param.meta_data,
173
- name="Meta Data",
174
- loading=self.param.updating,
175
- disabled=True,
176
- ),
177
- pn.pane.Markdown(self.code, name="Code"),
178
- dynamic=True,
179
- ),
180
- )
181
-
182
- @pn.depends("data", watch=True, on_init=True)
183
- def _update_timestamp(self):
184
- if self.data.empty:
185
- default_value = None
186
- options = [None]
187
- print("empty options")
188
- else:
189
- options = sorted(self.data["timestamp"].unique())
190
- default_value = options[0]
191
- print("options", options)
192
-
193
- self.param._timestamp.objects = options
194
- if not self._timestamp in options:
195
- self._timestamp = default_value
196
-
197
- @property
198
- def column(self):
199
- return META_DATA_COLUMNS[self.column_name]
200
-
201
- @pn.depends("_timestamp", "column_name", watch=True, on_init=True)
202
- def _update_plot(self):
203
- if self.data.empty or not self._timestamp:
204
- self.meta_data = self.data.T
205
- self.image = None
206
- self.plot = hv.RGB(np.array([]))
207
- else:
208
- with self.param.update(updating=True):
209
- row = self.data[self.data.timestamp == self._timestamp].iloc[0]
210
- self.meta_data = pd.DataFrame(row)
211
- self.image = image = pn.cache(get_image)(row, self.column)
212
- image_array = np.array(image)
213
- if image_array.ndim == 2:
214
- self.plot = hv.Image(image_array).opts(
215
- cmap="gray_r", xaxis=None, yaxis=None, colorbar=True
216
- )
217
- else:
218
- self.plot = hv.RGB(image_array).opts(xaxis=None, yaxis=None)
219
-
220
- @pn.depends("meta_data", "column_name")
221
- def code(self):
222
- if self.meta_data.empty:
223
- return ""
224
-
225
- parquet_url = self.meta_data.T["parquet_url"].iloc[0]
226
- parquet_row = self.meta_data.T["parquet_row"].iloc[0]
227
- return f"""\
228
- ```python
229
- from io import BytesIO
230
-
231
- import holoviews as hv
232
- import numpy as np
233
- import panel as pn
234
- import pyarrow.parquet as pq
235
- from fsspec.parquet import open_parquet_file
236
- from PIL import Image
237
-
238
- pn.extension()
239
-
240
- parquet_url = "{parquet_url}"
241
- parquet_row = {parquet_row}
242
- column = "{self.column}"
243
- with open_parquet_file(parquet_url, columns=[column]) as f:
244
- with pq.ParquetFile(f) as pf:
245
- first_row_group = pf.read_row_group(parquet_row, columns=[column])
246
-
247
- stream = BytesIO(first_row_group[column][0].as_py())
248
- image = Image.open(stream)
249
- image_array = np.array(image)
250
- if image_array.ndim==2:
251
- plot = hv.Image(image_array).opts(cmap="gray", colorbar=True)
252
- else:
253
- plot = hv.RGB(image_array)
254
-
255
- plot.opts(xaxis=None, yaxis=None)
256
-
257
- pn.panel(plot).servable()
258
- ```
259
- """
260
-
261
-
262
- class App(param.Parameterized):
263
- sidebar = param.Parameter()
264
- main = param.Parameter()
265
-
266
- def __init__(self, **params):
267
- super().__init__(**params)
268
-
269
- self.sidebar = self._create_sidebar()
270
- self.main = pn.FlexBox(
271
- pn.Column(
272
- pn.Row(
273
- pn.indicators.LoadingSpinner(value=True, size=50),
274
- "**Loading data...**",
275
- ),
276
- MAJOR_TOM_LYRICS,
277
- )
278
- )
279
-
280
- pn.state.onload(self._update_main)
281
-
282
- def _create_sidebar(self):
283
- return pn.Column(
284
- pn.pane.Image(
285
- MAJOR_TOM_LOGO, link_url=MAJOR_TOM_REF_URL, sizing_mode="stretch_width"
286
- ),
287
- pn.pane.Image(
288
- MAJOR_TOM_PICTURE,
289
- link_url=MAJOR_TOM_REF_URL,
290
- sizing_mode="stretch_width",
291
- ),
292
- DESCRIPTION,
293
- pn.pane.Image(PANEL_LOGO, link_url=PANEL_URL, width=200, margin=(10, 20)),
294
- pn.pane.Image(
295
- DATASHADER_LOGO, link_url=DATASHADER_URL, width=200, margin=(10, 20)
296
- ),
297
- )
298
-
299
- def _create_main_content(self):
300
- dataset = DatasetInput()
301
- map_input = MapInput(data=dataset.param.data)
302
- image_input = ImageInput(data=map_input.param.data_selected)
303
-
304
- return pn.Column(dataset, map_input), image_input
305
-
306
- def _update_main(self):
307
- self.main[:] = list(self._create_main_content())
308
-
309
-
310
- pn.extension("tabulator", design="fast")
311
-
312
- app = App()
313
-
314
- pn.template.FastListTemplate(
315
- title="Major TOM Explorer",
316
- main=[app.main],
317
- sidebar=[app.sidebar],
318
- main_layout=None,
319
- accent="#003247", # "#A01346"
320
- ).servable()
 
 
 
 
 
1
  import panel as pn
 
 
2
 
3
+ from components import App
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ if pn.state.served:
6
+ pn.extension("tabulator", design="fast")
7
 
8
+ app = App()
 
9
 
10
+ pn.template.FastListTemplate(
11
+ title="Major TOM Explorer",
12
+ main=[app.main],
13
+ sidebar=[app.sidebar],
14
+ main_layout=None,
15
+ accent="#003247", # "#A01346"
16
+ ).servable()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dask.dataframe as dd
2
+ import holoviews as hv
3
+ import numpy as np
4
+ import pandas as pd
5
+ import panel as pn
6
+ import param
7
+ from holoviews.operation.datashader import dynspread, rasterize
8
+
9
+ from utils import (
10
+ DATASETS,
11
+ DATASHADER_LOGO,
12
+ DATASHADER_URL,
13
+ DESCRIPTION,
14
+ ESA_EASTING,
15
+ ESA_NORTHING,
16
+ MAJOR_TOM_LOGO,
17
+ MAJOR_TOM_LYRICS,
18
+ MAJOR_TOM_PICTURE,
19
+ MAJOR_TOM_REF_URL,
20
+ META_DATA_COLUMNS,
21
+ PANEL_LOGO,
22
+ PANEL_URL,
23
+ get_closest_rows,
24
+ get_image,
25
+ get_meta_data,
26
+ )
27
+
28
+
29
+ class DatasetInput(pn.viewable.Viewer):
30
+ value = param.Selector(objects=DATASETS, allow_None=False, label="Dataset")
31
+
32
+ data = param.DataFrame(allow_None=False)
33
+
34
+ def __panel__(self):
35
+ return pn.widgets.RadioButtonGroup.from_param(
36
+ self.param.value, button_style="outline"
37
+ )
38
+
39
+ @pn.depends("value", watch=True, on_init=True)
40
+ def _update_data(self):
41
+ self.data = pn.cache(get_meta_data)(dataset=self.value)
42
+
43
+
44
+ class MapInput(pn.viewable.Viewer):
45
+ data = param.DataFrame(allow_refs=True, allow_None=False)
46
+
47
+ data_in_view = param.DataFrame(allow_None=False)
48
+ data_selected = param.DataFrame(allow_None=False)
49
+
50
+ _plot = param.Parameter(allow_None=False)
51
+ _pointer_x = param.Parameter(allow_None=False)
52
+ _pointer_y = param.Parameter(allow_None=False)
53
+ _range_xy = param.Parameter(allow_None=False)
54
+ _tap = param.Parameter(allow_None=False)
55
+
56
+ updating = param.Boolean()
57
+
58
+ def __panel__(self):
59
+ return pn.Column(
60
+ pn.pane.HoloViews(
61
+ self._plot, height=550, width=800, loading=self.param.updating
62
+ ),
63
+ self._description,
64
+ )
65
+
66
+ @param.depends("data", watch=True, on_init=True)
67
+ def _handle_data_dask_change(self):
68
+ with self.param.update(updating=True):
69
+ data_dask = dd.from_pandas(self.data).persist()
70
+ points = hv.Points(
71
+ data_dask, kdims=["centre_easting", "centre_northing"], vdims=[]
72
+ )
73
+
74
+ rangexy = hv.streams.RangeXY(source=points)
75
+ tap = hv.streams.Tap(source=points, x=ESA_EASTING, y=ESA_NORTHING)
76
+
77
+ agg = rasterize(
78
+ points, link_inputs=True, x_sampling=0.0001, y_sampling=0.0001
79
+ )
80
+ dyn = dynspread(agg)
81
+ dyn.opts(cmap="kr_r", colorbar=True)
82
+
83
+ pointerx = hv.streams.PointerX(x=ESA_EASTING, source=points)
84
+ pointery = hv.streams.PointerY(y=ESA_NORTHING, source=points)
85
+ vline = hv.DynamicMap(lambda x: hv.VLine(x), streams=[pointerx])
86
+ hline = hv.DynamicMap(lambda y: hv.HLine(y), streams=[pointery])
87
+ tiles = hv.Tiles(
88
+ "https://tile.openstreetmap.org/{Z}/{X}/{Y}.png", name="OSM"
89
+ ).opts(xlabel="Longitude", ylabel="Latitude")
90
+
91
+ self.param.update(
92
+ _plot=tiles * agg * dyn * hline * vline,
93
+ _pointer_x=pointerx,
94
+ _pointer_y=pointery,
95
+ _range_xy=rangexy,
96
+ _tap=tap,
97
+ )
98
+
99
+ update_viewed = pn.bind(
100
+ self._update_data_in_view,
101
+ rangexy.param.x_range,
102
+ rangexy.param.y_range,
103
+ watch=True,
104
+ )
105
+ update_viewed()
106
+
107
+ update_selected = pn.bind(
108
+ self._update_data_selected, tap.param.x, tap.param.y, watch=True
109
+ )
110
+ update_selected()
111
+
112
+ def _update_data_in_view(self, x_range, y_range):
113
+ if not x_range or not y_range:
114
+ self.data_in_view = self.data
115
+ return
116
+
117
+ data = self.data
118
+ data = data[
119
+ (data.centre_easting.between(*x_range))
120
+ & (data.centre_northing.between(*y_range))
121
+ ]
122
+ self.data_in_view = data.reset_index(drop=True)
123
+
124
+ def _update_data_selected(self, tap_x, tap_y):
125
+ self.data_selected = get_closest_rows(self.data, tap_x, tap_y)
126
+
127
+ @pn.depends("data_in_view")
128
+ def _description(self):
129
+ return f"Rows: {len(self.data_in_view):,}"
130
+
131
+
132
+ class ImageInput(pn.viewable.Viewer):
133
+ data = param.DataFrame(allow_refs=True, allow_None=False)
134
+ column_name = param.Selector(
135
+ default="Thumbnail", objects=list(META_DATA_COLUMNS), label="Image Type"
136
+ )
137
+ updating = param.Boolean()
138
+
139
+ meta_data = param.DataFrame()
140
+ image = param.Parameter()
141
+ plot = param.Parameter()
142
+
143
+ _timestamp = param.Selector(label="Timestamp", objects=[None])
144
+
145
+ def __panel__(self):
146
+ return pn.Column(
147
+ pn.Row(
148
+ pn.widgets.RadioButtonGroup.from_param(
149
+ self.param._timestamp,
150
+ button_style="outline",
151
+ align="end",
152
+ ),
153
+ pn.widgets.Select.from_param(
154
+ self.param.column_name, disabled=self.param.updating
155
+ ),
156
+ ),
157
+ pn.Tabs(
158
+ pn.pane.HoloViews(
159
+ self.param.plot,
160
+ loading=self.param.updating,
161
+ height=800,
162
+ width=800,
163
+ name="Interactive Image",
164
+ ),
165
+ pn.pane.Image(
166
+ self.param.image,
167
+ name="Static Image",
168
+ loading=self.param.updating,
169
+ width=800,
170
+ ),
171
+ pn.widgets.Tabulator(
172
+ self.param.meta_data,
173
+ name="Meta Data",
174
+ loading=self.param.updating,
175
+ disabled=True,
176
+ ),
177
+ pn.pane.Markdown(self.code, name="Code"),
178
+ dynamic=True,
179
+ ),
180
+ )
181
+
182
+ @pn.depends("data", watch=True, on_init=True)
183
+ def _update_timestamp(self):
184
+ if self.data.empty:
185
+ default_value = None
186
+ options = [None]
187
+ print("empty options")
188
+ else:
189
+ options = sorted(self.data["timestamp"].unique())
190
+ default_value = options[0]
191
+ print("options", options)
192
+
193
+ self.param._timestamp.objects = options
194
+ if not self._timestamp in options:
195
+ self._timestamp = default_value
196
+
197
+ @property
198
+ def column(self):
199
+ return META_DATA_COLUMNS[self.column_name]
200
+
201
+ @pn.depends("_timestamp", "column_name", watch=True, on_init=True)
202
+ def _update_plot(self):
203
+ if self.data.empty or not self._timestamp:
204
+ self.meta_data = self.data.T
205
+ self.image = None
206
+ self.plot = hv.RGB(np.array([]))
207
+ else:
208
+ with self.param.update(updating=True):
209
+ row = self.data[self.data.timestamp == self._timestamp].iloc[0]
210
+ self.meta_data = pd.DataFrame(row)
211
+ self.image = image = pn.cache(get_image)(row, self.column)
212
+ image_array = np.array(image)
213
+ if image_array.ndim == 2:
214
+ self.plot = hv.Image(image_array).opts(
215
+ cmap="gray_r", xaxis=None, yaxis=None, colorbar=True
216
+ )
217
+ else:
218
+ self.plot = hv.RGB(image_array).opts(xaxis=None, yaxis=None)
219
+
220
+ @pn.depends("meta_data", "column_name")
221
+ def code(self):
222
+ if self.meta_data.empty:
223
+ return ""
224
+
225
+ parquet_url = self.meta_data.T["parquet_url"].iloc[0]
226
+ parquet_row = self.meta_data.T["parquet_row"].iloc[0]
227
+ return f"""\
228
+ ```bash
229
+ pip install aiohttp fsspec holoviews numpy panel pyarrow requests
230
+ ```
231
+
232
+ ```python
233
+ from io import BytesIO
234
+
235
+ import holoviews as hv
236
+ import numpy as np
237
+ import panel as pn
238
+ import pyarrow.parquet as pq
239
+ from fsspec.parquet import open_parquet_file
240
+ from PIL import Image
241
+
242
+ pn.extension()
243
+
244
+ parquet_url = "{parquet_url}"
245
+ parquet_row = {parquet_row}
246
+ column = "{self.column}"
247
+ with open_parquet_file(parquet_url, columns=[column]) as f:
248
+ with pq.ParquetFile(f) as pf:
249
+ first_row_group = pf.read_row_group(parquet_row, columns=[column])
250
+
251
+ stream = BytesIO(first_row_group[column][0].as_py())
252
+ image = Image.open(stream)
253
+ image_array = np.array(image)
254
+ if image_array.ndim==2:
255
+ plot = hv.Image(image_array).opts(cmap="gray", colorbar=True)
256
+ else:
257
+ plot = hv.RGB(image_array)
258
+
259
+ plot.opts(xaxis=None, yaxis=None)
260
+
261
+ pn.panel(plot).servable()
262
+ ```
263
+
264
+ ```bash
265
+ panel serve app.py --autoreload
266
+ ```
267
+
268
+ """
269
+
270
+
271
+ class App(param.Parameterized):
272
+ sidebar = param.Parameter()
273
+ main = param.Parameter()
274
+
275
+ def __init__(self, **params):
276
+ super().__init__(**params)
277
+
278
+ self.sidebar = self._create_sidebar()
279
+ self.main = pn.FlexBox(
280
+ pn.Column(
281
+ pn.Row(
282
+ pn.indicators.LoadingSpinner(value=True, size=50),
283
+ "**Loading data...**",
284
+ ),
285
+ MAJOR_TOM_LYRICS,
286
+ )
287
+ )
288
+
289
+ pn.state.onload(self._update_main)
290
+
291
+ def _create_sidebar(self):
292
+ return pn.Column(
293
+ pn.pane.Image(
294
+ MAJOR_TOM_LOGO, link_url=MAJOR_TOM_REF_URL, sizing_mode="stretch_width"
295
+ ),
296
+ pn.pane.Image(
297
+ MAJOR_TOM_PICTURE,
298
+ link_url=MAJOR_TOM_REF_URL,
299
+ sizing_mode="stretch_width",
300
+ ),
301
+ DESCRIPTION,
302
+ pn.pane.Image(PANEL_LOGO, link_url=PANEL_URL, width=200, margin=(10, 20)),
303
+ pn.pane.Image(
304
+ DATASHADER_LOGO, link_url=DATASHADER_URL, width=200, margin=(10, 20)
305
+ ),
306
+ )
307
+
308
+ def _create_main_content(self):
309
+ dataset = DatasetInput()
310
+ map_input = MapInput(data=dataset.param.data)
311
+ image_input = ImageInput(data=map_input.param.data_selected)
312
+
313
+ return pn.Column(dataset, map_input), image_input
314
+
315
+ def _update_main(self):
316
+ self.main[:] = list(self._create_main_content())
utils.py CHANGED
@@ -14,6 +14,7 @@ MAJOR_TOM_PICTURE = (
14
  "https://upload.wikimedia.org/wikipedia/en/6/6d/Major_tom_space_oddity_video.JPG"
15
  )
16
  MAJOR_TOM_REF_URL = "https://huggingface.co/Major-TOM"
 
17
  PANEL_LOGO = "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png"
18
  PANEL_URL = "https://panel.holoviz.org"
19
  DATASHADER_LOGO = "https://datashader.org/_static/logo_horizontal.svg"
@@ -46,6 +47,8 @@ DESCRIPTION = f"""\
46
 
47
  This app provides a way of exploring samples present in the [MajorTOM-Core]({MAJOR_TOM_REF_URL}) dataset. It contains nearly every piece of Earth captured by ESA [Sentinel-2](https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-2) satellite.
48
 
 
 
49
  ## Instructions
50
 
51
  To find a sample, navigate on the map to a place of interest. Click the map to find a dataset sample at the location you clicked.
 
14
  "https://upload.wikimedia.org/wikipedia/en/6/6d/Major_tom_space_oddity_video.JPG"
15
  )
16
  MAJOR_TOM_REF_URL = "https://huggingface.co/Major-TOM"
17
+ MAJOR_TOM_ARXIV_URL = "https://www.arxiv.org/abs/2402.12095"
18
  PANEL_LOGO = "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png"
19
  PANEL_URL = "https://panel.holoviz.org"
20
  DATASHADER_LOGO = "https://datashader.org/_static/logo_horizontal.svg"
 
47
 
48
  This app provides a way of exploring samples present in the [MajorTOM-Core]({MAJOR_TOM_REF_URL}) dataset. It contains nearly every piece of Earth captured by ESA [Sentinel-2](https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-2) satellite.
49
 
50
+ [Website]({MAJOR_TOM_REF_URL}), [arXiv Paper]({MAJOR_TOM_ARXIV_URL})
51
+
52
  ## Instructions
53
 
54
  To find a sample, navigate on the map to a place of interest. Click the map to find a dataset sample at the location you clicked.