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.datais a tuple: add traces withadd_trace, and assign tofig.dataonly to reorder or drop existing traces. fig.batch_update()(a context manager) groups many property changes into one update message, important forFigureWidgetperformance.- 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; settype="category"explicitly. uirevisionin 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 + ChromeRemember
- 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
typeexplicitly when data could be misread; useuirevisionin apps
Watch out for
- Reading
fig.layout.xaxis.rangeto get the displayed range; it is None unless you set it.