Skip to content

Commit 3124420

Browse files
committed
Add sequential colorscale for ggplot2 based on the accent color
Also, some R CMD check fixes and other cleanup
1 parent 59fe934 commit 3124420

File tree

5 files changed

+112
-79
lines changed

5 files changed

+112
-79
lines changed

R/render-plot.R

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@
5151
#' (`bg`), foreground (`fg`), and accent (`accent`) colors inherit from the
5252
#' plot's containing HTML element(s)' CSS styling. When `autoTheme` is `TRUE`
5353
#' (or a list options), default theming rules are applied ggplot2, lattice, and
54-
#' base graphics. In addition, a `qualitative` color palette is set for each
55-
#' plotting framework to ensure a consistent and colour-blind safe palette.
56-
#' For `qualitative`, as well as (`fg`/`bg`/`accent`), you may supply your own
57-
#' color codes to override the defaults, or supply `NA` to prevent auto-theming
58-
#' logic from being applied
59-
#' (e.g., `autoTheme = list(accent="red", qualitative=NA)`).
54+
#' base graphics. Additionally, under certain conditions, `sequential` and
55+
#' `qualitative` color palettes are also set. The default `sequential` palette
56+
#' derives from the `accent` color, whereas the `qualitative` palette is based
57+
#' on the Okabe-Ito scale. To control auto-theming defaults, pass a list of
58+
#' options with the desired color codes (and/or `NA` to use plotting framework's
59+
#' defaults instead of the auto-theming defaults). For example,
60+
#' `autoTheme = list(accent="red", sequential=NA)` sets the `accent` to `"red"`,
61+
#' but also ensures ggplot2's sequential colorscale defaults still apply.
6062
#' @param outputArgs A list of arguments to be passed through to the implicit
6163
#' call to [plotOutput()] when `renderPlot` is used in an
6264
#' interactive R Markdown document.
@@ -287,10 +289,10 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, theme
287289
# NULL is the normal case, but in case any of the param setting calls
288290
# threw an error; in that case, not all of these four may have been
289291
# performed.
290-
if (!is.null(base_params)) { do.call(par, base_params) }
292+
if (!is.null(base_params)) { do.call(graphics::par, base_params) }
291293
if (!is.null(grid_params)) { do.call(grid::gpar, grid_params) }
292294
if (!is.null(lattice_params)) { lattice_set_par_list(lattice_params) }
293-
if (!is.null(old_palette)) { palette(old_palette) }
295+
if (!is.null(old_palette)) { grDevices::palette(old_palette) }
294296

295297
grDevices::dev.off(device)
296298
}
@@ -336,11 +338,11 @@ base_set_params <- function(theme) {
336338
params <- list()
337339
bg <- theme$bg
338340
if (!is.null(bg)) {
339-
params <- c(params, par(bg = bg))
341+
params <- c(params, graphics::par(bg = bg))
340342
}
341343
fg <- theme$fg
342344
if (!is.null(fg)) {
343-
params <- c(params, par(
345+
params <- c(params, graphics::par(
344346
fg = fg,
345347
col.axis = fg,
346348
col.lab = fg,
@@ -358,18 +360,23 @@ grid_set_params <- function(theme) {
358360

359361
lattice_set_params <- function(theme) {
360362
if (system.file(package = "lattice") == "") return()
361-
old_par <- lattice::trellis.par.get()
363+
old_par <- utils::getFromNamespace("trellis.par.get", "lattice")()
362364
bg <- theme$bg
363365
fg <- theme$fg
364366

365-
lattice::trellis.par.set(
367+
par_set <- utils::getFromNamespace("trellis.par.set", "lattice")
368+
par_set(
366369
# See figure 9.3 for an example of where grid gpar matters
367370
# http://lmdvr.r-forge.r-project.org/figures/figures.html
368371
grid.pars = list(col = fg),
369372
background = list(col = bg),
370373
reference.line = list(col = bg),
371-
panel.background = list(col = setAlpha(fg, 0.1)),
372-
strip.background = list(col = setAlpha(fg, 0.2)),
374+
panel.background = list(
375+
col = mix_colors(theme$bg, theme$fg, 0.1)
376+
),
377+
strip.background = list(
378+
col = mix_colors(theme$bg, theme$fg, 0.2)
379+
),
373380
strip.border = list(col = fg),
374381
axis.line = list(col = fg),
375382
axis.text = list(col = fg),
@@ -384,14 +391,16 @@ lattice_set_params <- function(theme) {
384391
plot.polygon = list(border = fg),
385392
superpose.polygon = list(border = fg),
386393
box.dot = list(col = fg),
387-
dot.line = list(col = setAlpha(fg, 0.2))
394+
dot.line = list(
395+
col = mix_colors(theme$bg, theme$fg, 0.2)
396+
)
388397
)
389398

390399
# For lattice, accent can be of length 2, one to specify
391400
# 'stroke' accent and one for fill accent
392401
accent <- rep(theme$accent, length.out = 2)
393402
if (sum(is.na(accent)) == 0) {
394-
lattice::trellis.par.set(
403+
par_set(
395404
plot.line = list(col = accent[[1]]),
396405
plot.symbol = list(col = accent[[1]]),
397406
dot.symbol = list(col = accent[[1]]),
@@ -405,8 +414,8 @@ lattice_set_params <- function(theme) {
405414
qualitative <- getQualitativeCodes(theme, 7)
406415
if (sum(is.na(qualitative)) == 0) {
407416
# I'm not in love with the idea of this; but alas, it's consistent with lattice's default
408-
region_pal <- colorRampPalette(c(qualitative[[1]], "white", qualitative[[2]]))
409-
lattice::trellis.par.set(
417+
region_pal <- grDevices::colorRampPalette(c(qualitative[[1]], "white", qualitative[[2]]))
418+
par_set(
410419
strip.shingle = list(col = qualitative),
411420
regions = list(col = region_pal(100)),
412421
superpose.line = list(col = qualitative),
@@ -420,33 +429,56 @@ lattice_set_params <- function(theme) {
420429

421430
lattice_set_par_list <- function(params) {
422431
if (system.file(package = "lattice") == "") return()
423-
lattice::trellis.par.set(theme = params)
432+
utils::getFromNamespace("trellis.par.set", "lattice")(theme = params)
424433
}
425434

426435
base_set_palette <- function(theme) {
427436
codes <- getQualitativeCodes(theme)
428-
if (isTRUE(is.na(codes))) palette() else palette(codes)
437+
if (isTRUE(is.na(codes))) grDevices::palette() else grDevices::palette(codes)
429438
}
430439

431440
getQualitativeCodes <- function(theme, n = NULL) {
432441
qualitative <- theme$qualitative
433442
if (isTRUE(is.na(qualitative)) || is.character(qualitative)) {
434443
return(qualitative)
435444
}
445+
# https://jfly.uni-koeln.de/color/
436446
# TODO: use another colorscale in dark mode?
447+
okabeIto <- c("#E69F00", "#009E73", "#0072B2", "#CC79A7", "#999999", "#D55E00", "#F0E442", "#56B4E9")
437448
if (is.null(n)) okabeIto else okabeIto[seq_len(n)]
438449
}
439450

440-
# https://jfly.uni-koeln.de/color/
441-
okabeIto <- c("#E69F00", "#009E73", "#0072B2", "#CC79A7", "#999999", "#D55E00", "#F0E442", "#56B4E9")
451+
# Currently only used for ggplot2
452+
getSequentialCodes <- function(theme, n = 8) {
453+
sequential <- theme$sequential
454+
if (isTRUE(is.na(sequential)) || is.character(sequential)) {
455+
return(sequential)
456+
}
457+
# This shouldn't really happen since ggplot2 depends on scales
458+
# (and this is only called in the ggplot2 case)
459+
if (system.file(package = "farver") == "") {
460+
warning("Computing default sequential codes (for autoTheme) requires the farver package.")
461+
return(NA)
462+
}
463+
decode_colour <- utils::getFromNamespace("decode_colour", "farver")
464+
if (system.file(package = "colorspace") == "") {
465+
warning("Computing default sequential codes (for autoTheme) requires the colorspace package.")
466+
return(NA)
467+
}
468+
sequential_hcl <- utils::getFromNamespace("sequential_hcl", "colorspace")
469+
hcl <- as.list(decode_colour(theme$accent, to = "hcl")[1, ])
470+
l <- c(hcl$l - 20, hcl$l + 20)
471+
c <- c(hcl$c + 20, hcl$c - 20)
472+
sequential_hcl(n = n, h = hcl$h, c = c, l = l)
473+
}
442474

443475
# A modified version of print.ggplot which returns the built ggplot object
444476
# as well as the gtable grob. This overrides the ggplot::print.ggplot
445477
# method, but only within the context of renderPlot. The reason this needs
446478
# to be a (pseudo) S3 method is so that, if an object has a class in
447479
# addition to ggplot, and there's a print method for that class, that we
448480
# won't override that method. https://github.com/rstudio/shiny/issues/841
449-
custom_print.ggplot <- function(theme) {
481+
custom_print.ggplot <- function(theme = list()) {
450482
function(x) {
451483
build <- ggplot_build_with_theme(x, theme)
452484
gtable <- ggplot2::ggplot_gtable(build)
@@ -464,7 +496,7 @@ custom_print.ggplot <- function(theme) {
464496
# use this function with a custom ggplot_build function (e.g. plotly) can do so
465497
# and geom defaults will still be restored after building
466498
ggplot_build_with_theme <- function(p, theme, ggplot_build = ggplot2::ggplot_build, newpage = TRUE) {
467-
if (is.null(theme)) return(ggplot_build(p))
499+
if (!length(theme)) return(ggplot_build(p))
468500
fg <- theme$fg
469501
bg <- theme$bg
470502
# Accent can be of length 2 because lattice
@@ -482,7 +514,10 @@ ggplot_build_with_theme <- function(p, theme, ggplot_build = ggplot2::ggplot_bui
482514
# from 'lower-level' geoms, like GeomPoint, GeomLine, GeomPolygon
483515
geoms <- c(
484516
lapply(p$layers, function(x) x$geom),
485-
lapply(c("GeomPoint", "GeomLine", "GeomPolygon"), getFromNamespace, "ggplot2")
517+
lapply(
518+
c("GeomPoint", "GeomLine", "GeomPolygon"),
519+
utils::getFromNamespace, "ggplot2"
520+
)
486521
)
487522

488523
# Remember defaults
@@ -520,30 +555,30 @@ ggplot_build_with_theme <- function(p, theme, ggplot_build = ggplot2::ggplot_bui
520555
}
521556

522557
ggtheme_auto <- function(bg, fg) {
523-
text <- element_text(colour = fg)
524-
line <- element_line(colour = fg)
525-
themeGray <- theme_gray()
558+
text <- ggplot2::element_text(colour = fg)
559+
line <- ggplot2::element_line(colour = fg)
560+
themeGray <- ggplot2::theme_gray()
526561

527-
theme(
562+
ggplot2::theme(
528563
line = line,
529564
text = text,
530565
axis.title = text,
531566
axis.text = text,
532567
axis.ticks = line,
533-
plot.background = element_rect(fill = bg, colour = "transparent"),
534-
panel.background = element_rect(
568+
plot.background = ggplot2::element_rect(fill = bg, colour = "transparent"),
569+
panel.background = ggplot2::element_rect(
535570
fill = adjust_color(themeGray$panel.background$fill, bg, fg)
536571
),
537-
panel.grid = element_line(colour = bg),
538-
legend.background = element_rect(fill = "transparent"),
539-
legend.box.background = element_rect(
572+
panel.grid = ggplot2::element_line(colour = bg),
573+
legend.background = ggplot2::element_rect(fill = "transparent"),
574+
legend.box.background = ggplot2::element_rect(
540575
fill = "transparent", colour = "transparent"
541576
),
542-
legend.key = element_rect(
577+
legend.key = ggplot2::element_rect(
543578
fill = adjust_color(themeGray$legend.key$fill, bg, fg),
544579
colour = bg
545580
),
546-
strip.background = element_rect(
581+
strip.background = ggplot2::element_rect(
547582
fill = adjust_color(themeGray$strip.background$fill, bg, fg)
548583
),
549584
strip.text = text
@@ -561,11 +596,9 @@ adjust_color <- function(color, bg, fg, accent = NA) {
561596

562597
# If a gray scale color, then the degree of gray determines
563598
# the mixing between fg (aka black) and bg (aka white)
564-
rgbs <- col2rgb(color, alpha = TRUE)[1:3,1]
599+
rgbs <- grDevices::col2rgb(color, alpha = TRUE)[1:3,1]
565600
if (sum(diff(rgbs)) == 0) {
566601
return(mix_colors(bg, fg, 1 - (rgbs[1] / 255)))
567-
# IDEA: instead of mixing colors with a colorRamp, perhaps it's better to adjust luminance?
568-
# return(scales::col2hcl(bg, l = luminance(color)))
569602
}
570603

571604
# At this point we should be dealing with an accent color...
@@ -576,7 +609,7 @@ adjust_color <- function(color, bg, fg, accent = NA) {
576609

577610
mix_colors <- function(bg, fg, amount) {
578611
if (!length(bg) || !length(fg)) return(NULL)
579-
mid_color <- colorRamp(c(bg, fg), alpha = TRUE)(amount)
612+
mid_color <- grDevices::colorRamp(c(bg, fg), alpha = TRUE)(amount)
580613
sprintf(
581614
"#%02X%02X%02X%02X",
582615
round(mid_color[1,1]),
@@ -589,9 +622,6 @@ mix_colors <- function(bg, fg, amount) {
589622
add_scale_defaults <- function(p, aesthetic = "colour", theme) {
590623
# If user has specified this scale type, then do nothing
591624
if (p$scales$has_scale(aesthetic)) return(p)
592-
# If palette is explicit NA, do nothing
593-
codes <- getQualitativeCodes(theme)
594-
if (isTRUE(is.na(codes))) return(p)
595625

596626
# Obtain the input values to the scale
597627
values <- lapply(p$layers, function(x) {
@@ -600,15 +630,27 @@ add_scale_defaults <- function(p, aesthetic = "colour", theme) {
600630
rlang::eval_tidy(aes_map[[aesthetic]], data)
601631
})
602632

603-
# At the moment, we only set a default for qualitative scales
604-
isQualitative <- all(vapply(values, function(x) is_discrete(x) && !is.ordered(x), logical(1)))
605-
if (!isQualitative) return(p)
633+
# Apply sequential default, if relevant
634+
isSequential <- all(vapply(values, is.numeric, logical(1)))
635+
if (isSequential) {
636+
seqCodes <- getSequentialCodes(theme)
637+
if (!isTRUE(is.na(seqCodes))) {
638+
f <- match.fun(paste0("scale_", aesthetic, "_gradientn"))
639+
p <- p + f(colors = seqCodes)
640+
}
641+
}
606642

607-
# Only apply scale if we have enough codes for it
608-
n <- length(unique(unlist(values)))
609-
if (n <= length(codes)) {
610-
f <- match.fun(paste0("scale_", aesthetic, "_manual"))
611-
p <- p + f(values = codes)
643+
# Apply qualitative default, if relevant (and we have enough codes)
644+
isQualitative <- all(vapply(values, function(x) is_discrete(x) && !is.ordered(x), logical(1)))
645+
if (isQualitative) {
646+
qualCodes <- getQualitativeCodes(theme)
647+
if (!isTRUE(is.na(qualCodes))) {
648+
n <- length(unique(unlist(values)))
649+
if (n <= length(qualCodes)) {
650+
f <- match.fun(paste0("scale_", aesthetic, "_manual"))
651+
p <- p + f(values = qualCodes)
652+
}
653+
}
612654
}
613655

614656
p
@@ -622,7 +664,7 @@ is_discrete <- function(x) {
622664
# ala Bootstrap's color-yiq()
623665
# https://getbootstrap.com/docs/4.4/getting-started/theming/#color-contrast
624666
color_yiq <- function(color) {
625-
rgb <- col2rgb(color)
667+
rgb <- grDevices::col2rgb(color)
626668
unname(
627669
(rgb["red", ] * 299 + rgb["green", ] * 587 + rgb["blue", ] * 114) / 1000
628670
)
@@ -791,6 +833,7 @@ color_yiq_islight <- function(color, threshold = 150) {
791833

792834
getCoordmap <- function(x, width, height, res) {
793835
if (inherits(x, "ggplot_build_gtable")) {
836+
794837
getGgplotCoordmap(x, width, height, res)
795838
} else {
796839
getPrevPlotCoordmap(width, height)
@@ -849,7 +892,6 @@ getPrevPlotCoordmap <- function(width, height) {
849892
getGgplotCoordmap <- function(p, width, height, res) {
850893
if (!inherits(p, "ggplot_build_gtable"))
851894
return(NULL)
852-
853895
tryCatch({
854896
# Get info from built ggplot object
855897
panel_info <- find_panel_info(p$build)

R/utils.R

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,18 +1812,6 @@ cat_line <- function(...) {
18121812
cat(paste(..., "\n", collapse = ""))
18131813
}
18141814

1815-
setAlpha <- function(colorStr, alpha = 1) {
1816-
colors <- t(col2rgb(colorStr, alpha = FALSE))
1817-
result <- sprintf("#%02X%02X%02X%02X",
1818-
colors[,"red"],
1819-
colors[,"green"],
1820-
colors[,"blue"],
1821-
round(pmin(1, pmax(0, alpha)) * 255)
1822-
)
1823-
result[is.na(colorStr)] <- NA_character_
1824-
result
1825-
}
1826-
18271815
# Test cases:
18281816
# Leading/trailing spaces
18291817
# Spaces around commas

man/renderCachedPlot.Rd

Lines changed: 8 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/renderPlot.Rd

Lines changed: 8 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-plot-coordmap.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ sortList <- function(x) {
77
}
88

99
# This will create print.ggplot in the current environment
10-
print.ggplot <- custom_print.ggplot
11-
10+
print.ggplot <- custom_print.ggplot()
1211

1312
test_that("ggplot coordmap", {
1413
dat <- data.frame(xvar = c(0, 5), yvar = c(10, 20))

0 commit comments

Comments
 (0)