Skip to content

Commit 16ab376

Browse files
committed
Scope default category order to x-axis only
Address review on #5776: y categoricals stay untouched so the autorange=reversed pattern from doc/python/gantt.md keeps listing tasks top-down instead of double-reversing. Adds guard test that px.timeline and horizontal px.bar with color emit no y categoryarray, while x data-order fix for #3198/#3291 is kept.
1 parent 06102aa commit 16ab376

3 files changed

Lines changed: 41 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1717
- Fix Plotly Express mutating lists passed to the `x` or `y` arguments in wide mode [[#5727](https://github.com/plotly/plotly.py/pull/5727)], with thanks to @cpruijsen for the contribution!
1818
- 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!
1919
- 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!
20-
- 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)]
20+
- Fix Plotly Express scrambling the order of the categorical x-axis when the data is split into several traces (e.g. by `color`); the x-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)]
21+
22+
## [7.1.0] - 2026-09-15
2123

2224
## [7.1.0] - 2026-09-15
2325

‎plotly/express/_core.py‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,31 +2532,36 @@ def get_groups_and_orders(args, grouper):
25322532

25332533
def _default_category_orders_for_axes(args, groups, orders):
25342534
"""
2535-
Add data-appearance orders for categorical x/y columns.
2535+
Add data-appearance order for the categorical x column.
25362536
25372537
When the data is split into several traces (e.g. by `color`), plotly.js
25382538
orders categories by first appearance across traces, which can differ from
25392539
the order in the underlying data. Recording the data-appearance order here
25402540
lets `set_cartesian_axis_opts` emit an explicit `categoryarray`, matching
25412541
the documented behavior that Plotly Express lays out categorical data in
25422542
the order in which it appears in the data. See #3198.
2543+
2544+
Only the x axis is handled: y categoricals are intentionally left alone
2545+
because `set_cartesian_axis_opts` reverses y orders for top-down display
2546+
and figures such as `px.timeline` / horizontal `px.bar` pair that with
2547+
`autorange="reversed"` (see `doc/python/gantt.md`), so an explicit y
2548+
`categoryarray` would double-reverse and flip the documented order.
25432549
"""
25442550
if len(groups) <= 1:
25452551
return orders
25462552
df = args["data_frame"]
2547-
for letter in ("x", "y"):
2548-
col = args.get(letter)
2549-
if not isinstance(col, str) or col in orders or col not in df.columns:
2550-
continue
2551-
uniques = df.get_column(col).unique(maintain_order=True).to_list()
2552-
categories = [
2553-
value
2554-
for value in uniques
2555-
if value is not None
2556-
and not (isinstance(value, float) and math.isnan(value))
2557-
]
2558-
if categories and all(isinstance(value, str) for value in categories):
2559-
orders[col] = categories
2553+
col = args.get("x")
2554+
if not isinstance(col, str) or col in orders or col not in df.columns:
2555+
return orders
2556+
uniques = df.get_column(col).unique(maintain_order=True).to_list()
2557+
categories = [
2558+
value
2559+
for value in uniques
2560+
if value is not None
2561+
and not (isinstance(value, float) and math.isnan(value))
2562+
]
2563+
if categories and all(isinstance(value, str) for value in categories):
2564+
orders[col] = categories
25602565
return orders
25612566

25622567

‎tests/test_optional/test_px/test_px.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,25 @@ def test_no_default_category_array_for_single_trace_or_numeric():
393393
assert fig["layout"]["xaxis"]["categoryorder"] is None
394394

395395

396+
def test_no_default_category_array_for_y_with_color():
397+
# https://github.com/plotly/plotly.py/pull/5776#pullrequestreview-5478271667
398+
# y categoricals must stay untouched so the `autorange="reversed"`
399+
# pattern from doc/python/gantt.md keeps listing tasks top-down.
400+
df = pd.DataFrame(
401+
[
402+
dict(Task="Job A", Start="2009-01-01", Finish="2009-02-28", Resource="Alex"),
403+
dict(Task="Job B", Start="2009-03-05", Finish="2009-04-15", Resource="Alex"),
404+
dict(Task="Job C", Start="2009-02-20", Finish="2009-05-30", Resource="Max"),
405+
]
406+
)
407+
fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Resource")
408+
assert fig["layout"]["yaxis"]["categoryorder"] is None
409+
assert fig["layout"]["yaxis"]["categoryarray"] is None
410+
fig = px.bar(x=[1, 2, 3, 4], y=list("abcd"), color=list("ppqq"), orientation="h")
411+
assert fig["layout"]["yaxis"]["categoryorder"] is None
412+
assert fig["layout"]["yaxis"]["categoryarray"] is None
413+
414+
396415
def test_permissive_defaults():
397416
msg = "'PxDefaults' object has no attribute 'should_not_work'"
398417
with pytest.raises(AttributeError, match=msg):

0 commit comments

Comments
 (0)