A set of techniques for simulating dual-axis plots in ggplot2 by normalizing or transforming one of the series and using sec.axis to add a secondary scale.
The ggplot2
package in R deliberately discourages dual-axis charts because they can confuse readers when the two scales are unrelated. Nonetheless, there are legitimate cases—such as overlaying temperature and precipitation—where plotting two series on different scales in one figure is useful. Because ggplot2
only allows a secondary axis that is a one-to-one transformation of the primary axis, engineers must adopt specific work-arounds to create the illusion of two independent y-axes.
Hadley Wickham’s design philosophy prioritizes clear, interpretable graphics. When two y-axes are free to use unrelated scales, readers can easily misread the magnitude of change on either series. Consequently, ggplot2 only permits transformed secondary axes via the sec.axis
argument—meaning the second axis must be a deterministic function of the first.
+ scale_y_continuous(sec.axis = ...)
unless one can be expressed as f(x) = a * x + b
of the other.sec_scaled = (sec - min(sec)) / (max(sec) - min(sec)) * (max(prim) - min(prim)) + min(prim)
scale_y_continuous(sec.axis = sec_axis(~inv_transform(.), name = "Secondary Metric"))
, where inv_transform()
reverses the scaling.library(ggplot2)
# Synthetic data
df <- data.frame(
month = as.Date('2023-01-01') + 0:11 * 30,
revenue = c(120, 150, 110, 180, 210, 190, 220, 250, 240, 260, 300, 310),
conversion = c(3.1, 3.4, 2.9, 3.6, 3.8, 3.5, 3.9, 4.1, 3.8, 4.0, 4.3, 4.5)
)
# 1. Identify limits
y1_min <- min(df$revenue)
y1_max <- max(df$revenue)
# 2. Scale conversion to revenue space
scale_factor <- (y1_max - y1_min) / (max(df$conversion) - min(df$conversion))
conv_scaled <- df$conversion * scale_factor + y1_min - min(df$conversion) * scale_factor
df$conv_scaled <- conv_scaled
# 3. Plot
p <- ggplot(df, aes(month)) +
geom_col(aes(y = revenue), fill = "steelblue", alpha = 0.7) +
geom_line(aes(y = conv_scaled), color = "darkorange", size = 1.2) +
scale_y_continuous(
name = "Monthly Revenue ($)",
sec.axis = sec_axis(~ (. - (y1_min - min(df$conversion) * scale_factor)) / scale_factor,
name = "Conversion Rate (%)")
) +
scale_x_date(date_labels = "%b %Y") +
theme_minimal()
print(p)
In the code above:
conv_scaled
brings conversion rate values into the revenue scale.sec_axis()
reverses that transformation so the secondary axis shows percentages.Because dual-axis plots can mislead, always:
If the audiences are not highly technical or the relationships between series are tenuous, favor facet_wrap()
or vertically stacked plots. These preserve clarity and avoid scale conflicts.
Stick to linear transformations (multiplication, addition) so tick spacing remains intuitive.
If the lower bounds of the primary and scaled secondary series differ, the visual zero point of one series may not correspond to zero on its own axis. Ensure transformations align baselines appropriately.
Without the correct inversion in sec_axis()
, the secondary ticks will be wrong, misleading the user. Always test by back-transforming a few reference values.
When the x-axis is character or factor, column widths can dominate the line visually. Convert to factor
positions or use position_dodge()
so neither series obscures the other.
Dual-axis plots often surface in KPI dashboards where engineers must communicate related but scale-mismatched metrics. Mastering the ggplot2 workaround enables teams to produce clear executive-level visuals without resorting to spreadsheet chart editors.
While Galaxy is primarily a SQL editor, its forthcoming visualization layer will likely consume aggregated query results. Engineers may retrieve revenue and conversion data via SQL in Galaxy, export to R, and apply the dual-axis workaround in ggplot2 for reporting.
Creating dual-axis charts in ggplot2 is intentionally non-trivial to protect against misleading graphics. By understanding the philosophy behind sec.axis
and applying systematic transformations, data engineers can responsibly implement two-axis plots when they truly add value. Always weigh the communication benefit against the cognitive cost to readers, and where doubt exists, reach for alternative designs.
Data analysts frequently need to overlay two metrics with different units—such as dollars and percentages—on a single timeline. Because ggplot2 forbids unrelated dual axes, knowing the proper workaround is essential for producing clear, accurate visualizations without abandoning the grammar of graphics.
Avoid them when the two metrics are only loosely related or when the audience is unlikely to understand different units. Separate panels often communicate trends more clearly.
sec.axis
function enough to plot two unrelated series?No. sec.axis
requires a one-to-one transformation of the primary axis. You must pre-scale the secondary series and supply the inverse transformation; otherwise, the chart is misleading.
Galaxy itself is a SQL editor, not a plotting library. However, engineers often query data in Galaxy, export results to R or Python, and then use the ggplot dual-axis workaround for reporting.
Not without manual grob manipulation or using other libraries like plotly
. ggplot2 deliberately enforces the transformation rule to maintain visual integrity.