Skip to content
1stepGrowLearnCompareGrow

Advanced Plotly Charts: Subplots, Animation and 3D (Plotly Part 2)

Advanced Plotly charts in Python: subplots, dual axes, animation, 3D and heatmaps, plus the axis-range mistake that silently drops points from animations.

Vanshika Nigam

ETL & Data Engineer, Accenture

9 min readUpdated
Share
On this page

Advanced Plotly charts are the tools for when one line or bar is not enough: make_subplots for several panels, secondary_y for two scales, animation_frame for change over time, and annotations for a chart that explains itself. Each is a few lines of code. The hard part is knowing when each one misleads.

Take animation. Copy the axis ranges from Plotly's own gapminder example and Plotly draws four data points (Kuwait in three years, Rwanda in one) off the chart, with no warning. This part of the series is for readers who already know Plotly Express from part one. You get runnable code for subplots, dual axes, animation, 3D, heatmaps and annotation, plus a table for picking the right one.

Every example below was run on Plotly 6.3. As of September 2026 the current release is Plotly 7.1, and nothing used here was removed in 7.0.

How do you build subplots in Plotly?

Multi-panel figures need make_subplots from plotly.subplots, as the subplots guide shows:

Python
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

x = np.linspace(0, 10, 100)

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=('Sine', 'Cosine', 'Damped', 'Noise'),
    horizontal_spacing=0.10,
    vertical_spacing=0.14,
)

fig.add_trace(go.Scatter(x=x, y=np.sin(x), name='sin'), row=1, col=1)
fig.add_trace(go.Scatter(x=x, y=np.cos(x), name='cos'), row=1, col=2)
fig.add_trace(go.Scatter(x=x, y=np.sin(x) * np.exp(-x / 5), name='damped'), row=2, col=1)

rng = np.random.default_rng(0)
fig.add_trace(go.Scatter(x=x, y=rng.normal(size=100), mode='markers', name='noise'),
              row=2, col=2)

fig.update_layout(height=600, showlegend=False, template='plotly_white',
                  title_text='Four panels')
fig.show()

Axis updates also take the same row/col arguments:

Python
fig.update_yaxes(title_text='Amplitude', row=1, col=1)
fig.update_xaxes(title_text='Time (s)', row=2, col=1)

If stacked panels share a time axis, pass shared_xaxes=True to make_subplots. Panels in the same column are then linked, so zooming one zooms the others, and only the bottom panel keeps its x tick labels.

When should you use a secondary y-axis?

Use one only for two series on genuinely different scales. The multiple axes guide uses the same pattern:

Python
fig = make_subplots(specs=[[{'secondary_y': True}]])

months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
revenue = [120, 145, 132, 178, 195, 210]
margin = [0.22, 0.24, 0.21, 0.27, 0.29, 0.28]

fig.add_trace(go.Bar(x=months, y=revenue, name='Revenue',
                     marker_color='#2563eb'), secondary_y=False)

fig.add_trace(go.Scatter(x=months, y=margin, name='Margin',
                         mode='lines+markers',
                         line={'color': '#f59e0b', 'width': 3}), secondary_y=True)

fig.update_yaxes(title_text='Revenue (₹k)', secondary_y=False)
fig.update_yaxes(title_text='Margin', tickformat='.0%', secondary_y=True)
fig.update_layout(template='plotly_white', hovermode='x unified')
fig.show()

A caution worth stating: dual axes let you make any two series look correlated by choosing the scales. Therefore, use them when the units really differ and the comparison is legitimate, and be suspicious when you see them elsewhere.

How do you animate a Plotly chart without the axes jumping?

Pass animation_frame to a Plotly Express function and fix range_x and range_y yourself. The single argument creates the animation; the fixed ranges are what make it readable:

Python
import plotly.express as px

gap = px.data.gapminder()

fig = px.scatter(
    gap,
    x='gdpPercap', y='lifeExp',
    size='pop', color='continent',
    hover_name='country',
    animation_frame='year',
    animation_group='country',
    log_x=True, size_max=55,
    range_x=[100, 150_000], range_y=[20, 90],
    title='Development over time',
)
fig.show()

animation_group keeps each country's point identified across frames, so it moves rather than disappearing and reappearing. The explicit range_x and range_y matter just as much: without them the axes rescale each frame and the motion becomes meaningless.

Worked example: choosing the ranges

The Plotly animation guide advises always fixing the ranges so the data stays visible, and its own example uses [100, 100000] and [25, 90]. However, check your data first:

Python
gap['gdpPercap'].max()   # 113523.1329 - Kuwait, 1957
gap['lifeExp'].min()     # 23.599      - Rwanda, 1992

Both values fall outside those example ranges, so three Kuwait frames and one Rwanda frame would be drawn off the chart. That is why the example above uses the wider [100, 150_000] and [20, 90], which cover all 12 frames from 1952 to 2007.

When is a 3D Plotly chart worth using?

Only when the third dimension is the message, such as the surface of a mathematical function or genuinely spatial data. For everything else, a 2D chart with colour, size or facets reads better:

Python
x = np.linspace(-5, 5, 60)
y = np.linspace(-5, 5, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = go.Figure(go.Surface(x=X, y=Y, z=Z, colorscale='Viridis'))
fig.update_layout(
    title='A surface',
    scene={'xaxis_title': 'x', 'yaxis_title': 'y', 'zaxis_title': 'z'},
    height=600,
)
fig.show()

Surfaces of a genuine mathematical function are a fair use, because the shape is the point. By contrast, a 3D bar chart of categorical data is almost never better than a heatmap or a faceted 2D chart, because the perspective distorts comparison and near objects hide far ones.

Why is annotation the most useful advanced Plotly feature?

A chart that states its own point is worth several that leave the reader to find it, so this is where extra effort pays off first.

Python
fig = go.Figure(go.Scatter(x=months, y=revenue, mode='lines+markers',
                           line={'color': '#2563eb', 'width': 3}))

fig.add_annotation(
    x='Apr', y=178,
    text='Pricing change<br>launched',
    showarrow=True, arrowhead=2, ax=-50, ay=-60,
    bgcolor='white', bordercolor='#cbd5e1', borderwidth=1, borderpad=6,
)

fig.add_vrect(x0='Apr', x1='Jun', fillcolor='#2563eb', opacity=0.06,
              line_width=0, annotation_text='Post-change')

fig.add_hline(y=150, line_dash='dash', line_color='#94a3b8',
              annotation_text='Target')

fig.update_layout(template='plotly_white', title='Revenue, with context')
fig.show()

add_vrect, add_hline and add_annotation between them cover most of what you need to make a chart self-explanatory. The shapes guide lists the matching add_vline and add_hrect, plus annotation_position for placing the label. On a category axis like this one, the month names work directly as positions.

How do you make a correlation heatmap in Plotly?

Use px.imshow on a correlation matrix, with zmin=-1 and zmax=1 so the neutral colour means zero. It is also the usual answer when you were reaching for 3D:

Python
corr = gap[['lifeExp', 'pop', 'gdpPercap', 'year']].corr()

fig = px.imshow(
    corr,
    text_auto='.2f',
    color_continuous_scale='RdBu_r',
    zmin=-1, zmax=1,
    title='Correlation matrix',
)
fig.show()

On the gapminder data, the strongest pairing is life expectancy with GDP per capita, at 0.58. Setting zmin and zmax symmetrically around zero matters for diverging scales; otherwise, the colour midpoint drifts and the chart misleads. The text_auto format string follows d3-format, as the heatmaps guide notes.

Which advanced Plotly chart should you use?

You want to show Use Avoid
Different charts side by side make_subplots several separate figures
The same chart per category facet_col in Plotly Express hand-built subplots
Two units on one timeline secondary_y=True dual axes for same-unit data
Change over time animation_frame with fixed ranges auto-scaling axes
A third numeric variable colour, size or a heatmap 3D bars
A mathematical surface go.Surface a flat contour if shape matters
The takeaway add_annotation, add_vrect, add_hline a long caption

Common mistakes with advanced Plotly charts

  • Forgetting row and col. Traces added without them do not go where you intended, so always pass both.
  • Leaving animation ranges automatic. The axes jump between frames; fix both ranges.
  • Copying example ranges without checking the data. Points outside the range silently vanish.
  • Using a diverging colour scale without zmin/zmax. The neutral colour stops meaning zero.
  • Styling before annotating. One clear annotation usually matters more than colour tweaks.

A practical default for presentable charts

Build with Express, style lightly, annotate once. In practice, the sequence that produces good advanced Plotly charts quickly is this: first build the chart with Express, then set template='plotly_white' and hovermode='x unified' on anything time-based. Finally, add one annotation naming the insight, and stop there, since further styling has rapidly diminishing returns.

The Sunday Growth Brief

One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.

No spam. Unsubscribe in one click.

Plotly Part 1 covers the basics. For static, print-quality output, see matplotlib styling and customisation. The arrays behind these examples are explained in NumPy Part 3 on broadcasting, and the planned guide to Plotly maps, treemaps and 3D plots goes further into specialist charts.

Frequently asked questions

How do I put two charts side by side in Plotly?

Use plotly.subplots.make_subplots(rows=1, cols=2), then add each trace with the row and col arguments. Plotly Express cannot merge two finished figures into one, so arbitrary multi-panel work generally means Graph Objects. The exception is small multiples of the same chart, where facet_col in Plotly Express is far shorter than building subplots by hand.

How do I add a second y-axis in Plotly?

Pass specs=[[{'secondary_y': True}]] to make_subplots, then add each trace with secondary_y=True or secondary_y=False, and title each axis with update_yaxes using the same argument. Use it sparingly, because dual axes make it easy to imply a relationship that is not there simply by choosing the two scales.

Are 3D plots in Plotly useful?

Rarely. Occlusion and perspective make values hard to compare, and a static screenshot loses the rotation that made the chart readable on screen. Prefer colour, size or faceting to encode a third variable. Keep 3D for genuinely spatial data or for surfaces of a mathematical function, where the shape itself is the message.

Why does my Plotly animation look wrong?

Usually the axes are rescaling on every frame, so points appear to jump even when the data barely changes. Set range_x and range_y wide enough to cover every frame. Also pass animation_group so each entity keeps its identity between frames and moves smoothly instead of disappearing and reappearing.

Written by

Vanshika Nigam

ETL & Data Engineer, Accenture

ETL developer at Accenture working with Informatica, Snowflake and DBT, specialising in data mapping, cleansing and pipeline performance.

SQLETL pipelinesSnowflakeData analysis

Related reading

Free weekly newsletter

Get the shortlist before everyone else

Every Sunday we send one email with the week’s sharpest course comparison, a career roadmap worth stealing, and the tech news and hiring signals we’re watching.

  • No fluff, ever
  • Unsubscribe anytime
  • 5-minute read

We never share your address. One click to leave.