Charts in products and pipelines break silently; because Plotly figures are data, you can unit-test them like any other output.
In applications, treat a chart as a pure function: data and options in, go.Figure out, no fig.show() and no global state inside. Benefits: the same builder serves Dash, Streamlit, notebooks and HTML exports, and it is testable.
Because a figure is plain structured data, tests can assert on exactly what the user will see:
- the number and types of traces (
[t.type for t in fig.data]); - data values, after converting to lists:
list(fig.data[0].y); - names, colors, axis titles, formats and hovertemplates;
- the absence of mistakes, such as no trace with an empty
x.
Prefer these targeted assertions over comparing the full JSON: whole-figure snapshots break on every library upgrade because plotly.py adds or renames defaults. When you do snapshot, store fig.to_plotly_json() restricted to the keys you own.
Make builders deterministic. Sort inputs, fix category orders (category_orders or explicit lists), and avoid relying on DataFrame row order for colors, so the same data always yields the same figure.
Keep styling out of builders: apply a registered template instead, so tests on content stay stable when the visual design changes.
Visual regression (comparing rendered PNGs via Kaleido) catches layout problems, but is slow and needs Chrome; reserve it for a few key charts.
Example
import plotly.graph_objects as go
import pandas as pd
def revenue_chart(df: pd.DataFrame, target: float) -> go.Figure:
df = df.sort_values("month")
fig = go.Figure(go.Bar(x=df["month"], y=df["revenue"], name="revenue"))
fig.add_hline(y=target, line_dash="dash")
fig.update_yaxes(title_text="Revenue ($)", tickformat=",.0f")
return fig
def test_revenue_chart():
df = pd.DataFrame({"month": ["2026-02", "2026-01"], "revenue": [12, 10]})
fig = revenue_chart(df, target=11)
assert [t.type for t in fig.data] == ["bar"]
assert list(fig.data[0].x) == ["2026-01", "2026-02"] # sorted
assert fig.layout.shapes[0].y0 == 11
assert fig.layout.yaxis.title.text == "Revenue ($)"
test_revenue_chart()
print("ok") # okRemember
- Write chart builders as pure functions returning a Figure
- Test the figure's data and layout with targeted assertions
- Make output deterministic and keep visual styling in templates
Watch out for
- Snapshotting the entire figure JSON, which breaks on every plotly upgrade even when the chart is unchanged.