Exercises: Plotly Graph Objects

These exercises assume import plotly.graph_objects as go, import plotly.io as pio, from plotly.subplots import make_subplots, and import pandas as pd. Install: pip install plotly kaleido.


Part A: Conceptual (6 problems)

A.1 ★☆☆ | Recall

Name the three top-level components of a Plotly Figure and describe what each contains.

Guidance **data**: a list of trace objects (go.Scatter, go.Bar, go.Heatmap, etc.), each a visual layer. **layout**: an object containing title, axes, legend, background, margins, annotations, shapes, updatemenus, and all other non-trace properties. **frames**: an optional list of frame objects for animation.

A.2 ★☆☆ | Recall

What is the difference between fig.update_layout() and fig.update_traces()?

Guidance `update_layout` modifies the layout object (title, axes, legend, annotations, etc.). `update_traces` modifies one or more traces (marker color, line width, mode, etc.). The difference matters because some properties live on traces and some on the layout — confusing them produces silent failures.

A.3 ★★☆ | Understand

Explain when you would choose Plotly Graph Objects over Plotly Express for a given task. Give two specific examples.

Guidance Use Graph Objects when Plotly Express cannot express the chart: (1) mixed trace types in one subplot (e.g., a scatter plus a line plus a band), (2) dual y-axes, (3) interactive buttons or dropdowns via updatemenus, (4) complex subplot layouts with spanning cells or mixed subplot types, (5) custom animation frames beyond what `animation_frame` provides. Examples: a chart with scatter + regression line + confidence band (needs mixed traces); a chart with a dropdown to switch between "cases" and "deaths" (needs updatemenus).

A.4 ★★☆ | Understand

What is the chapter's threshold concept ("Figures are data structures"), and why is it a different mental model from matplotlib?

Guidance In matplotlib, you issue imperative drawing commands against a canvas — ax.plot, ax.set_title, ax.set_xlim. In Plotly, you construct a Python data structure (a Figure object, equivalent to a nested dict) that describes what the chart should look like. The chart is rendered from the data structure by plotly.js, not by Python draw calls. Once you internalize this, Plotly's API feels declarative — you specify properties, not drawing steps.

A.5 ★★★ | Analyze

Explain why dual-y-axis charts are ethically fraught, with reference to the material in Chapter 4.

Guidance Dual-y-axis charts put two variables on different scales on the same plot. By manipulating the two scales, you can make any two series appear to correlate or anti-correlate regardless of the underlying relationship. This is a specific form of the lie-factor problem from Chapter 4: the visual relationship is an artifact of axis choice, not of the data. Mitigation: only use dual-axis when the two variables are genuinely related (e.g., temperature and CO2 with known physical connection); disclose the scale choices prominently; consider alternatives (scatter plot of x vs. y, or stacked charts with shared x-axis).

A.6 ★★★ | Evaluate

A colleague shows you a Plotly chart with 10 fig.update_traces and fig.update_layout calls chained after a Plotly Express call. What would you suggest?

Guidance If the chart requires that many post-construction updates, consider rewriting it from scratch using Graph Objects directly. The rewrite will usually be clearer because the intent is visible in the construction rather than spread across update calls. A rule of thumb: if your Plotly Express call requires more than about three chained updates, the Graph Objects version is probably easier to read and maintain.

Part B: Applied (10 problems)

B.1 ★☆☆ | Apply

Create a Graph Objects scatter plot of [1, 2, 3, 4] vs [1, 4, 9, 16] with markers and lines, a title, and axis labels.

Guidance
import plotly.graph_objects as go
fig = go.Figure(
    data=[go.Scatter(x=[1, 2, 3, 4], y=[1, 4, 9, 16], mode="lines+markers", name="y=x²")],
    layout=go.Layout(title="Squares", xaxis_title="x", yaxis_title="y"),
)
fig.show()

B.2 ★☆☆ | Apply

Add a second trace (y=x³) to the figure from B.1 and make sure both appear in the legend.

Guidance
fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[1, 8, 27, 64], mode="lines+markers", name="y=x³"))
By default, all traces appear in the legend with their `name` values.

B.3 ★★☆ | Apply

Build a 2×2 subplot figure using make_subplots with titles "A", "B", "C", "D" and shared x-axes. Place a scatter trace in each cell.

Guidance
from plotly.subplots import make_subplots

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=("A", "B", "C", "D"),
    shared_xaxes=True,
)
for i, (r, c) in enumerate([(1, 1), (1, 2), (2, 1), (2, 2)]):
    fig.add_trace(go.Scatter(x=list(range(10)), y=[j + i*5 for j in range(10)]), row=r, col=c)
fig.update_layout(height=600, showlegend=False)

B.4 ★★☆ | Apply

Create a dual-y-axis chart with temperature (red) on the primary axis and CO2 (blue) on the secondary axis, both over years.

Guidance
fig = make_subplots(specs=[[{"secondary_y": True}]])

fig.add_trace(go.Scatter(x=years, y=temperature, name="Temperature", line=dict(color="red")), secondary_y=False)
fig.add_trace(go.Scatter(x=years, y=co2, name="CO₂", line=dict(color="blue")), secondary_y=True)

fig.update_yaxes(title_text="Temperature (°C)", secondary_y=False)
fig.update_yaxes(title_text="CO₂ (ppm)", secondary_y=True)
Remember the ethical warning: this chart type requires a genuine physical relationship to be honest, and the axis scales should be obvious to the reader.

B.5 ★★☆ | Apply

Add an interactive dropdown to a scatter plot that switches between showing "all points" and "only high-value points."

Guidance
fig = go.Figure()
fig.add_trace(go.Scatter(x=df.x, y=df.y, mode="markers", name="All", visible=True))
fig.add_trace(go.Scatter(x=df[df.y > 50].x, y=df[df.y > 50].y, mode="markers", name="High", visible=False))

fig.update_layout(
    updatemenus=[dict(
        type="dropdown",
        buttons=[
            dict(label="All points", method="update", args=[{"visible": [True, False]}]),
            dict(label="High-value only", method="update", args=[{"visible": [False, True]}]),
        ],
    )]
)

B.6 ★★☆ | Apply

Add a horizontal reference line at y=0, a vertical reference line at x=2020, and a shaded region (vrect) between x=2008 and x=2009 labeled "Recession."

Guidance
fig.add_hline(y=0, line_dash="dash", line_color="gray")
fig.add_vline(x=2020, line_dash="dash", line_color="red", annotation_text="2020")
fig.add_vrect(x0=2008, x1=2009, fillcolor="lightgray", opacity=0.3, annotation_text="Recession")

B.7 ★★★ | Apply

Build a go.Heatmap with a custom diverging colorscale (blue → white → red), zmid=0, and cell annotations showing the values with two decimal places.

Guidance
import numpy as np
matrix = np.random.randn(5, 5)

fig = go.Figure(data=go.Heatmap(
    z=matrix,
    colorscale=[[0, "blue"], [0.5, "white"], [1, "red"]],
    zmid=0,
    text=np.round(matrix, 2),
    texttemplate="%{text}",
))
fig.show()

B.8 ★★★ | Apply

Create a go.Scattergl trace with 100,000 random points and compare rendering speed to the equivalent go.Scatter trace.

Guidance
import numpy as np
x = np.random.randn(100_000)
y = np.random.randn(100_000)

fig_gl = go.Figure(go.Scattergl(x=x, y=y, mode="markers", marker=dict(size=3)))
fig_svg = go.Figure(go.Scatter(x=x, y=y, mode="markers", marker=dict(size=3)))
Scattergl renders smoothly at 100k points; Scatter is sluggish. For 1M points, only Scattergl is practical — and even that starts to show limits.

B.9 ★★☆ | Apply

Inspect a figure with fig.to_dict() and identify the trace's mode, the layout's title, and the list of annotations.

Guidance
spec = fig.to_dict()
print(spec["data"][0]["mode"])         # trace mode
print(spec["layout"]["title"])           # layout title (may be a dict or a string)
print(spec["layout"].get("annotations", []))  # list of annotations
`to_dict()` is your debugging tool of choice when a figure does not look right — you can see exactly what Plotly will send to the renderer.

B.10 ★★★ | Create

Build a complete climate dashboard figure using Graph Objects: dual y-axis (temperature and CO2), three vertical annotations for key events (1958, 1988, 2015), a range slider, and a unified hover mode.

Guidance
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter(x=climate["year"], y=climate["temperature_anomaly"],
                          name="Temp (°C)", line=dict(color="red")), secondary_y=False)
fig.add_trace(go.Scatter(x=climate["year"], y=climate["co2_ppm"],
                          name="CO₂ (ppm)", line=dict(color="blue")), secondary_y=True)
fig.add_vline(x=1958, line_dash="dash", annotation_text="Mauna Loa")
fig.add_vline(x=1988, line_dash="dash", annotation_text="Hansen testimony")
fig.add_vline(x=2015, line_dash="dash", annotation_text="Paris Agreement")
fig.update_layout(
    title="Temperature and CO₂, 1880–2024",
    hovermode="x unified",
    xaxis_rangeslider_visible=True,
    template="simple_white",
)

Part C: Synthesis (4 problems)

C.1 ★★★ | Analyze

Take a Plotly Express figure from Chapter 20 and rewrite it as a Graph Objects figure. Compare the line counts and discuss when the Graph Objects version is worth the extra verbosity.

Guidance Graph Objects is usually 3-5× more lines for the same chart. The trade-off: more code, more control. The Graph Objects version is worth the effort when you need something Plotly Express cannot do (dual axes, interactive buttons, custom subplots). For standard charts, Plotly Express's one-liner is better. A hybrid workflow — Plotly Express for the base, Graph Objects for specific customizations — is the most common idiom.

C.2 ★★★ | Evaluate

A marketing chart shows website sessions (scale: 0-10000) and conversion rate (scale: 0-5%) on the same dual-y-axis chart. The two lines appear to track each other closely. Is this a legitimate use of dual-y, or is it misleading? Explain.

Guidance Probably misleading. The apparent tracking is a product of the chosen axis scales — you could make the lines diverge or converge arbitrarily by changing either axis's range. Sessions and conversion rate have no inherent unit relationship, so any perceived correlation should be verified with a scatter plot of one variable against the other. The dual-axis chart should either be replaced with a scatter plot (conversion rate vs. sessions over time, or conversion rate vs. sessions directly) or with two stacked line charts with a shared x-axis. If the chart stays, the axis scales must be clearly disclosed.

C.3 ★★★ | Create

Design a custom template for your organization with specific brand colors, a specific font family, and a default margin of 80px on all sides. Apply it to a Plotly Express chart.

Guidance
import plotly.graph_objects as go
import plotly.io as pio

brand_template = go.layout.Template(
    layout=dict(
        font=dict(family="Georgia, serif", size=13, color="#1a1a1a"),
        plot_bgcolor="white",
        margin=dict(l=80, r=80, t=80, b=80),
        colorway=["#E63946", "#F1FAEE", "#A8DADC", "#457B9D", "#1D3557"],
    )
)
pio.templates["brand"] = brand_template
pio.templates.default = "simple_white+brand"

import plotly.express as px
fig = px.scatter(df, x="x", y="y")
fig.show()

C.4 ★★★ | Evaluate

The chapter argues that "figures are data structures" in Plotly, in contrast to matplotlib's imperative drawing. What does this mental-model difference mean for debugging, reproducibility, and collaboration?

Guidance **Debugging**: you can inspect the full figure spec with `fig.to_dict()` and see exactly what Plotly will render. In matplotlib, you would have to step through draw calls and examine the Artist tree, which is harder. **Reproducibility**: Plotly figures can be serialized to JSON and re-loaded, producing an exact reproduction; matplotlib figures cannot be easily serialized this way (pickle works but is fragile). **Collaboration**: a Plotly JSON spec can be shared between Python and JavaScript workflows — the same spec renders identically in any environment that supports plotly.js. Matplotlib is Python-only. The data-structure mental model enables capabilities that the imperative mental model does not.

These exercises exercise Graph Objects features that Plotly Express does not expose. Chapter 22 introduces Altair, a third philosophy of interactive visualization based on the grammar of graphics and Vega-Lite.