2929from ..aggregation .plotly_aggregator_parser import PlotlyAggregatorParser
3030from .utils import round_number_str , round_td_str
3131
32+ # Configuration for properties that can be downsampled
33+ # Each entry is a tuple of (property_name, trace_path, hf_param_name)
34+ # - property_name: the name in the _hf_data_container
35+ # - trace_path: the path to access the property in a trace (e.g., "marker.color" -> ["marker", "color"])
36+ # - hf_param_name: the parameter name for high-frequency data (e.g., "hf_marker_color")
37+ DOWNSAMPLABLE_PROPERTIES = [
38+ ("text" , ["text" ], "hf_text" ),
39+ ("hovertext" , ["hovertext" ], "hf_hovertext" ),
40+ ("marker_angle" , ["marker" , "angle" ], "hf_marker_angle" ),
41+ ("marker_opacity" , ["marker" , "opacity" ], "hf_marker_opacity" ),
42+ ("marker_size" , ["marker" , "size" ], "hf_marker_size" ),
43+ ("marker_color" , ["marker" , "color" ], "hf_marker_color" ),
44+ ("marker_symbol" , ["marker" , "symbol" ], "hf_marker_symbol" ),
45+ ("customdata" , ["customdata" ], "hf_customdata" ),
46+ ]
47+
3248# A high-frequency data container
3349# NOTE: the attributes must all be valid trace attributes, with attribute levels
3450# separated by an '_' (e.g., 'marker_color' is valid) as the
3551# `_hf_data_container._asdict()` function is used in
3652# `AbstractFigureAggregator._construct_hf_data_dict`.
53+ # Create the _hf_data_container dynamically from the configuration
3754_hf_data_container = namedtuple (
3855 "DataContainer" ,
39- ["x" , "y" , "text" , "hovertext" , "marker_size" , "marker_color" , "customdata" ],
56+ ["x" , "y" ] + [ prop [ 0 ] for prop in DOWNSAMPLABLE_PROPERTIES ],
4057)
4158
4259
@@ -219,8 +236,7 @@ def _get_current_graph(self) -> dict:
219236 "data" : [
220237 {
221238 k : copy (trace [k ])
222- # TODO: why not "text" as well? -> we can use _hf_data_container.fields then
223- for k in set (trace .keys ()).difference ({"x" , "y" , "hovertext" })
239+ for k in set (trace .keys ()).difference ({_hf_data_container ._fields })
224240 }
225241 for trace in self ._data
226242 ],
@@ -385,16 +401,18 @@ def _nest_dict_rec(k: str, v: any, out: dict) -> None:
385401 else :
386402 out [k ] = v
387403
388- # Check if (hover)text also needs to be downsampled
389- for k in [ "text" , "hovertext" , "marker_size" , "marker_color" , "customdata" ] :
390- k_val = hf_trace_data .get (k )
404+ # Check if downsamplable properties also need to be downsampled
405+ for prop_name , _ , _ in DOWNSAMPLABLE_PROPERTIES :
406+ k_val = hf_trace_data .get (prop_name )
391407 if isinstance (k_val , (np .ndarray , pd .Series )):
392408 assert isinstance (
393409 hf_trace_data ["downsampler" ], DataPointSelector
394410 ), "Only DataPointSelector can downsample non-data trace array props."
395- _nest_dict_rec (k , k_val [start_idx + indices ], trace )
411+ # Use the same indices that were used for x and y aggregation
412+ # indices are relative to the slice, so we need to add start_idx
413+ _nest_dict_rec (prop_name , k_val [start_idx + indices ], trace )
396414 elif k_val is not None :
397- trace [k ] = k_val
415+ trace [prop_name ] = k_val
398416
399417 return trace
400418
@@ -549,10 +567,7 @@ def _parse_get_trace_props(
549567 trace : BaseTraceType ,
550568 hf_x : Iterable = None ,
551569 hf_y : Iterable = None ,
552- hf_text : Iterable = None ,
553- hf_hovertext : Iterable = None ,
554- hf_marker_size : Iterable = None ,
555- hf_marker_color : Iterable = None ,
570+ ** hf_properties ,
556571 ) -> _hf_data_container :
557572 """Parse and capture the possibly high-frequency trace-props in a datacontainer.
558573
@@ -603,47 +618,19 @@ def _parse_get_trace_props(
603618 if not hasattr (hf_y , "dtype" ):
604619 hf_y : np .ndarray = np .asarray (hf_y )
605620
606- hf_text = (
607- hf_text
608- if hf_text is not None
609- else (
610- trace ["text" ]
611- if hasattr (trace , "text" ) and trace ["text" ] is not None
612- else None
613- )
614- )
615-
616- hf_hovertext = (
617- hf_hovertext
618- if hf_hovertext is not None
619- else (
620- trace ["hovertext" ]
621- if hasattr (trace , "hovertext" ) and trace ["hovertext" ] is not None
622- else None
623- )
624- )
621+ # Parse downsamplable properties dynamically
622+ parsed_properties = {}
623+ for prop_name , trace_path , hf_param_name in DOWNSAMPLABLE_PROPERTIES :
624+ # Get the high-frequency value from parameters if provided
625+ hf_value = hf_properties .get (hf_param_name )
625626
626- hf_marker_size = (
627- trace ["marker" ]["size" ]
628- if (
629- hf_marker_size is None
630- and hasattr (trace , "marker" )
631- and "size" in trace ["marker" ]
632- )
633- else hf_marker_size
634- )
635-
636- hf_marker_color = (
637- trace ["marker" ]["color" ]
638- if (
639- hf_marker_color is None
640- and hasattr (trace , "marker" )
641- and "color" in trace ["marker" ]
642- )
643- else hf_marker_color
644- )
645-
646- hf_customdata = trace ["customdata" ] if hasattr (trace , "customdata" ) else None
627+ if hf_value is not None :
628+ # Use the provided high-frequency value
629+ parsed_properties [prop_name ] = hf_value
630+ else :
631+ # Try to get the value from the trace
632+ trace_value = self ._get_trace_property (trace , trace_path )
633+ parsed_properties [prop_name ] = trace_value
647634
648635 if trace ["type" ].lower () in self ._high_frequency_traces :
649636 if hf_x is None : # if no data as x or hf_x is passed
@@ -664,15 +651,11 @@ def _parse_get_trace_props(
664651 "(i.e., x and y, or hf_x and hf_y) to be <= 1 dimensional!"
665652 )
666653
667- # Note: this converts the hf property to a np.ndarray
668- if isinstance (hf_text , (tuple , list , np .ndarray , pd .Series )):
669- hf_text = np .asarray (hf_text )
670- if isinstance (hf_hovertext , (tuple , list , np .ndarray , pd .Series )):
671- hf_hovertext = np .asarray (hf_hovertext )
672- if isinstance (hf_marker_size , (tuple , list , np .ndarray , pd .Series )):
673- hf_marker_size = np .asarray (hf_marker_size )
674- if isinstance (hf_marker_color , (tuple , list , np .ndarray , pd .Series )):
675- hf_marker_color = np .asarray (hf_marker_color )
654+ # Note: this converts the hf properties to np.ndarray
655+ for prop_name , _ , _ in DOWNSAMPLABLE_PROPERTIES :
656+ prop_value = parsed_properties .get (prop_name )
657+ if isinstance (prop_value , (tuple , list , np .ndarray , pd .Series )):
658+ parsed_properties [prop_name ] = np .asarray (prop_value )
676659
677660 # Try to parse the hf_x data if it is of object type or
678661 if len (hf_x ) and (hf_x .dtype .type is np .str_ or hf_x .dtype == "object" ):
@@ -719,26 +702,77 @@ def _parse_get_trace_props(
719702 if hasattr (trace , "y" ):
720703 trace ["y" ] = hf_y
721704
722- if hasattr (trace , "text" ):
723- trace ["text" ] = hf_text
724-
725- if hasattr (trace , "hovertext" ):
726- trace ["hovertext" ] = 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 ,
735- hf_y ,
736- hf_text ,
737- hf_hovertext ,
738- hf_marker_size ,
739- hf_marker_color ,
740- hf_customdata ,
741- )
705+ # Set downsamplable properties if they exist
706+ for prop_name , trace_path , _ in DOWNSAMPLABLE_PROPERTIES :
707+ prop_value = parsed_properties .get (prop_name )
708+ if prop_value is not None :
709+ self ._set_trace_property (trace , trace_path , prop_value )
710+
711+ # Build the container with all properties
712+ container_args = [hf_x , hf_y ]
713+ for prop_name , _ , _ in DOWNSAMPLABLE_PROPERTIES :
714+ container_args .append (parsed_properties .get (prop_name ))
715+
716+ return _hf_data_container (* container_args )
717+
718+ def _get_trace_property (self , trace : BaseTraceType , trace_path : List [str ]) -> any :
719+ """Get a property from a trace using a path.
720+
721+ Parameters
722+ ----------
723+ trace : BaseTraceType
724+ The trace to get the property from.
725+ trace_path : List[str]
726+ The path to the property (e.g., ["marker", "color"]).
727+
728+ Returns
729+ -------
730+ any
731+ The property value or None if not found.
732+ """
733+ current = trace
734+ for path_component in trace_path :
735+ if hasattr (current , path_component ):
736+ current = getattr (current , path_component )
737+ elif isinstance (current , dict ) and path_component in current :
738+ current = current [path_component ]
739+ else :
740+ return None
741+ return current
742+
743+ def _set_trace_property (
744+ self , trace : BaseTraceType , trace_path : List [str ], value : any
745+ ) -> None :
746+ """Set a property on a trace using a path.
747+
748+ Parameters
749+ ----------
750+ trace : BaseTraceType
751+ The trace to set the property on.
752+ trace_path : List[str]
753+ The path to the property (e.g., ["marker", "color"]).
754+ value : any
755+ The value to set.
756+ """
757+ current = trace
758+ for path_component in trace_path [:- 1 ]:
759+ if hasattr (current , path_component ):
760+ current = getattr (current , path_component )
761+ elif isinstance (current , dict ):
762+ if path_component not in current :
763+ current [path_component ] = {}
764+ current = current [path_component ]
765+ else :
766+ # Create the path if it doesn't exist
767+ setattr (current , path_component , {})
768+ current = getattr (current , path_component )
769+
770+ # Set the final property
771+ final_component = trace_path [- 1 ]
772+ if hasattr (current , final_component ):
773+ setattr (current , final_component , value )
774+ elif isinstance (current , dict ):
775+ current [final_component ] = value
742776
743777 def _construct_hf_data_dict (
744778 self ,
@@ -853,10 +887,6 @@ def add_trace(
853887 # Use these if you want some speedups (and are working with really large data)
854888 hf_x : Iterable = None ,
855889 hf_y : Iterable = None ,
856- hf_text : Union [str , Iterable ] = None ,
857- hf_hovertext : Union [str , Iterable ] = None ,
858- hf_marker_size : Union [str , Iterable ] = None ,
859- hf_marker_color : Union [str , Iterable ] = None ,
860890 ** trace_kwargs ,
861891 ):
862892 """Add a trace to the figure.
@@ -900,22 +930,21 @@ def add_trace(
900930 hf_y: Iterable, optional
901931 The original high frequency values. If set, this has priority over the
902932 trace its data.
903- hf_text: Iterable, optional
904- The original high frequency text. If set, this has priority over the trace
905- its ``text`` argument.
906- hf_hovertext: Iterable, optional
907- The original high frequency hovertext. If set, this has priority over the
908- trace its ```hovertext`` argument.
909- hf_marker_size: Iterable, optional
910- The original high frequency marker size. If set, this has priority over the
911- trace its ``marker.size`` argument.
912- hf_marker_color: Iterable, optional
913- The original high frequency marker color. If set, this has priority over the
914- trace its ``marker.color`` argument.
915933 **trace_kwargs: dict
916934 Additional trace related keyword arguments.
917935 e.g.: row=.., col=..., secondary_y=...
918936
937+ High-frequency property parameters can also be passed:
938+ - hf_text: High-frequency text data
939+ - hf_hovertext: High-frequency hovertext data
940+ - hf_marker_size: High-frequency marker size data
941+ - hf_marker_color: High-frequency marker color data
942+ - hf_marker_symbol: High-frequency marker symbol data
943+ - hf_marker_angle: High-frequency marker angle data
944+ - hf_customdata: High-frequency customdata
945+
946+ These have priority over the corresponding trace properties.
947+
919948 !!! info "See Also"
920949 [`Figure.add_trace`](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html#plotly.graph_objects.Figure.add_trace>) docs.
921950
@@ -985,17 +1014,14 @@ def add_trace(
9851014 # These traces will determine the autoscale its RANGE!
9861015 # -> so also store when `limit_to_view` is set.
9871016 if trace ["type" ].lower () in self ._high_frequency_traces :
1017+ # Extract hf_* parameters from trace_kwargs
1018+ hf_properties = {}
1019+ for _ , _ , hf_param_name in DOWNSAMPLABLE_PROPERTIES :
1020+ if hf_param_name in trace_kwargs :
1021+ hf_properties [hf_param_name ] = trace_kwargs .pop (hf_param_name )
1022+
9881023 # construct the hf_data_container
989- # TODO in future version -> maybe regex on kwargs which start with `hf_`
990- dc = self ._parse_get_trace_props (
991- trace ,
992- hf_x ,
993- hf_y ,
994- hf_text ,
995- hf_hovertext ,
996- hf_marker_size ,
997- hf_marker_color ,
998- )
1024+ dc = self ._parse_get_trace_props (trace , hf_x , hf_y , ** hf_properties )
9991025
10001026 n_samples = len (dc .x )
10011027 if n_samples > max_out_s or limit_to_view :
@@ -1368,10 +1394,14 @@ def _construct_update_data(
13681394 layout_traces_list : List [dict ] = [relayout_data ]
13691395
13701396 # 2. Create the additional trace data for the frond-end
1371- relevant_keys = list (_hf_data_container ._fields ) + ["name" , "marker" ]
1397+ relevant_keys = ["name" , "marker" , "x" , "y" ] + [
1398+ prop_name for prop_name , _ , _ in DOWNSAMPLABLE_PROPERTIES
1399+ ]
1400+ # self._print("relevant keys", relevant_keys)
13721401 # Note that only updated trace-data will be sent to the client
13731402 for idx in updated_trace_indices :
13741403 trace = current_graph ["data" ][idx ]
1404+ # self._print("trace keys", dict(trace).keys())
13751405 # TODO: check if we can reduce even more
13761406 trace_reduced = {k : trace [k ] for k in relevant_keys if k in trace }
13771407
0 commit comments