3131 ColumnDataSource ,
3232 Div ,
3333 HoverTool ,
34+ Label ,
3435 LinearColorMapper ,
3536 Range1d ,
3637 WMTSTileSource ,
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