3737)
3838from bokeh .plotting import figure as bk_figure
3939
40- from .reader import SlicingReader , DiffSlicingReader , BufferedSlicingReader , RawSequentialBuffer
40+ from .reader import SlicingReader , DiffSlicingReader , BufferedSlicingReader , RawSequentialBuffer , \
41+ ResamplingSlicingReader
4142from .ui import (
4243 CURATED_COLORMAPS ,
4344 CURATED_COLORMAPS_WITH_SEP ,
@@ -827,57 +828,62 @@ def _setup_reader(self, base: SlicingReader, transform_name: str) -> SlicingRead
827828 chunk_size = self ._buffer_chunk_size
828829 min_chunk = 50
829830 max_chunk = 2000
831+ target_buf_s = 10.0
830832 if (
831833 transform_name
832834 and transform_name != "none"
833835 and transform_name in self ._transform_options
834836 ):
835837 spec_or_fn = self ._transform_options [transform_name ]
836- if isinstance (spec_or_fn , TransformSpec ):
837- freq_nanos = 0
838+ if not isinstance (spec_or_fn , TransformSpec ):
839+ raise TypeError (
840+ f"transform_options value for { transform_name !r} must be a "
841+ f"TransformSpec, got { type (spec_or_fn ).__name__ } . "
842+ "Use make_resample_transform(), make_moving_average_transform(), "
843+ "or make_godin_transform() from dsm2ui.animate."
844+ )
845+ freq_nanos = 0
846+ try :
847+ freq_nanos = int (
848+ pd .tseries .frequencies .to_offset (
849+ reader .time_index .freq
850+ ).nanos
851+ )
852+ except (AttributeError , TypeError ):
853+ pass
854+
855+ if spec_or_fn .kind == "aggregate" :
856+ filter_spec = spec_or_fn .filter_spec
857+ if filter_spec is not None :
858+ try :
859+ raw_overlap = filter_spec .get_overlap (freq_nanos )
860+ except (AttributeError , TypeError ):
861+ raw_overlap = 0
862+ if raw_overlap > 0 :
863+ reader = RawSequentialBuffer (reader )
864+ reader = StreamingTransformedSlicingReader (reader , filter_spec )
865+ reader = ResamplingSlicingReader (
866+ reader ,
867+ spec_or_fn .output_freq ,
868+ spec_or_fn .resample_agg ,
869+ )
870+ chunk_size = max (5 , self ._buffer_chunk_size // 20 )
871+ min_chunk = 2
872+ max_chunk = 365
873+ target_buf_s = 30.0
874+ else :
838875 try :
839- freq_nanos = int (
840- pd .tseries .frequencies .to_offset (
841- reader .time_index .freq
842- ).nanos
843- )
844876 raw_overlap = spec_or_fn .get_overlap (freq_nanos )
845877 except (AttributeError , TypeError ):
846878 raw_overlap = 0
847879 if raw_overlap > 0 :
848880 reader = RawSequentialBuffer (reader )
849881 reader = StreamingTransformedSlicingReader (reader , spec_or_fn )
850- # Scale chunk_size for coarse aggregate transforms so the number
851- # of raw steps per chunk stays roughly constant. See the same
852- # logic in GeoAnimatorManager._setup_reader for the rationale.
853- if (
854- spec_or_fn .kind == "aggregate"
855- and spec_or_fn .output_freq is not None
856- and freq_nanos > 0
857- ):
858- try :
859- out_ns = int (
860- pd .tseries .frequencies .to_offset (
861- spec_or_fn .output_freq
862- ).nanos
863- )
864- if out_ns > freq_nanos :
865- ratio = out_ns // freq_nanos
866- chunk_size = max (5 , self ._buffer_chunk_size // ratio )
867- min_chunk = max (2 , 50 // ratio )
868- max_chunk = max (chunk_size , 2000 // ratio )
869- except Exception :
870- pass
871- else :
872- raise TypeError (
873- f"transform_options value for { transform_name !r} must be a "
874- f"TransformSpec, got { type (spec_or_fn ).__name__ } . "
875- "Use make_resample_transform(), make_moving_average_transform(), "
876- "or make_godin_transform() from dsm2ui.animate."
877- )
882+
878883 return BufferedSlicingReader (
879884 reader , chunk_size = chunk_size , prefetch = True ,
880885 adaptive = True , min_chunk_size = min_chunk , max_chunk_size = max_chunk ,
886+ target_buffer_seconds = target_buf_s ,
881887 )
882888
883889 def _get_diff_reader (self ) -> SlicingReader :
@@ -1537,6 +1543,27 @@ def _compute() -> None:
15371543 for _tcb in self ._extra_transform_callbacks :
15381544 _tcb (_new_spec )
15391545
1546+ # ── Pre-load both maps in the background thread ──────────────────
1547+ # _apply_frame (and its _update_map_a / _update_map_b calls)
1548+ # triggers cold-start HDF5 reads on the Tornado IOLoop, which
1549+ # freezes the browser for the duration of the chunk load.
1550+ # Pre-fetch both maps here, off the IOLoop, then pass the
1551+ # pre-computed values to _apply for pure Bokeh model mutations.
1552+ ts = ti [nearest_idx ]
1553+ ts_str = ts .strftime ("%Y-%m-%d %H:%M" )
1554+ _geo_ids_a_snap = self ._geo_ids_a
1555+ _geo_ids_b_snap = self ._geo_ids_b
1556+ try :
1557+ _sa = new_reader_a .get_slice_nearest (ts )
1558+ vals_a = _sa .reindex (_geo_ids_a_snap ).fillna (np .nan ).tolist ()
1559+ except Exception :
1560+ vals_a = [float ("nan" )] * len (_geo_ids_a_snap )
1561+ try :
1562+ _sb = new_reader_b .get_slice_nearest (ts )
1563+ vals_b = _sb .reindex (_geo_ids_b_snap ).fillna (np .nan ).tolist ()
1564+ except Exception :
1565+ vals_b = [float ("nan" )] * len (_geo_ids_b_snap )
1566+
15401567 def _apply () -> None :
15411568 self ._reader_a = new_reader_a
15421569 self ._reader_b = new_reader_b
@@ -1552,9 +1579,12 @@ def _apply() -> None:
15521579 finally :
15531580 self ._syncing = False
15541581
1555- ts_str = ti [nearest_idx ].strftime ("%Y-%m-%d %H:%M" )
15561582 self ._time_div .text = f"<b>{ ts_str } </b>"
1557- self ._apply_frame (nearest_idx , ts_str )
1583+ # Pure Bokeh mutations — no I/O on the IOLoop.
1584+ self ._render_map_a_vals (ts , ts_str , vals_a )
1585+ self ._render_map_b_vals (ts , ts_str , vals_b )
1586+ for _cb in self ._extra_frame_callbacks :
1587+ _cb (ts )
15581588
15591589 for pane in (self ._pane_a , self ._pane_b , self ._pane_diff ):
15601590 pane .loading = False
0 commit comments