Skip to content

Commit f195939

Browse files
committed
feat: Add MapLabelSpec for customizable corner labels in GeoAnimatorManager
1 parent ba4e89f commit f195939

3 files changed

Lines changed: 276 additions & 7 deletions

File tree

dvue/animator/__init__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,19 @@
3838
"RawSequentialBuffer",
3939
"GeoAnimatorManager",
4040
"MultiGeoAnimatorManager",
41+
"MapLabelSpec",
4142
"CURATED_COLORMAPS",
4243
]
4344

4445

4546
def __getattr__(name: str):
46-
if name in ("GeoAnimatorManager", "CURATED_COLORMAPS"):
47-
from .ui import GeoAnimatorManager, CURATED_COLORMAPS # noqa: F401
48-
g = {"GeoAnimatorManager": GeoAnimatorManager, "CURATED_COLORMAPS": CURATED_COLORMAPS}
47+
if name in ("GeoAnimatorManager", "CURATED_COLORMAPS", "MapLabelSpec"):
48+
from .ui import GeoAnimatorManager, CURATED_COLORMAPS, MapLabelSpec # noqa: F401
49+
g = {
50+
"GeoAnimatorManager": GeoAnimatorManager,
51+
"CURATED_COLORMAPS": CURATED_COLORMAPS,
52+
"MapLabelSpec": MapLabelSpec,
53+
}
4954
return g[name]
5055
if name == "MultiGeoAnimatorManager":
5156
from .multi_ui import MultiGeoAnimatorManager # noqa: F401
@@ -54,9 +59,13 @@ def __getattr__(name: str):
5459

5560

5661
def __getattr__(name: str):
57-
if name in ("GeoAnimatorManager", "CURATED_COLORMAPS"):
58-
from .ui import GeoAnimatorManager, CURATED_COLORMAPS # noqa: F401
59-
g = {"GeoAnimatorManager": GeoAnimatorManager, "CURATED_COLORMAPS": CURATED_COLORMAPS}
62+
if name in ("GeoAnimatorManager", "CURATED_COLORMAPS", "MapLabelSpec"):
63+
from .ui import GeoAnimatorManager, CURATED_COLORMAPS, MapLabelSpec # noqa: F401
64+
g = {
65+
"GeoAnimatorManager": GeoAnimatorManager,
66+
"CURATED_COLORMAPS": CURATED_COLORMAPS,
67+
"MapLabelSpec": MapLabelSpec,
68+
}
6069
return g[name]
6170
if name == "MultiGeoAnimatorManager":
6271
from .multi_ui import MultiGeoAnimatorManager # noqa: F401

dvue/animator/multi_ui.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ColumnDataSource,
3232
Div,
3333
HoverTool,
34+
Label,
3435
LinearColorMapper,
3536
Range1d,
3637
WMTSTileSource,
@@ -56,6 +57,8 @@
5657
_format_level,
5758
TransformSpec,
5859
StreamingTransformedSlicingReader,
60+
MapLabelSpec,
61+
_VALID_CORNERS,
5962
)
6063

6164

@@ -127,6 +130,9 @@ def _build_bokeh_map(
127130
geo_id_column: str,
128131
shared_x_range=None,
129132
shared_y_range=None,
133+
map_label_spec: Optional["MapLabelSpec"] = None,
134+
map_width: int = 500,
135+
initial_label_text: str = "",
130136
) -> SimpleNamespace:
131137
"""Build one Bokeh figure and all associated rendering objects.
132138
@@ -137,7 +143,8 @@ def _build_bokeh_map(
137143
Returns a ``SimpleNamespace`` with attributes:
138144
``fig``, ``source``, ``mapper``, ``data_renderer``, ``tile_renderer``,
139145
``contour_source``, ``contour_renderer``,
140-
``contour_label_source``, ``contour_label_renderer``.
146+
``contour_label_source``, ``contour_label_renderer``,
147+
``corner_label``.
141148
"""
142149
source = ColumnDataSource({
143150
"xs": bk_xs, "ys": bk_ys,
@@ -230,6 +237,30 @@ def _build_bokeh_map(
230237
visible=False,
231238
)
232239

240+
# Corner timestamp label — screen-pixel coordinates, fixed in the canvas
241+
# container, not attached to map data coordinates.
242+
_spec = map_label_spec if map_label_spec is not None else MapLabelSpec()
243+
_corner = _spec.corner if _spec.corner in _VALID_CORNERS else "bottom_left"
244+
from .ui import _label_corner_pos
245+
_lx, _ly, _la, _lb = _label_corner_pos(_corner, map_width, map_height)
246+
corner_label = Label(
247+
x=_lx, y=_ly,
248+
x_units="screen", y_units="screen",
249+
text=initial_label_text,
250+
text_font_size=_spec.font_size,
251+
text_font_style="bold",
252+
text_color="black",
253+
text_align=_la,
254+
text_baseline=_lb,
255+
background_fill_color="white",
256+
background_fill_alpha=0.7,
257+
border_line_color="lightgrey",
258+
border_line_alpha=0.8,
259+
padding=4,
260+
visible=_spec.visible,
261+
)
262+
p.add_layout(corner_label)
263+
233264
return SimpleNamespace(
234265
fig=p,
235266
source=source,
@@ -241,6 +272,7 @@ def _build_bokeh_map(
241272
contour_renderer=contour_renderer,
242273
contour_label_source=contour_label_source,
243274
contour_label_renderer=contour_label_renderer,
275+
corner_label=corner_label,
244276
)
245277

246278

@@ -314,11 +346,17 @@ def __init__(
314346
transform_options=None,
315347
initial_transform: str = "none",
316348
buffer_chunk_size: int = 200,
349+
map_label_spec: Optional[MapLabelSpec] = None,
350+
map_label_corner: str = "bottom_left",
317351
**params,
318352
) -> None:
319353
# ----------------------------------------------------------------
320354
# 1. Store configuration
321355
# ----------------------------------------------------------------
356+
if map_label_spec is None:
357+
_corner = map_label_corner if map_label_corner in _VALID_CORNERS else "bottom_left"
358+
map_label_spec = MapLabelSpec(corner=_corner)
359+
self._map_label_spec = map_label_spec
322360
self._base_reader_a = reader_a
323361
self._base_reader_b = reader_b
324362
if gdf_b is None:
@@ -403,20 +441,24 @@ def __init__(
403441
# 7. Build Bokeh figures — all three share the same Range1d pair
404442
# ----------------------------------------------------------------
405443
ts0 = ti[0].strftime("%Y-%m-%d %H:%M")
444+
_spec = self._map_label_spec
445+
_init_label_text = _spec.format(ti[0], 0)
406446

407447
ma = _build_bokeh_map(
408448
self._gdf_a_proj, self._geo_ids_a, bk_xs_a, bk_ys_a,
409449
self._geom_type_a, init_vals_a,
410450
colormap, eff_vmin, eff_vmax, size, map_height,
411451
f"{title_a} \u2014 {ts0}", geo_id_column,
412452
shared_x_range=shared_x, shared_y_range=shared_y,
453+
map_label_spec=_spec, map_width=500, initial_label_text=_init_label_text,
413454
)
414455
mb = _build_bokeh_map(
415456
self._gdf_b_proj, self._geo_ids_b, bk_xs_b, bk_ys_b,
416457
self._geom_type_b, init_vals_b,
417458
colormap, eff_vmin, eff_vmax, size, map_height,
418459
f"{title_b} \u2014 {ts0}", geo_id_column,
419460
shared_x_range=shared_x, shared_y_range=shared_y,
461+
map_label_spec=_spec, map_width=500, initial_label_text=_init_label_text,
420462
)
421463
# Diff figure reuses map A's geometry and shares the viewport.
422464
md = _build_bokeh_map(
@@ -425,6 +467,7 @@ def __init__(
425467
colormap, eff_vmin, eff_vmax, size, map_height,
426468
f"{title_a} \u2212 {title_b} \u2014 {ts0}", geo_id_column,
427469
shared_x_range=shared_x, shared_y_range=shared_y,
470+
map_label_spec=_spec, map_width=1000, initial_label_text=_init_label_text,
428471
)
429472

430473
self._shared_x_range = shared_x
@@ -437,6 +480,9 @@ def __init__(
437480
self._colorbar_b = mb.colorbar
438481
self._fig_diff = md.fig; self._src_diff = md.source
439482
self._mapper_diff = md.mapper
483+
self._corner_label_a = ma.corner_label
484+
self._corner_label_b = mb.corner_label
485+
self._corner_label_diff = md.corner_label
440486
# Keep renderer references so show/hide checkboxes can toggle them.
441487
self._all_data_renderers = (
442488
ma.data_renderer, mb.data_renderer, md.data_renderer)
@@ -592,6 +638,11 @@ def __init__(
592638
sizing_mode="stretch_width", visible=False,
593639
)
594640
# Show / hide toggles for channels and basemap (all three figures).
641+
# Map corner label toggle — initial state from spec.
642+
self._map_label_check = pn.widgets.Checkbox(
643+
name="Show map label", value=self._map_label_spec.visible,
644+
sizing_mode="stretch_width",
645+
)
595646
# Channel and basemap opacity sliders (0 = invisible, 100 = fully opaque)
596647
self._channels_alpha_slider = pn.widgets.IntSlider(
597648
name="Channel lines opacity", start=0, end=100, value=100, step=5,
@@ -611,6 +662,7 @@ def __init__(
611662
self._size_slider,
612663
self._channels_alpha_slider,
613664
self._basemap_alpha_slider,
665+
self._map_label_check,
614666
title="Appearance", collapsed=False,
615667
sizing_mode="stretch_width",
616668
)
@@ -728,6 +780,7 @@ def __init__(
728780
self._contour_clip_slider.param.watch(self._on_contour_clip_change, "value")
729781
self._channels_alpha_slider.param.watch(self._on_channels_alpha_change, "value")
730782
self._basemap_alpha_slider.param.watch(self._on_basemap_alpha_change, "value")
783+
self._map_label_check.param.watch(self._on_map_label_toggle, "value")
731784
if self._transform_options:
732785
self._transform_select.param.watch(self._on_transform_change, "value")
733786
self._save_config_btn.on_click(self._on_save_config)
@@ -768,6 +821,7 @@ def collect_state(self) -> dict:
768821
"label_spacing": self._contour_label_spacing_slider.value,
769822
"clip_radius_km": self._contour_clip_slider.value,
770823
},
824+
"map_label": self._map_label_spec.to_dict(),
771825
"diff": {
772826
"show": self.show_diff,
773827
},
@@ -1025,6 +1079,8 @@ def _render_map_a_vals(self, ts: pd.Timestamp, ts_str: str, vals: list) -> None:
10251079
"_value": [(slice(None), vals)],
10261080
"_color_value": [(slice(None), vals)],
10271081
})
1082+
_label = self._map_label_spec.format(ts, self._time_slider.value)
1083+
self._corner_label_a.text = _label
10281084
self._fig_a.title.text = f"{self._title_a} \u2014 {ts_str}"
10291085
if self._ctour_a.renderer.visible:
10301086
eff_vmin, eff_vmax = self._current_clim()
@@ -1037,6 +1093,8 @@ def _render_map_b_vals(self, ts: pd.Timestamp, ts_str: str, vals: list) -> None:
10371093
"_value": [(slice(None), vals)],
10381094
"_color_value": [(slice(None), vals)],
10391095
})
1096+
_label = self._map_label_spec.format(ts, self._time_slider.value)
1097+
self._corner_label_b.text = _label
10401098
self._fig_b.title.text = f"{self._title_b} \u2014 {ts_str}"
10411099
if self._ctour_b.renderer.visible:
10421100
eff_vmin, eff_vmax = self._current_clim()
@@ -1052,6 +1110,8 @@ def _render_diff_vals(self, ts: pd.Timestamp, ts_str: str, vals: list) -> None:
10521110
"_color_value": vals,
10531111
"geo_id": self._geo_ids_a,
10541112
}
1113+
_label = self._map_label_spec.format(ts, self._time_slider.value)
1114+
self._corner_label_diff.text = _label
10551115
self._fig_diff.title.text = (
10561116
f"{self._title_a} \u2212 {self._title_b} \u2014 {ts_str}"
10571117
)
@@ -1344,6 +1404,22 @@ def _apply():
13441404
else:
13451405
_apply()
13461406

1407+
def _on_map_label_toggle(self, event: param.parameterized.Event) -> None:
1408+
"""Show or hide the corner timestamp label on all three maps."""
1409+
visible = bool(event.new)
1410+
self._map_label_spec.visible = visible
1411+
1412+
def _apply():
1413+
self._corner_label_a.visible = visible
1414+
self._corner_label_b.visible = visible
1415+
self._corner_label_diff.visible = visible
1416+
1417+
doc = self._active_doc()
1418+
if doc is not None:
1419+
doc.add_next_tick_callback(_apply)
1420+
else:
1421+
_apply()
1422+
13471423
# ------------------------------------------------------------------
13481424
# Callbacks — diff toggle
13491425
# ------------------------------------------------------------------

0 commit comments

Comments
 (0)