3131from ..aggregation .plotly_aggregator_parser import PlotlyAggregatorParser
3232from .utils import round_number_str , round_td_str
3333
34- _hf_data_container = namedtuple ("DataContainer" , ["x" , "y" , "text" , "hovertext" ])
34+ # A high-frequency data container
35+ # NOTE: the attributes must all be valid trace attributes, with attribute levels
36+ # separated by an '_' (e.g., 'marker_color' is valid) as the
37+ # `_hf_data_container._asdict()` function is used in
38+ # `AbstractFigureAggregator._construct_hf_data_dict`.
39+ _hf_data_container = namedtuple (
40+ "DataContainer" , ["x" , "y" , "text" , "hovertext" , "marker_size" , "marker_color" ]
41+ )
3542
3643
3744class AbstractFigureAggregator (BaseFigure , ABC ):
@@ -355,14 +362,22 @@ def _check_update_trace_data(
355362 hf_trace_data , end_idx - start_idx , agg_x
356363 )
357364
365+ def _nest_dict_rec (k : str , v : any , out : dict ) -> None :
366+ """Recursively nest a dict based on the key whose '_' indicates level."""
367+ k , * rest = k .split ("_" , 1 )
368+ if rest :
369+ _nest_dict_rec (rest [0 ], v , out .setdefault (k , {}))
370+ else :
371+ out [k ] = v
372+
358373 # Check if (hover)text also needs to be downsampled
359- for k in ["text" , "hovertext" ]:
374+ for k in ["text" , "hovertext" , "marker_size" , "marker_color" ]:
360375 k_val = hf_trace_data .get (k )
361376 if isinstance (k_val , (np .ndarray , pd .Series )):
362377 assert isinstance (
363378 hf_trace_data ["downsampler" ], DataPointSelector
364379 ), "Only DataPointSelector can downsample non-data trace array props."
365- trace [ k ] = k_val [start_idx + indices ]
380+ _nest_dict_rec ( k , k_val [start_idx + indices ], trace )
366381 elif k_val is not None :
367382 trace [k ] = k_val
368383
@@ -535,6 +550,8 @@ def _parse_get_trace_props(
535550 hf_y : Iterable = None ,
536551 hf_text : Iterable = None ,
537552 hf_hovertext : Iterable = None ,
553+ hf_marker_size : Iterable = None ,
554+ hf_marker_color : Iterable = None ,
538555 check_nans : bool = True ,
539556 ) -> _hf_data_container :
540557 """Parse and capture the possibly high-frequency trace-props in a datacontainer.
@@ -601,6 +618,26 @@ def _parse_get_trace_props(
601618 else None
602619 )
603620
621+ hf_marker_size = (
622+ trace ["marker" ]["size" ]
623+ if (
624+ hf_marker_size is None
625+ and hasattr (trace , "marker" )
626+ and "size" in trace ["marker" ]
627+ )
628+ else hf_marker_size
629+ )
630+
631+ hf_marker_color = (
632+ trace ["marker" ]["color" ]
633+ if (
634+ hf_marker_color is None
635+ and hasattr (trace , "marker" )
636+ and "color" in trace ["marker" ]
637+ )
638+ else hf_marker_color
639+ )
640+
604641 if trace ["type" ].lower () in self ._high_frequency_traces :
605642 if hf_x is None : # if no data as x or hf_x is passed
606643 if hf_y .ndim != 0 : # if hf_y is an array
@@ -687,8 +724,15 @@ def _parse_get_trace_props(
687724
688725 if hasattr (trace , "hovertext" ):
689726 trace ["hovertext" ] = hf_hovertext
690-
691- return _hf_data_container (hf_x , hf_y , hf_text , hf_hovertext )
727+ if hasattr (trace , "marker" ):
728+ if hasattr (trace .marker , "size" ):
729+ trace .marker .size = hf_marker_size
730+ if hasattr (trace .marker , "color" ):
731+ trace .marker .color = hf_marker_color
732+
733+ return _hf_data_container (
734+ hf_x , hf_y , hf_text , hf_hovertext , hf_marker_size , hf_marker_color
735+ )
692736
693737 def _construct_hf_data_dict (
694738 self ,
@@ -800,6 +844,8 @@ def add_trace(
800844 hf_y : Iterable = None ,
801845 hf_text : Union [str , Iterable ] = None ,
802846 hf_hovertext : Union [str , Iterable ] = None ,
847+ hf_marker_size : Union [str , Iterable ] = None ,
848+ hf_marker_color : Union [str , Iterable ] = None ,
803849 check_nans : bool = True ,
804850 ** trace_kwargs ,
805851 ):
@@ -850,6 +896,12 @@ def add_trace(
850896 hf_hovertext: Iterable, optional
851897 The original high frequency hovertext. If set, this has priority over the
852898 trace its ```hovertext`` argument.
899+ hf_marker_size: Iterable, optional
900+ The original high frequency marker size. If set, this has priority over the
901+ trace its ``marker.size`` argument.
902+ hf_marker_color: Iterable, optional
903+ The original high frequency marker color. If set, this has priority over the
904+ trace its ``marker.color`` argument.
853905 check_nans: boolean, optional
854906 If set to True, the trace's data will be checked for NaNs - which will be
855907 removed. By default True.
@@ -929,7 +981,14 @@ def add_trace(
929981 # construct the hf_data_container
930982 # TODO in future version -> maybe regex on kwargs which start with `hf_`
931983 dc = self ._parse_get_trace_props (
932- trace , hf_x , hf_y , hf_text , hf_hovertext , check_nans
984+ trace ,
985+ hf_x ,
986+ hf_y ,
987+ hf_text ,
988+ hf_hovertext ,
989+ hf_marker_size ,
990+ hf_marker_color ,
991+ check_nans ,
933992 )
934993
935994 # These traces will determine the autoscale its RANGE!
@@ -955,6 +1014,8 @@ def add_trace(
9551014 # We copy (by reference) all the non-data properties of the trace in
9561015 # the new trace.
9571016 trace = trace ._props # convert the trace into a dict
1017+ # NOTE: there is no need to store `marker` property here.
1018+ # If needed, it will be added to `trace` via `check_update_trace_data`
9581019 trace = {
9591020 k : trace [k ] for k in set (trace .keys ()).difference (set (dc ._fields ))
9601021 }
@@ -970,12 +1031,12 @@ def add_trace(
9701031 )
9711032 else :
9721033 self ._print (f"[i] NOT resampling { trace ['name' ]} - len={ n_samples } " )
973- for k in dc ._fields :
974- setattr (trace , k , getattr (dc , k ))
1034+ trace ._process_kwargs (** {k : getattr (dc , k ) for k in dc ._fields })
9751035 return super (AbstractFigureAggregator , self ).add_traces (
9761036 [trace ], ** self ._add_trace_to_add_traces_kwargs (trace_kwargs )
9771037 )
978- return super (AbstractFigureAggregator , self ).add_traces (
1038+
1039+ return super (self ._figure_class , self ).add_traces (
9791040 [trace ], ** self ._add_trace_to_add_traces_kwargs (trace_kwargs )
9801041 )
9811042
@@ -1184,7 +1245,10 @@ def replace(self, figure: go.Figure, convert_existing_traces: bool = True):
11841245 convert_existing_traces = convert_existing_traces ,
11851246 default_n_shown_samples = self ._global_n_shown_samples ,
11861247 default_downsampler = self ._global_downsampler ,
1248+ default_gap_handler = self ._global_gap_handler ,
11871249 resampled_trace_prefix_suffix = (self ._prefix , self ._suffix ),
1250+ show_mean_aggregation_size = self ._show_mean_aggregation_size ,
1251+ verbose = self ._print_verbose ,
11881252 )
11891253
11901254 def _parse_relayout (self , relayout_dict : dict ) -> dict :
@@ -1299,7 +1363,7 @@ def construct_update_data(
12991363 layout_traces_list : List [dict ] = [relayout_data ]
13001364
13011365 # 2. Create the additional trace data for the frond-end
1302- relevant_keys = list (_hf_data_container ._fields ) + ["name" ]
1366+ relevant_keys = list (_hf_data_container ._fields ) + ["name" , "marker" ]
13031367 # Note that only updated trace-data will be sent to the client
13041368 for idx in updated_trace_indices :
13051369 trace = current_graph ["data" ][idx ]
@@ -1355,6 +1419,7 @@ def _get_pr_props_keys(self) -> List[str]:
13551419 "_prefix" ,
13561420 "_suffix" ,
13571421 "_global_downsampler" ,
1422+ "_global_gap_handler" ,
13581423 ]
13591424
13601425 def __reduce__ (self ):
0 commit comments