Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion _plotly_utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions tests/test_plotly_utils/test_typed_array_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
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"), exactly as every real caller does. An array built with
an explicit non-native byte order (e.g. ">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="<f8")

np.testing.assert_array_equal(decoded_as_browser_would_read_it, v)