2

I have shiny app that has multiple plots with the same x-axis, plots need to be aligned vertically without merging, i.e.: plots must be independent shiny objects from each other. See below reproducible example:

library(ggplot2)
library(shiny)

# example data
df1 <- mtcars

# my custom theme, tried many variants, seems impossible?
myTheme <- function(){
  theme(legend.position = "none",
        plot.background = element_rect(fill = "lightblue"),
        axis.text.y = element_text(margin = margin(t = 0, r = 0, b = 0, l = 2, unit = "cm")),
        axis.title.y = element_text(margin = margin(t = 0, r = 0, b = 0, l = 1, unit = "cm")),
        plot.margin = unit(c(0, 0, 0, 1), "cm")
  )}


# The App with 2 plots
runApp(
  shinyApp(
    ui = bootstrapPage(
      plotOutput('plot1', width = 800, height = 200),
      plotOutput('plot2', width = 800, height = 100)
    ),
    server = function(input, output) {
      output$plot1 <- renderPlot({
        ggplot(df1, aes(mpg, gear)) +
          geom_point() +
          coord_cartesian(xlim = c(15, 25)) +
          myTheme()
      })
      output$plot2 <- renderPlot({ 
        ggplot(df1[ df1$gear %in% c(3, 5), ], aes(mpg, gear)) +
          scale_y_continuous(name = "longName", breaks = c(3, 5), labels = c("myLongThreeeee", "My Longggg FIVE")) +
          geom_area() +
          coord_cartesian(xlim = c(15, 25)) +
          myTheme()
      })
      
    }
  ))

enter image description here

I tried to set my own myTheme() with different settings on margin, etc. seems like it is impossible?

If impossible, not very pretty solution is suggested here, I will just pad Y-tick labels using single width font, something like:

scale_y_continuous(labels = function(label) sprintf('%15.2f', label)) +

Other alternatives?


Note: I am aware of many packages (grid, cowplot, egg, patchwork, etc) that merge plots into one ggplot object and align x-axis, then print. In my case they need to be separate, as user can change some ui settings, and only affected plots should refresh.


TL;DR: Is it possible to set fixed size for "axis title" and "axis tick labels" space in centimetres, points, e.g.: 1cm, 4cm?


Related post about plotly: Shiny - align plots axis

Community
  • 1
  • 1
zx8754
  • 52,746
  • 12
  • 114
  • 209
  • Perhaps I've misunderstood, but if you just update one plot don't you risk your plots becoming unaligned after an update? – Dan Feb 19 '18 at 18:37
  • @Lyngbakr no, x-axis is same for all plot all the time. Of course, it is OK for all plots to refresh if I change x-axis range. But ui is mostly about Y-axis, groups, colours, etc. – zx8754 Feb 19 '18 at 18:43

2 Answers2

2

Borrowing heavily on @Lyngbakr's answer.

We can set the width manually to a fixed size. This will always align the plots, but does not take the length of the axis labels into account. I don't see how you can take both sets of labels into account without making the plotting interdependent. This way, you can remove the margin stuff in your theme.

library(ggplot2)
library(shiny)

# example data
df1 <- mtcars

# my custom theme, tried many variants, seems impossible?
myTheme <- function(){
  theme(legend.position = "none",
        plot.background = element_rect(fill = "lightblue")
  )}


# The App with 2 plots
runApp(
  shinyApp(
    ui = bootstrapPage(
      plotOutput('plot1', width = 800, height = 200),
      plotOutput('plot2', width = 800, height = 100)
    ),
    server = function(input, output) {
      g1 <- ggplot(df1, aes(mpg, gear)) +
        geom_point() +
        coord_cartesian(xlim = c(15, 25)) +
        myTheme()

      t1 <- ggplot_gtable(ggplot_build(g1))

      g2 <- ggplot(df1[ df1$gear %in% c(3, 5), ], aes(mpg, gear)) +
        scale_y_continuous(name = "longName", breaks = c(3, 5), labels = c("myLongThreeeee", "My Longggg FIVE")) +
        geom_area() +
        coord_cartesian(xlim = c(15, 25)) +
        myTheme()

      t2 <- ggplot_gtable(ggplot_build(g2))

      fixed_width <- unit(4, 'cm')
      t1$widths[3] <- fixed_width
      t2$widths[3] <- fixed_width

      output$plot1 <- renderPlot(plot(t1))
      output$plot2 <- renderPlot(plot(t2))
    }
  ))

enter image description here

Axeman
  • 32,068
  • 8
  • 81
  • 94
  • Thank you for fixed width solution. I will know the length of ylabels, so setting fixed width for all plots will not mess up ylabels. – zx8754 Feb 20 '18 at 09:42
  • Anyway to keep ggplot as ggplot object, instead of converting them to grobs? As far as I know it is [impossible to convert](https://stackoverflow.com/a/46161273/680068) back to ggplot object, once it is a grob. – zx8754 Feb 20 '18 at 09:43
  • 1
    Not that I'm aware of. But if you delay grob manipulation till just before printing, it should hardly matter? `ggplot2:::print.ggplot` also converts, so you are just doing some of the steps manually. – Axeman Feb 20 '18 at 10:01
1

A bit cludgy, but this seems to work.

library(ggplot2)
library(shiny)

# example data
df1 <- mtcars

# my custom theme, tried many variants, seems impossible?
myTheme <- function(){
  theme(legend.position = "none",
        plot.background = element_rect(fill = "lightblue"),
        axis.text.y = element_text(margin = margin(t = 0, r = 0, b = 0, l = 2, unit = "cm")),
        axis.title.y = element_text(margin = margin(t = 0, r = 0, b = 0, l = 1, unit = "cm")),
        plot.margin = unit(c(0, 0, 0, 4), "cm")
  )}


# The App with 2 plots
  shinyApp(
    ui = bootstrapPage(
      plotOutput('plot1', width = 800, height = 200),
      plotOutput('plot2', width = 800, height = 100)
    ),
    server = function(input, output) {
      g1 <- ggplot(df1, aes(mpg, gear)) +
        geom_point() +
        coord_cartesian(xlim = c(15, 25)) +
        myTheme()

      t1 <- ggplot_gtable(ggplot_build(g1))

      g2 <- ggplot(df1[ df1$gear %in% c(3, 5), ], aes(mpg, gear)) +
        scale_y_continuous(name = "longName", breaks = c(3, 5), labels = c("myLongThreeeee", "My Longggg FIVE")) +
        geom_area() +
        coord_cartesian(xlim = c(15, 25)) +
        myTheme()

      t2 <- ggplot_gtable(ggplot_build(g2))

      maxwidth <- unit.pmax(t1$widths[2:3], t2$widths[2:3])

      t1$widths[2:3] <- maxwidth
      t2$widths[2:3] <- maxwidth

      output$plot1 <- renderPlot({
        plot(t1)
      })
      output$plot2 <- renderPlot({ 
        plot(t2)
      })

    }
  )
Dan
  • 11,370
  • 4
  • 43
  • 68
  • Will this not again make plots dependant to each other? If g2 changes then, maxwidth will change for both g1 and g2, and g1 will have to refresh too? – zx8754 Feb 19 '18 at 19:25
  • 1
    Probably. I don't really work in Shiny. But my point is that the mechanism is there to specify the axis title and axis label regions. I've made them dependent so that it defaults to the largest of the two, but you can set them to whatever you like. – Dan Feb 19 '18 at 19:44
  • I rarely work in Shiny, too. I will test some options with your approach. Thank you. – zx8754 Feb 19 '18 at 19:50
  • How would you assign a constant number to width? `t1$widths[2:3] <- 5cm`? – zx8754 Feb 19 '18 at 19:55