r-lib / isoband

isoband: An R package to generate contour lines and polygons.
https://isoband.r-lib.org
Other
130 stars 14 forks source link

Brainstorming for isolines_grob() API #3

Closed clauswilke closed 5 years ago

clauswilke commented 5 years ago

The isoband_grob() function generates a grob that can draw labeled isolines:

library(isoband)
library(grid)

x <- (1:ncol(volcano))/(ncol(volcano)+1)
y <- (nrow(volcano):1)/(nrow(volcano)+1)
lines <- isolines(x, y, volcano, 5*(20:38))

# draw labeled lines
grid.newpage()
grid.draw(isolines_grob(lines, breaks = 20*(5:10)))

The question is how to best provide graphical parameters. Currently, there is one gpar() argument, which applies to lines and text equally, and it recycles settings both among and within isolines:

grid.newpage()
grid.draw(
  isolines_grob(
    lines, breaks = 20*(5:10),
    gp = gpar(
      col = c("red", "green", "blue"),
      fontfamily = "Times", lty = 2, fontsize = 12
    )
  )
)

Clearly we need to do better. There are a couple of different options:

  1. Provide a list of gpar() arguments, one for each isoline, and one separately for text.
  2. Use only one gpar() argument but recycle graphical parameters only among isolines, never within.
  3. Provide graphical parameters in a large table, with one row per isoline and one column per parameter (color, line width, etc)

There's also the separate question of whether it is reasonable to assume that the font settings (font face, family, size) are constant throughout the entire grob or whether we should be able to change those settings for different labels. The current code allows these things to vary among individual labels, mostly by accident.

grid.newpage()
grid.draw(
  isolines_grob(
    lines, breaks = 20*(5:10),
    gp = gpar(
      fontfamily = c("Times", "Helvetica"),
      fontface = 1:4, 
      fontsize = c(12, 14, 20)
    )
  )
)

yutannihilation commented 5 years ago

Is it possible to make the lines and texts seperate grobs? Then, we can specify gpar() for them seperately. (Disclaimer: I'm not familiar with designing a function for grid...)

clauswilke commented 5 years ago

No. I need to clip the lines to the extent of the text labels, and I need to do so dynamically if the window is resized etc. As a consequence, lines and text labels need to be drawn together.

As I'm thinking more about it, I'm thinking that maybe option 2 is best. It would be most similar to how other grobs work. I would need one extra argument that allows users to override the text color in case they want it to be different from the line colors. Other than that, none of the line parameters overlap with the text parameters, as far as I can see.

yutannihilation commented 5 years ago

I need to do so dynamically

Ah, I got it! Thanks. Then I agree option 2 + the text colour is enough.

clauswilke commented 5 years ago

Apart from some grid awkwardnesses, coding this wasn't too bad, and I think the behavior is reasonably logical.

library(isoband)
library(grid)

x <- (1:ncol(volcano))/(ncol(volcano)+1)
y <- (nrow(volcano):1)/(nrow(volcano)+1)
lines <- isolines(x, y, volcano, 5*(20:38))

# draw labeled lines
g <- isolines_grob(
  lines, breaks = 20*(5:10),
  gp = gpar(
    lwd = c(2, 1, 1, 1),
    col = c("red", "green", "blue"),
    fontfamily = "Times", fontsize = c(8, 10, 12, 14, 16)
  )
)

grid.newpage()
pushViewport(viewport(gp = gpar(fontface = "bold", lty = c(1, 2, 2, 2))))
grid.draw(g)


g <- isolines_grob(
  lines, breaks = 20*(5:10),
  gp = gpar(
    lwd = c(2, 1, 1, 1),
    col = c("grey20", "grey50", "grey50", "grey50")
  ),
  label_col = "blue"
)

grid.newpage()
grid.draw(g)

Created on 2019-02-17 by the reprex package (v0.2.1)

clauswilke commented 5 years ago

I'm quite satisfied with this. I think it'll work as needed.

library(isoband)
library(grid)

viridis_pal <- colorRampPalette(
  c("#440154", "#414487", "#2A788E", "#22A884", "#7AD151", "#FDE725"),
  space = "Lab"
)

x <- (1:ncol(volcano))/(ncol(volcano)+1)
y <- (nrow(volcano):1)/(nrow(volcano)+1)
lines <- isolines(x, y, volcano, 5*(19:38))
bands <- isobands(x, y, volcano, 5*(18:38), 5*(19:39))

b <- isobands_grob(
  bands,
  gp = gpar(col = NA, fill = viridis_pal(21), alpha = 0.4)
)
l <- isolines_grob(
  lines, breaks = 20*(5:10),
  gp = gpar(
    lwd = c(.3, 1, .3, .3)
  )
)

grid.newpage()
grid.draw(b)
grid.draw(l)

Created on 2019-02-17 by the reprex package (v0.2.1)

eliocamp commented 5 years ago

This is stellar work! I should deprecate geom_contour_fill() and related functions from metR 😭️.

If I may chime in, is there a way of customising label placing and overall behaviour? For example, setting if text should be rotated to follow lines or not.

clauswilke commented 5 years ago

That doesn't currently exist but it's on my todo list. I want the code to be modular so it's easy to change the logic of the label placement.

clauswilke commented 5 years ago

I now have a basic framework that allows customization of label placement. More sophisticated or alternative label placement strategies can be implemented in the future. I'm closing this issue because I think the overall API is fine at this point.

library(isoband)
library(grid)

viridis_pal <- colorRampPalette(
  c("#440154", "#414487", "#2A788E", "#22A884", "#7AD151", "#FDE725"),
  space = "Lab"
)

x <- (1:ncol(volcano))/(ncol(volcano)+1)
y <- (nrow(volcano):1)/(nrow(volcano)+1)
lines <- isolines(x, y, volcano, 5*(19:38))
bands <- isobands(x, y, volcano, 5*(18:38), 5*(19:39))

b <- isobands_grob(
  bands,
  gp = gpar(col = NA, fill = viridis_pal(21), alpha = 0.4)
)
l <- isolines_grob(
  lines, breaks = 20*(5:10),
  gp = gpar(
    lwd = c(.3, 1, .3, .3)
  ),
  label_placer = label_placer_minmax(placement = "tl", rot_adjuster = angle_fixed())
)

grid.newpage()
grid.draw(b)
grid.draw(l)

Created on 2019-04-01 by the reprex package (v0.2.1)