2026, Jan 11 19:00
How to Make Plotly Use the Browser Timezone: Feed Epoch Milliseconds and Set x-axis Type to Date
Learn how to make Plotly render Pandas time series in the browser's timezone: send JavaScript epoch milliseconds and set x-axis type='date' to avoid UTC issues
Plotly and timezones often collide in surprising ways. A common case: you have a Pandas DataFrame indexed by datetimes built from UNIX timestamps, and you want the chart to render dates in the viewer's browser timezone, not in UTC or a hardcoded zone. The good news is that this is achievable without server-side timezone logic—if you feed Plotly the right type of x values and tell the axis what they are.
Reproducing the issue
The following example shows four typical approaches. The first uses plain numeric UNIX seconds, the next two use a Pandas DateTimeIndex (naive and localized to UTC), and the last converts to a specific timezone. The behavior mirrors what many teams observe in dashboards and notebooks.
import calendar
import time
import numpy as np
import pandas as pd
import plotly.graph_objects as go
def iso_to_epoch(ts: str) -> float:
return calendar.timegm(time.strptime(ts, '%Y-%m-%dT%H:%M:%SZ'))
cols = ['ts', 'val']
arr = np.array([
[iso_to_epoch('2024-01-01T00:00:00Z'), 0.0],
[iso_to_epoch('2024-01-01T01:00:00Z'), 1.0],
[iso_to_epoch('2024-01-01T02:00:00Z'), 2.0],
[iso_to_epoch('2024-01-01T03:00:00Z'), 3.0],
[iso_to_epoch('2024-01-01T04:00:00Z'), 4.0],
[iso_to_epoch('2024-01-01T05:00:00Z'), 5.0],
])
frame = pd.DataFrame(arr, columns=cols, index=pd.to_datetime(arr[:, 0], unit='s'), dtype=float)
fig1 = go.Figure()
fig1.add_trace(go.Scatter(
x=frame['ts'],
y=frame['val'],
))
fig1.update_layout(
title='Raw UNIX seconds as x',
margin=dict(l=10, r=10, t=40, b=10),
)
fig1.show(renderer='browser')
fig2 = go.Figure()
fig2.add_trace(go.Scatter(
x=frame.index,
y=frame['val'],
))
fig2.update_layout(
title='Naive Pandas DateTimeIndex',
margin=dict(l=10, r=10, t=40, b=10),
)
fig2.show(renderer='browser')
fig3 = go.Figure()
fig3.add_trace(go.Scatter(
x=frame.index.tz_localize('UTC'),
y=frame['val'],
))
fig3.update_layout(
title='UTC-localized DateTimeIndex',
margin=dict(l=10, r=10, t=40, b=10),
)
fig3.show(renderer='browser')
fig4 = go.Figure()
fig4.add_trace(go.Scatter(
x=frame.index.tz_localize('UTC').tz_convert('US/Pacific'),
y=frame['val'],
))
fig4.update_layout(
title='DateTimeIndex converted to a specific timezone',
margin=dict(l=10, r=10, t=40, b=10),
)
fig4.show(renderer='browser')Why the timestamps don’t follow the browser
When you pass plain floats as x, Plotly treats them as numbers unless instructed otherwise, so it renders a numeric axis. When you pass Pandas datetimes, Plotly can display proper dates, but timezone decisions are already baked in by the time the values hit the chart. Localizing to UTC gives UTC on the axis, and converting to a named timezone fixes the display to that zone regardless of the browser settings.
There are open discussions in Plotly around timezone handling, so relying on implicit conversions often leads to surprises. A robust approach is to hand Plotly exactly what the browser understands natively and clearly tell the axis to treat x as dates.
The fix: JavaScript timestamps and a date axis
JavaScript uses milliseconds since the UNIX epoch. If you send those milliseconds and set the x-axis type to 'date', Plotly will render using the browser's timezone automatically. This avoids Pandas timezone juggling and aligns the chart with how the viewer’s environment represents time.
import calendar
import time
import numpy as np
import plotly.graph_objects as go
def to_unix(s: str) -> float:
return calendar.timegm(time.strptime(s, '%Y-%m-%dT%H:%M:%SZ'))
labels = ['epoch', 'metric']
dataset = np.array([
[to_unix('2024-01-01T00:00:00Z'), 0.0],
[to_unix('2024-01-01T01:00:00Z'), 1.0],
[to_unix('2024-01-01T02:00:00Z'), 2.0],
[to_unix('2024-01-01T03:00:00Z'), 3.0],
[to_unix('2024-01-01T04:00:00Z'), 4.0],
[to_unix('2024-01-01T05:00:00Z'), 5.0],
])
chart = go.Figure()
chart.add_trace(go.Scatter(
x=dataset[:, 0] * 1000, # ms for JavaScript
y=dataset[:, 1],
))
chart.update_layout(
title='Browser-local time via JavaScript epoch ms',
margin=dict(l=10, r=10, t=40, b=10),
xaxis=dict(type='date'), # treat x as dates
)
chart.show(renderer='browser')A practical aside
You can parse ISO strings like '2024-01-01T00:00:00Z' with the datetime module and call .timestamp() to get UNIX seconds.
Why this matters
Charts that reflect the viewer’s local time reduce cognitive load and support distributed teams. They also minimize server-side complexity: no need to detect, store or convert timezones per request. By standardizing on epoch milliseconds and letting the browser render the local representation, you get predictable, efficient behavior.
Takeaways
If your goal is to display time in the browser’s timezone, avoid pushing pre-localized datetimes from Pandas to Plotly. Instead, pass epoch milliseconds and explicitly set the x-axis type to 'date'. This keeps time handling simple, sidesteps timezone pitfalls, and makes your Plotly visuals align with the user’s environment. If you still need server-side timezone views, Pandas operations like tz_localize and tz_convert remain useful—but for browser-local rendering, JavaScript timestamps plus a date axis is the clean path.