Reusable chart builders and testing figures

Plotly · Expert · 14 min

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")                                    # ok

Remember

  • 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.

Check yourself

  1. Why are targeted assertions preferred over comparing complete figure JSON?

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

Official docs: plotly.com/python/creating-and-updating-figures/