Home > Net >  How to make a ggplot2 object of strictly given dimensions?
How to make a ggplot2 object of strictly given dimensions?

Time:12-04

Imagine that you want to print out the plot and then cut it out with scissors. How do you make sure that the plot you draw has the correct dimensions?

An example:

You want to plot the ellipse formed when a plane cuts a cylinder of radius r, in millimeters, at an angle θ.

You want the plot printed on paper such that if you cut it out, you can wrap it around a physical cylinder of radius r and use it as a mitering pattern: if you cut the cylinder along the pattern's edges you get a perfectly straight cut at the angle θ.

To make sure that you got this right, you practice on two different cardboard cylinders: one from a roll of toilet paper, with radius equal to 22mm; the other, from a roll of paper towels, with radius equal to 19 mm. Here's the code:

library(tidyverse)

# Draw the ellipse you get when you intersect
# a cylinder of radius r by a plane at an
# angle theta (in degrees). Unfurl it for
# printing on a piece of paper that can then
# be cut and wrapped around a cylinder of
# radius r, so you can use it as a pattern
# for cutting the cylinder at angle theta.
# `steps` will set the resolution: the higher
# the number, the more precise the curve.
get_unfurled_ellipse <- function(r = 1,
                                 theta = 30,
                                 steps = 1000) {
  # based on the equation of an ellipse in standard form shown here:
  # https://saylordotorg.github.io/text_intermediate-algebra/s11-03-ellipses.html
  # with the parameterization that a = r and b = r / cos(theta * pi / 180)
  # where a is the minor radius and b is the major radius. The formula
  # above for b follows directly from looking at the vertical section of the
  # cylinder along its axis of symmetry in the plane that is perpendicular
  # to the intersecting plane: it's a trapezoid with the non-perpendicular
  # side equal to 2*b and the length of b can be derived as a / cos(theta * pi / 180)
  # This is easier drawn than explained. See also here:
  # https://mathworld.wolfram.com/CylindricalSegment.html
  top_half <- tibble(x = seq(-r, r, length.out = steps)) %>%
    mutate(y = sqrt((r^2 - x^2) * r / cos(theta * pi / 180)))
  bottom_half <- tibble(x = top_half$x   2*r) %>%
    mutate(y = - sqrt((r^2 - (x-2*r)^2) * r / cos(theta * pi / 180)))
  top_half %>%
    bind_rows(bottom_half)
}
plot_unfurled_ellipse <- function(r = 1, theta = 30, steps = 1000) {
  get_unfurled_ellipse(r, theta, steps) %>%
    ggplot(aes(x, y))  
    geom_point()  
    theme(axis.ticks.length=unit(r/steps, "mm"))
}
cylinders <- c(toilet_paper_roll = 22,
               paper_towel_roll = 19) %>%
  map(plot_unfurled_ellipse)

enter image description here

Shown above is the picture corresponding to the toilet paper cylinder. You print it out and start cutting at 0, follow the curve up, then down, return to the x axis, cut all the way through and you keep the bottom part. You should be able to roll it around the cardboard cylinder precisely, with the horizontal sections of the cut serving as flaps that you can tape on top of each other and they should overlap perfectly, because the curves start and end at the same point.

How do you make sure that the printed picture will be of the correct size for this, without any distortions along either axis?

CodePudding user response:

My comments don't seem to be getting through, so I'll put up part of the answer which is how to establish an aspect ratio of 1 which will hopefull allow the x and y axis dimensions to be on the same scale:

plot_unfurled_ellipse <- function(r = 1, theta = 30, steps = 1000) {
    get_unfurled_ellipse(r, theta, steps) %>%
        ggplot(aes(x, y))  
        geom_point()  
        theme(axis.ticks.length=unit(r/steps, "mm"))  coord_fixed(1)
}
cylinders <- c(toilet_paper_roll = 22,
               paper_towel_roll = 19) %>%
    map(plot_unfurled_ellipse) 

png()
print(cylinders)
#$toilet_paper_roll
# 
#$paper_towel_roll

dev.off()
#RStudioGD 
#        2 

Appears as plot001.png in my working directory

And the second plot:

enter image description here

The remaining task is to establish a mechanism to convert the somewhat arbitrary scale of plotting units to physical inces. This (I think) can be accomplished by setting the grid units with calls to

 units( ..., "in")

This answer might be helpful: R convert grid units of layout object to native

CodePudding user response:

OK, here's another attempt. As I suspected, setting the aspect ratio is redundant if you get the plot size right. If the units on both axes are measured in actual millimeters then the grid pattern will be square and the aspect ratio will be equal to 1 without having to set coord_fixed() explicitly. In addition, I had messed up the math in get_unfurled_ellipse(). I fixed it. I also modified the plot_unfurled_ellipse() so the x range starts at 0, not at -r. The breakthrough is that I am making use of the egg package to set the plot dimensions in millimeters explicitly. The new version of the code is this:

get_unfurled_ellipse <- function(r = 1,
                                 theta = 30,
                                 steps = 1000) {
  # based on the equation of an ellipse in standard form shown here:
  # https://saylordotorg.github.io/text_intermediate-algebra/s11-03-ellipses.html
  # with the parameterization that a = r and b = r / cos(theta * pi / 180)
  # where a is the minor radius and b is the major radius. The formula
  # above for b follows directly from looking at the vertical section of the
  # cylinder along its axis of symmetry in the plane that is perpendicular
  # to the intersecting plane: it's a trapezoid with the non-perpendicular
  # side equal to 2*b and the length of b can be derived as a / cos(theta * pi / 180)
  # This is easier drawn than explained. See also here:
  # https://mathworld.wolfram.com/CylindricalSegment.html
  top_half <- tibble(x = seq(0, 2*r, length.out = steps)) %>%
    mutate(y = sqrt(x * (2*r - x)) / cos(theta * pi / 180))
  bottom_half <- tibble(x = top_half$x   2*r) %>%
    mutate(y = - sqrt((x - 2*r) * (4*r - x)) / cos(theta * pi / 180))
  top_half %>%
    bind_rows(bottom_half)
}
plot_unfurled_ellipse <- function(r = 1, 
                                  theta = 30, 
                                  steps = 1000, 
                                  dot_size = .5) {
  df <- get_unfurled_ellipse(r, theta, steps)
  xlims <- c(0, max(df$x))
  ylims <- c(min(df$y), max(df$y))
  df %>%
    ggplot(aes(x, y))  
    geom_point(size = dot_size)  
    theme(axis.ticks.length=unit(r/steps, "mm"))   
    scale_x_continuous(limits = xlims)  
    scale_y_continuous(limits = ylims)
}
cylinders <- c(toilet_paper_roll = 22,
               paper_towel_roll = 19) %>%
  map(plot_unfurled_ellipse)

cylinder_grobs <- tibble(plot = cylinders) %>% 
  mutate(title = names(plot)) %>% 
  pmap(.f = function(plot, title) {
    x <- plot   
      ggtitle(title)
    
    x %>% 
      egg::set_panel_size(width = unit(max(.$data$x), "mm"), 
                          height = unit(2 * max(.$data$y), "mm")) %>% 
      arrangeGrob()
  })

But my math is still not right. The complete width of the unfurled ellipse is equal to 4*r -- two ellipse halves next to each other. But that's not what it should be if this pattern needs to be wrapped around the cylinder and fit exactly. Rather, the width should be 2*pi*r -- same as the circumference of the cylinder. I think that multiplying the x values by pi/2, and the y values by pi/(2*cos(theta*pi/180)) will fix it.

  • Related