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 @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed
- 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!
- 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!


## [7.1.0] - 2026-09-15
Expand Down
9 changes: 7 additions & 2 deletions plotly/express/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1958,7 +1958,10 @@ def process_dataframe_hierarchy(args):
_check_dataframe_all_leaves(df[path[::-1]])
discrete_color = not _is_continuous(df, args["color"]) if args["color"] else False

df = df.lazy()
# Keep track of the original row order, so that the sectors can be sorted by
# first appearance after each group_by (Polars' group_by does not keep order).
row_index_colname = _generate_temporary_column_name(n_bytes=16, columns=df.columns)
df = df.with_row_index(row_index_colname).lazy()

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

discrete_aggs = []
continuous_aggs = []
Expand Down Expand Up @@ -2049,7 +2053,7 @@ def process_dataframe_hierarchy(args):
agg_f[args["color"]] = nw.sum(args["color"])

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

for col in cols: # for hover_data, custom_data etc.
Expand Down Expand Up @@ -2092,6 +2096,7 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra
dfg = (
df.group_by(path[i:], drop_null_keys=True)
.agg(**agg_f)
.sort(row_index_colname)
.pipe(post_agg, continuous_aggs, discrete_aggs)
)

Expand Down
26 changes: 26 additions & 0 deletions tests/test_optional/test_px/test_px_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,32 @@ def test_sunburst_treemap_with_path(constructor):
assert fig.data[0].values[-1] == 8


@pytest.mark.parametrize("px_fn", [px.sunburst, px.treemap, px.icicle])
def test_sunburst_treemap_with_path_order(constructor, px_fn):
# Sectors should follow the order of first appearance in the data, whatever
# the dataframe backend (Polars' group_by does not keep the row order).
df = constructor(
dict(
regions=["South", "North", "South", "West", "North", "West"],
sectors=["Tech", "Finance", "Finance", "Tech", "Tech", "Finance"],
values=[1, 2, 3, 4, 5, 6],
)
)
fig = px_fn(df, path=["regions", "sectors"], values="values")
assert list(fig.data[0].ids) == [
"South/Tech",
"North/Finance",
"South/Finance",
"West/Tech",
"North/Tech",
"West/Finance",
"South",
"North",
"West",
]
assert list(fig.data[0].values) == [1, 2, 3, 4, 5, 6, 4, 7, 10]


def test_sunburst_treemap_with_path_and_hover(backend):
df = px.data.tips(return_type=backend)
fig = px.sunburst(
Expand Down