Skip to content

Commit f670752

Browse files
authored
Merge pull request #5766 from Irahan2/fix-hierarchy-group-order
Make px.sunburst/treemap/icicle sector order deterministic for Polars input
2 parents 05e3618 + c166412 commit f670752

3 files changed

Lines changed: 41 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66

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!
9+
- 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!
910

1011

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

‎plotly/express/_core.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,7 +1958,12 @@ def process_dataframe_hierarchy(args):
19581958
_check_dataframe_all_leaves(df[path[::-1]])
19591959
discrete_color = not _is_continuous(df, args["color"]) if args["color"] else False
19601960

1961-
df = df.lazy()
1961+
# Keep track of the original row order, so that the sectors can be sorted by
1962+
# first appearance after each group_by (Polars' group_by does not keep order).
1963+
# TODO: drop this column and use group_by(maintain_order=True) once narwhals
1964+
# exposes it, see #5769 and narwhals-dev/narwhals#3309.
1965+
row_index_colname = _generate_temporary_column_name(n_bytes=16, columns=df.columns)
1966+
df = df.with_row_index(row_index_colname).lazy()
19621967

19631968
new_path = [col_name + "_path_copy" for col_name in path]
19641969
df = df.with_columns(
@@ -1997,6 +2002,7 @@ def process_dataframe_hierarchy(args):
19972002
# Since count_colname is always in agg_f, it can be used later to normalize color
19982003
# in the continuous case after some gymnastic
19992004
agg_f[count_colname] = nw.sum(count_colname)
2005+
agg_f[row_index_colname] = nw.min(row_index_colname)
20002006

20012007
discrete_aggs = []
20022008
continuous_aggs = []
@@ -2049,7 +2055,7 @@ def process_dataframe_hierarchy(args):
20492055
agg_f[args["color"]] = nw.sum(args["color"])
20502056

20512057
# Other columns (for color, hover_data, custom_data etc.)
2052-
cols = list(set(df.collect_schema().names()).difference(path))
2058+
cols = list(set(df.collect_schema().names()).difference([*path, row_index_colname]))
20532059
df = df.with_columns(nw.col(c).cast(nw.String()) for c in cols if c not in agg_f)
20542060

20552061
for col in cols: # for hover_data, custom_data etc.
@@ -2092,6 +2098,7 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra
20922098
dfg = (
20932099
df.group_by(path[i:], drop_null_keys=True)
20942100
.agg(**agg_f)
2101+
.sort(row_index_colname)
20952102
.pipe(post_agg, continuous_aggs, discrete_aggs)
20962103
)
20972104

‎tests/test_optional/test_px/test_px_functions.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,37 @@ def test_sunburst_treemap_with_path(constructor):
233233
assert fig.data[0].values[-1] == 8
234234

235235

236+
@pytest.mark.parametrize("px_fn", [px.sunburst, px.treemap, px.icicle])
237+
def test_sunburst_treemap_with_path_order(constructor, px_fn):
238+
if _pandas_version_at_least("3.0.0") and constructor == pandas_pyarrow_constructor:
239+
pytest.skip(
240+
"known issue with pandas 3 + pandas_pyarrow_constructor + px hierarchy charts (https://github.com/plotly/plotly.py/issues/5571)"
241+
)
242+
243+
# Sectors should follow the order of first appearance in the data, whatever
244+
# the dataframe backend (Polars' group_by does not keep the row order).
245+
df = constructor(
246+
dict(
247+
regions=["South", "North", "South", "West", "North", "West"],
248+
sectors=["Tech", "Finance", "Finance", "Tech", "Tech", "Finance"],
249+
values=[1, 2, 3, 4, 5, 6],
250+
)
251+
)
252+
fig = px_fn(df, path=["regions", "sectors"], values="values")
253+
assert list(fig.data[0].ids) == [
254+
"South/Tech",
255+
"North/Finance",
256+
"South/Finance",
257+
"West/Tech",
258+
"North/Tech",
259+
"West/Finance",
260+
"South",
261+
"North",
262+
"West",
263+
]
264+
assert list(fig.data[0].values) == [1, 2, 3, 4, 5, 6, 4, 7, 10]
265+
266+
236267
def test_sunburst_treemap_with_path_and_hover(backend):
237268
df = px.data.tips(return_type=backend)
238269
fig = px.sunburst(

0 commit comments

Comments
 (0)