Skip to content

Serialize figures without deep copying them - #5813

Open
T4rk1n wants to merge 2 commits into
mainfrom
perf/json-skip-deepcopy
Open

T4rk1n wants to merge 2 commits into
mainfrom
perf/json-skip-deepcopy

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Oct 14, 2026

Copy link
Copy Markdown

Link to issue

Reported in plotly/dash#4065. The Dash side is plotly/dash#4067.

Description of change

to_json_plotly and PlotlyJSONEncoder no longer deep copy figures, and with orjson only the values orjson can't serialize go through clean_to_json_compatible.

  • BaseFigure._to_json_dict() and BasePlotlyType._to_json_dict() return the object's data without the deepcopy that to_dict() / to_plotly_json() do. Typed arrays are converted with a new to_base64_copy, which copies only the containers it changes, so the figure is never mutated. Subclasses that override to_dict or to_plotly_json still go through their override.
  • The orjson path passes default= to orjson.dumps, so one non-native value (a go.Figure, a pd.Timestamp) no longer sends the whole payload through the Python walk. Anything that still fails takes the old path, so errors are unchanged.
  • PlotlyJSONEncoder.default uses _to_json_dict and returns isoformat() for plain datetime / date. Subclasses keep the old method order, so their encode_as_* overrides still apply.

Demo

10 traces x 20k points, to_json_plotly median ms, before -> after:

data value orjson json
numpy array go.Figure 5.3 -> 5.0 13.2 -> 12.7
list of float go.Figure 36 -> 5.8 82 -> 53
list of float {"f": go.Figure, "t": pd.Timestamp} 61 -> 5.9 82 -> 53
pd.DatetimeIndex {"f": go.Figure, "t": pd.Timestamp} 41 -> 11 74 -> 74
list of datetime go.Figure 179 -> 10.5 496 -> 125
list of datetime {"f": go.Figure, "t": pd.Timestamp} 304 -> 10 490 -> 126

Output is byte-identical to main on both engines, pretty and compact, for px figures, frames, templates, geojson, tuples, numpy/pandas/datetime/Decimal/NaN values, and mixed dicts. Known differences, both with orjson:

  • A raw numpy datetime64 array sitting next to a value orjson can't serialize now uses orjson's form (2024-01-01T05:00:00) instead of datetime_as_string (2024-01-01T05:00:00.000000000). That is the same output you already get when the array is on its own.
  • Some payloads that used to raise TypeError: Type is not JSON serializable: NaTType now serialize, as they already did with the json engine.

Testing strategy

  • test_figure_is_not_deep_copied (all engines) fails serialization if deepcopy is called, checks the output against to_dict(), and checks that the figure's numpy arrays are left as they were.
  • test_to_base64_copy_leaves_input_untouched covers the copy-on-write helper.
  • test_subclass_overrides_are_kept and test_encoder_subclass_overrides_are_kept cover subclasses that override to_dict / to_plotly_json and the encoder's encode_as_* methods.

tests/test_io, tests/test_core, tests/test_plotly_utils and tests/test_optional pass locally, apart from failures that also happen on main here (test_trendline_on_timeseries[pyarrow-*] and a colorscale test that depends on test order).

Guidelines

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants