0

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?

Gabi
  • 1,303
  • 16
  • 20
  • The functions do not throw any errors but it's unclear what the use case might be.Looking for a succinct description of what we should see when we plot. – IRTFM Dec 03 '21 at 22:23
  • The plot above is the first element of the `cylinders` list. The use case is as described: it's a mitering pattern for cutting a cardboard cylinder at a 30-degree angle. – Gabi Dec 03 '21 at 22:49
  • I'd be happy to have you spell it out. I suspect you simply need to set the aspect ratio for the x and y axes to 1 and I'm pretty sure that's already been asked and answered. I understand what the physical test is but That a bunch of code that produces nothing on my machine. – IRTFM Dec 03 '21 at 22:58
  • The aspect ratio of 1 will be a direct consequence of the plot being on a 1:1 scale. If the axes are in millimeters, and if the plot is printed in millimeters, the grid pattern will show squares and the aspect ratio will be 1. But the aspect ratio alone -- and setting it directly -- is no help if the scale is off. You're right that this is a matter of setting some ggplot parameter right, but I've never seen this use case before. People who cut and weld metals don't use R for mitering patterns, and evidently R users don't use ggplot2 for whatever shop work they might do. – Gabi Dec 03 '21 at 23:27
  • 1
    You could use [`ggh4x::force_panelsizes()`](https://teunbrand.github.io/ggh4x/reference/force_panelsizes.html) to set exact dimensions of a panel. You still have to consider that the axes are still expanded by 10% (by default). – teunbrand Dec 04 '21 at 11:49

2 Answers2

1

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

IRTFM
  • 258,963
  • 21
  • 364
  • 487
0

OK, done. 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. The breakthrough is that I am making use of the egg package to set the plot dimensions in millimeters explicitly and the math shown right above Figure 3 here. The new version of the code is this:

library(tidyverse)
library(egg)
# 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.
# 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) {
  # imagine that the ellipse that cuts this cylinder
  # is itself cut through the middle by a circle that
  # is parallel to the bottom of the cylinder. Now
  # you have two wedges: one above the circle, one
  # below it. The goal is to express any point on
  # the elliptical (long) arc in the wedge as a function
  # of the point on the circular (short) arc that falls
  # directly below it. That is the vertical projection of
  # the ellipse's major radius inside that wedge.
  top_half <- tibble(x = seq(0, pi*r, length.out = steps)) %>%
    mutate(h = r * tan(theta * pi / 180) * sin(x / r))
  bottom_half <- top_half %>%
    mutate(x = x + pi*r,
           h = -h)
  top_half %>%
    bind_rows(bottom_half) %>%
    rename(y = h)
}
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, expand = c(0, 0)) +
    scale_y_continuous(limits = ylims, expand = c(0, 0))
}
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()
  })

When these grobs are exported as pdf on US Letter in landscape mode and printed out at 100% scale the dimensions on paper match exactly those intended: actual millimeters. Now I have a mitering pattern.

Gabi
  • 1,303
  • 16
  • 20