@@ -386,6 +386,55 @@ def _generator(i):
386386 yield x
387387
388388
389+ def _recursive_update (d , u ):
390+ """Recursively update dict d with values from dict u."""
391+ for k , v in u .items ():
392+ if isinstance (v , dict ) and k in d and isinstance (d [k ], dict ):
393+ _recursive_update (d [k ], v )
394+ else :
395+ d [k ] = v
396+
397+
398+ class _RawDictProxy :
399+ """Wraps a raw dict to make .update() compatible with graph object semantics.
400+
401+ The generated update_* methods in _figure.py call obj.update(patch,
402+ overwrite=overwrite, **kwargs) on selected objects. Plain dicts don't
403+ accept 'overwrite', so this proxy intercepts .update() and delegates
404+ to _recursive_update or dict.update as appropriate.
405+ """
406+
407+ __slots__ = ("_d" ,)
408+
409+ def __init__ (self , d ):
410+ self ._d = d
411+
412+ def __getitem__ (self , key ):
413+ return self ._d [key ]
414+
415+ def __setitem__ (self , key , value ):
416+ self ._d [key ] = value
417+
418+ def __contains__ (self , key ):
419+ return key in self ._d
420+
421+ def get (self , key , default = None ):
422+ return self ._d .get (key , default )
423+
424+ def update (self , patch = None , overwrite = False , ** kwargs ):
425+ updates = {** (patch or {}), ** kwargs }
426+ if overwrite :
427+ self ._d .update (updates )
428+ else :
429+ _recursive_update (self ._d , updates )
430+
431+ def __repr__ (self ):
432+ return repr (self ._d )
433+
434+ def to_plotly_json (self ):
435+ return self ._d
436+
437+
389438def _set_property_provided_value (obj , name , arg , provided ):
390439 """
391440 Initialize a property of this object using the provided value
@@ -462,13 +511,22 @@ class is a subclass of both BaseFigure and widgets.DOMWidget.
462511 skipped silently. If False (default) invalid properties in the
463512 figure specification will result in a ValueError
464513
514+ raw: bool
515+ If True, the figure is constructed in "raw mode". In this mode,
516+ no validation is performed, and data/layout are stored as
517+ plain dictionaries rather than Plotly graph objects. This
518+ significantly improves construction performance for large figures
519+ but disables property validation and some convenience features.
520+ Defaults to plotly.config.raw (False by default).
521+
465522 Raises
466523 ------
467524 ValueError
468525 if a property in the specification of data, layout, or frames
469526 is invalid AND skip_invalid is False
470527 """
471528 from .validator_cache import ValidatorCache
529+ from plotly import config
472530
473531 data_validator = ValidatorCache .get_validator ("" , "data" )
474532 frames_validator = ValidatorCache .get_validator ("" , "frames" )
@@ -478,9 +536,9 @@ class is a subclass of both BaseFigure and widgets.DOMWidget.
478536
479537 # Initialize validation
480538 self ._validate = kwargs .pop ("_validate" , True )
481- self ._as_dict_mode = kwargs .pop ("_as_dict " , False )
539+ self ._raw = kwargs .pop ("raw " , config . raw )
482540
483- if self ._as_dict_mode :
541+ if self ._raw :
484542 # Fast path: minimal init for to_dict()/show()/to_json() to work.
485543 self ._grid_str = None
486544 self ._grid_ref = None
@@ -505,6 +563,11 @@ class is a subclass of both BaseFigure and widgets.DOMWidget.
505563 # Frames
506564 self ._frame_objs = ()
507565
566+ # Batch mode (needed by BaseFigure.update / batch_update)
567+ self ._in_batch_mode = False
568+ self ._batch_trace_edits = OrderedDict ()
569+ self ._batch_layout_edits = OrderedDict ()
570+
508571 return # Skip everything else
509572
510573 # Assign layout_plotly to layout
@@ -934,6 +997,25 @@ def update(self, dict1=None, overwrite=False, **kwargs):
934997 BaseFigure
935998 Updated figure
936999 """
1000+ if getattr (self , "_raw" , False ):
1001+ for d in [dict1 , kwargs ]:
1002+ if d :
1003+ for k , v in d .items ():
1004+ if k == "data" :
1005+ if overwrite :
1006+ self ._data = list (v ) if v else []
1007+ else :
1008+ self ._data .extend (v if isinstance (v , list ) else [v ])
1009+ self ._data_defaults = [{} for _ in self ._data ]
1010+ elif k == "layout" :
1011+ if overwrite :
1012+ self ._layout = v if isinstance (v , dict ) else {}
1013+ else :
1014+ _recursive_update (self ._layout , v )
1015+ elif k == "frames" :
1016+ pass # Frames not supported in raw mode
1017+ return self
1018+
9371019 with self .batch_update ():
9381020 for d in [dict1 , kwargs ]:
9391021 if d :
@@ -1002,7 +1084,7 @@ def data(self):
10021084 -------
10031085 tuple[BaseTraceType]
10041086 """
1005- if getattr (self , "_as_dict_mode " , False ):
1087+ if getattr (self , "_raw " , False ):
10061088 return tuple (self ._data )
10071089 return self ["data" ]
10081090
@@ -1147,6 +1229,8 @@ def select_traces(self, selector=None, row=None, col=None, secondary_y=None):
11471229 Select traces from a particular subplot cell and/or traces
11481230 that satisfy custom selection criteria.
11491231
1232+ In raw mode, row/col/secondary_y filtering is skipped.
1233+
11501234 Parameters
11511235 ----------
11521236 selector: dict, function, int, str or None (default None)
@@ -1225,6 +1309,11 @@ def select_traces(self, selector=None, row=None, col=None, secondary_y=None):
12251309 )
12261310
12271311 def _perform_select_traces (self , filter_by_subplot , grid_subplot_refs , selector ):
1312+ if getattr (self , "_raw" , False ):
1313+ return _generator (
1314+ t for t in self ._data if self ._selector_matches (t , selector )
1315+ )
1316+
12281317 from plotly ._subplots import _get_subplot_ref_for_trace
12291318
12301319 # functions for filtering
@@ -1412,6 +1501,16 @@ def update_traces(
14121501 self
14131502 Returns the Figure object that the method was called on
14141503 """
1504+ if getattr (self , "_raw" , False ):
1505+ updates = {** (patch or {}), ** kwargs }
1506+ for trace in self ._data :
1507+ if self ._selector_matches (trace , selector ):
1508+ if overwrite :
1509+ trace .update (updates )
1510+ else :
1511+ _recursive_update (trace , updates )
1512+ return self
1513+
14151514 for trace in self .select_traces (
14161515 selector = selector , row = row , col = col , secondary_y = secondary_y
14171516 ):
@@ -1442,15 +1541,7 @@ def update_layout(self, dict1=None, overwrite=False, **kwargs):
14421541 BaseFigure
14431542 The Figure object that the update_layout method was called on
14441543 """
1445- if getattr (self , "_as_dict_mode" , False ):
1446-
1447- def _recursive_update (d , u ):
1448- for k , v in u .items ():
1449- if isinstance (v , dict ) and k in d and isinstance (d [k ], dict ):
1450- _recursive_update (d [k ], v )
1451- else :
1452- d [k ] = v
1453-
1544+ if getattr (self , "_raw" , False ):
14541545 if overwrite :
14551546 if dict1 :
14561547 self ._layout .update (dict1 )
@@ -1472,6 +1563,16 @@ def _select_layout_subplots_by_prefix(
14721563 """
14731564 Helper called by code generated select_* methods
14741565 """
1566+ if getattr (self , "_raw" , False ):
1567+ # In raw mode, iterate layout keys matching the prefix.
1568+ # row/col/secondary_y filtering is skipped (no grid_ref).
1569+ layout = self ._layout
1570+ objs = [
1571+ _RawDictProxy (layout [k ])
1572+ for k in _natural_sort_strings (list (layout ))
1573+ if k .startswith (prefix ) and isinstance (layout [k ], dict )
1574+ ]
1575+ return _generator (self ._filter_by_selector (objs , [], selector ))
14751576
14761577 if row is not None or col is not None or secondary_y is not None :
14771578 # Build mapping from container keys ('xaxis2', 'scene4', etc.)
@@ -1523,6 +1624,12 @@ def _select_annotations_like(
15231624 Helper to select annotation-like elements from a layout object array.
15241625 Compatible with layout.annotations, layout.shapes, and layout.images
15251626 """
1627+ if getattr (self , "_raw" , False ):
1628+ objs = self ._layout .get (prop , [])
1629+ return _generator (
1630+ _RawDictProxy (o ) for o in objs if self ._selector_matches (o , selector )
1631+ )
1632+
15261633 xref_to_col = {}
15271634 yref_to_row = {}
15281635 yref_to_secondary_y = {}
@@ -1573,7 +1680,7 @@ def _add_annotation_like(
15731680 secondary_y = None ,
15741681 exclude_empty_subplots = False ,
15751682 ):
1576- if getattr (self , "_as_dict_mode " , False ):
1683+ if getattr (self , "_raw " , False ):
15771684 if hasattr (new_obj , "to_plotly_json" ):
15781685 obj_dict = new_obj .to_plotly_json ()
15791686 elif isinstance (new_obj , dict ):
@@ -2266,7 +2373,7 @@ def add_traces(
22662373 Figure(...)
22672374 """
22682375
2269- if getattr (self , "_as_dict_mode " , False ):
2376+ if getattr (self , "_raw " , False ):
22702377 if not isinstance (data , (list , tuple )):
22712378 data = [data ]
22722379 self ._data .extend (data )
@@ -2626,7 +2733,7 @@ def layout(self):
26262733 -------
26272734 plotly.graph_objs.Layout
26282735 """
2629- if getattr (self , "_as_dict_mode " , False ):
2736+ if getattr (self , "_raw " , False ):
26302737 return self ._layout
26312738 return self ["layout" ]
26322739
@@ -4138,7 +4245,7 @@ def _process_multiple_axis_spanning_shapes(
41384245 Add a shape or multiple shapes and call _make_axis_spanning_layout_object on
41394246 all the new shapes.
41404247 """
4141- if getattr (self , "_as_dict_mode " , False ):
4248+ if getattr (self , "_raw " , False ):
41424249 shape_kwargs , annotation_kwargs = shapeannotation .split_dict_by_key_prefix (
41434250 kwargs , "annotation_"
41444251 )
@@ -4427,7 +4534,7 @@ class BasePlotlyType(object):
44274534 _valid_props = set ()
44284535
44294536 def __new__ (cls , * args , ** kwargs ):
4430- if kwargs .pop ("_as_dict " , False ):
4537+ if kwargs .pop ("raw " , False ):
44314538 kwargs .pop ("skip_invalid" , None )
44324539 kwargs .pop ("_validate" , None )
44334540 return kwargs
@@ -4444,8 +4551,8 @@ def __init__(self, plotly_name, **kwargs):
44444551 kwargs : dict
44454552 Invalid props/values to raise on
44464553 """
4447- # Remove _as_dict if it was passed (handled by __new__)
4448- kwargs .pop ("_as_dict " , None )
4554+ # Remove raw if it was passed (handled by __new__)
4555+ kwargs .pop ("raw " , None )
44494556
44504557 # ### _skip_invalid ##
44514558 # If True, then invalid properties should be skipped, if False then
@@ -4551,7 +4658,7 @@ def _process_kwargs(self, **kwargs):
45514658 """
45524659 Process any extra kwargs that are not predefined as constructor params
45534660 """
4554- kwargs .pop ("_as_dict " , None )
4661+ kwargs .pop ("raw " , None )
45554662 for k , v in kwargs .items ():
45564663 err = _check_path_in_prop_tree (self , k , error_cast = ValueError )
45574664 if err is None :
@@ -6129,7 +6236,22 @@ class BaseTraceType(BaseTraceHierarchyType):
61296236 """
61306237
61316238 def __new__ (cls , * args , ** kwargs ):
6132- if kwargs .pop ("_as_dict" , False ):
6239+ """
6240+ Construct a new trace object.
6241+
6242+ Parameters
6243+ ----------
6244+ *args :
6245+ Positional arguments for standard trace construction.
6246+ raw : bool
6247+ If True, returns a plain dictionary instead of a trace object.
6248+ This is for performance optimization. Defaults to plotly.config.raw.
6249+ **kwargs :
6250+ Keyword arguments for trace properties.
6251+ """
6252+ from plotly import config
6253+
6254+ if kwargs .pop ("raw" , config .raw ):
61336255 kwargs .pop ("skip_invalid" , None )
61346256 kwargs .pop ("_validate" , None )
61356257 kwargs ["type" ] = cls ._path_str
0 commit comments