29
29
from ..aggregation .plotly_aggregator_parser import PlotlyAggregatorParser
30
30
from .utils import round_number_str , round_td_str
31
31
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
+
32
48
# A high-frequency data container
33
49
# NOTE: the attributes must all be valid trace attributes, with attribute levels
34
50
# separated by an '_' (e.g., 'marker_color' is valid) as the
35
51
# `_hf_data_container._asdict()` function is used in
36
52
# `AbstractFigureAggregator._construct_hf_data_dict`.
53
+ # Create the _hf_data_container dynamically from the configuration
37
54
_hf_data_container = namedtuple (
38
55
"DataContainer" ,
39
- ["x" , "y" , "text" , "hovertext" , "marker_size" , "marker_color" , "customdata" ],
56
+ ["x" , "y" ] + [ prop [ 0 ] for prop in DOWNSAMPLABLE_PROPERTIES ],
40
57
)
41
58
42
59
@@ -219,8 +236,7 @@ def _get_current_graph(self) -> dict:
219
236
"data" : [
220
237
{
221
238
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 })
224
240
}
225
241
for trace in self ._data
226
242
],
@@ -385,16 +401,18 @@ def _nest_dict_rec(k: str, v: any, out: dict) -> None:
385
401
else :
386
402
out [k ] = v
387
403
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 )
391
407
if isinstance (k_val , (np .ndarray , pd .Series )):
392
408
assert isinstance (
393
409
hf_trace_data ["downsampler" ], DataPointSelector
394
410
), "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 )
396
414
elif k_val is not None :
397
- trace [k ] = k_val
415
+ trace [prop_name ] = k_val
398
416
399
417
return trace
400
418
@@ -549,10 +567,7 @@ def _parse_get_trace_props(
549
567
trace : BaseTraceType ,
550
568
hf_x : Iterable = None ,
551
569
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 ,
556
571
) -> _hf_data_container :
557
572
"""Parse and capture the possibly high-frequency trace-props in a datacontainer.
558
573
@@ -603,47 +618,19 @@ def _parse_get_trace_props(
603
618
if not hasattr (hf_y , "dtype" ):
604
619
hf_y : np .ndarray = np .asarray (hf_y )
605
620
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 )
625
626
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
647
634
648
635
if trace ["type" ].lower () in self ._high_frequency_traces :
649
636
if hf_x is None : # if no data as x or hf_x is passed
@@ -664,15 +651,11 @@ def _parse_get_trace_props(
664
651
"(i.e., x and y, or hf_x and hf_y) to be <= 1 dimensional!"
665
652
)
666
653
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 )
676
659
677
660
# Try to parse the hf_x data if it is of object type or
678
661
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(
719
702
if hasattr (trace , "y" ):
720
703
trace ["y" ] = hf_y
721
704
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
742
776
743
777
def _construct_hf_data_dict (
744
778
self ,
@@ -853,10 +887,6 @@ def add_trace(
853
887
# Use these if you want some speedups (and are working with really large data)
854
888
hf_x : Iterable = None ,
855
889
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 ,
860
890
** trace_kwargs ,
861
891
):
862
892
"""Add a trace to the figure.
@@ -900,22 +930,21 @@ def add_trace(
900
930
hf_y: Iterable, optional
901
931
The original high frequency values. If set, this has priority over the
902
932
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.
915
933
**trace_kwargs: dict
916
934
Additional trace related keyword arguments.
917
935
e.g.: row=.., col=..., secondary_y=...
918
936
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
+
919
948
!!! info "See Also"
920
949
[`Figure.add_trace`](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html#plotly.graph_objects.Figure.add_trace>) docs.
921
950
@@ -985,17 +1014,14 @@ def add_trace(
985
1014
# These traces will determine the autoscale its RANGE!
986
1015
# -> so also store when `limit_to_view` is set.
987
1016
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
+
988
1023
# 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 )
999
1025
1000
1026
n_samples = len (dc .x )
1001
1027
if n_samples > max_out_s or limit_to_view :
@@ -1368,10 +1394,14 @@ def _construct_update_data(
1368
1394
layout_traces_list : List [dict ] = [relayout_data ]
1369
1395
1370
1396
# 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)
1372
1401
# Note that only updated trace-data will be sent to the client
1373
1402
for idx in updated_trace_indices :
1374
1403
trace = current_graph ["data" ][idx ]
1404
+ # self._print("trace keys", dict(trace).keys())
1375
1405
# TODO: check if we can reduce even more
1376
1406
trace_reduced = {k : trace [k ] for k in relevant_keys if k in trace }
1377
1407
0 commit comments