Skip to content

Commit 0b37698

Browse files
committed
feat: Implement raw=True mode for high-performance figure construction
- Implements in BaseFigure and trace constructors - Adds global config - Optimizes , , - Adds comprehensive tests in
1 parent e24fcbb commit 0b37698

File tree

6 files changed

+429
-160
lines changed

6 files changed

+429
-160
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## Unreleased
66

77
### Added
8-
- Add `_as_dict=True` parameter to graph object constructors and `go.Figure` for high-performance figure construction, bypassing validation and object creation [[#5514](https://github.com/plotly/plotly.py/issues/5514)].
8+
- Add `raw=True` parameter to graph object constructors and `go.Figure` for high-performance figure construction, bypassing validation and object creation. Includes fast paths for `update_traces`, `for_each_trace`, `select_traces`, `update_annotations`, `update_shapes`, and other `update_*` methods [[#5514](https://github.com/plotly/plotly.py/issues/5514)].
99
Benchmarks show significant speedups:
1010
- Trace creation: ~26x faster
1111
- Figure creation: ~52x faster

plotly/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
colors,
4444
io,
4545
data,
46+
config,
4647
)
4748
from plotly.version import __version__
4849

@@ -54,6 +55,7 @@
5455
"colors",
5556
"io",
5657
"data",
58+
"config",
5759
"__version__",
5860
]
5961

@@ -73,6 +75,7 @@
7375
".colors",
7476
".io",
7577
".data",
78+
".config",
7679
],
7780
[".version.__version__"],
7881
)

plotly/basedatatypes.py

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
389438
def _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

plotly/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Global configuration for plotly.
3+
4+
Attributes
5+
----------
6+
raw : bool
7+
If True, all future Figure and Trace constructors will default to raw=True.
8+
This enables raw mode globally, skipping validation for performance.
9+
Default is False.
10+
"""
11+
12+
raw = False

0 commit comments

Comments
 (0)