Figure JSON, validation and plotly.js internals

Plotly · Expert · 14 min

Debugging odd rendering, writing generic chart code or interoperating with JavaScript requires knowing what plotly.js actually receives.

The Python graph-object classes are generated from the plotly.js schema, a JSON description of every trace type and attribute with its type, allowed values and default. That is why validation errors list every valid property and why each Python release pins a specific plotly.js version (plotly.offline.get_plotlyjs_version()).

What you build in Python is only the explicit part of the figure. When plotly.js draws it, it fills in defaults from the schema, the template and computed values (autoranges, tick positions, axis types guessed from the data). So fig.layout.xaxis.range is None in Python even though the chart has a range. fig.full_figure_for_development() runs plotly.js through Kaleido and returns the fully resolved figure, the way to see what was actually used.

Other internals worth knowing:

  • Each Python attribute set is validated and coerced (lists to tuples, NumPy arrays kept as arrays). fig.data is a tuple: add traces with add_trace, and assign to fig.data only to reorder or drop existing traces.
  • fig.batch_update() (a context manager) groups many property changes into one update message, important for FigureWidget performance.
  • Axis types are inferred from data: strings become "category", date-like strings become "date". Numeric-looking strings such as "2024" may be treated as numbers; set type="category" explicitly.
  • uirevision in layout keeps the user's zoom and legend state when an app re-renders the figure.

help(go.Scatter.marker) or the reference pages document every attribute.

Example

import plotly.graph_objects as go
import plotly.express as px
from plotly.offline import get_plotlyjs_version

print(get_plotlyjs_version())                  # 4.x for Plotly 7

fig = go.Figure(go.Bar(x=["2024", "2025", "2026"], y=[3, 5, 4]))
print(fig.layout.xaxis.range)                  # None: filled in by plotly.js
fig.update_xaxes(type="category")              # don't let "2024" be a number

print(type(fig.data).__name__)                 # tuple
fig.add_trace(go.Scatter(x=["2024", "2026"], y=[3, 4]))
fig.data = fig.data[::-1]                      # only reorder/drop allowed
print([t.type for t in fig.data])              # ['scatter', 'bar']

with fig.batch_update():                       # one message for widgets
    fig.data[1].marker.color = "#3F4F75"
    fig.layout.title.text = "Batched"

fig.update_layout(uirevision="keep")           # keep zoom across app re-renders
print(fig.to_plotly_json()["layout"]["uirevision"])  # keep
# full = fig.full_figure_for_development()     # needs kaleido + Chrome

Remember

  • Python classes are generated from the plotly.js schema; versions are pinned together
  • plotly.js fills in defaults; full_figure_for_development() shows the resolved figure
  • Set axis type explicitly when data could be misread; use uirevision in apps

Watch out for

  • Reading fig.layout.xaxis.range to get the displayed range; it is None unless you set it.

Check yourself

  1. Your x values are the strings '1', '2', '10' meant as labels, but the axis treats them as numbers. What fixes it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: plotly.com/python/reference/