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

- **Author:** Vanshika Nigam — ETL & Data Engineer, Accenture (https://www.1stepgrow.com/authors/vanshika-nigam/)
- **Published:** Jul 31, 2026 · **Updated:** Sep 17, 2026
- **Topic:** Python & Programming · **Format:** Guide · **Read time:** 9 min
- **Canonical URL:** https://www.1stepgrow.com/articles/advanced-plotly-charts/

## Key takeaways

- make_subplots is the entry point for multi-panel figures; pass specs when you need a secondary y-axis.
- Animation is a single argument in Plotly Express, but fixed axis ranges are what make the motion readable.
- 3D charts are usually worse than a good 2D chart, so use them only when the third dimension carries real information.
- An annotation that names the insight does more for comprehension than any styling change.

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](https://www.1stepgrow.com/articles/plotly-tutorial). 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](https://plotly.com/python/subplots/) 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](https://plotly.com/python/multiple-axes/) 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](https://plotly.com/python/animations/) 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](https://plotly.com/python/horizontal-vertical-shapes/) 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](https://plotly.com/python/heatmaps/) 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.



## Related reading

[Plotly Part 1](https://www.1stepgrow.com/articles/plotly-tutorial) covers the basics. For static, print-quality output, see [matplotlib styling and customisation](https://www.1stepgrow.com/articles/matplotlib-styling-customization). The arrays behind these examples are explained in [NumPy Part 3 on broadcasting](https://www.1stepgrow.com/articles/numpy-broadcasting), and the planned guide to [Plotly maps, treemaps and 3D plots](https://www.1stepgrow.com/articles/plotly-maps-treemaps-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.

---
_Source: 1stepGrow (https://www.1stepgrow.com/articles/advanced-plotly-charts/). Cite with the title, "1stepGrow" and a link._
