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 @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix `mpl_to_plotly` crashing on figures with fully transparent or alpha-carrying colors, such as boxplot outlier markers (`facecolor="none"`), rgba line colors, and `color="none"` text and axis labels; all matplotlib colors are now exported through a single helper that maps `"none"` to transparent rgba and keeps or overrides alpha as matplotlib does [[#5700](https://github.com/plotly/plotly.py/pull/5700)], with thanks to @robertoffmoura for the contribution!
- Export tick label colors to the plotly tickfont in `mpl_to_plotly` [[#5716](https://github.com/plotly/plotly.py/pull/5716)], 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 `px.timeline` bars ending an hour early or late when timezone-aware dates span a daylight saving time change [[#5796](https://github.com/plotly/plotly.py/pull/5796)], with thanks to @mottopanikeiku for the contribution!

## [7.1.0] - 2026-09-15

Expand Down
13 changes: 10 additions & 3 deletions plotly/express/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2193,11 +2193,18 @@ def process_dataframe_timeline(args):
"Both x_start and x_end must refer to data convertible to datetimes."
) from exc

# Match the local times displayed on date axes when calculating bar lengths.
schema = df.schema
x_start, x_end = [
nw.col(col).dt.replace_time_zone(None)
if schema[col] == nw.Datetime and schema[col].time_zone is not None
else nw.col(col)
for col in (args["x_start"], args["x_end"])
]

# note that we are not adding any columns to the data frame here, so no risk of overwrite
args["data_frame"] = df.with_columns(
(nw.col(args["x_end"]) - nw.col(args["x_start"]))
.dt.total_milliseconds()
.alias(args["x_end"])
(x_end - x_start).dt.total_milliseconds().alias(args["x_end"])
)
args["x"] = args["x_end"]
args["base"] = args["x_start"]
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 @@ -680,6 +680,32 @@ def test_timeline_cols_already_temporal(constructor, datetime_columns):
assert fig.layout.xaxis.title.text is None


@pytest.mark.parametrize("time_zone", [None, "UTC", "US/Pacific"])
def test_timeline_daylight_saving(constructor, time_zone):
# Regression for https://github.com/plotly/plotly.py/issues/4611
starts = ["2024-03-09", "2024-03-10", "2024-03-11", "2023-11-05"]
finishes = ["2024-03-10", "2024-03-11", "2024-03-12", "2023-11-06"]
df = nw.from_native(
constructor({"Start": starts, "Finish": finishes, "Task": ["Job A"] * 4})
).with_columns(
nw.col("Start", "Finish")
.str.to_datetime(format="%Y-%m-%d")
.dt.replace_time_zone(time_zone)
)

fig = px.timeline(df.to_native(), x_start="Start", x_end="Finish", y="Task")

# Date axes use local wall-clock coordinates: consecutive midnights are
# 24 hours apart even when the elapsed time is 23 or 25 hours.
assert_array_equal(fig.data[0].x, [24 * 60 * 60 * 1000] * 4)
base = np.asarray(fig.data[0].base, dtype="datetime64[ms]")
assert_array_equal(base, np.array(starts, dtype="datetime64[ms]"))
assert_array_equal(
base + fig.data[0].x.astype("timedelta64[ms]"),
np.array(finishes, dtype="datetime64[ms]"),
)


def test_empty_histogram():
"""Empty px.histogram() should not raise, matching scatter/bar/pie behavior.

Expand Down