Skip to content

PlotlyJS direct plot() fails with ZonedDateTime x-axis, but add_trace!() with Scatter works #520

Open

Description

@MChristianBF

Describe the bug
Direct plotting with ZonedDateTime as x-values fails, while the same data works when added via add_trace! and Scatter.

using Dates
using TimeZones
using PlotlyJS

# 1) Create vectors: 10 random y-values, DateTime x-vector, and ZonedDateTime x-vector.
n = 10
YVector = rand(n)
TimeVector = collect(now():Hour(1):now() + Hour(n - 1))
TimeVectorZoned = ZonedDateTime.(TimeVector, Ref(localzone()))

# 2) Baseline: DateTime works with direct plot call.
plot(TimeVector, YVector, mode="lines+markers", name="DateTime direct plot")

# 3) Repro: ZonedDateTime direct plot call (known issue).
plot(TimeVectorZoned, YVector, mode="lines+markers", name="ZonedDateTime direct plot")

# 4) Workaround: add_trace! with scatter using the same zoned x-vector works.
p_add_trace = Plot()
add_trace!(p_add_trace, scatter(
	x=TimeVectorZoned,
	y=YVector,
	mode="lines+markers",
	name="ZonedDateTime via add_trace!"
))
display(p_add_trace)

Version info
Julia Version 1.12.5
Commit 5fe89b8ddc (2026-02-09 16:05 UTC)
Build Info:
Official https://julialang.org release
Platform Info:
OS: Windows (x86_64-w64-mingw32)
CPU: 8 × 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
WORD_SIZE: 64
LLVM: libLLVM-18.1.7 (ORCJIT, tigerlake)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 8 virtual cores)
Environment:
JULIA_EDITOR = code
JULIA_VSCODE_REPL = 1

[336ed68f] CSV v0.10.16
[aaaa29a8] Clustering v0.15.8
[a93c6f00] DataFrames v1.8.1
[38e38edf] GLM v1.9.3
[cd3eb016] HTTP v1.11.0
[87dc4568] HiGHS v1.23.0
[033835bb] JLD2 v0.6.4
⌅ [682c06a0] JSON v0.21.4
[4076af6c] JuMP v1.30.0
[c03570c3] Memoize v0.4.4
[f0f68f2c] PlotlyJS v0.18.17
[92933f4c] ProgressMeter v1.11.0
[10745b16] Statistics v1.11.1
[f269a46b] TimeZones v1.22.2
[fdbf4ff8] XLSX v0.11.2
[ade2ca70] Dates v1.11.0
Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use status --outdated

Activity

  1. jd-foster commented on Apr 13, 2026

    @jd-foster
    Collaborator

    Thanks for drawing attention to this case.

    A possibly simpler workaround is

    plot(string.(TimeVectorZoned), YVector, mode="lines+markers", name="ZonedDateTime direct plot")

    where we directly pass for the input labels as String type and let plotly interpret them (this is what is happening lower down in your workaround).
    Perhaps we should document this for similar cases.

  2. MChristianBF commented on Apr 13, 2026

    @MChristianBF
    Author

    Hey, thanks for the respons @jd-foster !
    I'm would not say I'm a professional programmer, so I'm happy to help the ways I can.
    So thanks to the team behind PlotlyJS.jl!

    Good to know that that is a workarround!
    But then one thing came to mind, and I wanted to validate a scenario.

    If I start with UTC ZonedDateTime values, convert them to two different time zones (for example, Europe/Lisbon and Europe/Warsaw), and then plot both series together, I would expect the data points to overlap because they represent the same absolute instants in time.

    At the same time, I can also see another valid interpretation: the chart could display each series using its own local clock representation on the x-axis. So the question seems less about timestamp correctness and more about intended plotting behavior.

    In my use case, data is stored in UTC and then converted to local time for visualization, so end users can read the timeline more easily. Given that, what PlotlyJS is doing now seems reasonable, although I am not 100% sure what behavior I want yet, but thats not my decision.

    So I think the key clarification is:
    Should plotting align by absolute instant (overlapping lines), or preserve local clock-time labels per timezone (which may appear shifted)?

    I can confirm that the plot looks the same when I plot the string values for both locations, and do now "overlap" both Lisabon and Warsaw.
    But if one wanted that then how would one agree on selected timezone that is shown on the x axsis? Then one would need 2 x axsis. And that might be out of scope. But would an awesome feature.

    using Dates
    using TimeZones
    using PlotlyJS
    
    n = 10
    YVector = collect(1:n)
    
    # Start from a UTC base time.
    base_time_utc = ZonedDateTime(DateTime(2026, 4, 13, 0, 0, 0), tz"UTC")
    TimeVectorUTC = [base_time_utc + Hour(i - 1) for i in 1:n]
    
    # Convert to Portugal/Lisbon and Europe/Warsaw.
    TimeVectorLisbon = astimezone.(TimeVectorUTC, Ref(tz"Europe/Lisbon"))
    TimeVectorWarsaw = astimezone.(TimeVectorUTC, Ref(tz"Europe/Warsaw"))
    
    # Convert to strings for direct plotting workaround.
    StringsLisbon = string.(TimeVectorLisbon)
    StringsWarsaw = string.(TimeVectorWarsaw)
    
    trace_lisbon = scatter(
        x = TimeVectorLisbon,
        y = YVector,
        mode = "lines+markers",
        name = "Lisbon TimeVector",
        line = attr(color = "blue")
    )
    
    trace_warsaw = scatter(
        x = TimeVectorWarsaw,
        y = YVector,
        mode = "lines+markers",
        name = "Warsaw TimeVector",
        line = attr(color = "red")
    )
    
    trace_lisbon_string = scatter(
        x = StringsLisbon,
        y = YVector,
        mode = "lines+markers",
        name = "Lisbon string",
        line = attr(color = "cyan", dash = "dash")
    )
    
    trace_warsaw_string = scatter(
        x = StringsWarsaw,
        y = YVector,
        mode = "lines+markers",
        name = "Warsaw string",
        line = attr(color = "pink", dash = "dash")
    )
    
    p = plot(
        [trace_lisbon, trace_warsaw, trace_lisbon_string, trace_warsaw_string],
        Layout(
            title = "Timezone plot test: TimeVector + string (Lisbon vs Warsaw)",
            xaxis_title = "Local time",
            yaxis_title = "YVector"
        )
    )
    
    display(p)
    
  3. jd-foster commented on Apr 13, 2026

    @jd-foster
    Collaborator

    I understand that the underlying plotly.js library does not try to handle time-zone conversions internally, but allows enough flexibility to work with user needs.

    One example is to use subplots to that align, adapting your example:

    using Dates
    using TimeZones
    using PlotlyJS
    
    n = 10
    YVector = collect(1:n)
    
    # Start from a UTC base time.
    base_time_utc = ZonedDateTime(DateTime(2026, 4, 13, 0, 0, 0), tz"UTC")
    TimeVectorUTC = [base_time_utc + Hour(i - 1) for i in 1:n]
    
    # Convert to Portugal/Lisbon and Europe/Warsaw.
    TimeVectorLisbon = astimezone.(TimeVectorUTC, Ref(tz"Europe/Lisbon"))
    TimeVectorWarsaw = astimezone.(TimeVectorUTC, Ref(tz"Europe/Warsaw"))
    
    trace_lisbon = scatter(
        x = TimeVectorLisbon,
        y = YVector,
        mode = "lines+markers",
        name = "Lisbon TimeVector",
        line = attr(color = "blue")
    )
    
    trace_warsaw = scatter(
        x = TimeVectorWarsaw,
        y = YVector,
        mode = "lines+markers",
        name = "Warsaw TimeVector",
        line = attr(color = "red")
    )
    
    lisbon_offset = first(TimeVectorLisbon).zone.offset
    warsaw_offset = first(TimeVectorWarsaw).zone.offset
    
    q = make_subplots(rows=2, cols=1, vertical_spacing=0.37);
    add_trace!(q, trace_lisbon, row=1, col=1);
    add_trace!(q, trace_warsaw, row=2, col=1);
    relayout!(q.plot.layout, 
        Layout(
            title = "Timezone plot test: TimeVector w/ UTC (Lisbon vs Warsaw)",
            yaxis_title = "YVector",
            yaxis2_title = "YVector",
            xaxis  = attr(tickformat="%H:%M\nUTC$(lisbon_offset) %b %e, %Y",
                          title = "Lisbon time",
                          side="bottom"),
            xaxis2 = attr(tickformat="%H:%M\nUTC$(warsaw_offset) %b %e, %Y",
                          title="Warsaw time",
                          side="top")
        )
    )
    display(q)
    Image
  4. changed the title [-]PlotlyJS direct plot fails with ZonedDateTime x-axis, but add_trace! with Scatter works[/-] [+]PlotlyJS direct plot() fails with ZonedDateTime x-axis, but add_trace!() with Scatter works[/+] on Apr 13, 2026
  5. jd-foster commented on Apr 26, 2026

    @jd-foster
    Collaborator

    Anything we could change within this package to help here?

  6. MChristianBF commented on Apr 26, 2026

    @MChristianBF
    Author

    I don’t think so. If you’re referring to the TimeZoneOffset workaround, I don't think it’s necessary to have smarter implementation. Since its very specific.
    As long as
    plot(TimeVectorZoned, YVector, mode="lines+markers", name="ZonedDateTime direct plot")
    is fixed, I’m happy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions