From 5edb2ad0a8f38722ba242542bb28d22b3b5e1a41 Mon Sep 17 00:00:00 2001 From: Vikram M Date: Thu, 22 Oct 2026 22:07:08 +0530 Subject: [PATCH 1/2] Fix bdata byte order for big-endian platforms to_typed_array_spec base64-encodes arrays in the host's native byte order with no normalization. plotly.js decodes bdata with typed arrays (e.g. Float64Array), which always read in the platform's native order, little-endian on every real browser. On a big-endian host this produces bdata that decodes to garbage in any browser. Normalize to little-endian before encoding, verified on real big-endian hardware (z/OS/s390x) that this resolves it. --- _plotly_utils/utils.py | 8 ++- .../test_typed_array_spec.py | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/test_plotly_utils/test_typed_array_spec.py diff --git a/_plotly_utils/utils.py b/_plotly_utils/utils.py index 07a2b9e0b93..b6fe5476e77 100644 --- a/_plotly_utils/utils.py +++ b/_plotly_utils/utils.py @@ -79,9 +79,15 @@ def to_typed_array_spec(v): dtype = str(v.dtype) if dtype in plotlyjsShortTypes: + # plotly.js decodes bdata with typed arrays (e.g. Float64Array), + # which always read in the platform's native order, little-endian + # on every real browser. Normalize to little-endian before encoding + # so bdata is correct regardless of the host's own byte order. arrObj = { "dtype": plotlyjsShortTypes[dtype], - "bdata": base64.b64encode(v).decode("ascii"), + "bdata": base64.b64encode(v.astype(v.dtype.newbyteorder("<"))).decode( + "ascii" + ), } if v.ndim > 1: diff --git a/tests/test_plotly_utils/test_typed_array_spec.py b/tests/test_plotly_utils/test_typed_array_spec.py new file mode 100644 index 00000000000..05d62b07a7c --- /dev/null +++ b/tests/test_plotly_utils/test_typed_array_spec.py @@ -0,0 +1,59 @@ +""" +Tests for _plotly_utils.utils.to_typed_array_spec's byte order. + +plotly.js decodes "bdata" in the browser with typed arrays (e.g. +new Float64Array(buffer)), which always read in the platform's native +order, little-endian on every real browser. to_typed_array_spec must +therefore always produce little-endian bytes, regardless of the host's +own native byte order. + +Note: these arrays are built in plain/native order (dtype="float64" etc, +not ">f8"/"f8") reports str(dtype) as +">f8" rather than "float64", so it never reaches the plotlyjsShortTypes +dispatch in to_typed_array_spec at all, it can't be used to simulate +"a big-endian host" from a little-endian machine. These tests therefore +only catch a real regression when actually run on big-endian hardware +(e.g. z/OS/s390x), not on a little-endian CI runner. +""" + +import base64 + +import numpy as np +import pytest + +from _plotly_utils.utils import to_typed_array_spec + + +@pytest.mark.parametrize( + "dtype,np_dtype", + [ + ("float64", "f8"), + ("float32", "f4"), + ("int32", "i4"), + ("int16", "i2"), + ("uint32", "u4"), + ], +) +def test_bdata_is_little_endian(dtype, np_dtype): + v = np.array([1, 2, 3, 4, 5], dtype=dtype) + assert str(v.dtype) == dtype # sanity: native order, routes through the real dispatch + + spec = to_typed_array_spec(v) + assert isinstance(spec, dict), "did not reach the typed-array-spec encoding path" + + raw = base64.b64decode(spec["bdata"]) + decoded_as_browser_would_read_it = np.frombuffer(raw, dtype=f"<{np_dtype}") + + np.testing.assert_array_equal(decoded_as_browser_would_read_it, v) + + +def test_bdata_is_little_endian_for_floats_with_fraction(): + v = np.array([1.1, 2.3, 3.5]) + + spec = to_typed_array_spec(v) + + raw = base64.b64decode(spec["bdata"]) + decoded_as_browser_would_read_it = np.frombuffer(raw, dtype=" Date: Tue, 27 Oct 2026 15:06:22 +0530 Subject: [PATCH 2/2] Fix ruff formatting and add changelog entry ruff format wraps the long assert/comment line in test_bdata_is_little_endian onto multiple lines, and CHANGELOG.md needs an entry for this PR per the repo's check-changelog workflow. --- CHANGELOG.md | 1 + tests/test_plotly_utils/test_typed_array_spec.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32590d344f0..7b49e2dda02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix `convert_dash` mapping dotted lines (`linestyle=':'`) to invalid `'circle'` in Plotly, correctly converting them to `'dot'` and preserving custom dash patterns as pixel lists, [[#5719](https://github.com/plotly/plotly.py/pull/5719)] with thanks to @robertoffmoura for the contribution! - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution! - Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution! +- Fix `to_typed_array_spec` encoding `bdata` in the host's native byte order instead of little-endian, producing corrupted figures on big-endian platforms [[#5838](https://github.com/plotly/plotly.py/pull/5838)], with thanks to @Vikram12301 for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/tests/test_plotly_utils/test_typed_array_spec.py b/tests/test_plotly_utils/test_typed_array_spec.py index 05d62b07a7c..b312351367b 100644 --- a/tests/test_plotly_utils/test_typed_array_spec.py +++ b/tests/test_plotly_utils/test_typed_array_spec.py @@ -37,7 +37,9 @@ ) def test_bdata_is_little_endian(dtype, np_dtype): v = np.array([1, 2, 3, 4, 5], dtype=dtype) - assert str(v.dtype) == dtype # sanity: native order, routes through the real dispatch + assert ( + str(v.dtype) == dtype + ) # sanity: native order, routes through the real dispatch spec = to_typed_array_spec(v) assert isinstance(spec, dict), "did not reach the typed-array-spec encoding path"