diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cda725b32..d6153b9d6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Updated +- The JSON serialization of plotly figures had been accelerated by handling + differently figures with and without NaN and Inf values ([#2880](https://github.com/plotly/plotly.py/pull/2880)). + +### Updated + - Updated Plotly.js to version 1.55.2. See the [plotly.js CHANGELOG](https://github.com/plotly/plotly.js/blob/v1.55.2/CHANGELOG.md) for more information. These changes are reflected in the auto-generated `plotly.graph_objects` module. - `px.imshow` has a new `binary_string` boolean argument, which passes the image data as a b64 binary string when True. Using binary strings allow for diff --git a/packages/python/plotly/_plotly_utils/utils.py b/packages/python/plotly/_plotly_utils/utils.py index cbf8d3a6b98..16b74b098f7 100644 --- a/packages/python/plotly/_plotly_utils/utils.py +++ b/packages/python/plotly/_plotly_utils/utils.py @@ -40,10 +40,14 @@ def encode(self, o): Note that setting invalid separators will cause a failure at this step. """ - # this will raise errors in a normal-expected way encoded_o = super(PlotlyJSONEncoder, self).encode(o) - + # Brute force guessing whether NaN or Infinity values are in the string + # We catch false positive cases (e.g. strings such as titles, labels etc.) + # but this is ok since the intention is to skip the decoding / reencoding + # step when it's completely safe + if not ("NaN" in encoded_o or "Infinity" in encoded_o): + return encoded_o # now: # 1. `loads` to switch Infinity, -Infinity, NaN to None # 2. `dumps` again so you get 'null' instead of extended JSON diff --git a/packages/python/plotly/plotly/tests/test_core/test_utils/test_utils.py b/packages/python/plotly/plotly/tests/test_core/test_utils/test_utils.py index a3732d85256..6122f27e7ee 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_utils/test_utils.py +++ b/packages/python/plotly/plotly/tests/test_core/test_utils/test_utils.py @@ -1,11 +1,13 @@ from __future__ import absolute_import -from inspect import getargspec from unittest import TestCase import json as _json from plotly.utils import PlotlyJSONEncoder, get_by_path, node_generator +from time import time +import numpy as np +import plotly.graph_objects as go class TestJSONEncoder(TestCase): @@ -19,6 +21,38 @@ def test_invalid_encode_exception(self): with self.assertRaises(TypeError): _json.dumps({"a": {1}}, cls=PlotlyJSONEncoder) + def test_fast_track_finite_arrays(self): + # if NaN or Infinity is found in the json dump + # of a figure, it is decoded and re-encoded to replace these values + # with null. This test checks that NaN and Infinity values are + # indeed converted to null, and that the encoding of figures + # without inf or nan is faster (because we can avoid decoding + # and reencoding). + z = np.random.randn(100, 100) + x = np.arange(100.0) + fig_1 = go.Figure(go.Heatmap(z=z, x=x)) + t1 = time() + json_str_1 = _json.dumps(fig_1, cls=PlotlyJSONEncoder) + t2 = time() + x[0] = np.nan + x[1] = np.inf + fig_2 = go.Figure(go.Heatmap(z=z, x=x)) + t3 = time() + json_str_2 = _json.dumps(fig_2, cls=PlotlyJSONEncoder) + t4 = time() + assert t2 - t1 < t4 - t3 + assert "null" in json_str_2 + assert "NaN" not in json_str_2 + assert "Infinity" not in json_str_2 + x = np.arange(100.0) + fig_3 = go.Figure(go.Heatmap(z=z, x=x)) + fig_3.update_layout(title_text="Infinity") + t5 = time() + json_str_3 = _json.dumps(fig_3, cls=PlotlyJSONEncoder) + t6 = time() + assert t2 - t1 < t6 - t5 + assert "Infinity" in json_str_3 + class TestGetByPath(TestCase): def test_get_by_path(self):