Question Details

No question body available.

Tags

r ggplot2 facet-wrap

Answers (4)

April 29, 2025 Score: 3 Rep: 26,563 Quality: Medium Completeness: 80%

Perhaps try building two plots (i.e. a, b, c in one, and Total in the other) then joining them together using the patchwork package, e.g.

library(tidyverse)

set.seed(123) data % ggplot(aes(x = as.factor(Year), y = Value, group = Treatment)) + geomline(aes(color = Treatment), size = 1) + geompoint(aes(color = Treatment, shape = Treatment), size = 3) + scaleycontinuous(limits = c(0, 200)) + facetwrap(~Component, scales = "freex", ncol = 4) + scalecolormanual(values = c("Control" = "blue", "Treatment" = "red"))

b % filter(Component == "Total") %>% ggplot(aes(x = as.factor(Year), y = Value, group = Treatment)) + geomline(aes(color = Treatment), size = 1) + geompoint(aes(color = Treatment, shape = Treatment), size = 3) + scaleycontinuous(limits = c(0, 500)) + facetwrap(~Component, scales = "freex", ncol = 4) + scalecolormanual(values = c("Control" = "blue", "Treatment" = "red"))

library(patchwork) a + b + plotlayout(guides = "collect", width = c(3,1), axistitles = "collect")

combined plot

Created on 2025-04-29 with reprex v2.1.1

Would this approach work for your use-case?

April 29, 2025 Score: 2 Rep: 167,786 Quality: Medium Completeness: 100%

This can be resolved in a few "creative" ways, in addition to the excellent patchwork recommendation by jaredmamrot. (In fact, I think that's the most straight-forward for interpretation. I offer alternatives in case your real data and use-case are not as cut-and-dried as in the question.)

  1. Using coordcartesianpanels from ggplot2::coordcartesian on facets (using my code in my gist), you can do this. Note that we stop using scaleycontinuous, change to scales="free", and add the panels portion:

    ggplot(datalong, aes(x = as.factor(Year), y = Value, group = Treatment)) +
      geomline(aes(color = Treatment), size = 1) +
      geompoint(aes(color = Treatment, shape = Treatment), size = 3) +
      # scaleycontinuous(limits = c(0, 200)) +
      facetwrap(~Component, scales = "free", ncol = 4) +
      scalecolormanual(values = c("Control" = "blue", "Treatment" = "red")) +
      coordcartesianpanels(
        panellimits = tibble::tribble(
          ~ Component, ~ ymin, ~ ymax,
          "a", 0, 200,
          "b", 0, 200,
          "c", 0, 200,
          "Total", 0, 500
        )
      )
    

    ggplot with four facets, three y-limits are the same, the fourth is different

    If you keep scales="fixed" or omit it completely, the plot still has all points we do not see an appropriate axis for Total. (We could add a sec.axis as we do in #2, though if you're going that route it seems less useful to bring in a gist-based dependency.)

  2. Scale the total and use a sec.axis. This works here because conveniently the second axis will be on the right side, and that right-most panel is the only one with a different y-axis.

    datalong |>
      mutate(
        Value2 = ifelse(Component != "Total", Value,
                         scales::rescale(Value, from=c(0, max(Value)), to=c(0, max(Value[Component != "Total"]))))
      ) |>
      ggplot(aes(x = as.factor(Year), y = Value, group = Treatment)) +
      geomline(aes(color = Treatment), size = 1,
                data = ~ filter(.x, Component != "Total")) +
      geompoint(aes(color = Treatment, shape = Treatment), size = 3,
                data = ~ filter(.x, Component != "Total")) +
      geomline(aes(y = Value2, color = Treatment), size = 1,
                data = ~ filter(.x, Component == "Total")) +
      geompoint(aes(y = Value2, color = Treatment, shape = Treatment), size = 3,
                data = ~ filter(.x, Component == "Total")) +
      scaleycontinuous(
        limits = c(0, 200),
        sec.axis = secaxis(
          name = "Total",
          transform = ~ scales::rescale(.x, from=c(0, max(datalong$Value[datalong$Component != "Total"])),
                                        to=c(0, max(datalong$Value)))
        )
      ) +
      facetwrap(~Component, scales = "fixed", ncol = 4) +
      scalecolormanual(values = c("Control" = "blue", "Treatment" = "red")) 
    

    ggplot with a different axis on the last panel, using sec.axis

    While I think this approach may be "clever", I don't really like the fact that it's not abundantly clear to which panel(s) the second axis applies. Adding the name="Total" to the second axis is a start, but I don't know that it's quite enough to prevent somebody mis-interpreting some portion of the plot.

    (Some history: the addition of sec.axis= took a long time to convince the ggplot2-devs, or perhaps one dev in particular, in order to add to ggplot2. The use of multiple axis scales can easily be confusing, leading to at times significant misinterpretation of the results. Even then, I feel that it could easily be streamlined considerably ... but doing so would be encouraging a technique that many vis-designers/analysts feel is risky. In my opinion if we introduced significant space between "c" and "Total" panes, it might aid the eye in separating the two axes ... but that starts getting into the "layout" functionality that patchwork provides, which is already well covered in jared_mamrot's answer.)

April 30, 2025 Score: 2 Rep: 131,012 Quality: Medium Completeness: 80%

Surprised that no one has offered ggh4x::facettedposscales which personally is my first choice when it comes to setting the scales individually for each panel, i.e. instead of using patchwork or @r2evans's custom coord we can do:

library(ggplot2)
library(ggh4x)

ggplot(datalong, aes(x = as.factor(Year), y = Value, group = Treatment)) + geomline(aes(color = Treatment), linewidth = 1) + geompoint(aes(color = Treatment, shape = Treatment), size = 3) + scaleycontinuous(limits = c(0, 200)) + facetwrap(~Component, scales = "freey", ncol = 4) + ggh4x::facettedposscales( y = list( # Set the limits for Total Component == "Total" ~ scaleycontinuous(limits = c(0, 500)), # Remove the axes for the inner panels Component != "a" ~ scaleycontinuous( limits = c(0, 200), guide = "none" ) ) ) + scalecolormanual( values = c("Control" = "blue", "Treatment" = "red") )

And, of course, this also allows the axis to be set to the right if required:

ggplot(datalong, aes(x = as.factor(Year), y = Value, group = Treatment)) +
  geomline(aes(color = Treatment), linewidth = 1) +
  geompoint(aes(color = Treatment, shape = Treatment), size = 3) +
  scaleycontinuous(limits = c(0, 200)) +
  facetwrap(~Component, scales = "freey", ncol = 4) +
  ggh4x::facettedposscales(
    y = list(
      Component == "Total" ~ scaleycontinuous(
        limits = c(0, 500), position = "right"
      ),
      Component != "a" ~ scaleycontinuous(
        limits = c(0, 200), guide = "none"
      )
    )
  ) +
  scalecolormanual(
    values = c("Control" = "blue", "Treatment" = "red")
  )

EDIT The only option I see to add a title when using facettedposscales would be the legendry package but it requires some effort:

library(legendry)

ggplot(datalong, aes(x = as.factor(Year), y = Value, group = Treatment)) + geomline(aes(color = Treatment), linewidth = 1) + geompoint(aes(color = Treatment, shape = Treatment), size = 3) + scaleycontinuous(limits = c(0, 200)) + facetwrap(~Component, scales = "freey", ncol = 4) + ggh4x::facettedposscales( y = list( Component == "Total" ~ scaleycontinuous( limits = c(0, 500), guide = legendry::composestack( primitiveticks(key = keyauto()), primitivelabels(key = keyauto()), primitivetitle(title = "Total", theme = theme( axis.title.y = elementtext(vjust = .5) )) ) ), Component != "a" ~ scaleycontinuous( limits = c(0, 200), guide = "none" ) ) ) + scalecolormanual( values = c("Control" = "blue", "Treatment" = "red") )

enter image description here

April 29, 2025 Score: 1 Rep: 33,952 Quality: Medium Completeness: 60%

If you don't want to manually create the plots for Total and other components and also define the widths for {patchwork} manually, you can split your data for Total and other components, then create the plots (facets) in a loop, and finally wrap them at the end.

datalong |> 
  split(~Component == "Total") |> 
  purrr::imap(~ggplot(.x, aes(x = as.factor(Year), y = Value, 
                              group = Treatment)) +
                geomline(aes(color = Treatment), 
                          linewidth = 1) +
                geompoint(aes(color = Treatment, shape = Treatment), 
                           size = 3) +
                scaleycontinuous(limits = c(0, ifelse(.y, 500, 200))) +
                scalexdiscrete("Year") +
                facetwrap(~Component, scales = "fixed") +
                scalecolormanual(values = c("Control" = "blue", 
                                              "Treatment" = "red"))) |> 
  patchwork::wrapplots(guides = "collect", axistitles = "collect",
                        widths = ndistinct(datalong$Component) |> 
                          (\(.) c((.-1), 1))())


If you must put the axis for Total on the right, which I highly advise against, we can simply use position = "right" in scaley....

datalong |> 
  split(~Component == "Total") |> 
  purrr::imap(~ggplot(.x, aes(x = as.factor(Year), y = Value, 
                              group = Treatment)) +
                geomline(aes(color = Treatment), 
                          linewidth = 1) +
                geompoint(aes(color = Treatment, shape = Treatment), 
                           size = 3) +
                scaleycontinuous(limits = c(0, ifelse(.y, 500, 200)),
                                   position = ifelse(.y, "right", "left"),
                                   name = ifelse(.y, "Total", "Value")) +
                scalexdiscrete("Year") +
                facetwrap(~Component, scales = "fixed") +
                scalecolormanual(values = c("Control" = "blue", 
                                              "Treatment" = "red"))) |> 
  patchwork::wrapplots(guides = "collect", axistitles = "collect",
                        widths = ndistinct(datalong$Component) |> 
                          (\(.) c((.-1), 1))())