Dual Axis Charts: Use With Caution
A dual axis chart is defensible only when the two series have genuinely different units โ like revenue in dollars and order count โ and each axis is clearly tied to its series. For anything else, the free choice of two scales lets you manufacture whatever visual correlation you want, so most of the time indexing or small multiples is the honest choice.
When Is a Dual Axis Chart Justified?
Use one when the series are in different units, are causally related in a way the audience already accepts, and you visually bind each series to its axis with matching colors. Temperature vs. rainfall, price vs. volume, revenue vs. margin percent are the classic legitimate cases.
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots()
ax1.plot(months, revenue, color="tab:blue")
ax1.set_ylabel("Revenue ($)", color="tab:blue")
ax1.tick_params(axis="y", labelcolor="tab:blue")
ax2 = ax1.twinx()
ax2.plot(months, orders, color="tab:orange")
ax2.set_ylabel("Orders", color="tab:orange")
ax2.tick_params(axis="y", labelcolor="tab:orange")
How Does Axis Scaling Manipulate Correlation Perception?
Because each axis has its own min, max, and zero point, you can stretch or shift either scale until the two lines overlap almost perfectly โ or diverge dramatically โ without changing the data. Readers judge the relationship by how closely the lines track each other, which here is an artifact of your scale choices, not the data.
This is how most "spurious correlation" charts are made: pick two rising series, tune the right-hand axis until they kiss, and the eye infers causation. If a small change to an axis range changes the story, the chart was never telling one.
What Are the Alternatives to a Dual Axis Chart?
Index both series to 100 at the start and plot them on one shared axis, or split them into small multiples stacked with a shared x-axis. Indexing shows relative growth honestly; separate panels show each series' own shape without implying false alignment.
# Index to first value = 100, one shared axis
rev_idx = 100 * revenue / revenue[0]
ord_idx = 100 * orders / orders[0]
plt.plot(months, rev_idx, label="Revenue (indexed)")
plt.plot(months, ord_idx, label="Orders (indexed)")
plt.legend()
Common Pitfalls
- Two series with the same unit: if both are dollars, they belong on one axis. Two dollar axes exist only to distort.
- Unlabeled or uncolored axes: if readers can't instantly tell which line reads on which axis, the chart fails.
- Tuning scales until lines overlap: overlapping lines on independent scales prove nothing โ compute the correlation instead.
- Bar plus line combos with a hidden zero: bars need a zero baseline; a truncated secondary axis behind bars is doubly misleading.
- More than two axes: a third y-axis is never the answer.
Pro Tip: Before publishing a dual axis chart, re-render it with each axis range perturbed by 20%. If the visual story changes, switch to indexed lines or separate panels.
โ Back to Visualization Tips