Repository navigation
Description
Description
Plotting a timeseries the usual way, go.Scatter(x=series.index, y=series), is very slow if the index is a
timezone-aware pd.DatetimeIndex. For 100,000 points, go.Figure(...) plus fig.to_json() takes about 2.3 s.
With the same timestamps as a timezone-aware pd.Series, or as a timezone-naive DatetimeIndex, it takes
about 0.05 to 0.08 s.
Timezone-aware indexes are common: data from databases, APIs or pd.to_datetime(..., utc=True) usually comes with
datetime64[ns, UTC] timestamps, and series.index is what most code passes as x.
#4250 reported that a pandas.Index slows down figure creation. It was closed after the switch to narwhals in
plotly 6, based on measurements with a timezone-naive index. The reporter had mentioned timezone-aware
indexes, but that case was not measured again. It is still slow in plotly 6.9.0 and 7.1.0.
Screenshots/Video
Not applicable, this is about performance.
Steps to reproduce
Run this script:
import time
import numpy as np
import pandas as pd
import plotly
import plotly.graph_objects as go
import plotly.io as pio
n = 100_000
index = pd.date_range("2024-01-01", periods=n, freq="min", tz="Europe/Berlin")
y = np.random.default_rng(0).standard_normal(n)
def bench(label, x):
t0 = time.perf_counter()
fig = go.Figure(go.Scatter(x=x, y=y))
t1 = time.perf_counter()
fig.to_json()
t2 = time.perf_counter()
print(f"{label:<24} x dtype: {str(fig.data[0].x.dtype):<15} Figure(): {t1 - t0:6.3f} s to_json(): {t2 - t1:6.3f} s")
print("plotly", plotly.__version__, "| pandas", pd.__version__, "| json engine", pio.json.config.default_engine)
bench("tz-naive DatetimeIndex", index.tz_localize(None))
bench("tz-aware Series", index.to_series())
bench("tz-aware DatetimeIndex", index)Output (Python 3.14, Linux):
plotly 6.9.0 | pandas 3.1.0 | json engine auto
tz-naive DatetimeIndex x dtype: datetime64[us] Figure(): 0.035 s to_json(): 0.042 s
tz-aware Series x dtype: datetime64[us] Figure(): 0.008 s to_json(): 0.039 s
tz-aware DatetimeIndex x dtype: object Figure(): 0.902 s to_json(): 1.363 s
plotly 7.1.0 | pandas 3.1.0 | json engine auto
tz-naive DatetimeIndex x dtype: datetime64[us] Figure(): 0.033 s to_json(): 0.042 s
tz-aware Series x dtype: datetime64[us] Figure(): 0.008 s to_json(): 0.038 s
tz-aware DatetimeIndex x dtype: object Figure(): 0.894 s to_json(): 1.372 s
It is the same with pandas 2.3.3 (0.42 s + 0.72 s for the timezone-aware index), and with orjson installed
(0.91 s + 1.01 s).
Notes
Cause
copy_to_readonly_numpy_array in _plotly_utils/basevalidators.py converts the values with narwhals.
For a timezone-aware datetime Series, it calls replace_time_zone(None) ("so that local time is displayed")
and gets a datetime64 array. A pd.Index is not a narwhals Series, so nw.from_native(..., pass_through=True)
returns it unchanged, and np.array(v) then gives an object array of pd.Timestamp.
That object array is handled one element at a time:
copy.deepcopycopies everyTimestampinBaseFigure.__init__and again into_dict(), whichto_json()calls.PlotlyJSONEncoder.defaultencodes everyTimestamp, going throughencode_as_pandas,encode_as_numpyand
encode_as_datetimefor each one.
Expected behavior
A timezone-aware DatetimeIndex should be as fast as a timezone-aware Series.
The plot would not change. Today the index is serialized as strings with UTC offset, like
"2024-01-01T00:00:00+01:00". plotly.js ignores the offset and shows the wall-clock time, which is exactly what the
Series path produces with replace_time_zone(None). Only the JSON would change, losing the offset suffix.
Possible fix
In copy_to_readonly_numpy_array, handle a timezone-aware pandas DatetimeIndex like the Series path does:
pd = get_module("pandas", should_load=False)
if pd is not None and isinstance(v, pd.DatetimeIndex) and v.tz is not None:
# Remove time zone so that local time is displayed, like for Series
v = v.tz_localize(None)Placed directly before nw.from_native(...), this brings the timezone-aware index in the example above down to
0.002 s for Figure() and 0.038 s for to_json(). Alternatively, convert any pandas Index to a Series before
nw.from_native, so that indexes always take the same path as Series.
Workaround until then: pass x=series.index.tz_localize(None) or x=series.index.to_series().
Related
- Usage of a pandas.Index slows down figure generation of plotly #4250: "Usage of a pandas.Index slows down figure generation of plotly". Closed for plotly 6.0.0rc0, measured with a
timezone-naive index. - Is
v = np.array(v.dt.to_pydatetime())still necessary? #4836 / feat: make plotly-express dataframe agnostic via narwhals #4790: switched the Series path to narwhals anddatetime64. This made Series and naive indexes fast, but
not timezone-aware indexes. - Serialize figures without deep copying them, add a msgspec engine #5813 (open): serializes figures without deep copying them. This would reduce the
to_json()part, but the index
would still be an object array of Timestamps, deep-copied inFigure()and encoded one by one.
Disclaimer
Claude Code helped in finding, analyzing, reproducing and in writting this issue. I personally checked the reproduction code and will answer personally to questions!
Activity
MarcoGorelli commentedon Nov 2, 2026
thanks for the report! yup, object arrays are to be avoided at all costs
it's true that Narwhals doesn't have special handling for Index objects, so your suggested fix looks reasonable. Regarding the comment though, I would suggest something like "# Remove time zone so that local time is displayed, so that converting to numpy doesn't result in object dtype"
KoolADE85 commentedon Nov 2, 2026
Thanks for reporting this. The team will take a look and follow up.