Repository navigation
Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to issue
Reported in plotly/dash#4065. The Dash side is plotly/dash#4067.
Description of change
Faster JSON for figures, on every engine:
to_json_plotlyandPlotlyJSONEncoderno longer deep copy figures.BaseFigure._to_json_dict()andBasePlotlyType._to_json_dict()return the object's data without the deepcopy thatto_dict()/to_plotly_json()do. Typed arrays are converted with a newto_base64_copy, which copies only the containers it changes, so the figure is never mutated. Subclasses that overrideto_dictorto_plotly_jsonstill go through their override.default=toorjson.dumps, so one non-native value (ago.Figure, apd.Timestamp) no longer sends the whole payload through the Python walk. Anything that still fails takes the old path, so errors are unchanged.PlotlyJSONEncoder.defaultuses_to_json_dictand returnsisoformat()for plaindatetime/date. Subclasses keep the old method order, so theirencode_as_*overrides still apply."msgspec"engine."auto"now picks orjson, then msgspec, then json. Dash installs msgspec, so Dash users without orjson get most of the orjson speedup. Values msgspec can't take natively go through anenc_hook, and anything it fails on goes to the json engine, so errors match the json engine.Demo
10 traces x 20k points,
to_json_plotly(go.Figure), median ms:A dict holding a
go.Figureand apd.Timestampgives the same numbers as the figure alone after the change. Before the change, orjson took 61 ms for list of float and 304 ms for list of datetime on that payload.Output
The orjson and json engines give byte-identical output to
mainon px figures, frames, templates, geojson, tuples, numpy/pandas/datetime/Decimal/NaN values, and mixed dicts, pretty and compact. Known differences, with orjson only:datetime64array sitting next to a value orjson can't serialize now uses orjson's form (2024-01-01T05:00:00) instead ofdatetime_as_string(2024-01-01T05:00:00.000000000). That is the same output you already get when the array is on its own.TypeError: Type is not JSON serializable: NaTTypenow serialize, as they already did with the json engine.I compared the msgspec engine with the json engine on about 250 values, each nested, top level and in a list, pretty and compact. Every figure, graph object, numpy, pandas and PIL case matches. The differences:
\uescaped. It decodes to the same JSON, as with orjson.datetime/timevalues are written asZinstead of+00:00. Offsets with a seconds part are truncated to minutes. plotly.js acceptsZand ignores the offset.1E+400) is written as is, where json writesnull.True/Falsedict keys are written as"1"/"0", and NaN/inf float keys as"nan"/"inf".np.timedelta64, UUID, Enum, dataclasses and date/UUID/numpy-int dict keys serialize where json raised. orjson already accepts several of these.msgspec writes non-finite Decimals and Decimal dict keys as invalid JSON. The engine checks the output's syntax (decoding to
msgspec.Raw, about 1 ms per 8 MB) and falls back to the json engine when it is invalid.from_json_plotly(engine="msgspec")falls back tojson.loadsfor the NaN and Infinity literals msgspec rejects.Testing strategy
test_figure_is_not_deep_copied(all engines) fails serialization ifdeepcopyis called, checks the output againstto_dict(), and checks that the figure's numpy arrays are left as they were.test_to_base64_copy_leaves_input_untouchedcovers the copy-on-write helper.test_subclass_overrides_are_keptandtest_encoder_subclass_overrides_are_keptcover subclasses that overrideto_dict/to_plotly_jsonand the encoder'sencode_as_*methods.msgspecis added to the engines intest_to_from_plotly_json.py, so every existing exact-output test runs on it.autopicking msgspec without orjson,default_engine = "msgspec", datetime Series with NaT, non-finite Decimals, circular values, Decimal keys, and reading NaN literals.dev_optional, with the matchinguv.lockentries (0.18.6 on Python 3.8, 0.20 on 3.9, 0.22 on 3.10+). The JSON tests pass with both 0.18.6 and 0.22.tests/test_io,tests/test_core,tests/test_plotly_utilsandtests/test_optionalpass locally, apart from failures that also happen onmainhere (test_trendline_on_timeseries[pyarrow-*]and a colorscale test that depends on test order).Guidelines