Skip to content

Commit 85cb798

Browse files
committed
Fix px scrambling categorical axis order when data is split into traces
1 parent d586d22 commit 85cb798

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77
### Fixed
88
- Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#3441](https://github.com/plotly/plotly.py/issues/3441)], with thanks to @hb1915 for the contribution!
99
- 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 [[#5765](https://github.com/plotly/plotly.py/issues/5765)], with thanks to @Irahan2 for the contribution!
10+
- Fix Plotly Express scrambling the order of categorical axes when the data is split into several traces (e.g. by `color`); the axis now follows the order of first appearance in the underlying data [[#3198](https://github.com/plotly/plotly.py/issues/3198), [#3291](https://github.com/plotly/plotly.py/issues/3291)]
1011

1112

1213
## [7.1.0] - 2026-09-15

‎plotly/express/_core.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,6 +2529,36 @@ def get_groups_and_orders(args, grouper):
25292529
return groups, orders
25302530

25312531

2532+
def _default_category_orders_for_axes(args, groups, orders):
2533+
"""
2534+
Add data-appearance orders for categorical x/y columns.
2535+
2536+
When the data is split into several traces (e.g. by `color`), plotly.js
2537+
orders categories by first appearance across traces, which can differ from
2538+
the order in the underlying data. Recording the data-appearance order here
2539+
lets `set_cartesian_axis_opts` emit an explicit `categoryarray`, matching
2540+
the documented behavior that Plotly Express lays out categorical data in
2541+
the order in which it appears in the data. See #3198.
2542+
"""
2543+
if len(groups) <= 1:
2544+
return orders
2545+
df = args["data_frame"]
2546+
for letter in ("x", "y"):
2547+
col = args.get(letter)
2548+
if not isinstance(col, str) or col in orders or col not in df.columns:
2549+
continue
2550+
uniques = df.get_column(col).unique(maintain_order=True).to_list()
2551+
categories = [
2552+
value
2553+
for value in uniques
2554+
if value is not None
2555+
and not (isinstance(value, float) and math.isnan(value))
2556+
]
2557+
if categories and all(isinstance(value, str) for value in categories):
2558+
orders[col] = categories
2559+
return orders
2560+
2561+
25322562
def make_figure(args, constructor, trace_patch=None, layout_patch=None):
25332563
trace_patch = trace_patch or {}
25342564
layout_patch = layout_patch or {}
@@ -2555,6 +2585,7 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None):
25552585
)
25562586
grouper = [x.grouper or one_group for x in grouped_mappings] or [one_group]
25572587
groups, orders = get_groups_and_orders(args, grouper)
2588+
orders = _default_category_orders_for_axes(args, groups, orders)
25582589

25592590
col_labels = []
25602591
row_labels = []

‎tests/test_optional/test_px/test_px.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,38 @@ def test_category_order_with_category_as_x(backend):
361361
assert set(trace["x"]) == {"Thur", "Fri", "Sat", "Sun"}
362362

363363

364+
def test_category_order_defaults_to_data_order_with_color():
365+
# https://github.com/plotly/plotly.py/issues/3198
366+
# Splitting into one trace per color must not change the axis order:
367+
# it should follow first appearance in the underlying data.
368+
fig = px.line(
369+
x=["A", "B", None, "B", "C", None, "C", "D", None, "D", "E", None],
370+
y=[1, 1, None, 2, 2, None, 3, 3, None, 4, 4, None],
371+
color=[1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1],
372+
)
373+
assert fig["layout"]["xaxis"]["categoryorder"] == "array"
374+
assert fig["layout"]["xaxis"]["categoryarray"] == ("A", "B", "C", "D", "E")
375+
376+
377+
def test_category_order_defaults_to_data_order_with_color_bar():
378+
# https://github.com/plotly/plotly.py/issues/3291
379+
df = pd.DataFrame(
380+
{"x": ["a", "b", "c", "d"], "y": [1, 2, 3, 4], "z": ["p", "q", "p", "q"]}
381+
)
382+
fig = px.bar(df, x="x", y="y", color="z")
383+
assert fig["layout"]["xaxis"]["categoryorder"] == "array"
384+
assert fig["layout"]["xaxis"]["categoryarray"] == ("a", "b", "c", "d")
385+
386+
387+
def test_no_default_category_array_for_single_trace_or_numeric():
388+
# A single trace already follows data order, and numeric axes
389+
# must never get a category array.
390+
fig = px.line(x=["A", "B", "C"], y=[1, 2, 3])
391+
assert fig["layout"]["xaxis"]["categoryorder"] is None
392+
fig = px.line(x=[1, 2, 3], y=[1, 2, 3], color=["a", "b", "a"])
393+
assert fig["layout"]["xaxis"]["categoryorder"] is None
394+
395+
364396
def test_permissive_defaults():
365397
msg = "'PxDefaults' object has no attribute 'should_not_work'"
366398
with pytest.raises(AttributeError, match=msg):

0 commit comments

Comments
 (0)