From a833650c20b075aa954e52d0d54b490ede61a0e8 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 08:54:07 -0700 Subject: [PATCH 01/22] type_tile --- R/type_tile.R | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 R/type_tile.R diff --git a/R/type_tile.R b/R/type_tile.R new file mode 100644 index 00000000..4be45385 --- /dev/null +++ b/R/type_tile.R @@ -0,0 +1,182 @@ +#' Tile (heatmap) plot type +#' +#' @description Type function for tile plots, i.e. a grid of rectangles whose +#' fill colour encodes a third variable. This is the standard building block +#' of heatmaps, correlation matrices, and calendar plots. `type_heatmap` is an +#' alias for `type_tile`. +#' +#' @details Tile plots are specified as `z ~ x` with the fill variable passed as +#' the `by` grouping, i.e. `tinyplot(y ~ x | z, type = "tile")`. The `x` and +#' `y` variables may be factors, characters, or numerics; the `by` variable +#' supplies the fill and will typically be numeric, yielding a continuous +#' colour gradient and colourbar legend. Omitting `by` leaves the tiles +#' unfilled, since there is nothing for the fill to encode; pass an explicit +#' `fill` (or `bg`) if you want a uniform colour in that case. +#' +#' Unlike the closely-related \code{\link{type_rect}}, which requires explicit +#' `xmin`/`xmax`/`ymin`/`ymax` bounds, `type_tile()` derives the tile bounds +#' for you: each tile is centred on its `x`/`y` position and extends +#' `width/2` and `height/2` in each direction. Categorical axes are converted +#' to consecutive integer positions and the axis tick labels are taken from +#' the factor levels automatically. +#' +#' Explicit bounds still take precedence. Passing any of `xmin`, `xmax`, +#' `ymin`, or `ymax` leaves that dimension untouched, which is useful for +#' irregular or unequal-width tiles (e.g. binned continuous data). Bounds may +#' be given for one axis while the other is derived. +#' +#' Note that tiles are opaque and drawn edge-to-edge, so the default axis +#' padding and grid lines of most themes are redundant (and the grid is hidden +#' behind the tiles in any case). We therefore ship a dedicated `"heatmap"` +#' theme that removes the padding and grid, rotates the tick labels, and +#' switches to a sequential palette. See [`tinytheme()`] and the Examples. +#' +#' @param width,height Numeric tile dimensions in data units. Both default to +#' `1`, which produces contiguous tiles on categorical (or unit-spaced +#' numeric) axes. Values below `1` inset the tiles, leaving gaps between them. +#' Recycled across tiles, so a vector may be used for variable sizes. +#' +#' @examples +#' # It is recommended to use the dedicated "heatmap" theme for this type +#' tinytheme("heatmap") +#' +#' # Correlation matrix of the base `attitude` dataset. The `x` and `y` +#' # variables are factors, so tile bounds and axis labels are derived +#' # automatically. +#' catt = as.data.frame(cor(attitude)) +#' catt = cbind( +#' stack(catt), +#' ind2 = factor(row.names(catt), levels = row.names(catt)) +#' ) +#' +#' tinyplot(ind2 ~ ind | values, data = catt, type = "tile") +#' +#' # slightly fancier version, where we suppress the legend but layer on the values +#' # as text +#' tinyplot( +#' ind2 ~ ind | values, data = catt, +#' type = "tile", +#' legend = FALSE, +#' xlab = NA, ylab = NA, +#' main = "Correlation matrix of base attitude dataset" +#' ) +#' tinyplot_add(type = "text", labels = round(catt$values, 2), col = "white") +#' +#' # aside: "heatmap" is an alias for "tile" +#' tinyplot(ind2 ~ ind | values, data = catt, type = "heatmap") +#' +#' # Pass scaled tile widths and heights through type_tile() for a gridded look +#' tinyplot( +#' ind2 ~ ind | values, data = catt, +#' type = type_tile(width = 0.9, height = 0.9) +#' ) +#' +#' # Numeric axes work too, e.g. a (long-format) matrix of volcano heights +#' volc = data.frame( +#' x = as.vector(row(volcano)), +#' y = as.vector(col(volcano)), +#' elevation = as.vector(volcano) +#' ) +#' tinyplot( +#' y ~ x | elevation, data = volc, +#' type = "tile", +#' theme = "void", # void theme looks better with this numeric example +#' xlab = NA, ylab = NA, +#' main = "Maunga Whau volcano" +#' ) +#' +#' ## restore the default theme +#' tinytheme() +#' +#' @seealso \code{\link{type_rect}} for the lower-level rectangle type that +#' `type_tile()` builds on, and [`tinytheme()`] for the companion `"heatmap"` +#' theme. +#' +#' @export +type_tile = function(width = 1, height = 1) { + assert_numeric(width) + assert_numeric(height) + out = list( + draw = draw_rect(), + data = data_tile(width = width, height = height), + name = "tile" + ) + class(out) = "tinyplot_type" + return(out) +} + +#' @rdname type_tile +#' @export +type_heatmap = type_tile + + +data_tile = function(width = 1, height = 1) { + fun = function(settings, ...) { + env2env( + settings, + environment(), + c("datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg", "fill", "null_by") + ) + + # Tiles are a filled mark: the `by` variable encodes the *fill*, not the + # outline. Default `bg` to the palette so that a bare `type = "tile"` is + # filled, matching how the user would otherwise have to spell it out with + # `fill = "by"`. An explicit bg/fill still wins. + # + # Without a `by` variable there is nothing for the fill to encode, so leave + # the tiles unfilled (cf. type_rect()) rather than flooding every one with + # the same flat colour, which would read as a solid black grid. + if (is.null(bg) && is.null(fill) && !isTRUE(null_by)) bg = "by" + + # A categorical axis carries its own tick labels, so convert to consecutive + # integer positions and hand the levels off to the axis machinery. Numeric + # axes are already positional and keep their default (computed) ticks. + for (ax in c("x", "y")) { + v = datapoints[[ax]] + if (is.null(v) || !(is.factor(v) || is.character(v))) next + if (!is.factor(v)) v = factor(v) + labs = seq_along(levels(v)) + names(labs) = levels(v) + datapoints[[ax]] = as.numeric(v) + if (ax == "x") { + xlabs = xlabs %||% labs + # cf. data_barplot(): "l" keeps the labels but drops the tick marks, + # which have no meaning for a categorical position. + if (identical(xaxt, "s")) xaxt = "l" + } else { + ylabs = ylabs %||% labs + if (identical(yaxt, "s")) yaxt = "l" + } + } + + # Derive the tile bounds, but never clobber user-supplied ones: an explicit + # xmin/xmax (or ymin/ymax) is how irregular or unequally-sized tiles get + # specified, so each axis is derived only if *both* of its bounds are absent. + if (is.null(datapoints[["xmin"]]) && is.null(datapoints[["xmax"]])) { + w = rep_len(width, nrow(datapoints)) / 2 + datapoints[["xmin"]] = datapoints[["x"]] - w + datapoints[["xmax"]] = datapoints[["x"]] + w + } + if (is.null(datapoints[["ymin"]]) && is.null(datapoints[["ymax"]])) { + h = rep_len(height, nrow(datapoints)) / 2 + datapoints[["ymin"]] = datapoints[["y"]] - h + datapoints[["ymax"]] = datapoints[["y"]] + h + } + + # Match type_rect()'s legend keys for the discrete case. A numeric `by` + # renders a colourbar instead, where these are simply ignored. + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% par("lwd") + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + + env2env( + environment(), + settings, + c("datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg") + ) + } + return(fun) +} From 5fea9950a0115e9cd26ce764bbca9909ad89c3be Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 08:57:46 -0700 Subject: [PATCH 02/22] tweak to hexbin example while we're at it --- R/type_hexbin.R | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/R/type_hexbin.R b/R/type_hexbin.R index 28a5cd06..8f19beec 100644 --- a/R/type_hexbin.R +++ b/R/type_hexbin.R @@ -101,12 +101,11 @@ #' # 2) Continuous grouping variable: each cell is coloured by its mean. #' # Example: Create a long version of the `volcano` dataset, and plot its #' # elevations onto a gridded terrain map. -#' volc = local({ -#' v = setNames(stack(as.data.frame(volcano)), c("elevation", "y")) -#' v$y = as.numeric(gsub("^V", "", v$y)) -#' v$x = seq_len(nrow(volcano)) -#' v -#' }) +#' volc = data.frame( +#' x = as.vector(row(volcano)), +#' y = as.vector(col(volcano)), +#' elevation = as.vector(volcano) +#' ) #' tinyplot( #' y ~ x | elevation, data = volc, #' type = "hexbin", xbins = 50, From b6976c38746d8120c09bd3c71563de1486ac3b4a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 08:58:28 -0700 Subject: [PATCH 03/22] heatmap theme --- R/tinytheme.R | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/R/tinytheme.R b/R/tinytheme.R index 6840722b..b56ac61f 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -33,6 +33,7 @@ #' - `"tufte"` (*): floating axes and minimalist plot artifacts in the style of Edward Tufte. #' - `"float"` (*): builds on `"tufte"` with outward ticks, fewer tick marks, and a "dark" qualitative palette. #' - `"void"` (*): switches off all axes, titles, legends, etc. +#' - `"heatmap"` (*): a specialized theme for tile plots and heatmaps (see [`type_tile()`]). Builds off of `"clean2"`, but removes the axis padding so that the tiles meet the panel edge, drops the (redundant) grid lines, rotates the tick labels and removes their tick marks, and defaults to the "tealgrn" sequential palette. Not recommended for non-tile plots. #' - `"ridge"` (*): a specialized theme for ridge plots (see [`type_ridge()`]). Builds off of `"clean"`, but adds ridge-specific tweaks (e.g. default "Zissou 1" palette for discrete colors, solid horizontal grid lines, and minor adjustments to y-axis labels). Not recommended for non-ridge plots. #' - `"ridge2"` (*): removes the plot frame (box) from `"ridge"`, but retains the x-axis line. Again, not recommended for non-ridge plots. #' @param ... Named arguments to override specific theme settings. These @@ -194,7 +195,7 @@ tinytheme = function( "clean", "clean2", "bw", "linedraw", "classic", "minimal", "ipsum", "ipsum2", "dark", "socviz", "broadsheet", "nber", "web", - "ridge", "ridge2", + "heatmap", "ridge", "ridge2", "tufte", "float", "void" ), ..., @@ -225,6 +226,7 @@ tinytheme = function( "ipsum2" = theme_ipsum2, "minimal" = theme_minimal, "nber" = theme_nber, + "heatmap" = theme_heatmap, "ridge" = theme_ridge, "ridge2" = theme_ridge2, "socviz" = theme_socviz, @@ -300,7 +302,7 @@ builtin_themes = c( "clean", "clean2", "bw", "linedraw", "classic", "minimal", "ipsum", "ipsum2", "dark", "socviz", "broadsheet", "nber", "web", - "ridge", "ridge2", + "heatmap", "ridge", "ridge2", "tufte", "float", "void" ) @@ -359,6 +361,12 @@ theme_default = list( side.sub = 1, tck = NA, tcl = par("tcl"), # -0.5 + # `theme_default` doubles as the reset baseline for tinytheme(), so every + # parameter that *any* theme sets has to appear here -- otherwise nothing + # restores it and the setting leaks into subsequent (incl. base) plots. The + # axis styles below are only touched by the "heatmap" theme so far. + xaxs = par("xaxs"), # "r" + yaxs = par("yaxs"), # "r" xaxt = "standard", yaxt = "standard" ) @@ -534,6 +542,21 @@ theme_dark = modifyList(theme_minimal, list( # derivatives of clean/clean2 +# Companion theme for type_tile() / type_heatmap(). Tiles are opaque and drawn +# edge-to-edge, so the usual axis padding leaves them floating inside the panel +# and the grid is hidden behind them regardless. Long categorical labels are the +# norm for correlation matrices, hence the rotated, tick-less axes. +theme_heatmap = modifyList(theme_clean2, list( + tinytheme = "heatmap", + gap.axis = 0, + grid = FALSE, + las = 2, + palette.sequential = "tealgrn", + tcl = 0, + xaxs = "i", + yaxs = "i" +)) + theme_ridge = modifyList(theme_clean, list( tinytheme = "ridge", col.default = "black", # keep black ridgelines; Zissou is for gradient fills From 51302493ef7b41d7f787ce577838daa5d55e9fd8 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 08:59:55 -0700 Subject: [PATCH 04/22] known type --- R/sanitize_type.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/sanitize_type.R b/R/sanitize_type.R index c2611456..f0be0ae5 100644 --- a/R/sanitize_type.R +++ b/R/sanitize_type.R @@ -50,6 +50,7 @@ sanitize_type = function(settings) { "spline", "summary", "text", + "tile", "heatmap", "violin", "vline" ) @@ -114,6 +115,8 @@ sanitize_type = function(settings) { "spline" = type_spline, "summary" = type_summary, "text" = type_text, + "tile" = type_tile, + "heatmap" = type_tile, "violin" = type_violin, "vline" = type_vline, type # default case (incl. line-family chars, handled below) From 3ad0e74c7ba9dea14b31182bef69afab7afce81a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 09:00:17 -0700 Subject: [PATCH 05/22] docs and namespace --- NAMESPACE | 2 + man/tinyplot-package.Rd | 1 + man/tinytheme.Rd | 3 +- man/type_hexbin.Rd | 11 ++-- man/type_tile.Rd | 108 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 man/type_tile.Rd diff --git a/NAMESPACE b/NAMESPACE index ce4e7391..9d72b654 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -29,6 +29,7 @@ export(type_ellipse) export(type_errorbar) export(type_function) export(type_glm) +export(type_heatmap) export(type_hexbin) export(type_hist) export(type_histogram) @@ -51,6 +52,7 @@ export(type_spineplot) export(type_spline) export(type_summary) export(type_text) +export(type_tile) export(type_violin) export(type_vline) importFrom(grDevices,adjustcolor) diff --git a/man/tinyplot-package.Rd b/man/tinyplot-package.Rd index 5684b661..1e46fc28 100644 --- a/man/tinyplot-package.Rd +++ b/man/tinyplot-package.Rd @@ -28,6 +28,7 @@ Authors: Other contributors: \itemize{ \item Etienne Bacher \email{etienne.bacher@protonmail.com} [contributor] + \item Miura Meng (\href{https://orcid.org/0009-0004-1522-1997}{ORCID}) [contributor] } } diff --git a/man/tinytheme.Rd b/man/tinytheme.Rd index 78542b61..cc5261db 100644 --- a/man/tinytheme.Rd +++ b/man/tinytheme.Rd @@ -7,7 +7,7 @@ tinytheme( theme = c("default", "basic", "dynamic", "clean", "clean2", "bw", "linedraw", "classic", "minimal", "ipsum", "ipsum2", "dark", "socviz", "broadsheet", "nber", - "web", "ridge", "ridge2", "tufte", "float", "void"), + "web", "heatmap", "ridge", "ridge2", "tufte", "float", "void"), ..., register = NULL ) @@ -50,6 +50,7 @@ dynamic plots are marked with an asterisk (*) below. \item \code{"float"} (*): builds on \code{"tufte"} with outward ticks, fewer tick marks, and a "dark" qualitative palette. } \item \code{"void"} (*): switches off all axes, titles, legends, etc. +\item \code{"heatmap"} (*): a specialized theme for tile plots and heatmaps (see \code{\link[=type_tile]{type_tile()}}). Builds off of \code{"clean2"}, but removes the axis padding so that the tiles meet the panel edge, drops the (redundant) grid lines, rotates the tick labels and removes their tick marks, and defaults to the "tealgrn" sequential palette. Not recommended for non-tile plots. \item \code{"ridge"} (*): a specialized theme for ridge plots (see \code{\link[=type_ridge]{type_ridge()}}). Builds off of \code{"clean"}, but adds ridge-specific tweaks (e.g. default "Zissou 1" palette for discrete colors, solid horizontal grid lines, and minor adjustments to y-axis labels). Not recommended for non-ridge plots. \itemize{ \item \code{"ridge2"} (*): removes the plot frame (box) from \code{"ridge"}, but retains the x-axis line. Again, not recommended for non-ridge plots. diff --git a/man/type_hexbin.Rd b/man/type_hexbin.Rd index e0f028b0..a5f9c463 100644 --- a/man/type_hexbin.Rd +++ b/man/type_hexbin.Rd @@ -110,12 +110,11 @@ tinyplot(y ~ x | g, data = dat, type = "hexbin") # 2) Continuous grouping variable: each cell is coloured by its mean. # Example: Create a long version of the `volcano` dataset, and plot its # elevations onto a gridded terrain map. -volc = local({ - v = setNames(stack(as.data.frame(volcano)), c("elevation", "y")) - v$y = as.numeric(gsub("^V", "", v$y)) - v$x = seq_len(nrow(volcano)) - v -}) +volc = data.frame( + x = as.vector(row(volcano)), + y = as.vector(col(volcano)), + elevation = as.vector(volcano) +) tinyplot( y ~ x | elevation, data = volc, type = "hexbin", xbins = 50, diff --git a/man/type_tile.Rd b/man/type_tile.Rd new file mode 100644 index 00000000..a9fcb05c --- /dev/null +++ b/man/type_tile.Rd @@ -0,0 +1,108 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/type_tile.R +\name{type_tile} +\alias{type_tile} +\alias{type_heatmap} +\title{Tile (heatmap) plot type} +\usage{ +type_tile(width = 1, height = 1) + +type_heatmap(width = 1, height = 1) +} +\arguments{ +\item{width, height}{Numeric tile dimensions in data units. Both default to +\code{1}, which produces contiguous tiles on categorical (or unit-spaced +numeric) axes. Values below \code{1} inset the tiles, leaving gaps between them. +Recycled across tiles, so a vector may be used for variable sizes.} +} +\description{ +Type function for tile plots, i.e. a grid of rectangles whose +fill colour encodes a third variable. This is the standard building block +of heatmaps, correlation matrices, and calendar plots. \code{type_heatmap} is an +alias for \code{type_tile}. +} +\details{ +Tile plots are specified as \code{z ~ x} with the fill variable passed as +the \code{by} grouping, i.e. \code{tinyplot(y ~ x | z, type = "tile")}. The \code{x} and +\code{y} variables may be factors, characters, or numerics; the \code{by} variable +supplies the fill and will typically be numeric, yielding a continuous +colour gradient and colourbar legend. Omitting \code{by} leaves the tiles +unfilled, since there is nothing for the fill to encode; pass an explicit +\code{fill} (or \code{bg}) if you want a uniform colour in that case. + +Unlike the closely-related \code{\link{type_rect}}, which requires explicit +\code{xmin}/\code{xmax}/\code{ymin}/\code{ymax} bounds, \code{type_tile()} derives the tile bounds +for you: each tile is centred on its \code{x}/\code{y} position and extends +\code{width/2} and \code{height/2} in each direction. Categorical axes are converted +to consecutive integer positions and the axis tick labels are taken from +the factor levels automatically. + +Explicit bounds still take precedence. Passing any of \code{xmin}, \code{xmax}, +\code{ymin}, or \code{ymax} leaves that dimension untouched, which is useful for +irregular or unequal-width tiles (e.g. binned continuous data). Bounds may +be given for one axis while the other is derived. + +Note that tiles are opaque and drawn edge-to-edge, so the default axis +padding and grid lines of most themes are redundant (and the grid is hidden +behind the tiles in any case). We therefore ship a dedicated \code{"heatmap"} +theme that removes the padding and grid, rotates the tick labels, and +switches to a sequential palette. See \code{\link[=tinytheme]{tinytheme()}} and the Examples. +} +\examples{ +# It is recommended to use the dedicated "heatmap" theme for this type +tinytheme("heatmap") + +# Correlation matrix of the base `attitude` dataset. The `x` and `y` +# variables are factors, so tile bounds and axis labels are derived +# automatically. +catt = as.data.frame(cor(attitude)) +catt = cbind( + stack(catt), + ind2 = factor(row.names(catt), levels = row.names(catt)) +) + +tinyplot(ind2 ~ ind | values, data = catt, type = "tile") + +# slightly fancier version, where we suppress the legend but layer on the values +# as text +tinyplot( + ind2 ~ ind | values, data = catt, + type = "tile", + legend = FALSE, + xlab = NA, ylab = NA, + main = "Correlation matrix of base attitude dataset" +) +tinyplot_add(type = "text", labels = round(catt$values, 2), col = "white") + +# aside: "heatmap" is an alias for "tile" +tinyplot(ind2 ~ ind | values, data = catt, type = "heatmap") + +# Pass scaled tile widths and heights through type_tile() for a gridded look +tinyplot( + ind2 ~ ind | values, data = catt, + type = type_tile(width = 0.9, height = 0.9) +) + +# Numeric axes work too, e.g. a (long-format) matrix of volcano heights +volc = data.frame( + x = as.vector(row(volcano)), + y = as.vector(col(volcano)), + elevation = as.vector(volcano) +) +tinyplot( + y ~ x | elevation, data = volc, + type = "tile", + theme = "void", # void theme looks better with this numeric example + xlab = NA, ylab = NA, + main = "Maunga Whau volcano" +) + +## restore the default theme +tinytheme() + +} +\seealso{ +\code{\link{type_rect}} for the lower-level rectangle type that +\code{type_tile()} builds on, and \code{\link[=tinytheme]{tinytheme()}} for the companion \code{"heatmap"} +theme. +} From da3f910d678a457bdb9086d72de2e54276525772 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 09:01:01 -0700 Subject: [PATCH 06/22] simplify null_y facet logic (test) --- R/facet.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/R/facet.R b/R/facet.R index 8843be5d..9e9e22fb 100644 --- a/R/facet.R +++ b/R/facet.R @@ -357,10 +357,13 @@ draw_facet_window = function( ) if (!is.null(xaxb)) args_x$at = xaxb if (!is.null(yaxb)) args_y$at = yaxb - # `xlabs` is only non-NULL when a type has placed categorical data on the - # x-axis, so its presence is the signal to draw labelled ticks. + # `xlabs`/`ylabs` are only non-NULL when a type has placed categorical data + # on that axis, so their presence is the signal to draw labelled ticks. + # The y-side previously listed the eligible types by name, but every type + # that populates `ylabs` does so precisely because it has categories to + # label, making the extra condition redundant (#665). type_range_x = !is.null(xlabs) - type_range_y = !is.null(ylabs) && (type == "p" || (isTRUE(flip) && type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "violin"))) + type_range_y = !is.null(ylabs) if (type_range_x) { args_x = modifyList(args_x, list(at = xlabs, labels = names(xlabs))) } From b7b71063bc462c8f4f9e54be64e2fa540e2dd147 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 12:08:39 -0700 Subject: [PATCH 07/22] doc tweak --- R/type_tile.R | 28 +++++++++++++++------------- man/type_tile.Rd | 28 +++++++++++++++------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/R/type_tile.R b/R/type_tile.R index 4be45385..ce4c06e1 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -40,36 +40,38 @@ #' # It is recommended to use the dedicated "heatmap" theme for this type #' tinytheme("heatmap") #' -#' # Correlation matrix of the base `attitude` dataset. The `x` and `y` -#' # variables are factors, so tile bounds and axis labels are derived -#' # automatically. -#' catt = as.data.frame(cor(attitude)) -#' catt = cbind( -#' stack(catt), -#' ind2 = factor(row.names(catt), levels = row.names(catt)) -#' ) +#' # Correlation matrix of the base `attitude` dataset in "long" form. +#' catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") #' -#' tinyplot(ind2 ~ ind | values, data = catt, type = "tile") +#' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") #' #' # slightly fancier version, where we suppress the legend but layer on the values #' # as text #' tinyplot( -#' ind2 ~ ind | values, data = catt, +#' Var1 ~ Var2 | Correlation, data = catt, #' type = "tile", #' legend = FALSE, #' xlab = NA, ylab = NA, #' main = "Correlation matrix of base attitude dataset" #' ) -#' tinyplot_add(type = "text", labels = round(catt$values, 2), col = "white") +#' tinyplot_add(type = "text", labels = round(catt$Correlation, 2), col = "white") #' #' # aside: "heatmap" is an alias for "tile" -#' tinyplot(ind2 ~ ind | values, data = catt, type = "heatmap") +#' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") #' #' # Pass scaled tile widths and heights through type_tile() for a gridded look #' tinyplot( -#' ind2 ~ ind | values, data = catt, +#' Var1 ~ Var2 | Correlation, data = catt, #' type = type_tile(width = 0.9, height = 0.9) #' ) +#' +#' # It doesn't really work for this example, but you can easily switch to a +#' # diverging palettes if it makes sense for your data +#' tinyplot( +#' Var1 ~ Var2 | Correlation, data = catt, +#' type = type_tile(width = 0.9, height = 0.9), +#' palette = "tropic" +#' ) #' #' # Numeric axes work too, e.g. a (long-format) matrix of volcano heights #' volc = data.frame( diff --git a/man/type_tile.Rd b/man/type_tile.Rd index a9fcb05c..d6863820 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -52,37 +52,39 @@ switches to a sequential palette. See \code{\link[=tinytheme]{tinytheme()}} and # It is recommended to use the dedicated "heatmap" theme for this type tinytheme("heatmap") -# Correlation matrix of the base `attitude` dataset. The `x` and `y` -# variables are factors, so tile bounds and axis labels are derived -# automatically. -catt = as.data.frame(cor(attitude)) -catt = cbind( - stack(catt), - ind2 = factor(row.names(catt), levels = row.names(catt)) -) +# Correlation matrix of the base `attitude` dataset in "long" form. +catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") -tinyplot(ind2 ~ ind | values, data = catt, type = "tile") +tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") # slightly fancier version, where we suppress the legend but layer on the values # as text tinyplot( - ind2 ~ ind | values, data = catt, + Var1 ~ Var2 | Correlation, data = catt, type = "tile", legend = FALSE, xlab = NA, ylab = NA, main = "Correlation matrix of base attitude dataset" ) -tinyplot_add(type = "text", labels = round(catt$values, 2), col = "white") +tinyplot_add(type = "text", labels = round(catt$Correlation, 2), col = "white") # aside: "heatmap" is an alias for "tile" -tinyplot(ind2 ~ ind | values, data = catt, type = "heatmap") +tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") # Pass scaled tile widths and heights through type_tile() for a gridded look tinyplot( - ind2 ~ ind | values, data = catt, + Var1 ~ Var2 | Correlation, data = catt, type = type_tile(width = 0.9, height = 0.9) ) +# It doesn't really work for this example, but you can easily switch to a +# diverging palettes if it makes sense for your data +tinyplot( + Var1 ~ Var2 | Correlation, data = catt, + type = type_tile(width = 0.9, height = 0.9), + palette = "tropic" +) + # Numeric axes work too, e.g. a (long-format) matrix of volcano heights volc = data.frame( x = as.vector(row(volcano)), From 9c15accad1cf18c0ba1cd7247823f1ca8ef9360b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 12:22:46 -0700 Subject: [PATCH 08/22] tests --- inst/tinytest/_tinysnapshot/tile_basic.svg | 124 + inst/tinytest/_tinysnapshot/tile_facet.svg | 101 + inst/tinytest/_tinysnapshot/tile_fancy.svg | 151 + .../_tinysnapshot/tile_numeric_axes.svg | 5365 +++++++++++++++++ .../_tinysnapshot/tinytheme_heatmap.svg | 97 + .../tinytheme_single_heatmap.svg | 83 + inst/tinytest/test-type_tile.R | 71 + vignettes/types.qmd | 1 + 8 files changed, 5993 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/tile_basic.svg create mode 100644 inst/tinytest/_tinysnapshot/tile_facet.svg create mode 100644 inst/tinytest/_tinysnapshot/tile_fancy.svg create mode 100644 inst/tinytest/_tinysnapshot/tile_numeric_axes.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_heatmap.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_heatmap.svg create mode 100644 inst/tinytest/test-type_tile.R diff --git a/inst/tinytest/_tinysnapshot/tile_basic.svg b/inst/tinytest/_tinysnapshot/tile_basic.svg new file mode 100644 index 00000000..50c29301 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tile_basic.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 +- - +- - +- - +- - +- - +Correlation + + + + + + + +Var2 +Var1 + + +rating +complaints +privileges +learning +raises +critical +advance +rating +complaints +privileges +learning +raises +critical +advance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tile_facet.svg b/inst/tinytest/_tinysnapshot/tile_facet.svg new file mode 100644 index 00000000..9eb7b7d7 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tile_facet.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + 4 + 8 + 12 +- - +- - +- - +v + + + + + + + +a +b + + + + + + + + + +x +y +z +p +q + +G1 + + + + + + + + + +x +y +z + +G2 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tile_fancy.svg b/inst/tinytest/_tinysnapshot/tile_fancy.svg new file mode 100644 index 00000000..3f18ca8c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tile_fancy.svg @@ -0,0 +1,151 @@ + + + + + + + + + + + + + +Correlation matrix of base attitude dataset +rating +complaints +privileges +learning +raises +critical +advance +rating +complaints +privileges +learning +raises +critical +advance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 +0.83 +0.43 +0.62 +0.59 +0.16 +0.16 +0.83 +1 +0.56 +0.6 +0.67 +0.19 +0.22 +0.43 +0.56 +1 +0.49 +0.45 +0.15 +0.34 +0.62 +0.6 +0.49 +1 +0.64 +0.12 +0.53 +0.59 +0.67 +0.45 +0.64 +1 +0.38 +0.57 +0.16 +0.19 +0.15 +0.12 +0.38 +1 +0.28 +0.16 +0.22 +0.34 +0.53 +0.57 +0.28 +1 + + + diff --git a/inst/tinytest/_tinysnapshot/tile_numeric_axes.svg b/inst/tinytest/_tinysnapshot/tile_numeric_axes.svg new file mode 100644 index 00000000..98fec62e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tile_numeric_axes.svg @@ -0,0 +1,5365 @@ + + + + + + + + + + + + + + + 100 + 120 + 140 + 160 + 180 +- - +- - +- - +- - +- - +elevation + + + + + + + +Maunga Whau volcano + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_heatmap.svg b/inst/tinytest/_tinysnapshot/tinytheme_heatmap.svg new file mode 100644 index 00000000..97cf61e1 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_heatmap.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("heatmap") + + + + + + + +Title of the plot +hp +mpg + + +100 +150 +200 +250 +300 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_heatmap.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_heatmap.svg new file mode 100644 index 00000000..46a6e6fd --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_heatmap.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + +tinytheme("heatmap") +Title of the plot +hp +mpg +100 +150 +200 +250 +300 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_tile.R b/inst/tinytest/test-type_tile.R new file mode 100644 index 00000000..570dca36 --- /dev/null +++ b/inst/tinytest/test-type_tile.R @@ -0,0 +1,71 @@ +source("helpers.R") +using("tinysnapshot") + +# shared fixture: correlation matrix of the base `attitude` dataset, in the same +# "long" form used by the type_tile() examples +catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") + +# "tile" and "heatmap" are aliases, as are type_tile() and type_heatmap(), +# so all four spellings must produce an identical plot. +f = function() { + tinyplot( + Var1 ~ Var2 | Correlation, data = catt, type = "tile", + theme = "heatmap" + ) +} +expect_snapshot_plot(f, label = "tile_basic") + +# "heatmap" alias (should be identical to the above) +f = function() { + tinyplot( + Var1 ~ Var2 | Correlation, data = catt, type = "heatmap", + theme = "heatmap" + ) +} +expect_snapshot_plot(f, label = "tile_basic") + +# fancy version, including gridded spacing and added labels +f = function() { + tinyplot( + Var1 ~ Var2 | Correlation, data = catt, + type = type_tile(width = 0.9, height = 0.9), + theme = "heatmap", + legend = FALSE, xlab = NA, ylab = NA, + main = "Correlation matrix of base attitude dataset" + ) + tinyplot_add( + type = "text", + labels = round(catt$Correlation, 2), + col = "white" + ) +} +expect_snapshot_plot(f, label = "tile_fancy") + +# numeric axes: no factor conversion, ticks stay numeric +volc = data.frame( + x = as.vector(row(volcano)), + y = as.vector(col(volcano)), + elevation = as.vector(volcano) +) +f = function() { + tinyplot( + y ~ x | elevation, data = volc, + type = "tile", + theme = "void", + xlab = NA, ylab = NA, + main = "Maunga Whau volcano" + ) +} +expect_snapshot_plot(f, label = "tile_numeric_axes") + +# faceting: categorical tick labels must survive on both axes in every panel +d = expand.grid( + a = factor(c("x", "y", "z")), + b = factor(c("p", "q")), + g = factor(c("G1", "G2")) +) +d$v = seq_len(nrow(d)) +f = function() { + tinyplot(b ~ a | v, facet = ~g, data = d, type = "tile", theme = "heatmap") +} +expect_snapshot_plot(f, label = "tile_facet") diff --git a/vignettes/types.qmd b/vignettes/types.qmd index 9cc943ae..0ca92afd 100644 --- a/vignettes/types.qmd +++ b/vignettes/types.qmd @@ -87,6 +87,7 @@ a convenience string (with default behaviour) or a companion `type_*()` function | `"ridge"` | `type_ridge()` | Creates a ridgeline (aka joy) plot. | [link](/man/type_ridge.qmd) | | `"rug"` | `type_rug()` | Adds a rug to an existing plot. | [link](/man/type_rug.qmd) | | `"spineplot"` / `"spine"` | `type_spineplot()` | Creates a spine plot or spinogram. | [link](/man/type_spineplot.qmd) | +| `"tile"` / `"heatmap"` | `type_tile()` | Creates a tile plot or heatmap. | [link](/man/type_tile.qmd) | | `"violin"` | `type_violin()` | Creates a violin plot. | [link](/man/type_violin.qmd) | #### Models From 358b36f47a3de2b5e109ecf0cc97dc46a28968ac Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 12:23:11 -0700 Subject: [PATCH 09/22] website --- altdoc/quarto_website.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index 90105e92..befa4ce1 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -100,6 +100,8 @@ website: file: man/type_ellipse.qmd - text: type_density file: man/type_density.qmd + - text: type_heatmap + file: man/type_tile.qmd - text: type_hexbin file: man/type_hexbin.qmd - text: type_histogram @@ -114,6 +116,8 @@ website: file: man/type_rug.qmd - text: type_spineplot file: man/type_spineplot.qmd + - text: type_tile + file: man/type_tile.qmd - text: type_violin file: man/type_violin.qmd - section: "Models" From 5f1238bf21e42f1a6f8b6132e8f2fb33c70f60e0 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 12:41:00 -0700 Subject: [PATCH 10/22] docs: ensure consistency of types documentation --- R/tinyplot.R | 7 +++++-- altdoc/quarto_website.yml | 26 ++++++++++++-------------- man/tinyplot.Rd | 7 +++++-- vignettes/types.qmd | 7 ++++--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index 68561183..25a08a06 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -115,23 +115,26 @@ #' - Shapes: #' - `"area"` / [`type_area()`]: Plots the area under the curve from `y` = 0 to `y` = f(`x`). #' - `"errorbar"` / [`type_errorbar()`]: Adds error bars to points; requires `ymin` and `ymax`. +#' - `"jitter"` / [`type_jitter()`]: Jittered points. #' - `"pointrange"` / [`type_pointrange()`]: Combines points with error bars. #' - `"polygon"` / [`type_polygon()`]: Draws polygons. #' - `"polypath"` / [`type_polypath()`]: Draws a path whose vertices are given in `x` and `y`. #' - `"rect"` / [`type_rect()`]: Draws rectangles; requires `xmin`, `xmax`, `ymin`, and `ymax`. #' - `"ribbon"` / [`type_ribbon()`]: Creates a filled area between `ymin` and `ymax`. +#' - `"rug"` / [`type_rug()`]: Adds a rug to an existing plot. #' - `"segments"` / [`type_segments()`]: Draws line segments between pairs of points. #' - `"text"` / [`type_text()`]: Add text annotations. +#' - `"tile"` / `"heatmap"` / [`type_tile()`]: Draws a grid of tiles (heatmap), with the fill given by `by`. #' - Visualizations: #' - `"barplot"` / [`type_barplot()`]: Creates a bar plot. #' - `"boxplot"` / [`type_boxplot()`]: Creates a box-and-whisker plot. #' - `"chull"` / [`type_chull()`]: Draws convex hull(s) around grouped points. #' - `"density"` / [`type_density()`]: Plots the density estimate of a variable. +#' - `"ellipse"` / [`type_ellipse()`]: Draws confidence ellipse(s) around grouped points. +#' - `"hexbin"` / [`type_hexbin()`]: Creates a hexagonal bin plot, a 2D analogue of a histogram. #' - `"histogram"` / [`type_histogram()`]: Creates a histogram of a single variable. -#' - `"jitter"` / [`type_jitter()`]: Jittered points. #' - `"qq"` / [`type_qq()`]: Creates a quantile-quantile plot. #' - `"ridge"` / [`type_ridge()`]: Creates a ridgeline (aka joy) plot. -#' - `"rug"` / [`type_rug()`]: Adds a rug to an existing plot. #' - `"spineplot"` / [`type_spineplot()`]: Creates a spineplot or spinogram. #' - `"violin"` / [`type_violin()`]: Creates a violin plot. #' - Models: diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index befa4ce1..c2f676b6 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -70,6 +70,8 @@ website: file: man/type_ribbon.qmd - text: type_errorbar file: man/type_errorbar.qmd + - text: type_jitter + file: man/type_jitter.qmd - text: type_lines file: man/type_lines.qmd - text: type_pointrange @@ -84,10 +86,14 @@ website: file: man/type_rect.qmd - text: type_ribbon file: man/type_ribbon.qmd - - text: type_text - file: man/type_text.qmd + - text: type_rug + file: man/type_rug.qmd - text: type_segments file: man/type_segments.qmd + - text: type_text + file: man/type_text.qmd + - text: type_tile + file: man/type_tile.qmd - section: "Visualizations" contents: - text: type_barplot @@ -96,38 +102,30 @@ website: file: man/type_boxplot.qmd - text: type_chull file: man/type_chull.qmd - - text: type_ellipse - file: man/type_ellipse.qmd - text: type_density file: man/type_density.qmd - - text: type_heatmap - file: man/type_tile.qmd + - text: type_ellipse + file: man/type_ellipse.qmd - text: type_hexbin file: man/type_hexbin.qmd - text: type_histogram file: man/type_histogram.qmd - - text: type_jitter - file: man/type_jitter.qmd - text: type_qq file: man/type_qq.qmd - text: type_ridge file: man/type_ridge.qmd - - text: type_rug - file: man/type_rug.qmd - text: type_spineplot file: man/type_spineplot.qmd - - text: type_tile - file: man/type_tile.qmd - text: type_violin file: man/type_violin.qmd - section: "Models" contents: - text: type_glm file: man/type_glm.qmd - - text: type_loess - file: man/type_loess.qmd - text: type_lm file: man/type_lm.qmd + - text: type_loess + file: man/type_loess.qmd - text: type_spline file: man/type_spline.qmd - section: "Functions" diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 14da3a10..832b3b36 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -244,13 +244,16 @@ type of plot desired. \itemize{ \item \code{"area"} / \code{\link[=type_area]{type_area()}}: Plots the area under the curve from \code{y} = 0 to \code{y} = f(\code{x}). \item \code{"errorbar"} / \code{\link[=type_errorbar]{type_errorbar()}}: Adds error bars to points; requires \code{ymin} and \code{ymax}. +\item \code{"jitter"} / \code{\link[=type_jitter]{type_jitter()}}: Jittered points. \item \code{"pointrange"} / \code{\link[=type_pointrange]{type_pointrange()}}: Combines points with error bars. \item \code{"polygon"} / \code{\link[=type_polygon]{type_polygon()}}: Draws polygons. \item \code{"polypath"} / \code{\link[=type_polypath]{type_polypath()}}: Draws a path whose vertices are given in \code{x} and \code{y}. \item \code{"rect"} / \code{\link[=type_rect]{type_rect()}}: Draws rectangles; requires \code{xmin}, \code{xmax}, \code{ymin}, and \code{ymax}. \item \code{"ribbon"} / \code{\link[=type_ribbon]{type_ribbon()}}: Creates a filled area between \code{ymin} and \code{ymax}. +\item \code{"rug"} / \code{\link[=type_rug]{type_rug()}}: Adds a rug to an existing plot. \item \code{"segments"} / \code{\link[=type_segments]{type_segments()}}: Draws line segments between pairs of points. \item \code{"text"} / \code{\link[=type_text]{type_text()}}: Add text annotations. +\item \code{"tile"} / \code{"heatmap"} / \code{\link[=type_tile]{type_tile()}}: Draws a grid of tiles (heatmap), with the fill given by \code{by}. } \item Visualizations: \itemize{ @@ -258,11 +261,11 @@ type of plot desired. \item \code{"boxplot"} / \code{\link[=type_boxplot]{type_boxplot()}}: Creates a box-and-whisker plot. \item \code{"chull"} / \code{\link[=type_chull]{type_chull()}}: Draws convex hull(s) around grouped points. \item \code{"density"} / \code{\link[=type_density]{type_density()}}: Plots the density estimate of a variable. +\item \code{"ellipse"} / \code{\link[=type_ellipse]{type_ellipse()}}: Draws confidence ellipse(s) around grouped points. +\item \code{"hexbin"} / \code{\link[=type_hexbin]{type_hexbin()}}: Creates a hexagonal bin plot, a 2D analogue of a histogram. \item \code{"histogram"} / \code{\link[=type_histogram]{type_histogram()}}: Creates a histogram of a single variable. -\item \code{"jitter"} / \code{\link[=type_jitter]{type_jitter()}}: Jittered points. \item \code{"qq"} / \code{\link[=type_qq]{type_qq()}}: Creates a quantile-quantile plot. \item \code{"ridge"} / \code{\link[=type_ridge]{type_ridge()}}: Creates a ridgeline (aka joy) plot. -\item \code{"rug"} / \code{\link[=type_rug]{type_rug()}}: Adds a rug to an existing plot. \item \code{"spineplot"} / \code{\link[=type_spineplot]{type_spineplot()}}: Creates a spineplot or spinogram. \item \code{"violin"} / \code{\link[=type_violin]{type_violin()}}: Creates a violin plot. } diff --git a/vignettes/types.qmd b/vignettes/types.qmd index 0ca92afd..30c08d78 100644 --- a/vignettes/types.qmd +++ b/vignettes/types.qmd @@ -62,6 +62,7 @@ a convenience string (with default behaviour) or a companion `type_*()` function |-----------------------|---------------------|----------------------------------------------------------------|------| | `"area"` | `type_area()` | Plots the area under the curve from `y` = 0 to `y` = f(`x`). | [link](/man/type_ribbon.qmd) | | `"errorbar"` | `type_errorbar()` | Adds error bars to points; requires `ymin` and `ymax`. | [link](/man/type_errorbar.qmd) | +| `"jitter"` / `"j"` | `type_jitter()` | Jittered points. | [link](/man/type_jitter.qmd) | | `"l"` / `"b"` / etc. | `type_lines()` | Draws lines and line-alike (same as base `"l"`, `"b"`, etc.) | [link](/man/type_lines.qmd) | | `"pointrange"` | `type_pointrange()` | Combines points with error bars. | [link](/man/type_errorbar.qmd) | | `"p"` | `type_points()` | Draws points (same as base `"p"`). | [link](/man/type_points.qmd) | @@ -69,8 +70,10 @@ a convenience string (with default behaviour) or a companion `type_*()` function | `"polypath"` | `type_polypath()` | Draws a path whose vertices are given in `x` and `y`. | [link](/man/type_polypath.qmd) | | `"rect"` | `type_rect()` | Draws rectangles; requires `xmin`, `xmax`, `ymin`, and `ymax`. | [link](/man/type_rect.qmd) | | `"ribbon"` | `type_ribbon()` | Creates a filled area between `ymin` and `ymax`. | [link](/man/type_ribbon.qmd) | +| `"rug"` | `type_rug()` | Adds a rug to an existing plot. | [link](/man/type_rug.qmd) | | `"segments"` | `type_segments()` | Draws line segments between pairs of points. | [link](/man/type_segments.qmd) | | `"text"` | `type_text()` | Adds text annotations to a plot. | [link](/man/type_text.qmd) | +| `"tile"` / `"heatmap"`| `type_tile()` | Draws a grid of tiles (heatmap); fill given by `by`. | [link](/man/type_tile.qmd) | #### Visualizations @@ -81,13 +84,11 @@ a convenience string (with default behaviour) or a companion `type_*()` function | `"chull"` | `type_chull()` | Draws convex hull(s) around grouped points. | [link](/man/type_chull.qmd) | | `"density"` | `type_density()` | Plots the density estimate of a variable. | [link](/man/type_density.qmd) | | `"ellipse"` | `type_ellipse()` | Draws confidence ellipse(s) around grouped points. | [link](/man/type_ellipse.qmd) | +| `"hexbin"` | `type_hexbin()` | Creates a hexagonal bin plot (2D histogram). | [link](/man/type_hexbin.qmd) | | `"histogram"` / `"hist"` | `type_histogram()` | Creates a histogram of a single variable. | [link](/man/type_histogram.qmd) | -| `"jitter"` / `"j"` | `type_jitter()` | Jittered points. | [link](/man/type_jitter.qmd) | | `"qq"` | `type_qq()` | Creates a quantile-quantile plot. | [link](/man/type_qq.qmd) | | `"ridge"` | `type_ridge()` | Creates a ridgeline (aka joy) plot. | [link](/man/type_ridge.qmd) | -| `"rug"` | `type_rug()` | Adds a rug to an existing plot. | [link](/man/type_rug.qmd) | | `"spineplot"` / `"spine"` | `type_spineplot()` | Creates a spine plot or spinogram. | [link](/man/type_spineplot.qmd) | -| `"tile"` / `"heatmap"` | `type_tile()` | Creates a tile plot or heatmap. | [link](/man/type_tile.qmd) | | `"violin"` | `type_violin()` | Creates a violin plot. | [link](/man/type_violin.qmd) | #### Models From 2334fb5f97b4b5cbfaf97e15caff76d753a84463 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 14:10:58 -0700 Subject: [PATCH 11/22] themes vignette --- altdoc/pkgdown.yml | 2 +- vignettes/themes.qmd | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index 729e0533..0d2dc353 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -2,7 +2,7 @@ altdoc: 0.7.3 pandoc: 3.10.1 pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-07-29T21:54:53+0000 +last_built: 2026-08-02T21:07:48+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/vignettes/themes.qmd b/vignettes/themes.qmd index 3e116e05..f8900c27 100644 --- a/vignettes/themes.qmd +++ b/vignettes/themes.qmd @@ -156,12 +156,13 @@ p("void") ``` ::: {.callout-note} -The specialized `"ridge"` and `"ridge2"` themes are only intended for use with -ridge plot types. +The specialized `"ridge(2)"` and `"heatmap"` themes are only intended for use +with their respective types. ::: ```{r} -p2 = function(theme = "ridge") { +p2 = function(theme = c("ridge", "ridge2")) { + theme = match.arg(theme) tinyplot( species ~ body_mass | species, data = penguins, @@ -173,11 +174,28 @@ p2 = function(theme = "ridge") { ) box("outer", lty = 2) } - p2("ridge") p2("ridge2") ``` +```{r} +# For tiles/heatmap, better to use a different dataset +p3 = function(theme = "heatmap") { + catt = as.data.frame(as.table(cor(attitude))) + tinyplot( + Var1 ~ Var2 | Freq, + data = catt, + type = "tile", # or, "heatmap" (alias) + main = paste0('theme = "', theme, '"'), + sub = "subtitle", + cap = "caption", + theme = theme + ) + box("outer", lty = 2) +} +p3("heatmap") +``` + Please feel free to make suggestions about themes, or contribute new themes by [opening a Pull Request on Github.](https://github.com/grantmcdermott/tinyplot) From 9b59adc1c7ab6a2b7fcce46a3365727f657a1626 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 2 Aug 2026 14:26:10 -0700 Subject: [PATCH 12/22] news --- NEWS.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9281cc89..bbb2a422 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,8 +10,14 @@ where the formatting is also better._ #### New plot types -- `type_hexbin()` (equivalently, `type = "hexbin"`) for hexagonal bin plots, a - 2D analogue of a histogram. (#667 @grantmcdermott) +- `type_hexbin()` / `"hexbin"` for hexagonal bin plots, a 2D analogue of a + histogram. (#667 @grantmcdermott) +- `type_tile()` / `"tile"` for tile plots and heatmaps, i.e. a grid of + rectangles whose fill encodes a third variable. `type_heatmap()` / `"heatmap"` + is an alias. (#677 @grantmcdermott) + - Ships with a companion `"heatmap"` theme, which removes the axis padding so + that tiles meet the panel edge, tick labels are always rotated against their + axis the tick labels, and defaults to the "tealgrn" sequential palette. #### Other new features From 78b96e96506b84fce58a999c1f6301f5e0f60f4a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 3 Aug 2026 17:23:49 -0700 Subject: [PATCH 13/22] demo reverse axes --- R/type_tile.R | 19 +- inst/tinytest/_tinysnapshot/tile_fancy.svg | 194 ++++++++++----------- inst/tinytest/test-type_tile.R | 13 +- man/type_tile.Rd | 19 +- 4 files changed, 125 insertions(+), 120 deletions(-) diff --git a/R/type_tile.R b/R/type_tile.R index ce4c06e1..111d2ab7 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -44,20 +44,23 @@ #' catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") #' #' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") +#' +#' # aside: "heatmap" is an alias for "tile" +#' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") #' -#' # slightly fancier version, where we suppress the legend but layer on the values -#' # as text +#' # fancier version where we reverse the y-axis (to mimic the usual correlation +#' # matrix layout), add white borders around each tile, and suppress the legend +#' # but layer on the values as text #' tinyplot( #' Var1 ~ Var2 | Correlation, data = catt, #' type = "tile", +#' col = "white", #' legend = FALSE, +#' main = "Correlation matrix of base attitude dataset", #' xlab = NA, ylab = NA, -#' main = "Correlation matrix of base attitude dataset" +#' ylim = "rev" #' ) -#' tinyplot_add(type = "text", labels = round(catt$Correlation, 2), col = "white") -#' -#' # aside: "heatmap" is an alias for "tile" -#' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") +#' tinyplot_add(type = "text", labels = round(catt$Correlation, 2)) #' #' # Pass scaled tile widths and heights through type_tile() for a gridded look #' tinyplot( @@ -73,7 +76,7 @@ #' palette = "tropic" #' ) #' -#' # Numeric axes work too, e.g. a (long-format) matrix of volcano heights +#' # Numeric axes work too, e.g. a (reshaped long) data.frame of volcano heights #' volc = data.frame( #' x = as.vector(row(volcano)), #' y = as.vector(col(volcano)), diff --git a/inst/tinytest/_tinysnapshot/tile_fancy.svg b/inst/tinytest/_tinysnapshot/tile_fancy.svg index 3f18ca8c..4201e3e8 100644 --- a/inst/tinytest/_tinysnapshot/tile_fancy.svg +++ b/inst/tinytest/_tinysnapshot/tile_fancy.svg @@ -34,13 +34,13 @@ raises critical advance -rating -complaints -privileges +advance +critical +raises learning -raises -critical -advance +privileges +complaints +rating @@ -48,104 +48,104 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1 -0.83 -0.43 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 +0.83 +0.43 0.62 -0.59 -0.16 -0.16 -0.83 -1 -0.56 +0.59 +0.16 +0.16 +0.83 +1 +0.56 0.6 -0.67 -0.19 -0.22 -0.43 -0.56 -1 +0.67 +0.19 +0.22 +0.43 +0.56 +1 0.49 -0.45 -0.15 -0.34 -0.62 -0.6 -0.49 +0.45 +0.15 +0.34 +0.62 +0.6 +0.49 1 -0.64 -0.12 -0.53 -0.59 -0.67 -0.45 +0.64 +0.12 +0.53 +0.59 +0.67 +0.45 0.64 -1 -0.38 -0.57 -0.16 -0.19 -0.15 +1 +0.38 +0.57 +0.16 +0.19 +0.15 0.12 -0.38 -1 -0.28 -0.16 -0.22 -0.34 +0.38 +1 +0.28 +0.16 +0.22 +0.34 0.53 -0.57 -0.28 -1 +0.57 +0.28 +1 diff --git a/inst/tinytest/test-type_tile.R b/inst/tinytest/test-type_tile.R index 570dca36..b4ffc3b4 100644 --- a/inst/tinytest/test-type_tile.R +++ b/inst/tinytest/test-type_tile.R @@ -30,14 +30,13 @@ f = function() { Var1 ~ Var2 | Correlation, data = catt, type = type_tile(width = 0.9, height = 0.9), theme = "heatmap", - legend = FALSE, xlab = NA, ylab = NA, - main = "Correlation matrix of base attitude dataset" - ) - tinyplot_add( - type = "text", - labels = round(catt$Correlation, 2), - col = "white" + col = "white", + legend = FALSE, + main = "Correlation matrix of base attitude dataset", + xlab = NA, ylab = NA, + ylim = "rev" ) + tinyplot_add(type = "text", labels = round(catt$Correlation, 2)) } expect_snapshot_plot(f, label = "tile_fancy") diff --git a/man/type_tile.Rd b/man/type_tile.Rd index d6863820..a0fc98ac 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -57,19 +57,22 @@ catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") -# slightly fancier version, where we suppress the legend but layer on the values -# as text +# aside: "heatmap" is an alias for "tile" +tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") + +# fancier version where we reverse the y-axis (to mimic the usual correlation +# matrix layout), add white borders around each tile, and suppress the legend +# but layer on the values as text tinyplot( Var1 ~ Var2 | Correlation, data = catt, type = "tile", + col = "white", legend = FALSE, + main = "Correlation matrix of base attitude dataset", xlab = NA, ylab = NA, - main = "Correlation matrix of base attitude dataset" + ylim = "rev" ) -tinyplot_add(type = "text", labels = round(catt$Correlation, 2), col = "white") - -# aside: "heatmap" is an alias for "tile" -tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") +tinyplot_add(type = "text", labels = round(catt$Correlation, 2)) # Pass scaled tile widths and heights through type_tile() for a gridded look tinyplot( @@ -85,7 +88,7 @@ tinyplot( palette = "tropic" ) -# Numeric axes work too, e.g. a (long-format) matrix of volcano heights +# Numeric axes work too, e.g. a (reshaped long) data.frame of volcano heights volc = data.frame( x = as.vector(row(volcano)), y = as.vector(col(volcano)), From 513e0123489a54e78c64ca05a22f72155b66af4c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 4 Aug 2026 17:04:55 -0700 Subject: [PATCH 14/22] separate out dedicated `type_heatmap()` type - behaves much closer to base R heatmap(), including scaling per column etc. --- NAMESPACE | 1 + NEWS.md | 17 +- R/sanitize_type.R | 2 +- R/tinyplot.R | 3 +- R/type_tile.R | 229 ++++++++- altdoc/quarto_website.yml | 2 + .../_tinysnapshot/heatmap_scale_x.svg | 442 +++++++++++++++++ .../_tinysnapshot/heatmap_scale_x_zscore.svg | 446 ++++++++++++++++++ inst/tinytest/_tinysnapshot/tile_scale_x.svg | 442 +++++++++++++++++ inst/tinytest/test-type_tile.R | 40 ++ man/tinyplot.Rd | 3 +- man/type_tile.Rd | 90 +++- vignettes/types.qmd | 3 +- 13 files changed, 1686 insertions(+), 34 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/heatmap_scale_x.svg create mode 100644 inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg create mode 100644 inst/tinytest/_tinysnapshot/tile_scale_x.svg diff --git a/NAMESPACE b/NAMESPACE index 9d72b654..0dcfbb08 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -137,6 +137,7 @@ importFrom(stats,qnorm) importFrom(stats,qt) importFrom(stats,quantile) importFrom(stats,reformulate) +importFrom(stats,sd) importFrom(stats,setNames) importFrom(stats,spline) importFrom(stats,terms) diff --git a/NEWS.md b/NEWS.md index bbb2a422..2d463a00 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,12 +12,11 @@ where the formatting is also better._ - `type_hexbin()` / `"hexbin"` for hexagonal bin plots, a 2D analogue of a histogram. (#667 @grantmcdermott) -- `type_tile()` / `"tile"` for tile plots and heatmaps, i.e. a grid of - rectangles whose fill encodes a third variable. `type_heatmap()` / `"heatmap"` - is an alias. (#677 @grantmcdermott) - - Ships with a companion `"heatmap"` theme, which removes the axis padding so - that tiles meet the panel edge, tick labels are always rotated against their - axis the tick labels, and defaults to the "tealgrn" sequential palette. +- `type_tile()` / `"tile"` for tile plots, i.e. a grid of rectangles whose fill + encodes a third variable. (#677 @grantmcdermott) +- `type_heatmap()` / `"heatmap"` builds on `type_tile()`, adding a `scale` + argument that rescales the fill values *within* each category of one axis. + This is anaologous to base R's `heatmap()` function. (#677 @grantmcdermott) #### Other new features @@ -50,6 +49,12 @@ where the formatting is also better._ `"cat"` (console), in any combination; a destination the user has already labelled is left alone. Shared bandwidths are reported once and named as joint, individual bandwidths per group. (#287 @haomeng797-ship-it) +- Themes: + - `"heatmap"` provides a dedicated companion theme to the new `type_tile()` + and `type_heatmap()` types (see above). The theme removes all axis padding, + so that tiles meet the panel edge, and also rotates the tick labels against + their respective axes. Colour fills default to the "tealgrn" sequential + palette. (#677 @grantmcdermott) ### Bug fixes diff --git a/R/sanitize_type.R b/R/sanitize_type.R index f0be0ae5..074336ca 100644 --- a/R/sanitize_type.R +++ b/R/sanitize_type.R @@ -116,7 +116,7 @@ sanitize_type = function(settings) { "summary" = type_summary, "text" = type_text, "tile" = type_tile, - "heatmap" = type_tile, + "heatmap" = type_heatmap, "violin" = type_violin, "vline" = type_vline, type # default case (incl. line-family chars, handled below) diff --git a/R/tinyplot.R b/R/tinyplot.R index 25a08a06..5f7d3b8f 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -124,13 +124,14 @@ #' - `"rug"` / [`type_rug()`]: Adds a rug to an existing plot. #' - `"segments"` / [`type_segments()`]: Draws line segments between pairs of points. #' - `"text"` / [`type_text()`]: Add text annotations. -#' - `"tile"` / `"heatmap"` / [`type_tile()`]: Draws a grid of tiles (heatmap), with the fill given by `by`. +#' - `"tile"` / [`type_tile()`]: Draws a grid of tiles, with the fill given by `by`. #' - Visualizations: #' - `"barplot"` / [`type_barplot()`]: Creates a bar plot. #' - `"boxplot"` / [`type_boxplot()`]: Creates a box-and-whisker plot. #' - `"chull"` / [`type_chull()`]: Draws convex hull(s) around grouped points. #' - `"density"` / [`type_density()`]: Plots the density estimate of a variable. #' - `"ellipse"` / [`type_ellipse()`]: Draws confidence ellipse(s) around grouped points. +#' - `"heatmap"` / [`type_heatmap()`]: Draws a grid of tiles, optionally rescaling the fill along one axis. #' - `"hexbin"` / [`type_hexbin()`]: Creates a hexagonal bin plot, a 2D analogue of a histogram. #' - `"histogram"` / [`type_histogram()`]: Creates a histogram of a single variable. #' - `"qq"` / [`type_qq()`]: Creates a quantile-quantile plot. diff --git a/R/type_tile.R b/R/type_tile.R index 111d2ab7..bf9fd7f4 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -1,9 +1,14 @@ -#' Tile (heatmap) plot type +#' Tile and heatmap plot types #' -#' @description Type function for tile plots, i.e. a grid of rectangles whose -#' fill colour encodes a third variable. This is the standard building block -#' of heatmaps, correlation matrices, and calendar plots. `type_heatmap` is an -#' alias for `type_tile`. +#' @description Type functions for tile plots, i.e. a grid of rectangles whose +#' fill colour encodes a third variable. `type_tile()` is the default building +#' block for these gridded shapes, drawing the values exactly as supplied. It +#' underpins heatmaps, correlation matrices, calendar plots, confusion +#' matrices, and similar displays. +#' +#' `type_heatmap()` is a specialised case that first rescales the fill values +#' within each category of one axis. Reach for it when those values are not +#' already on a common scale. #' #' @details Tile plots are specified as `z ~ x` with the fill variable passed as #' the `by` grouping, i.e. `tinyplot(y ~ x | z, type = "tile")`. The `x` and @@ -31,22 +36,27 @@ #' theme that removes the padding and grid, rotates the tick labels, and #' switches to a sequential palette. See [`tinytheme()`] and the Examples. #' +#' `type_heatmap()`'s `scale` argument is the analogue of the `scale` argument +#' in base R's \code{\link[stats]{heatmap}}. For exact parity with the +#' latter, which z-scores along the chosen margin, pair it with +#' `method = "zscore"`; our default instead rescales each group onto the unit +#' (\[0, 1\]) interval. +#' #' @param width,height Numeric tile dimensions in data units. Both default to #' `1`, which produces contiguous tiles on categorical (or unit-spaced #' numeric) axes. Values below `1` inset the tiles, leaving gaps between them. #' Recycled across tiles, so a vector may be used for variable sizes. -#' #' @examples -#' # It is recommended to use the dedicated "heatmap" theme for this type +#' # It is recommended to use the dedicated "heatmap" theme for tile plots #' tinytheme("heatmap") #' +#' # +#' ## type_tile ---- +#' #' # Correlation matrix of the base `attitude` dataset in "long" form. #' catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") #' #' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") -#' -#' # aside: "heatmap" is an alias for "tile" -#' tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") #' #' # fancier version where we reverse the y-axis (to mimic the usual correlation #' # matrix layout), add white borders around each tile, and suppress the legend @@ -90,6 +100,34 @@ #' main = "Maunga Whau volcano" #' ) #' +#' # +#' ## type_heatmap ---- +#' +#' # Raw data matrices are usually dominated by their largest-magnitude column. +#' # `type_heatmap()` can rescale within each column to make the rest legible. +#' mt = as.data.frame(as.table(as.matrix(mtcars))) +#' +#' # first, the unscaled version: only `disp` and `hp` are visible +#' tinyplot( +#' Var1 ~ Var2 | Freq, data = mt, +#' type = "heatmap", +#' xlab = NA, ylab = NA +#' ) +#' +#' # and now rescaled within each x variable (i.e., column) +#' tinyplot( +#' Var1 ~ Var2 | Freq, data = mt, +#' type = type_heatmap(scale = "x"), +#' xlab = NA, ylab = NA +#' ) +#' +#' # use `method = "zscore"` for parity with base R's `heatmap(scale = "column")` +#' tinyplot( +#' Var1 ~ Var2 | Freq, data = mt, +#' type = type_heatmap(scale = "x", method = "zscore"), +#' xlab = NA, ylab = NA +#' ) +#' #' ## restore the default theme #' tinytheme() #' @@ -110,17 +148,111 @@ type_tile = function(width = 1, height = 1) { return(out) } + #' @rdname type_tile +#' @param scale Character. Should the `by` (fill) values be rescaled *within* +#' each category of one axis? One of `"none"` (default, i.e. the raw values +#' are used), `"x"`, or `"y"`. Rescaling is what makes a raw matrix legible +#' when its variables span very different magnitudes: left alone, the +#' largest-magnitude column monopolises the entire colour ramp. See Examples. +#' +#' Note that `"x"` and `"y"` refer to the axes *as written in the formula*, +#' i.e. before any `flip = TRUE` is applied. We deliberately avoid base R's +#' `"row"`/`"column"` wording, since a tile's position depends on which +#' variable the user placed where in the formula, so there is no fixed matrix +#' orientation to refer to. +#' +#' Rescaling is computed independently per facet; pooling across facets would +#' pin a panel on a different scale to one end of the ramp and lose its +#' internal structure. Since rescaled values are no longer in the units of the +#' `by` variable, the legend title is annotated accordingly. +#' @param method Character. How should the values be rescaled, if `scale` is not +#' `"none"`? Either `"rescale"` (default) to map each group onto the unit +#' interval \[0, 1\], or `"zscore"` to centre each group and divide by its +#' standard deviation. Ignored when `scale = "none"`. +#' +#' Groups with no spread---a constant column, or a single tile---would divide +#' by zero, so they are set to the midpoint of the target range (`0.5` and `0` +#' respectively) and a warning is emitted. +#' +#' @importFrom stats sd #' @export -type_heatmap = type_tile +type_heatmap = function( + width = 1, + height = 1, + scale = c("none", "x", "y"), + method = c("rescale", "zscore")) { + assert_numeric(width) + assert_numeric(height) + if (length(scale) > 1L) scale = scale[1L] + assert_choice(scale, c("none", "x", "y")) + if (length(method) > 1L) method = method[1L] + assert_choice(method, c("rescale", "zscore")) + out = list( + draw = draw_rect(), + data = data_tile( + width = width, height = height, scale = scale, method = method + ), + # Deliberately reports "tile": the two types are interchangeable as far as + # the rest of the pipeline is concerned, and nothing downstream needs to + # tell them apart. Keeps the option of diverging later. + name = "tile" + ) + class(out) = "tinyplot_type" + return(out) +} -data_tile = function(width = 1, height = 1) { +## Rescale `by` within each level of `g`, either to the unit interval +## (method = "rescale") or as a z-score (method = "zscore"). Both divide by a +## measure of spread, so a group with no spread (all values identical, or a +## single observation) would produce NaN. That is much worse than it sounds: +## `range()` of a vector containing one NaN is NaN, so the draw loop's colour +## indices all become NA and tiles blank out across the *whole* plot, not just +## the offending group. Map such groups to the midpoint of the target range +## instead, and report them back so the caller can warn -- a silently flattened +## group otherwise reads as a genuine mid-scale value. +scale_by_group = function(by, g, method = "rescale") { + gi = if (is.factor(g)) g else factor(g) + mid = if (identical(method, "zscore")) 0 else 0.5 + flat = character(0) + out = unsplit( + lapply(split(seq_along(by), gi), function(ix) { + v = by[ix] + if (identical(method, "zscore")) { + s = sd(v, na.rm = TRUE) + if (!is.finite(s) || s == 0) { + flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) + return(rep.int(mid, length(v))) + } + return((v - mean(v, na.rm = TRUE)) / s) + } + # rescale_num()'s default `from` is range(x), which propagates an NA to + # every element, so compute the range with na.rm explicitly. + rng = range(v, na.rm = TRUE) + if (!all(is.finite(rng)) || diff(rng) == 0) { + flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) + return(rep.int(mid, length(v))) + } + rescale_num(v, from = rng, to = c(0, 1)) + }), + gi + ) + attr(out, "flat") = flat + out +} + + +data_tile = function( + width = 1, height = 1, scale = "none", method = "rescale") { fun = function(settings, ...) { env2env( settings, environment(), - c("datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg", "fill", "null_by") + c( + "datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg", "fill", + "null_by", "by", "by_dep", "legend_args" + ) ) # Tiles are a filled mark: the `by` variable encodes the *fill*, not the @@ -133,6 +265,72 @@ data_tile = function(width = 1, height = 1) { # the same flat colour, which would read as a solid black grid. if (is.null(bg) && is.null(fill) && !isTRUE(null_by)) bg = "by" + # Optional z-scoring of the fill values within each x (or y) category, cf. + # `heatmap(scale=)`. Must happen *before* the factor -> integer conversion + # below, which needs the axis variables still as factors to group on. Note + # this keys off the axis as written in the formula: flip_datapoints() runs + # later in the pipeline, so `flip` does not invert the meaning. + if (!identical(scale, "none")) { + if (isTRUE(null_by) || !is.numeric(datapoints[["by"]])) { + # Nothing numeric to standardize. Also catches `facet = "by"`, which + # coerces `by` to a factor upstream in sanitize_facet(). Warn rather + # than error: the plot is still perfectly drawable, just unscaled. + warning( + "`type_tile(scale=)` requires a numeric `by` (fill) variable. ", + "Ignoring `scale`.", + call. = FALSE + ) + } else { + # Group on the axis position *and* the facet, so each panel is scaled + # independently. Pooling across panels defeats the purpose: a panel on + # a different order of magnitude would pin its whole range to one end + # of the ramp and lose all within-panel structure. `datapoints$facet` + # is always present (a constant "" when unfaceted). + grp = interaction( + datapoints[[scale]], datapoints[["facet"]], drop = TRUE + ) + z = scale_by_group(datapoints[["by"]], grp, method = method) + flat = attr(z, "flat") + if (length(flat) > 0L) { + warning( + sprintf( + paste( + "No variation within %d %s of `%s`;", + "set to the scale midpoint: %s" + ), + length(flat), if (length(flat) > 1L) "groups" else "group", + scale, paste(flat, collapse = ", ") + ), + call. = FALSE + ) + } + if (anyNA(z)) { + warning( + "Missing values in `by`; those tiles are left unfilled.", + call. = FALSE + ) + } + attributes(z) = NULL + # Both slots are needed: the tile fills read `datapoints$by`, but the + # gradient legend's tick labels come from the bare `by`, so updating + # only one would leave the colourbar numbers disagreeing with the + # colours (cf. type_hexbin()). + datapoints[["by"]] = z + by = z + # A scaled fill is no longer in the units of the `by` variable, so a + # legend still titled e.g. "Freq" would be actively misleading. Note the + # formula method has already pre-filled the title with the variable + # name, so annotate whatever is there rather than only filling a blank. + # The grepl() guard keeps this idempotent under tinyplot_add() replay. + sfx = if (identical(method, "zscore")) "(z-score)" else "(rescaled)" + ttl = legend_args[["title"]] %||% by_dep + if (is.character(ttl) && length(ttl) == 1L && nzchar(ttl) && + !grepl(sfx, ttl, fixed = TRUE)) { + legend_args[["title"]] = paste0(ttl, "\n", sfx) + } + } + } + # A categorical axis carries its own tick labels, so convert to consecutive # integer positions and hand the levels off to the axis machinery. Numeric # axes are already positional and keep their default (computed) ticks. @@ -180,7 +378,10 @@ data_tile = function(width = 1, height = 1) { env2env( environment(), settings, - c("datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg") + c( + "datapoints", "xlabs", "ylabs", "xaxt", "yaxt", "bg", "by", + "legend_args" + ) ) } return(fun) diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index c2f676b6..ef9a346d 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -106,6 +106,8 @@ website: file: man/type_density.qmd - text: type_ellipse file: man/type_ellipse.qmd + - text: type_heatmap + file: man/type_tile.qmd - text: type_hexbin file: man/type_hexbin.qmd - text: type_histogram diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg new file mode 100644 index 00000000..9fbe5215 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + 0.2 + 0.6 + 1.0 +- - +- - +- - +Freq +(rescaled) +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Mazda RX4 +Mazda RX4 Wag +Datsun 710 +Hornet 4 Drive +Hornet Sportabout +Valiant +Duster 360 +Merc 240D +Merc 230 +Merc 280 +Merc 280C +Merc 450SE +Merc 450SL +Merc 450SLC +Cadillac Fleetwood +Lincoln Continental +Chrysler Imperial +Fiat 128 +Honda Civic +Toyota Corolla +Toyota Corona +Dodge Challenger +AMC Javelin +Camaro Z28 +Pontiac Firebird +Fiat X1-9 +Porsche 914-2 +Lotus Europa +Ford Pantera L +Ferrari Dino +Maserati Bora +Volvo 142E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg new file mode 100644 index 00000000..17df5e33 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg @@ -0,0 +1,446 @@ + + + + + + + + + + + + + + + -1 + 0 + 1 + 2 + 3 +- - +- - +- - +- - +- - +Freq +(z-score) +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Mazda RX4 +Mazda RX4 Wag +Datsun 710 +Hornet 4 Drive +Hornet Sportabout +Valiant +Duster 360 +Merc 240D +Merc 230 +Merc 280 +Merc 280C +Merc 450SE +Merc 450SL +Merc 450SLC +Cadillac Fleetwood +Lincoln Continental +Chrysler Imperial +Fiat 128 +Honda Civic +Toyota Corolla +Toyota Corona +Dodge Challenger +AMC Javelin +Camaro Z28 +Pontiac Firebird +Fiat X1-9 +Porsche 914-2 +Lotus Europa +Ford Pantera L +Ferrari Dino +Maserati Bora +Volvo 142E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tile_scale_x.svg b/inst/tinytest/_tinysnapshot/tile_scale_x.svg new file mode 100644 index 00000000..9fbe5215 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tile_scale_x.svg @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + 0.2 + 0.6 + 1.0 +- - +- - +- - +Freq +(rescaled) +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Mazda RX4 +Mazda RX4 Wag +Datsun 710 +Hornet 4 Drive +Hornet Sportabout +Valiant +Duster 360 +Merc 240D +Merc 230 +Merc 280 +Merc 280C +Merc 450SE +Merc 450SL +Merc 450SLC +Cadillac Fleetwood +Lincoln Continental +Chrysler Imperial +Fiat 128 +Honda Civic +Toyota Corolla +Toyota Corona +Dodge Challenger +AMC Javelin +Camaro Z28 +Pontiac Firebird +Fiat X1-9 +Porsche 914-2 +Lotus Europa +Ford Pantera L +Ferrari Dino +Maserati Bora +Volvo 142E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_tile.R b/inst/tinytest/test-type_tile.R index b4ffc3b4..1c78fb41 100644 --- a/inst/tinytest/test-type_tile.R +++ b/inst/tinytest/test-type_tile.R @@ -68,3 +68,43 @@ f = function() { tinyplot(b ~ a | v, facet = ~g, data = d, type = "tile", theme = "heatmap") } expect_snapshot_plot(f, label = "tile_facet") + + +# +## type_heatmap(): scale/method, cf. base R `heatmap(scale=)` +# + +# A raw data matrix is the motivating case: unscaled, `disp`/`hp` monopolise the +# colour ramp and the other nine columns are indistinguishable. +mt = as.data.frame(as.table(as.matrix(mtcars))) + +f = function() { + tinyplot( + Var1 ~ Var2 | Freq, data = mt, + type = type_heatmap(scale = "x"), + theme = "heatmap", + xlab = NA, ylab = NA + ) +} +expect_snapshot_plot(f, label = "heatmap_scale_x") + +f = function() { + tinyplot( + Var1 ~ Var2 | Freq, data = mt, + type = type_heatmap(scale = "x", method = "zscore"), + theme = "heatmap", + xlab = NA, ylab = NA + ) +} +expect_snapshot_plot(f, label = "heatmap_scale_x_zscore") + +# `scale = "none"` is the default, so a bare type_heatmap() must reproduce the +# plain type_tile() fixture exactly (asserted against the *same* label) +f = function() { + tinyplot( + Var1 ~ Var2 | Correlation, data = catt, type = type_heatmap(scale = "none"), + theme = "heatmap" + ) +} +expect_snapshot_plot(f, label = "tile_basic") + diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 832b3b36..11a792a4 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -253,7 +253,7 @@ type of plot desired. \item \code{"rug"} / \code{\link[=type_rug]{type_rug()}}: Adds a rug to an existing plot. \item \code{"segments"} / \code{\link[=type_segments]{type_segments()}}: Draws line segments between pairs of points. \item \code{"text"} / \code{\link[=type_text]{type_text()}}: Add text annotations. -\item \code{"tile"} / \code{"heatmap"} / \code{\link[=type_tile]{type_tile()}}: Draws a grid of tiles (heatmap), with the fill given by \code{by}. +\item \code{"tile"} / \code{\link[=type_tile]{type_tile()}}: Draws a grid of tiles, with the fill given by \code{by}. } \item Visualizations: \itemize{ @@ -262,6 +262,7 @@ type of plot desired. \item \code{"chull"} / \code{\link[=type_chull]{type_chull()}}: Draws convex hull(s) around grouped points. \item \code{"density"} / \code{\link[=type_density]{type_density()}}: Plots the density estimate of a variable. \item \code{"ellipse"} / \code{\link[=type_ellipse]{type_ellipse()}}: Draws confidence ellipse(s) around grouped points. +\item \code{"heatmap"} / \code{\link[=type_heatmap]{type_heatmap()}}: Draws a grid of tiles, optionally rescaling the fill along one axis. \item \code{"hexbin"} / \code{\link[=type_hexbin]{type_hexbin()}}: Creates a hexagonal bin plot, a 2D analogue of a histogram. \item \code{"histogram"} / \code{\link[=type_histogram]{type_histogram()}}: Creates a histogram of a single variable. \item \code{"qq"} / \code{\link[=type_qq]{type_qq()}}: Creates a quantile-quantile plot. diff --git a/man/type_tile.Rd b/man/type_tile.Rd index a0fc98ac..ab151b04 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -3,23 +3,59 @@ \name{type_tile} \alias{type_tile} \alias{type_heatmap} -\title{Tile (heatmap) plot type} +\title{Tile and heatmap plot types} \usage{ type_tile(width = 1, height = 1) -type_heatmap(width = 1, height = 1) +type_heatmap( + width = 1, + height = 1, + scale = c("none", "x", "y"), + method = c("rescale", "zscore") +) } \arguments{ \item{width, height}{Numeric tile dimensions in data units. Both default to \code{1}, which produces contiguous tiles on categorical (or unit-spaced numeric) axes. Values below \code{1} inset the tiles, leaving gaps between them. Recycled across tiles, so a vector may be used for variable sizes.} + +\item{scale}{Character. Should the \code{by} (fill) values be rescaled \emph{within} +each category of one axis? One of \code{"none"} (default, i.e. the raw values +are used), \code{"x"}, or \code{"y"}. Rescaling is what makes a raw matrix legible +when its variables span very different magnitudes: left alone, the +largest-magnitude column monopolises the entire colour ramp. See Examples. + +Note that \code{"x"} and \code{"y"} refer to the axes \emph{as written in the formula}, +i.e. before any \code{flip = TRUE} is applied. We deliberately avoid base R's +\code{"row"}/\code{"column"} wording, since a tile's position depends on which +variable the user placed where in the formula, so there is no fixed matrix +orientation to refer to. + +Rescaling is computed independently per facet; pooling across facets would +pin a panel on a different scale to one end of the ramp and lose its +internal structure. Since rescaled values are no longer in the units of the +\code{by} variable, the legend title is annotated accordingly.} + +\item{method}{Character. How should the values be rescaled, if \code{scale} is not +\code{"none"}? Either \code{"rescale"} (default) to map each group onto the unit +interval [0, 1], or \code{"zscore"} to centre each group and divide by its +standard deviation. Ignored when \code{scale = "none"}. + +Groups with no spread---a constant column, or a single tile---would divide +by zero, so they are set to the midpoint of the target range (\code{0.5} and \code{0} +respectively) and a warning is emitted.} } \description{ -Type function for tile plots, i.e. a grid of rectangles whose -fill colour encodes a third variable. This is the standard building block -of heatmaps, correlation matrices, and calendar plots. \code{type_heatmap} is an -alias for \code{type_tile}. +Type functions for tile plots, i.e. a grid of rectangles whose +fill colour encodes a third variable. \code{type_tile()} is the default building +block for these gridded shapes, drawing the values exactly as supplied. It +underpins heatmaps, correlation matrices, calendar plots, confusion +matrices, and similar displays. + +\code{type_heatmap()} is a specialised case that first rescales the fill values +within each category of one axis. Reach for it when those values are not +already on a common scale. } \details{ Tile plots are specified as \code{z ~ x} with the fill variable passed as @@ -47,19 +83,25 @@ padding and grid lines of most themes are redundant (and the grid is hidden behind the tiles in any case). We therefore ship a dedicated \code{"heatmap"} theme that removes the padding and grid, rotates the tick labels, and switches to a sequential palette. See \code{\link[=tinytheme]{tinytheme()}} and the Examples. + +\code{type_heatmap()}'s \code{scale} argument is the analogue of the \code{scale} argument +in base R's \code{\link[stats]{heatmap}}. For exact parity with the +latter, which z-scores along the chosen margin, pair it with +\code{method = "zscore"}; our default instead rescales each group onto the unit +([0, 1]) interval. } \examples{ -# It is recommended to use the dedicated "heatmap" theme for this type +# It is recommended to use the dedicated "heatmap" theme for tile plots tinytheme("heatmap") +# +## type_tile ---- + # Correlation matrix of the base `attitude` dataset in "long" form. catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "tile") -# aside: "heatmap" is an alias for "tile" -tinyplot(Var1 ~ Var2 | Correlation, data = catt, type = "heatmap") - # fancier version where we reverse the y-axis (to mimic the usual correlation # matrix layout), add white borders around each tile, and suppress the legend # but layer on the values as text @@ -102,6 +144,34 @@ tinyplot( main = "Maunga Whau volcano" ) +# +## type_heatmap ---- + +# Raw data matrices are usually dominated by their largest-magnitude column. +# `type_heatmap()` can rescale within each column to make the rest legible. +mt = as.data.frame(as.table(as.matrix(mtcars))) + +# first, the unscaled version: only `disp` and `hp` are visible +tinyplot( + Var1 ~ Var2 | Freq, data = mt, + type = "heatmap", + xlab = NA, ylab = NA +) + +# and now rescaled within each x variable (i.e., column) +tinyplot( + Var1 ~ Var2 | Freq, data = mt, + type = type_heatmap(scale = "x"), + xlab = NA, ylab = NA +) + +# use `method = "zscore"` for parity with base R's `heatmap(scale = "column")` +tinyplot( + Var1 ~ Var2 | Freq, data = mt, + type = type_heatmap(scale = "x", method = "zscore"), + xlab = NA, ylab = NA +) + ## restore the default theme tinytheme() diff --git a/vignettes/types.qmd b/vignettes/types.qmd index 30c08d78..c8c28577 100644 --- a/vignettes/types.qmd +++ b/vignettes/types.qmd @@ -73,7 +73,7 @@ a convenience string (with default behaviour) or a companion `type_*()` function | `"rug"` | `type_rug()` | Adds a rug to an existing plot. | [link](/man/type_rug.qmd) | | `"segments"` | `type_segments()` | Draws line segments between pairs of points. | [link](/man/type_segments.qmd) | | `"text"` | `type_text()` | Adds text annotations to a plot. | [link](/man/type_text.qmd) | -| `"tile"` / `"heatmap"`| `type_tile()` | Draws a grid of tiles (heatmap); fill given by `by`. | [link](/man/type_tile.qmd) | +| `"tile"` | `type_tile()` | Draws a grid of tiles; fill given by `by`. | [link](/man/type_tile.qmd) | #### Visualizations @@ -84,6 +84,7 @@ a convenience string (with default behaviour) or a companion `type_*()` function | `"chull"` | `type_chull()` | Draws convex hull(s) around grouped points. | [link](/man/type_chull.qmd) | | `"density"` | `type_density()` | Plots the density estimate of a variable. | [link](/man/type_density.qmd) | | `"ellipse"` | `type_ellipse()` | Draws confidence ellipse(s) around grouped points. | [link](/man/type_ellipse.qmd) | +| `"heatmap"` | `type_heatmap()` | Tiles, optionally rescaled along one axis. | [link](/man/type_tile.qmd) | | `"hexbin"` | `type_hexbin()` | Creates a hexagonal bin plot (2D histogram). | [link](/man/type_hexbin.qmd) | | `"histogram"` / `"hist"` | `type_histogram()` | Creates a histogram of a single variable. | [link](/man/type_histogram.qmd) | | `"qq"` | `type_qq()` | Creates a quantile-quantile plot. | [link](/man/type_qq.qmd) | From 8d32c50128c6960e3f0ac75225c683214385a4b1 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 4 Aug 2026 20:01:05 -0700 Subject: [PATCH 15/22] tinyplot.matrix compat and document --- R/tinyplot.matrix.R | 64 ++++++++++++++++++++++++++++++++++++++++-- R/type_tile.R | 5 ++++ man/tinyplot.matrix.Rd | 20 +++++++++++-- man/type_tile.Rd | 5 ++++ 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R index b2fc29c1..03af133d 100644 --- a/R/tinyplot.matrix.R +++ b/R/tinyplot.matrix.R @@ -13,17 +13,30 @@ #' used as the group (and legend) labels. Single-column matrices are drawn as #' a simple index plot with no grouping or legend. #' +#' The `"tile"` and `"heatmap"` types are an exception, since the matplot +#' convention makes little sense for them. Instead the matrix is laid out as a +#' grid---columns along the x-axis, rows along the y-axis---with the matrix +#' *values* supplied as the fill. Row order is reversed so that the first row +#' sits at the top, matching how one reads a matrix (cf. +#' \code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}). Both axis +#' titles are suppressed, since the dimnames already label the ticks, and so +#' is the legend, since the fill merely re-encodes the matrix's own values. +#' Pass an explicit `legend` (or `xlab`/`ylab`) to override either. See +#' Examples. +#' #' @param x an object of class `"matrix"`. #' @param type plot type passed on to `tinyplot`. Defaults to `"p"` (points). #' @param legend specification passed on to `tinyplot`. The default is to draw a -#' legend when the matrix has named columns, and to suppress it otherwise. +#' legend when the matrix has named columns, and to suppress it otherwise. For +#' `"tile"` and `"heatmap"` types it is suppressed by default. #' @param facet specification of `facet` passed on to `tinyplot`. The only #' accepted non-`NULL` value is the `"by"` convenience string, which facets #' the plot by matrix column. #' @param xlab,ylab axis labels passed on to `tinyplot`. `ylab` defaults to the #' deparsed matrix name. `xlab` defaults to `"Index"` when the matrix has no #' row names; when it does, the row names already label the ticks so the -#' x-axis title is suppressed. +#' x-axis title is suppressed. For `"tile"` and `"heatmap"` types both +#' titles default to `NA`, since the dimnames label both axes. #' @param ... further arguments passed to `tinyplot`. #' #' @returns No return value, called for the side effect of producing a plot. @@ -36,10 +49,13 @@ #' tinyplot(VADeaths, type = "b") #' tinyplot(VADeaths, type = "b", legend = "direct", theme = "socviz") #' tinyplot(VADeaths, type = "b", legend = FALSE, facet = "by", theme = "socviz") -#' +#' #' # equivalent plot to an example in `?matplot` #' sines = outer(1:20, 1:4, function(x, y) sin(x / 20 * pi * y)) #' tinyplot(sines, type = "o", pch = "by", lty = "by", col = rainbow(ncol(sines))) +#' +#' # `"tile"` + `"heatmap"` types lay the matrix out as a grid instead +#' tinyplot(VADeaths, type = "heatmap", theme = "heatmap", col = "white") #' #' @export tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = NULL, ylab = NULL, ...) { @@ -50,6 +66,48 @@ tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = N if (is.null(type)) type = "p" dep_x = deparse1(substitute(x)) dims = dim(x) + + ## Tile and heatmap types need a different mapping to the matplot convention + ## below: they want the matrix laid out as a grid (columns on x, rows on y) + ## with the *values* supplied as the fill, rather than a series per column + ## with the values on y. Detect via the resolved type name, so that both the + ## convenience strings and the type_*() constructors are covered. + tname = if (inherits(type, "tinyplot_type")) type[["name"]] else type + if (is.character(tname) && length(tname) == 1L && + tname %in% c("tile", "heatmap")) { + rnms = rownames(x) + cnms = colnames(x) + xx = if (is.null(cnms)) { + factor(rep(seq_len(dims[2]), each = dims[1])) + } else { + factor(rep(cnms, each = dims[1]), levels = cnms) + } + ## Reverse the row levels so that row 1 sits at the *top* of the plot, + ## matching how one reads a matrix (cf. `heatmap()`, `image()`). + yy = if (is.null(rnms)) { + factor(rep(seq_len(dims[1]), times = dims[2]), + levels = rev(seq_len(dims[1]))) + } else { + factor(rep(rnms, times = dims[2]), levels = rev(rnms)) + } + ## Both axes are labelled by the matrix dimnames, so axis titles would be + ## redundant. Ditto the legend: the fill encodes the matrix's own values, so + ## a colourbar adds little for a bare `tinyplot(m, type = "heatmap")` call. + ## Users who want one can still ask for it explicitly. + if (is.null(xlab)) xlab = NA + if (is.null(ylab)) ylab = NA + if (is.null(legend)) legend = FALSE + return(tinyplot.default( + x = xx, y = yy, + type = type, + by = as.vector(x), + facet = facet, + legend = legend, + xlab = xlab, + ylab = ylab, + ... + )) + } if (dims[2] == 1L) { bby = NULL legend = FALSE diff --git a/R/type_tile.R b/R/type_tile.R index bf9fd7f4..6c84b6dd 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -128,6 +128,11 @@ #' xlab = NA, ylab = NA #' ) #' +#' # +#' ## aside: use tinyplot.matrix directly to avoid reshaping ---- +#' +#' tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white") +#' #' ## restore the default theme #' tinytheme() #' diff --git a/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd index 2aae1d0b..db3c311b 100644 --- a/man/tinyplot.matrix.Rd +++ b/man/tinyplot.matrix.Rd @@ -20,7 +20,8 @@ \item{type}{plot type passed on to \code{tinyplot}. Defaults to \code{"p"} (points).} \item{legend}{specification passed on to \code{tinyplot}. The default is to draw a -legend when the matrix has named columns, and to suppress it otherwise.} +legend when the matrix has named columns, and to suppress it otherwise. For +\code{"tile"} and \code{"heatmap"} types it is suppressed by default.} \item{facet}{specification of \code{facet} passed on to \code{tinyplot}. The only accepted non-\code{NULL} value is the \code{"by"} convenience string, which facets @@ -29,7 +30,8 @@ the plot by matrix column.} \item{xlab, ylab}{axis labels passed on to \code{tinyplot}. \code{ylab} defaults to the deparsed matrix name. \code{xlab} defaults to \code{"Index"} when the matrix has no row names; when it does, the row names already label the ticks so the -x-axis title is suppressed.} +x-axis title is suppressed. For \code{"tile"} and \code{"heatmap"} types both +titles default to \code{NA}, since the dimnames label both axes.} \item{...}{further arguments passed to \code{tinyplot}.} } @@ -50,6 +52,17 @@ faceted via \code{facet = "by"}. This mirrors the base R matrix against the row numbers. If the matrix has column names, these are used as the group (and legend) labels. Single-column matrices are drawn as a simple index plot with no grouping or legend. + +The \code{"tile"} and \code{"heatmap"} types are an exception, since the matplot +convention makes little sense for them. Instead the matrix is laid out as a +grid---columns along the x-axis, rows along the y-axis---with the matrix +\emph{values} supplied as the fill. Row order is reversed so that the first row +sits at the top, matching how one reads a matrix (cf. +\code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}). Both axis +titles are suppressed, since the dimnames already label the ticks, and so +is the legend, since the fill merely re-encodes the matrix's own values. +Pass an explicit \code{legend} (or \code{xlab}/\code{ylab}) to override either. See +Examples. } \examples{ # basic use @@ -62,6 +75,9 @@ tinyplot(VADeaths, type = "b", legend = FALSE, facet = "by", theme = "socviz") sines = outer(1:20, 1:4, function(x, y) sin(x / 20 * pi * y)) tinyplot(sines, type = "o", pch = "by", lty = "by", col = rainbow(ncol(sines))) +# `"tile"` + `"heatmap"` types lay the matrix out as a grid instead +tinyplot(VADeaths, type = "heatmap", theme = "heatmap", col = "white") + } \seealso{ \code{\link[graphics]{matplot}} diff --git a/man/type_tile.Rd b/man/type_tile.Rd index ab151b04..3da51892 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -172,6 +172,11 @@ tinyplot( xlab = NA, ylab = NA ) +# +## aside: use tinyplot.matrix directly to avoid reshaping ---- + +tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white") + ## restore the default theme tinytheme() From fb71e4d1dff11ca3938c4346ecac661167d4b307 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 4 Aug 2026 20:23:19 -0700 Subject: [PATCH 16/22] switch to zscore as default --- R/type_tile.R | 48 ++++++++++++++++++++++++++++++++---------------- man/type_tile.Rd | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/R/type_tile.R b/R/type_tile.R index 6c84b6dd..9526a10b 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -37,10 +37,16 @@ #' switches to a sequential palette. See [`tinytheme()`] and the Examples. #' #' `type_heatmap()`'s `scale` argument is the analogue of the `scale` argument -#' in base R's \code{\link[stats]{heatmap}}. For exact parity with the -#' latter, which z-scores along the chosen margin, pair it with -#' `method = "zscore"`; our default instead rescales each group onto the unit -#' (\[0, 1\]) interval. +#' in base R's \code{\link[stats]{heatmap}}, and like the latter it z-scores +#' along the chosen margin by default. Pass `method = "rescale"` to map each +#' group onto the unit \[0, 1\] interval instead. +#' +#' Either way, note that scaling along a margin necessarily discards the +#' *relative* spread of each group: a narrow-range column will occupy as much +#' of the colour ramp as a wide-range one, since both are divided by their own +#' measure of spread. That is the price of making a matrix of incomparable +#' units legible; use `scale = "none"` (or `type_tile()`) when preserving +#' cross-group magnitudes matters more. #' #' @param width,height Numeric tile dimensions in data units. Both default to #' `1`, which produces contiguous tiles on categorical (or unit-spaced @@ -114,17 +120,20 @@ #' xlab = NA, ylab = NA #' ) #' -#' # and now rescaled within each x variable (i.e., column) +#' # and now scaled within each x variable (i.e., column). The default is to +#' # z-score, matching base R's `heatmap(scale = "column")`. #' tinyplot( #' Var1 ~ Var2 | Freq, data = mt, #' type = type_heatmap(scale = "x"), #' xlab = NA, ylab = NA #' ) #' -#' # use `method = "zscore"` for parity with base R's `heatmap(scale = "column")` +#' # `method = "rescale"` maps each column onto [0, 1] instead. This uses the +#' # colour ramp more fully, at the cost of pinning every column's min and max to +#' # the same two colours. #' tinyplot( #' Var1 ~ Var2 | Freq, data = mt, -#' type = type_heatmap(scale = "x", method = "zscore"), +#' type = type_heatmap(scale = "x", method = "rescale"), #' xlab = NA, ylab = NA #' ) #' @@ -155,9 +164,9 @@ type_tile = function(width = 1, height = 1) { #' @rdname type_tile -#' @param scale Character. Should the `by` (fill) values be rescaled *within* +#' @param scale Character. Should the `by` (fill) values be scaled *within* #' each category of one axis? One of `"none"` (default, i.e. the raw values -#' are used), `"x"`, or `"y"`. Rescaling is what makes a raw matrix legible +#' are used), `"x"`, or `"y"`. Scaling is what makes a raw matrix legible #' when its variables span very different magnitudes: left alone, the #' largest-magnitude column monopolises the entire colour ramp. See Examples. #' @@ -172,9 +181,16 @@ type_tile = function(width = 1, height = 1) { #' internal structure. Since rescaled values are no longer in the units of the #' `by` variable, the legend title is annotated accordingly. #' @param method Character. How should the values be rescaled, if `scale` is not -#' `"none"`? Either `"rescale"` (default) to map each group onto the unit -#' interval \[0, 1\], or `"zscore"` to centre each group and divide by its -#' standard deviation. Ignored when `scale = "none"`. +#' `"none"`? Either `"zscore"` (default) to centre each group and divide by its +#' standard deviation, or `"rescale"` to map each group onto the unit interval +#' \[0, 1\]. Ignored when `scale = "none"`. +#' +#' `"zscore"` matches base R's \code{\link[stats]{heatmap}} and keeps values +#' comparable across groups, since `-1` means "one standard deviation below +#' this group's mean" everywhere. `"rescale"` instead pins every group's +#' minimum and maximum to the ends of the colour ramp, which uses the palette +#' more fully but makes the endpoints an artefact of the transform rather than +#' a feature of the data. #' #' Groups with no spread---a constant column, or a single tile---would divide #' by zero, so they are set to the midpoint of the target range (`0.5` and `0` @@ -186,13 +202,13 @@ type_heatmap = function( width = 1, height = 1, scale = c("none", "x", "y"), - method = c("rescale", "zscore")) { + method = c("zscore", "rescale")) { assert_numeric(width) assert_numeric(height) if (length(scale) > 1L) scale = scale[1L] assert_choice(scale, c("none", "x", "y")) if (length(method) > 1L) method = method[1L] - assert_choice(method, c("rescale", "zscore")) + assert_choice(method, c("zscore", "rescale")) out = list( draw = draw_rect(), data = data_tile( @@ -217,7 +233,7 @@ type_heatmap = function( ## the offending group. Map such groups to the midpoint of the target range ## instead, and report them back so the caller can warn -- a silently flattened ## group otherwise reads as a genuine mid-scale value. -scale_by_group = function(by, g, method = "rescale") { +scale_by_group = function(by, g, method = "zscore") { gi = if (is.factor(g)) g else factor(g) mid = if (identical(method, "zscore")) 0 else 0.5 flat = character(0) @@ -249,7 +265,7 @@ scale_by_group = function(by, g, method = "rescale") { data_tile = function( - width = 1, height = 1, scale = "none", method = "rescale") { + width = 1, height = 1, scale = "none", method = "zscore") { fun = function(settings, ...) { env2env( settings, diff --git a/man/type_tile.Rd b/man/type_tile.Rd index 3da51892..80f8b7e6 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -11,7 +11,7 @@ type_heatmap( width = 1, height = 1, scale = c("none", "x", "y"), - method = c("rescale", "zscore") + method = c("zscore", "rescale") ) } \arguments{ @@ -20,9 +20,9 @@ type_heatmap( numeric) axes. Values below \code{1} inset the tiles, leaving gaps between them. Recycled across tiles, so a vector may be used for variable sizes.} -\item{scale}{Character. Should the \code{by} (fill) values be rescaled \emph{within} +\item{scale}{Character. Should the \code{by} (fill) values be scaled \emph{within} each category of one axis? One of \code{"none"} (default, i.e. the raw values -are used), \code{"x"}, or \code{"y"}. Rescaling is what makes a raw matrix legible +are used), \code{"x"}, or \code{"y"}. Scaling is what makes a raw matrix legible when its variables span very different magnitudes: left alone, the largest-magnitude column monopolises the entire colour ramp. See Examples. @@ -38,9 +38,16 @@ internal structure. Since rescaled values are no longer in the units of the \code{by} variable, the legend title is annotated accordingly.} \item{method}{Character. How should the values be rescaled, if \code{scale} is not -\code{"none"}? Either \code{"rescale"} (default) to map each group onto the unit -interval [0, 1], or \code{"zscore"} to centre each group and divide by its -standard deviation. Ignored when \code{scale = "none"}. +\code{"none"}? Either \code{"zscore"} (default) to centre each group and divide by its +standard deviation, or \code{"rescale"} to map each group onto the unit interval +[0, 1]. Ignored when \code{scale = "none"}. + +\code{"zscore"} matches base R's \code{\link[stats]{heatmap}} and keeps values +comparable across groups, since \code{-1} means "one standard deviation below +this group's mean" everywhere. \code{"rescale"} instead pins every group's +minimum and maximum to the ends of the colour ramp, which uses the palette +more fully but makes the endpoints an artefact of the transform rather than +a feature of the data. Groups with no spread---a constant column, or a single tile---would divide by zero, so they are set to the midpoint of the target range (\code{0.5} and \code{0} @@ -85,10 +92,16 @@ theme that removes the padding and grid, rotates the tick labels, and switches to a sequential palette. See \code{\link[=tinytheme]{tinytheme()}} and the Examples. \code{type_heatmap()}'s \code{scale} argument is the analogue of the \code{scale} argument -in base R's \code{\link[stats]{heatmap}}. For exact parity with the -latter, which z-scores along the chosen margin, pair it with -\code{method = "zscore"}; our default instead rescales each group onto the unit -([0, 1]) interval. +in base R's \code{\link[stats]{heatmap}}, and like the latter it z-scores +along the chosen margin by default. Pass \code{method = "rescale"} to map each +group onto the unit [0, 1] interval instead. + +Either way, note that scaling along a margin necessarily discards the +\emph{relative} spread of each group: a narrow-range column will occupy as much +of the colour ramp as a wide-range one, since both are divided by their own +measure of spread. That is the price of making a matrix of incomparable +units legible; use \code{scale = "none"} (or \code{type_tile()}) when preserving +cross-group magnitudes matters more. } \examples{ # It is recommended to use the dedicated "heatmap" theme for tile plots @@ -158,17 +171,20 @@ tinyplot( xlab = NA, ylab = NA ) -# and now rescaled within each x variable (i.e., column) +# and now scaled within each x variable (i.e., column). The default is to +# z-score, matching base R's `heatmap(scale = "column")`. tinyplot( Var1 ~ Var2 | Freq, data = mt, type = type_heatmap(scale = "x"), xlab = NA, ylab = NA ) -# use `method = "zscore"` for parity with base R's `heatmap(scale = "column")` +# `method = "rescale"` maps each column onto [0, 1] instead. This uses the +# colour ramp more fully, at the cost of pinning every column's min and max to +# the same two colours. tinyplot( Var1 ~ Var2 | Freq, data = mt, - type = type_heatmap(scale = "x", method = "zscore"), + type = type_heatmap(scale = "x", method = "rescale"), xlab = NA, ylab = NA ) From bf2b37e24122bc35c1fd97ee32533e6d831e68a3 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 4 Aug 2026 20:23:58 -0700 Subject: [PATCH 17/22] news --- NEWS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2d463a00..fe7d356e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,8 +15,9 @@ where the formatting is also better._ - `type_tile()` / `"tile"` for tile plots, i.e. a grid of rectangles whose fill encodes a third variable. (#677 @grantmcdermott) - `type_heatmap()` / `"heatmap"` builds on `type_tile()`, adding a `scale` - argument that rescales the fill values *within* each category of one axis. - This is anaologous to base R's `heatmap()` function. (#677 @grantmcdermott) + argument that scales the fill values *within* each category of one axis. This + is analogous to base R's `heatmap()` function, and like the latter it z-scores + along the chosen margin by default. (#677 @grantmcdermott) #### Other new features From 0843e8e3205ac657655c2b0c5fe976657ab9089c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 4 Aug 2026 20:29:05 -0700 Subject: [PATCH 18/22] redo tests --- .../_tinysnapshot/heatmap_scale_x.svg | 754 +++++++++--------- .../_tinysnapshot/heatmap_scale_x_rescale.svg | 442 ++++++++++ .../tinytest/_tinysnapshot/matrix_heatmap.svg | 67 ++ .../_tinysnapshot/matrix_heatmap_scale.svg | 433 ++++++++++ inst/tinytest/test-matrix.R | 20 + inst/tinytest/test-type_tile.R | 6 +- 6 files changed, 1344 insertions(+), 378 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg create mode 100644 inst/tinytest/_tinysnapshot/matrix_heatmap.svg create mode 100644 inst/tinytest/_tinysnapshot/matrix_heatmap_scale.svg diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg index 9fbe5215..17df5e33 100644 --- a/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg @@ -26,26 +26,30 @@ - - 0.2 - 0.6 - 1.0 -- - -- - -- - -Freq -(rescaled) -mpg -cyl -disp -hp -drat -wt -qsec -vs -am -gear -carb + + -1 + 0 + 1 + 2 + 3 +- - +- - +- - +- - +- - +Freq +(z-score) +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb Mazda RX4 Mazda RX4 Wag Datsun 710 @@ -80,363 +84,363 @@ Volvo 142E - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg new file mode 100644 index 00000000..9fbe5215 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + 0.2 + 0.6 + 1.0 +- - +- - +- - +Freq +(rescaled) +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Mazda RX4 +Mazda RX4 Wag +Datsun 710 +Hornet 4 Drive +Hornet Sportabout +Valiant +Duster 360 +Merc 240D +Merc 230 +Merc 280 +Merc 280C +Merc 450SE +Merc 450SL +Merc 450SLC +Cadillac Fleetwood +Lincoln Continental +Chrysler Imperial +Fiat 128 +Honda Civic +Toyota Corolla +Toyota Corona +Dodge Challenger +AMC Javelin +Camaro Z28 +Pontiac Firebird +Fiat X1-9 +Porsche 914-2 +Lotus Europa +Ford Pantera L +Ferrari Dino +Maserati Bora +Volvo 142E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/matrix_heatmap.svg b/inst/tinytest/_tinysnapshot/matrix_heatmap.svg new file mode 100644 index 00000000..932cb3fb --- /dev/null +++ b/inst/tinytest/_tinysnapshot/matrix_heatmap.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + +Rural Male +Rural Female +Urban Male +Urban Female +70-74 +65-69 +60-64 +55-59 +50-54 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/matrix_heatmap_scale.svg b/inst/tinytest/_tinysnapshot/matrix_heatmap_scale.svg new file mode 100644 index 00000000..b5a48a24 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/matrix_heatmap_scale.svg @@ -0,0 +1,433 @@ + + + + + + + + + + + + + +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Volvo 142E +Maserati Bora +Ferrari Dino +Ford Pantera L +Lotus Europa +Porsche 914-2 +Fiat X1-9 +Pontiac Firebird +Camaro Z28 +AMC Javelin +Dodge Challenger +Toyota Corona +Toyota Corolla +Honda Civic +Fiat 128 +Chrysler Imperial +Lincoln Continental +Cadillac Fleetwood +Merc 450SLC +Merc 450SL +Merc 450SE +Merc 280C +Merc 280 +Merc 230 +Merc 240D +Duster 360 +Valiant +Hornet Sportabout +Hornet 4 Drive +Datsun 710 +Mazda RX4 Wag +Mazda RX4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-matrix.R b/inst/tinytest/test-matrix.R index 11f54670..8f3d10a5 100644 --- a/inst/tinytest/test-matrix.R +++ b/inst/tinytest/test-matrix.R @@ -15,3 +15,23 @@ expect_snapshot_plot(f, label = "matrix_type_b") # faceting by column f = function() tinyplot(VADeaths, type = "o", facet = "by") expect_snapshot_plot(f, label = "matrix_facet") + + +# +## tile / heatmap types get a grid layout instead of the matplot convention +# + +f = function() tinyplot(VADeaths, type = "heatmap", theme = "heatmap") +expect_snapshot_plot(f, label = "matrix_heatmap") + +# "tile" takes the same layout, just without the rescaling option +f = function() tinyplot(VADeaths, type = "tile", theme = "heatmap") +expect_snapshot_plot(f, label = "matrix_heatmap") + +# rescaling within each column, for matrices whose columns are on different +# scales. This is the motivating case: unscaled, `disp`/`hp` swamp everything. +f = function() { + tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), + theme = "heatmap") +} +expect_snapshot_plot(f, label = "matrix_heatmap_scale") diff --git a/inst/tinytest/test-type_tile.R b/inst/tinytest/test-type_tile.R index 1c78fb41..7f9417c7 100644 --- a/inst/tinytest/test-type_tile.R +++ b/inst/tinytest/test-type_tile.R @@ -88,15 +88,16 @@ f = function() { } expect_snapshot_plot(f, label = "heatmap_scale_x") +# `method = "rescale"` is the alternative to the z-score default f = function() { tinyplot( Var1 ~ Var2 | Freq, data = mt, - type = type_heatmap(scale = "x", method = "zscore"), + type = type_heatmap(scale = "x", method = "rescale"), theme = "heatmap", xlab = NA, ylab = NA ) } -expect_snapshot_plot(f, label = "heatmap_scale_x_zscore") +expect_snapshot_plot(f, label = "heatmap_scale_x_rescale") # `scale = "none"` is the default, so a bare type_heatmap() must reproduce the # plain type_tile() fixture exactly (asserted against the *same* label) @@ -107,4 +108,3 @@ f = function() { ) } expect_snapshot_plot(f, label = "tile_basic") - From a3b316148084dea412ff6796b68b299738b07c07 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 11 Aug 2026 19:43:42 -0700 Subject: [PATCH 19/22] make ylim = 'rev' default to type_heatmap --- NEWS.md | 4 +- R/tinyplot.matrix.R | 45 +- R/type_tile.R | 33 +- .../_tinysnapshot/heatmap_scale_x.svg | 754 +++++++++--------- .../_tinysnapshot/heatmap_scale_x_rescale.svg | 754 +++++++++--------- .../_tinysnapshot/heatmap_scale_x_zscore.svg | 446 ----------- inst/tinytest/_tinysnapshot/tile_scale_x.svg | 442 ---------- inst/tinytest/test-type_tile.R | 16 +- man/tinyplot.matrix.Rd | 5 +- man/type_tile.Rd | 6 + 10 files changed, 836 insertions(+), 1669 deletions(-) delete mode 100644 inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg delete mode 100644 inst/tinytest/_tinysnapshot/tile_scale_x.svg diff --git a/NEWS.md b/NEWS.md index fe7d356e..365a43e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,7 +17,9 @@ where the formatting is also better._ - `type_heatmap()` / `"heatmap"` builds on `type_tile()`, adding a `scale` argument that scales the fill values *within* each category of one axis. This is analogous to base R's `heatmap()` function, and like the latter it z-scores - along the chosen margin by default. (#677 @grantmcdermott) + along the chosen margin by default. It also reverses the y-axis by default, so + that the first row sits at the top (again matching `heatmap()`); pass an + explicit `ylim` to override. (#677 @grantmcdermott) #### Other new features diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R index 03af133d..03260bcd 100644 --- a/R/tinyplot.matrix.R +++ b/R/tinyplot.matrix.R @@ -16,9 +16,10 @@ #' The `"tile"` and `"heatmap"` types are an exception, since the matplot #' convention makes little sense for them. Instead the matrix is laid out as a #' grid---columns along the x-axis, rows along the y-axis---with the matrix -#' *values* supplied as the fill. Row order is reversed so that the first row +#' *values* supplied as the fill. The y-axis is reversed so that the first row #' sits at the top, matching how one reads a matrix (cf. -#' \code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}). Both axis +#' \code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}); pass an +#' explicit `ylim` to override. Both axis #' titles are suppressed, since the dimnames already label the ticks, and so #' is the legend, since the fill merely re-encodes the matrix's own values. #' Pass an explicit `legend` (or `xlab`/`ylab`) to override either. See @@ -82,13 +83,14 @@ tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = N } else { factor(rep(cnms, each = dims[1]), levels = cnms) } - ## Reverse the row levels so that row 1 sits at the *top* of the plot, - ## matching how one reads a matrix (cf. `heatmap()`, `image()`). + ## Note the row levels are *not* reversed here. Row 1 belongs at the *top* + ## of a matrix display (cf. `heatmap()`, `image()`), but we get that by + ## defaulting the y-axis to reversed below, which keeps it overridable via + ## `ylim`. Reversing the levels *and* the axis would cancel out. yy = if (is.null(rnms)) { - factor(rep(seq_len(dims[1]), times = dims[2]), - levels = rev(seq_len(dims[1]))) + factor(rep(seq_len(dims[1]), times = dims[2])) } else { - factor(rep(rnms, times = dims[2]), levels = rev(rnms)) + factor(rep(rnms, times = dims[2]), levels = rnms) } ## Both axes are labelled by the matrix dimnames, so axis titles would be ## redundant. Ditto the legend: the fill encodes the matrix's own values, so @@ -97,15 +99,26 @@ tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = N if (is.null(xlab)) xlab = NA if (is.null(ylab)) ylab = NA if (is.null(legend)) legend = FALSE - return(tinyplot.default( - x = xx, y = yy, - type = type, - by = as.vector(x), - facet = facet, - legend = legend, - xlab = xlab, - ylab = ylab, - ... + ## Applies to "tile" as well as "heatmap": the matrix *layout* is what + ## implies the orientation here, not the choice of type. (type_heatmap() + ## additionally defaults to this on its own, for the formula method; the two + ## are idempotent and so compose safely.) + dots = list(...) + if (!"ylim" %in% names(dots)) dots[["ylim"]] = "reverse" + return(do.call( + tinyplot.default, + c( + list( + x = xx, y = yy, + type = type, + by = as.vector(x), + facet = facet, + legend = legend, + xlab = xlab, + ylab = ylab + ), + dots + ) )) } if (dims[2] == 1L) { diff --git a/R/type_tile.R b/R/type_tile.R index 9526a10b..08b2f3b0 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -41,6 +41,12 @@ #' along the chosen margin by default. Pass `method = "rescale"` to map each #' group onto the unit \[0, 1\] interval instead. #' +#' `type_heatmap()` also reverses the y-axis by default, so that the first +#' row sits at the top, matching how one reads a matrix (and again cf. base +#' R's `heatmap()` and \code{\link[graphics]{image}}). Pass an explicit `ylim` +#' to override. `type_tile()` makes no such adjustment, since it draws the +#' values exactly as supplied. +#' #' Either way, note that scaling along a margin necessarily discards the #' *relative* spread of each group: a narrow-range column will occupy as much #' of the colour ramp as a wide-range one, since both are divided by their own @@ -211,7 +217,7 @@ type_heatmap = function( assert_choice(method, c("zscore", "rescale")) out = list( draw = draw_rect(), - data = data_tile( + data = data_heatmap( width = width, height = height, scale = scale, method = method ), # Deliberately reports "tile": the two types are interchangeable as far as @@ -264,6 +270,31 @@ scale_by_group = function(by, g, method = "zscore") { } +## type_heatmap() is data_tile() plus one extra convention: the first row sits +## at the *top*, matching how one reads a matrix (cf. `heatmap()`, `image()`). +## Kept separate from data_tile() so that type_tile() keeps drawing values +## exactly as supplied. +data_heatmap = function( + width = 1, height = 1, scale = "none", method = "zscore") { + tile_fun = data_tile( + width = width, height = height, scale = scale, method = method + ) + fun = function(settings, ...) { + tile_fun(settings, ...) + # Only default the reversal when the user has left `ylim` alone: an explicit + # `ylim` is a direct instruction about axis direction and must win. We set + # the already-parsed `rev_y` flag rather than `ylim = "reverse"`, because + # sanitize_lim_rev() resolves that keyword much earlier in the pipeline, so + # a character `ylim` set here would reach lim_args() unparsed. The flag is + # also idempotent (so it cannot double-reverse if something upstream has + # asked for the same thing) and flip_datapoints() knows to swap it under + # `flip = TRUE`. + if (isTRUE(settings$null_ylim)) settings$rev_y = TRUE + } + return(fun) +} + + data_tile = function( width = 1, height = 1, scale = "none", method = "zscore") { fun = function(settings, ...) { diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg index 17df5e33..d40a0667 100644 --- a/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x.svg @@ -50,38 +50,38 @@ am gear carb -Mazda RX4 -Mazda RX4 Wag -Datsun 710 -Hornet 4 Drive -Hornet Sportabout -Valiant -Duster 360 -Merc 240D -Merc 230 -Merc 280 -Merc 280C -Merc 450SE -Merc 450SL -Merc 450SLC -Cadillac Fleetwood -Lincoln Continental -Chrysler Imperial -Fiat 128 -Honda Civic -Toyota Corolla -Toyota Corona -Dodge Challenger -AMC Javelin -Camaro Z28 -Pontiac Firebird -Fiat X1-9 -Porsche 914-2 -Lotus Europa -Ford Pantera L -Ferrari Dino -Maserati Bora -Volvo 142E +Volvo 142E +Maserati Bora +Ferrari Dino +Ford Pantera L +Lotus Europa +Porsche 914-2 +Fiat X1-9 +Pontiac Firebird +Camaro Z28 +AMC Javelin +Dodge Challenger +Toyota Corona +Toyota Corolla +Honda Civic +Fiat 128 +Chrysler Imperial +Lincoln Continental +Cadillac Fleetwood +Merc 450SLC +Merc 450SL +Merc 450SE +Merc 280C +Merc 280 +Merc 230 +Merc 240D +Duster 360 +Valiant +Hornet Sportabout +Hornet 4 Drive +Datsun 710 +Mazda RX4 Wag +Mazda RX4 @@ -89,358 +89,358 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg index 9fbe5215..df030db1 100644 --- a/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg +++ b/inst/tinytest/_tinysnapshot/heatmap_scale_x_rescale.svg @@ -46,38 +46,38 @@ am gear carb -Mazda RX4 -Mazda RX4 Wag -Datsun 710 -Hornet 4 Drive -Hornet Sportabout -Valiant -Duster 360 -Merc 240D -Merc 230 -Merc 280 -Merc 280C -Merc 450SE -Merc 450SL -Merc 450SLC -Cadillac Fleetwood -Lincoln Continental -Chrysler Imperial -Fiat 128 -Honda Civic -Toyota Corolla -Toyota Corona -Dodge Challenger -AMC Javelin -Camaro Z28 -Pontiac Firebird -Fiat X1-9 -Porsche 914-2 -Lotus Europa -Ford Pantera L -Ferrari Dino -Maserati Bora -Volvo 142E +Volvo 142E +Maserati Bora +Ferrari Dino +Ford Pantera L +Lotus Europa +Porsche 914-2 +Fiat X1-9 +Pontiac Firebird +Camaro Z28 +AMC Javelin +Dodge Challenger +Toyota Corona +Toyota Corolla +Honda Civic +Fiat 128 +Chrysler Imperial +Lincoln Continental +Cadillac Fleetwood +Merc 450SLC +Merc 450SL +Merc 450SE +Merc 280C +Merc 280 +Merc 230 +Merc 240D +Duster 360 +Valiant +Hornet Sportabout +Hornet 4 Drive +Datsun 710 +Mazda RX4 Wag +Mazda RX4 @@ -85,358 +85,358 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg b/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg deleted file mode 100644 index 17df5e33..00000000 --- a/inst/tinytest/_tinysnapshot/heatmap_scale_x_zscore.svg +++ /dev/null @@ -1,446 +0,0 @@ - - - - - - - - - - - - - - - -1 - 0 - 1 - 2 - 3 -- - -- - -- - -- - -- - -Freq -(z-score) -mpg -cyl -disp -hp -drat -wt -qsec -vs -am -gear -carb -Mazda RX4 -Mazda RX4 Wag -Datsun 710 -Hornet 4 Drive -Hornet Sportabout -Valiant -Duster 360 -Merc 240D -Merc 230 -Merc 280 -Merc 280C -Merc 450SE -Merc 450SL -Merc 450SLC -Cadillac Fleetwood -Lincoln Continental -Chrysler Imperial -Fiat 128 -Honda Civic -Toyota Corolla -Toyota Corona -Dodge Challenger -AMC Javelin -Camaro Z28 -Pontiac Firebird -Fiat X1-9 -Porsche 914-2 -Lotus Europa -Ford Pantera L -Ferrari Dino -Maserati Bora -Volvo 142E - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/inst/tinytest/_tinysnapshot/tile_scale_x.svg b/inst/tinytest/_tinysnapshot/tile_scale_x.svg deleted file mode 100644 index 9fbe5215..00000000 --- a/inst/tinytest/_tinysnapshot/tile_scale_x.svg +++ /dev/null @@ -1,442 +0,0 @@ - - - - - - - - - - - - - - - 0.2 - 0.6 - 1.0 -- - -- - -- - -Freq -(rescaled) -mpg -cyl -disp -hp -drat -wt -qsec -vs -am -gear -carb -Mazda RX4 -Mazda RX4 Wag -Datsun 710 -Hornet 4 Drive -Hornet Sportabout -Valiant -Duster 360 -Merc 240D -Merc 230 -Merc 280 -Merc 280C -Merc 450SE -Merc 450SL -Merc 450SLC -Cadillac Fleetwood -Lincoln Continental -Chrysler Imperial -Fiat 128 -Honda Civic -Toyota Corolla -Toyota Corona -Dodge Challenger -AMC Javelin -Camaro Z28 -Pontiac Firebird -Fiat X1-9 -Porsche 914-2 -Lotus Europa -Ford Pantera L -Ferrari Dino -Maserati Bora -Volvo 142E - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/inst/tinytest/test-type_tile.R b/inst/tinytest/test-type_tile.R index 7f9417c7..bee7504e 100644 --- a/inst/tinytest/test-type_tile.R +++ b/inst/tinytest/test-type_tile.R @@ -5,8 +5,9 @@ using("tinysnapshot") # "long" form used by the type_tile() examples catt = as.data.frame(as.table(cor(attitude)), responseName = "Correlation") -# "tile" and "heatmap" are aliases, as are type_tile() and type_heatmap(), -# so all four spellings must produce an identical plot. +# "tile" and type_tile() are aliases and must produce an identical plot. Note +# that "heatmap" / type_heatmap() are *not* interchangeable with these, since +# they additionally reverse the y-axis (see the type_heatmap() section below). f = function() { tinyplot( Var1 ~ Var2 | Correlation, data = catt, type = "tile", @@ -15,10 +16,10 @@ f = function() { } expect_snapshot_plot(f, label = "tile_basic") -# "heatmap" alias (should be identical to the above) +# type_tile() constructor (should be identical to the above) f = function() { tinyplot( - Var1 ~ Var2 | Correlation, data = catt, type = "heatmap", + Var1 ~ Var2 | Correlation, data = catt, type = type_tile(), theme = "heatmap" ) } @@ -99,12 +100,13 @@ f = function() { } expect_snapshot_plot(f, label = "heatmap_scale_x_rescale") -# `scale = "none"` is the default, so a bare type_heatmap() must reproduce the -# plain type_tile() fixture exactly (asserted against the *same* label) +# `scale = "none"` is the default, so a bare type_heatmap() applies no rescaling. +# It does still reverse the y-axis, so pinning `ylim` back to normal is what +# recovers the plain type_tile() fixture exactly (asserted against that label). f = function() { tinyplot( Var1 ~ Var2 | Correlation, data = catt, type = type_heatmap(scale = "none"), - theme = "heatmap" + theme = "heatmap", ylim = c(0.5, 7.5) ) } expect_snapshot_plot(f, label = "tile_basic") diff --git a/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd index db3c311b..e39f60a4 100644 --- a/man/tinyplot.matrix.Rd +++ b/man/tinyplot.matrix.Rd @@ -56,9 +56,10 @@ a simple index plot with no grouping or legend. The \code{"tile"} and \code{"heatmap"} types are an exception, since the matplot convention makes little sense for them. Instead the matrix is laid out as a grid---columns along the x-axis, rows along the y-axis---with the matrix -\emph{values} supplied as the fill. Row order is reversed so that the first row +\emph{values} supplied as the fill. The y-axis is reversed so that the first row sits at the top, matching how one reads a matrix (cf. -\code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}). Both axis +\code{\link[stats]{heatmap}} and \code{\link[graphics]{image}}); pass an +explicit \code{ylim} to override. Both axis titles are suppressed, since the dimnames already label the ticks, and so is the legend, since the fill merely re-encodes the matrix's own values. Pass an explicit \code{legend} (or \code{xlab}/\code{ylab}) to override either. See diff --git a/man/type_tile.Rd b/man/type_tile.Rd index 80f8b7e6..63e0c13b 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -96,6 +96,12 @@ in base R's \code{\link[stats]{heatmap}}, and like the latter it z-scores along the chosen margin by default. Pass \code{method = "rescale"} to map each group onto the unit [0, 1] interval instead. +\code{type_heatmap()} also reverses the y-axis by default, so that the first +row sits at the top, matching how one reads a matrix (and again cf. base +R's \code{heatmap()} and \code{\link[graphics]{image}}). Pass an explicit \code{ylim} +to override. \code{type_tile()} makes no such adjustment, since it draws the +values exactly as supplied. + Either way, note that scaling along a margin necessarily discards the \emph{relative} spread of each group: a narrow-range column will occupy as much of the colour ramp as a wide-range one, since both are divided by their own From d1e90d10549169c12d5a52c2450a1911d8ca8dbb Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 11 Aug 2026 19:43:55 -0700 Subject: [PATCH 20/22] roxygen2 update --- DESCRIPTION | 2 +- NAMESPACE | 192 +++++++++++++++++++++++++++------------------------- 2 files changed, 101 insertions(+), 93 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index d156c97f..c523db08 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -63,4 +63,4 @@ Encoding: UTF-8 URL: https://grantmcdermott.com/tinyplot/ BugReports: https://github.com/grantmcdermott/tinyplot/issues Roxygen: list(markdown = TRUE) -Config/roxygen2/version: 8.0.0 +Config/roxygen2/version: 8.1.0 diff --git a/NAMESPACE b/NAMESPACE index 0dcfbb08..90bf6e3d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -55,96 +55,104 @@ export(type_text) export(type_tile) export(type_violin) export(type_vline) -importFrom(grDevices,adjustcolor) -importFrom(grDevices,as.raster) -importFrom(grDevices,axisTicks) -importFrom(grDevices,cairo_pdf) -importFrom(grDevices,chull) -importFrom(grDevices,col2rgb) -importFrom(grDevices,colorRampPalette) -importFrom(grDevices,convertColor) -importFrom(grDevices,dev.cur) -importFrom(grDevices,dev.list) -importFrom(grDevices,dev.new) -importFrom(grDevices,dev.off) -importFrom(grDevices,extendrange) -importFrom(grDevices,gray.colors) -importFrom(grDevices,hcl) -importFrom(grDevices,hcl.colors) -importFrom(grDevices,hcl.pals) -importFrom(grDevices,jpeg) -importFrom(grDevices,nclass.Sturges) -importFrom(grDevices,palette) -importFrom(grDevices,palette.colors) -importFrom(grDevices,palette.pals) -importFrom(grDevices,pdf) -importFrom(grDevices,png) -importFrom(grDevices,recordGraphics) -importFrom(grDevices,svg) -importFrom(grDevices,xy.coords) -importFrom(graphics,Axis) -importFrom(graphics,abline) -importFrom(graphics,arrows) -importFrom(graphics,axTicks) -importFrom(graphics,axis) -importFrom(graphics,box) -importFrom(graphics,boxplot) -importFrom(graphics,grconvertX) -importFrom(graphics,grconvertY) -importFrom(graphics,hist) -importFrom(graphics,legend) -importFrom(graphics,lines) -importFrom(graphics,mtext) -importFrom(graphics,par) -importFrom(graphics,plot.default) -importFrom(graphics,plot.new) -importFrom(graphics,plot.window) -importFrom(graphics,points) -importFrom(graphics,polygon) -importFrom(graphics,polypath) -importFrom(graphics,rasterImage) -importFrom(graphics,rect) -importFrom(graphics,rug) -importFrom(graphics,segments) -importFrom(graphics,strheight) -importFrom(graphics,strwidth) -importFrom(graphics,text) -importFrom(graphics,title) -importFrom(graphics,xinch) -importFrom(stats,aggregate) -importFrom(stats,approx) -importFrom(stats,as.formula) -importFrom(stats,ave) -importFrom(stats,bw.SJ) -importFrom(stats,bw.bcv) -importFrom(stats,bw.nrd) -importFrom(stats,bw.nrd0) -importFrom(stats,bw.ucv) -importFrom(stats,cov) -importFrom(stats,density) -importFrom(stats,dnorm) -importFrom(stats,glm) -importFrom(stats,lm) -importFrom(stats,loess) -importFrom(stats,loess.control) -importFrom(stats,median) -importFrom(stats,model.frame) -importFrom(stats,na.omit) -importFrom(stats,ppoints) -importFrom(stats,predict) -importFrom(stats,qchisq) -importFrom(stats,qnorm) -importFrom(stats,qt) -importFrom(stats,quantile) -importFrom(stats,reformulate) -importFrom(stats,sd) -importFrom(stats,setNames) -importFrom(stats,spline) -importFrom(stats,terms) -importFrom(stats,time) -importFrom(stats,weighted.mean) +importFrom(grDevices, + adjustcolor, + as.raster, + axisTicks, + cairo_pdf, + chull, + col2rgb, + colorRampPalette, + convertColor, + dev.cur, + dev.list, + dev.new, + dev.off, + extendrange, + gray.colors, + hcl, + hcl.colors, + hcl.pals, + jpeg, + nclass.Sturges, + palette, + palette.colors, + palette.pals, + pdf, + png, + recordGraphics, + svg, + xy.coords +) +importFrom(graphics, + Axis, + abline, + arrows, + axTicks, + axis, + box, + boxplot, + grconvertX, + grconvertY, + hist, + legend, + lines, + mtext, + par, + plot.default, + plot.new, + plot.window, + points, + polygon, + polypath, + rasterImage, + rect, + rug, + segments, + strheight, + strwidth, + text, + title, + xinch +) +importFrom(stats, + aggregate, + approx, + as.formula, + ave, + bw.SJ, + bw.bcv, + bw.nrd, + bw.nrd0, + bw.ucv, + cov, + density, + dnorm, + glm, + lm, + loess, + loess.control, + median, + model.frame, + na.omit, + ppoints, + predict, + qchisq, + qnorm, + qt, + quantile, + reformulate, + sd, + setNames, + spline, + terms, + time, + weighted.mean +) importFrom(tools,file_ext) -importFrom(utils,globalVariables) -importFrom(utils,head) -importFrom(utils,modifyList) -importFrom(utils,tail) +importFrom(utils, + globalVariables, + head, + modifyList, + tail +) From f9230b6a2dfa72eb6cbbdce733f9f527ad701a80 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 14 Aug 2026 19:25:30 -0700 Subject: [PATCH 21/22] per-axis tick label scaling (`cex.xaxs` / `cex.yaxs`) --- NEWS.md | 15 + R/facet.R | 30 +- R/tinyplot.R | 14 +- R/tinytheme.R | 12 +- R/tpar.R | 8 + R/type_tile.R | 7 +- R/utils.R | 6 +- altdoc/pkgdown.yml | 2 +- .../margins_per_axis_cex_heatmap.svg | 433 ++++++++++++++++++ inst/tinytest/test-margins.R | 13 +- man/tpar.Rd | 4 + man/type_tile.Rd | 7 +- 12 files changed, 529 insertions(+), 22 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/margins_per_axis_cex_heatmap.svg diff --git a/NEWS.md b/NEWS.md index 365a43e4..5ee8e6ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -52,6 +52,11 @@ where the formatting is also better._ `"cat"` (console), in any combination; a destination the user has already labelled is left alone. Shared bandwidths are reported once and named as joint, individual bandwidths per group. (#287 @haomeng797-ship-it) +- New `cex.xaxs` and `cex.yaxs` graphical parameters allow the x- and y-axis + tick labels to be sized independently, e.g. `tpar(cex.yaxs = 0.6)` to shrink a + long list of category names on the y-axis without also shrinking the x-axis. + Both default to `NULL`, in which case the shared `cex.axis` value is used, so + existing plots are unaffected. (#677 @grantmcdermott) - Themes: - `"heatmap"` provides a dedicated companion theme to the new `type_tile()` and `type_heatmap()` types (see above). The theme removes all axis padding, @@ -72,6 +77,16 @@ where the formatting is also better._ hand and so silently ignored `cex.axis`, `lwd.axis` and `lty.axis` (plus their per-side variants), which was most visible under themes that set them, e.g. `tinytheme("bw")`. (#673 @grantmcdermott) +- Axis tick labels now honour the themed `cex.axis` value. The internal axis call + passed it as `cex`, which base `axis()` ignores in favour of `cex.axis` when + sizing tick labels, so the setting had no effect on label size. This also means + the per-side `cex.xaxs`/`cex.yaxs` parameters (see above) take effect. + (#677 @grantmcdermott) +- Dynamic margins now measure each axis at its own tick-label size. The margin + and whitespace calculations read only the shared `cex.axis`, so a plot that + set `cex.xaxs`/`cex.yaxs` to different values clipped the labels on the larger + axis and reserved dead whitespace on the smaller one, e.g. + `tinytheme("heatmap", cex.xaxs = 2, cex.yaxs = 0.5)`. (#677 @grantmcdermott) - Grouped and faceted plots no longer redraw axes once per empty group. This was most visible for `"spineplot"` types (e.g. `facet = "by"`), where the self-drawn axis labels were overplotted several times and rendered too heavy. diff --git a/R/facet.R b/R/facet.R index 9e9e22fb..247c277d 100644 --- a/R/facet.R +++ b/R/facet.R @@ -152,6 +152,11 @@ draw_facet_window = function( # Use that as the base instead of par("mar") which may have been # reset by the before.plot.new hook. side.sub = get_tpar("side.sub", tpar_list = tpars, default = 3) + # Tick labels are measured at their own side's cex, falling back to the + # shared par("cex.axis"). Measuring both sides at the shared value clips + # the wider axis and reserves dead space on the narrower one. + .cex_xaxs = get_tpar("cex.xaxs", tpar_list = tpars, default = par("cex.axis")) + .cex_yaxs = get_tpar("cex.yaxs", tpar_list = tpars, default = par("cex.axis")) omar = dynmar_computed omar[3] = dynmar_computed[3] + (1 + facet_newlines + 0.1) * facet_text # Ensure fmar[3] doesn't exceed omar[3] - 0.1, which would make @@ -169,7 +174,7 @@ draw_facet_window = function( yaxlabs_all = lapply(yfree_split, function(yf) { axisTicks(usr = extendrange(range(yf, na.rm = TRUE), f = 0.04), log = par("ylog")) }) - widths = vapply(yaxlabs_all, function(labs) max(strwidth(labs, "inches", cex = par("cex.axis"))), numeric(1L)) + widths = vapply(yaxlabs_all, function(labs) max(strwidth(labs, "inches", cex = .cex_yaxs)), numeric(1L)) yaxlabs = yaxlabs_all[[which.max(widths)]] } else { yaxlabs = axisTicks(usr = extendrange(ylim, f = 0.04), log = par("ylog")) @@ -177,7 +182,7 @@ draw_facet_window = function( } if (!is.null(yaxl)) yaxlabs = tinylabel(yaxlabs, yaxl) # whtsbp = grconvertX(max(strwidth(yaxl, "figure")), from = "nfc", to = "lines") - 1 - whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = par("cex.axis"))), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 0.5 + whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = .cex_yaxs)), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 0.5 if (whtsbp > 0) { omar = omar + c(0, whtsbp, 0, 0) * cex_fct_adj fmar[2] = fmar[2] + whtsbp * cex_fct_adj @@ -200,14 +205,14 @@ draw_facet_window = function( xaxlabs_all = lapply(xfree_split, function(xf) { axisTicks(usr = extendrange(range(xf, na.rm = TRUE), f = 0.04), log = par("xlog")) }) - widths = vapply(xaxlabs_all, function(labs) max(strwidth(labs, "inches", cex = par("cex.axis"))), numeric(1L)) + widths = vapply(xaxlabs_all, function(labs) max(strwidth(labs, "inches", cex = .cex_xaxs)), numeric(1L)) xaxlabs = xaxlabs_all[[which.max(widths)]] } else { xaxlabs = if (is.null(xlabs)) axisTicks(usr = extendrange(xlim, f = 0.04), log = par("xlog")) else if (!is.null(names(xlabs))) names(xlabs) else xlabs } if (!is.null(xaxl)) xaxlabs = tinylabel(xaxlabs, xaxl) - whtsbp = grconvertX(max(strwidth(xaxlabs, "figure", cex = par("cex.axis"))), from = "nfc", to = "lines") - 0.5 + whtsbp = grconvertX(max(strwidth(xaxlabs, "figure", cex = .cex_xaxs)), from = "nfc", to = "lines") - 0.5 if (whtsbp > 0) { omar = omar + c(whtsbp, 0, 0, 0) * cex_fct_adj fmar[1] = fmar[1] + whtsbp * cex_fct_adj @@ -258,6 +263,9 @@ draw_facet_window = function( # in tinyplot.default and passed via dynmar_computed; use them directly. # Tick-label *width/height* (whtsbp) is added further below. side.sub = get_tpar("side.sub", tpar_list = tpars, default = 3) + # Per-side tick-label cex; see the faceted branch above. + .cex_xaxs = get_tpar("cex.xaxs", tpar_list = tpars, default = par("cex.axis")) + .cex_yaxs = get_tpar("cex.yaxs", tpar_list = tpars, default = par("cex.axis")) omar = dynmar_computed # reserve RHS margin for types with a secondary axis (e.g. spineplot) if (isTRUE(type_hints[["has_rhs_axis"]])) omar[4] = 2.1 @@ -271,8 +279,8 @@ draw_facet_window = function( yaxlabs = axisTicks(usr = ylim_usr, log = par("ylog")) } if (!is.null(yaxl)) yaxlabs = tinylabel(yaxlabs, yaxl) - # whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = par("cex.axis"))), from = "nfc", to = "lines") - 1 - whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = par("cex.axis"))), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 0.5 + # whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = .cex_yaxs)), from = "nfc", to = "lines") - 1 + whtsbp = grconvertX(max(strwidth(yaxlabs, "figure", cex = .cex_yaxs)), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 0.5 omar[2] = omar[2] + whtsbp } if (par("las") %in% 2:3) { @@ -282,7 +290,7 @@ draw_facet_window = function( xaxlabs = if (is.null(xlabs)) axisTicks(usr = xlim_usr, log = par("xlog")) else if (!is.null(names(xlabs))) names(xlabs) else xlabs if (!is.null(xaxl)) xaxlabs = tinylabel(xaxlabs, xaxl) - whtsbp = grconvertX(max(strwidth(xaxlabs, "figure", cex = par("cex.axis"))), from = "nfc", to = "lines") - 0.5 + whtsbp = grconvertX(max(strwidth(xaxlabs, "figure", cex = .cex_xaxs)), from = "nfc", to = "lines") - 0.5 omar[1] = omar[1] + whtsbp } @@ -337,11 +345,15 @@ draw_facet_window = function( # axes, frame.plot and grid if (isTRUE(axes) || isTRUE(facet.args[["free"]])) { + # Note `cex.axis` rather than `cex`: base `axis()` sizes its tick labels + # from the former and silently ignores the latter, so passing `cex` here + # would leave `cex.xaxs`/`cex.yaxs` (and any theme that sets them) with no + # effect on label size. args_x = list(x, side = xside, type = xaxt, labeller = xaxl, - cex = get_tpar(c("cex.xaxs", "cex.axis"), 0.8, tpar_list = tpars), + cex.axis = get_tpar(c("cex.xaxs", "cex.axis"), 0.8, tpar_list = tpars), lwd = get_tpar(c("lwd.xaxs", "lwd.axis"), 1, tpar_list = tpars), lty = get_tpar(c("lty.xaxs", "lty.axis"), 1, tpar_list = tpars) ) @@ -351,7 +363,7 @@ draw_facet_window = function( side = yside, type = yaxt, labeller = yaxl, - cex = .ca, + cex.axis = .ca, lwd = get_tpar(c("lwd.yaxs", "lwd.axis"), 1, tpar_list = tpars), lty = get_tpar(c("lty.yaxs", "lty.axis"), 1, tpar_list = tpars) ) diff --git a/R/tinyplot.R b/R/tinyplot.R index 5f7d3b8f..6434c0e8 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1138,10 +1138,14 @@ tinyplot.default = function( } if (!is.null(.tpars[["mar"]])) .theme_mar = .tpars[["mar"]] - .cex_axis = get_tpar("cex.axis", tpar_list = .tpars, default = 1) + # Tick-label cex is per-side (cex.xaxs/cex.yaxs), each falling back to the + # shared cex.axis. Measuring both axes with cex.axis alone clips the x + # labels and leaves dead whitespace on the y when the two differ. + .cex_xaxs = get_tpar(c("cex.xaxs", "cex.axis"), tpar_list = .tpars, default = 1) + .cex_yaxs = get_tpar(c("cex.yaxs", "cex.axis"), tpar_list = .tpars, default = 1) .cex_lab = get_tpar(c("cex.ylab", "cex.lab"), tpar_list = .tpars, default = 1) .las = get_tpar("las", tpar_list = .tpars, default = par("las")) - .ymgp_shift = if (.las %in% c(0L, 1L)) 0.5 * (.cex_axis - 1) else 0 + .ymgp_shift = if (.las %in% c(0L, 1L)) 0.5 * (.cex_yaxs - 1) else 0 .ylab_cex_shift = 0.5 * (.cex_lab - 1) # Detect outer-legend sides (order: bottom, left, top, right). @@ -1198,7 +1202,7 @@ tinyplot.default = function( # Compute whtsbp (tick-label width/height bump). Read `las` from .tpars # (the theme definition) rather than par() — par("las") isn't set to the # theme's intended value until the before.plot.new hook fires, but this - # block runs before that. Pass .cex_axis to strwidth so measurements + # block runs before that. Pass the per-side cex to strwidth so measurements # reflect the intended text size (par("cex.axis") isn't set yet either). .whtsbp = c(0, 0, 0, 0) .whtsbp_y_raw = 0 @@ -1213,7 +1217,7 @@ tinyplot.default = function( yaxlabs = axisTicks(usr = ylim_usr, log = par("ylog")) } if (!is.null(yaxl)) yaxlabs = tinylabel(yaxlabs, yaxl) - .whtsbp_y_raw = grconvertX(max(strwidth(yaxlabs, "figure", cex = .cex_axis)), from = "nfc", to = "lines") - + .whtsbp_y_raw = grconvertX(max(strwidth(yaxlabs, "figure", cex = .cex_yaxs)), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 0.5 if (is.finite(.whtsbp_y_raw)) .whtsbp[2] = .whtsbp_y_raw } @@ -1222,7 +1226,7 @@ tinyplot.default = function( xaxlabs = if (is.null(xlabs)) axisTicks(usr = xlim_usr, log = par("xlog")) else if (!is.null(names(xlabs))) names(xlabs) else xlabs if (!is.null(xaxl)) xaxlabs = tinylabel(xaxlabs, xaxl) - .whtsbp_x_raw = grconvertX(max(strwidth(xaxlabs, "figure", cex = .cex_axis)), from = "nfc", to = "lines") - 0.5 + .whtsbp_x_raw = grconvertX(max(strwidth(xaxlabs, "figure", cex = .cex_xaxs)), from = "nfc", to = "lines") - 0.5 if (is.finite(.whtsbp_x_raw)) .whtsbp[1] = .whtsbp_x_raw } diff --git a/R/tinytheme.R b/R/tinytheme.R index b56ac61f..a8366ab4 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -258,10 +258,14 @@ tinytheme = function( if (isTRUE(settings[["dynmar"]]) && !("mgp" %in% names(dots))) { .ga = settings[["gap.axis"]] %||% 0.2 .gl = settings[["gap.lab"]] %||% 1.0 - .ca = settings[["cex.axis"]] %||% 1 - # FIXME: mgp is shared across sides, so we use the larger label cex to + # FIXME: mgp is shared across sides, so we use the larger tick/label cex to # avoid clipping on either axis. Ideally we'd set side-specific mgp when - # cex.xlab and cex.ylab differ. + # cex.xaxs and cex.yaxs (or cex.xlab and cex.ylab) differ. + .ca = max( + settings[["cex.axis"]] %||% 1, + settings[["cex.xaxs"]] %||% 0, + settings[["cex.yaxs"]] %||% 0 + ) .cl = max( settings[["cex.lab"]] %||% 1, settings[["cex.xlab"]] %||% 0, @@ -322,6 +326,8 @@ theme_default = list( cex.main = par("cex.main"), #1.2, cex.cap = 1, cex.sub = par("cex.sub"), #1, + cex.xaxs = NULL, # defer to cex.axis unless set explicitly + cex.yaxs = NULL, # defer to cex.axis unless set explicitly cex.xlab = NULL, # defer to par("cex.lab") unless set explicitly cex.ylab = NULL, # defer to par("cex.lab") unless set explicitly col = par("col"), #"black", diff --git a/R/tpar.R b/R/tpar.R index ef0ec206..9bb1a403 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -54,7 +54,9 @@ #' * `adj.ylab`: Numeric value between 0 and 1 controlling the alignment of the y-axis label. #' * `cairo`: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If `FALSE`, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if `tinyplot(..., file = ".pdf")` is called. Defaults to the value of `capabilities("cairo")`. #' * `cex.cap`: Numeric expansion factor for the plot caption text. Defaults to `1` for the default, basic, and dynamic themes, and `0.8` for clean/classic and their descendants. +#' * `cex.xaxs`, `cex.yaxs`: Numeric expansion factors for the x- and y-axis tick labels, respectively. Both default to `NULL`, whereby the shared `cex.axis` value is used instead (which in turn falls back to `par("cex.axis")`). Set one of them to size a single axis' tick labels independently of the other, e.g. `cex.yaxs` to shrink a long list of category names on the y-axis without also shrinking the x-axis. Compare `cex.xlab` and `cex.ylab`, which do the same for the axis _titles_. #' * `col.cap`: Character specifying the colour of the plot caption. Defaults to `"black"`. +#' * `col.xaxs`, `col.yaxs`: Characters (or integers) specifying the colour of the x- and y-axis tick labels, respectively. Both default to `NULL`, whereby the shared `col.axis` value is used instead. #' * `col.default`: Default colour for single-group displays (i.e. plots without a `by` grouping). Can be `NULL`, a length-1 character colour, or a length-1 numeric index into `palette.qualitative`. Defaults to `NULL`, in which case the first colour of the active qualitative palette is used (or base `palette()[1]`, typically black, if no theme is set). A character value sets the single-group colour independently of the multi-group palette. A numeric value `i` selects the `i`th colour of `palette.qualitative` as the single-group default; a *negative* index additionally drops that colour from the palette used for grouped plots. For example, `col.default = -1` paired with `palette.qualitative = "Okabe-Ito"` uses black (the leading palette colour) for single-group plots and an Okabe-Ito-minus-black palette for grouped plots, avoiding the need to maintain a separate "no-black" palette. #' * `font.cap`: Integer specifying the font face for the plot caption (`1` = plain, `2` = bold, `3` = italic, `4` = bold italic). Defaults to `1`. #' * `line.cap`: Numeric specifying the margin line on which to draw the caption. If `NULL` (default), computed automatically based on the available bottom margin. @@ -76,6 +78,8 @@ #' * `grid.lwd`: Non-negative numeric giving the line width of the panel grid lines. Defaults to `1`. #' * `grid`: Logical or character indicating whether a background panel grid should be added to plots automatically. Defaults to `NULL`, which is equivalent to `FALSE`. In addition to logical values, a character string can be used to control axis-specific grids: uppercase letters (`"X"`, `"Y"`, `"XY"`) draw grid lines at the standard axis tick positions (equivalent to `TRUE`), while lowercase letters (`"x"`, `"y"`, `"xy"`) draw a finer grid with additional lines at the midpoints between ticks. #' * `lmar`: A numeric vector of form `c(inner, outer)` that gives the margin padding, in terms of lines, around the automatic `tinyplot` legend. Defaults to `c(1.0, 0.1)`. The inner margin is the gap between the legend and the plot region, and the outer margin is the gap between the legend and the edge of the graphics device. +#' * `lty.xaxs`, `lty.yaxs`: Line types for the x- and y-axis lines, respectively. Both default to `NULL`, whereby the shared `lty.axis` value is used instead. +#' * `lwd.xaxs`, `lwd.yaxs`: Line widths for the x- and y-axis lines, respectively. Both default to `NULL`, whereby the shared `lwd.axis` value is used instead. #' * `palette.qualitative`: Palette for qualitative colors. See the `palette` argument in `?tinyplot`. #' * `palette.sequential`: Palette for sequential colors. See the `palette` argument in `?tinyplot`. #' * `ribbon.alpha`: Numeric factor in the range `[0,1]` for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is `0.2`. @@ -233,6 +237,8 @@ known_tpar = c( "adj.xlab", "adj.ylab", "cex.cap", + "cex.xaxs", + "cex.yaxs", "cex.xlab", "cex.ylab", "col.cap", @@ -293,6 +299,8 @@ assert_tpar = function(.tpar) { assert_numeric(.tpar[["adj.sub"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.sub") assert_numeric(.tpar[["adj.xlab"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.xlab") assert_numeric(.tpar[["adj.ylab"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.ylab") + assert_numeric(.tpar[["cex.xaxs"]], len = 1, lower = 0, null.ok = TRUE, name = "cex.xaxs") + assert_numeric(.tpar[["cex.yaxs"]], len = 1, lower = 0, null.ok = TRUE, name = "cex.yaxs") assert_flag(.tpar[["cairo"]], name = "cairo") assert_flag(.tpar[["dynmar"]], null.ok = FALSE, name = "dynmar") assert_choice(.tpar[["ljust"]], choice = c("left", "center", "l", "c"), null.ok = TRUE, name = "ljust") diff --git a/R/type_tile.R b/R/type_tile.R index 08b2f3b0..e402ef13 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -144,10 +144,15 @@ #' ) #' #' # -#' ## aside: use tinyplot.matrix directly to avoid reshaping ---- +#' ## tips ---- #' +#' # tip 1: use tinyplot.matrix() directly to avoid reshaping #' tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white") #' +#' # tip 2: use per-axis tick label scaling (cex) for dense heatmaps +#' tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white", +#' theme = list("heatmap", cex.yaxs = 0.75, cex.xaxs = 1.5)) +#' #' ## restore the default theme #' tinytheme() #' diff --git a/R/utils.R b/R/utils.R index 43a283d8..f2d18bd2 100644 --- a/R/utils.R +++ b/R/utils.R @@ -36,7 +36,11 @@ dynmar_side = function(side, label, main = NULL, sub = NULL, cap = NULL, mgp = get_tpar("mgp", tpar_list = tpars) tcl = get_tpar("tcl", tpar_list = tpars, default = par("tcl")) tick_extent = if (side %in% 1:2 && isTRUE(axis_on)) { - cex_axis = get_tpar("cex.axis", tpar_list = tpars, default = 1) + # Per-side cex, falling back to the shared cex.axis (as for cex_lab below). + cex_axis = get_tpar( + if (side == 1L) c("cex.xaxs", "cex.axis") else c("cex.yaxs", "cex.axis"), + tpar_list = tpars, default = 1 + ) max(0, -tcl) + mgp[2] + 0.4 * cex_axis + 0.6 } else 0 label_extent = 0 diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index 0d2dc353..8d478515 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -2,7 +2,7 @@ altdoc: 0.7.3 pandoc: 3.10.1 pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-08-02T21:07:48+0000 +last_built: 2026-08-15T02:19:49+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/inst/tinytest/_tinysnapshot/margins_per_axis_cex_heatmap.svg b/inst/tinytest/_tinysnapshot/margins_per_axis_cex_heatmap.svg new file mode 100644 index 00000000..3111f96a --- /dev/null +++ b/inst/tinytest/_tinysnapshot/margins_per_axis_cex_heatmap.svg @@ -0,0 +1,433 @@ + + + + + + + + + + + + + +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Volvo 142E +Maserati Bora +Ferrari Dino +Ford Pantera L +Lotus Europa +Porsche 914-2 +Fiat X1-9 +Pontiac Firebird +Camaro Z28 +AMC Javelin +Dodge Challenger +Toyota Corona +Toyota Corolla +Honda Civic +Fiat 128 +Chrysler Imperial +Lincoln Continental +Cadillac Fleetwood +Merc 450SLC +Merc 450SL +Merc 450SE +Merc 280C +Merc 280 +Merc 230 +Merc 240D +Duster 360 +Valiant +Hornet Sportabout +Hornet 4 Drive +Datsun 710 +Mazda RX4 Wag +Mazda RX4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-margins.R b/inst/tinytest/test-margins.R index d2d05a78..183c963c 100644 --- a/inst/tinytest/test-margins.R +++ b/inst/tinytest/test-margins.R @@ -233,4 +233,15 @@ f = function() { tinyplot(100, yaxb = 100, xaxb = 1, xlab = "Xx", ylab = "Yy", type = "n") tinytheme() } -expect_snapshot_plot(f, label = "margins_whtsbp_3digit") \ No newline at end of file +expect_snapshot_plot(f, label = "margins_whtsbp_3digit") + +# Per-axis tick-label cex must reach the margin calculations, not just the axis +# call. Reading only the shared cex.axis clipped the labels on whichever axis was +# scaled up and left dead whitespace on the one scaled down (#677). +f = function() { + tinyplot( + as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white", + theme = list("heatmap", cex.yaxs = 0.5, cex.xaxs = 2) + ) +} +expect_snapshot_plot(f, label = "margins_per_axis_cex_heatmap") \ No newline at end of file diff --git a/man/tpar.Rd b/man/tpar.Rd index 72c90376..4139bf17 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -65,7 +65,9 @@ you should rather use \code{par()} instead. \item \code{adj.ylab}: Numeric value between 0 and 1 controlling the alignment of the y-axis label. \item \code{cairo}: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If \code{FALSE}, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if \code{tinyplot(..., file = ".pdf")} is called. Defaults to the value of \code{capabilities("cairo")}. \item \code{cex.cap}: Numeric expansion factor for the plot caption text. Defaults to \code{1} for the default, basic, and dynamic themes, and \code{0.8} for clean/classic and their descendants. +\item \code{cex.xaxs}, \code{cex.yaxs}: Numeric expansion factors for the x- and y-axis tick labels, respectively. Both default to \code{NULL}, whereby the shared \code{cex.axis} value is used instead (which in turn falls back to \code{par("cex.axis")}). Set one of them to size a single axis' tick labels independently of the other, e.g. \code{cex.yaxs} to shrink a long list of category names on the y-axis without also shrinking the x-axis. Compare \code{cex.xlab} and \code{cex.ylab}, which do the same for the axis \emph{titles}. \item \code{col.cap}: Character specifying the colour of the plot caption. Defaults to \code{"black"}. +\item \code{col.xaxs}, \code{col.yaxs}: Characters (or integers) specifying the colour of the x- and y-axis tick labels, respectively. Both default to \code{NULL}, whereby the shared \code{col.axis} value is used instead. \item \code{col.default}: Default colour for single-group displays (i.e. plots without a \code{by} grouping). Can be \code{NULL}, a length-1 character colour, or a length-1 numeric index into \code{palette.qualitative}. Defaults to \code{NULL}, in which case the first colour of the active qualitative palette is used (or base \code{palette()[1]}, typically black, if no theme is set). A character value sets the single-group colour independently of the multi-group palette. A numeric value \code{i} selects the \code{i}th colour of \code{palette.qualitative} as the single-group default; a \emph{negative} index additionally drops that colour from the palette used for grouped plots. For example, \code{col.default = -1} paired with \code{palette.qualitative = "Okabe-Ito"} uses black (the leading palette colour) for single-group plots and an Okabe-Ito-minus-black palette for grouped plots, avoiding the need to maintain a separate "no-black" palette. \item \code{font.cap}: Integer specifying the font face for the plot caption (\code{1} = plain, \code{2} = bold, \code{3} = italic, \code{4} = bold italic). Defaults to \code{1}. \item \code{line.cap}: Numeric specifying the margin line on which to draw the caption. If \code{NULL} (default), computed automatically based on the available bottom margin. @@ -87,6 +89,8 @@ you should rather use \code{par()} instead. \item \code{grid.lwd}: Non-negative numeric giving the line width of the panel grid lines. Defaults to \code{1}. \item \code{grid}: Logical or character indicating whether a background panel grid should be added to plots automatically. Defaults to \code{NULL}, which is equivalent to \code{FALSE}. In addition to logical values, a character string can be used to control axis-specific grids: uppercase letters (\code{"X"}, \code{"Y"}, \code{"XY"}) draw grid lines at the standard axis tick positions (equivalent to \code{TRUE}), while lowercase letters (\code{"x"}, \code{"y"}, \code{"xy"}) draw a finer grid with additional lines at the midpoints between ticks. \item \code{lmar}: A numeric vector of form \code{c(inner, outer)} that gives the margin padding, in terms of lines, around the automatic \code{tinyplot} legend. Defaults to \code{c(1.0, 0.1)}. The inner margin is the gap between the legend and the plot region, and the outer margin is the gap between the legend and the edge of the graphics device. +\item \code{lty.xaxs}, \code{lty.yaxs}: Line types for the x- and y-axis lines, respectively. Both default to \code{NULL}, whereby the shared \code{lty.axis} value is used instead. +\item \code{lwd.xaxs}, \code{lwd.yaxs}: Line widths for the x- and y-axis lines, respectively. Both default to \code{NULL}, whereby the shared \code{lwd.axis} value is used instead. \item \code{palette.qualitative}: Palette for qualitative colors. See the \code{palette} argument in \code{?tinyplot}. \item \code{palette.sequential}: Palette for sequential colors. See the \code{palette} argument in \code{?tinyplot}. \item \code{ribbon.alpha}: Numeric factor in the range \verb{[0,1]} for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is \code{0.2}. diff --git a/man/type_tile.Rd b/man/type_tile.Rd index 63e0c13b..7e62dc90 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -195,10 +195,15 @@ tinyplot( ) # -## aside: use tinyplot.matrix directly to avoid reshaping ---- +## tips ---- +# tip 1: use tinyplot.matrix() directly to avoid reshaping tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white") +# tip 2: use per-axis tick label scaling (cex) for dense heatmaps +tinyplot(as.matrix(mtcars), type = type_heatmap(scale = "x"), col = "white", + theme = list("heatmap", cex.yaxs = 0.75, cex.xaxs = 1.5)) + ## restore the default theme tinytheme() From 71bfe8be1ad163030138e30bd5925d21b5f5702c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 14 Aug 2026 19:55:59 -0700 Subject: [PATCH 22/22] separate R files --- R/type_heatmap.R | 129 +++++++++++++++++++++++++++++++++++++++++++++++ R/type_tile.R | 129 ++--------------------------------------------- man/type_tile.Rd | 2 +- 3 files changed, 135 insertions(+), 125 deletions(-) create mode 100644 R/type_heatmap.R diff --git a/R/type_heatmap.R b/R/type_heatmap.R new file mode 100644 index 00000000..7130064c --- /dev/null +++ b/R/type_heatmap.R @@ -0,0 +1,129 @@ +#' @rdname type_tile +#' @param scale Character. Should the `by` (fill) values be scaled *within* +#' each category of one axis? One of `"none"` (default, i.e. the raw values +#' are used), `"x"`, or `"y"`. Scaling is what makes a raw matrix legible +#' when its variables span very different magnitudes: left alone, the +#' largest-magnitude column monopolises the entire colour ramp. See Examples. +#' +#' Note that `"x"` and `"y"` refer to the axes *as written in the formula*, +#' i.e. before any `flip = TRUE` is applied. We deliberately avoid base R's +#' `"row"`/`"column"` wording, since a tile's position depends on which +#' variable the user placed where in the formula, so there is no fixed matrix +#' orientation to refer to. +#' +#' Rescaling is computed independently per facet; pooling across facets would +#' pin a panel on a different scale to one end of the ramp and lose its +#' internal structure. Since rescaled values are no longer in the units of the +#' `by` variable, the legend title is annotated accordingly. +#' @param method Character. How should the values be rescaled, if `scale` is not +#' `"none"`? Either `"zscore"` (default) to centre each group and divide by its +#' standard deviation, or `"rescale"` to map each group onto the unit interval +#' \[0, 1\]. Ignored when `scale = "none"`. +#' +#' `"zscore"` matches base R's \code{\link[stats]{heatmap}} and keeps values +#' comparable across groups, since `-1` means "one standard deviation below +#' this group's mean" everywhere. `"rescale"` instead pins every group's +#' minimum and maximum to the ends of the colour ramp, which uses the palette +#' more fully but makes the endpoints an artefact of the transform rather than +#' a feature of the data. +#' +#' Groups with no spread---a constant column, or a single tile---would divide +#' by zero, so they are set to the midpoint of the target range (`0.5` and `0` +#' respectively) and a warning is emitted. +#' +#' @importFrom stats sd +#' @order 2 +#' @export +type_heatmap = function( + width = 1, + height = 1, + scale = c("none", "x", "y"), + method = c("zscore", "rescale")) { + assert_numeric(width) + assert_numeric(height) + if (length(scale) > 1L) scale = scale[1L] + assert_choice(scale, c("none", "x", "y")) + if (length(method) > 1L) method = method[1L] + assert_choice(method, c("zscore", "rescale")) + out = list( + draw = draw_rect(), + data = data_heatmap( + width = width, height = height, scale = scale, method = method + ), + # Deliberately reports "tile": the two types are interchangeable as far as + # the rest of the pipeline is concerned, and nothing downstream needs to + # tell them apart. Keeps the option of diverging later. + name = "tile" + ) + class(out) = "tinyplot_type" + return(out) +} + + +## type_heatmap() is data_tile() plus one extra convention: the first row sits +## at the *top*, matching how one reads a matrix (cf. `heatmap()`, `image()`). +## Kept separate from data_tile() so that type_tile() keeps drawing values +## exactly as supplied. +data_heatmap = function( + width = 1, height = 1, scale = "none", method = "zscore") { + tile_fun = data_tile( + width = width, height = height, scale = scale, method = method + ) + fun = function(settings, ...) { + tile_fun(settings, ...) + # Only default the reversal when the user has left `ylim` alone: an explicit + # `ylim` is a direct instruction about axis direction and must win. We set + # the already-parsed `rev_y` flag rather than `ylim = "reverse"`, because + # sanitize_lim_rev() resolves that keyword much earlier in the pipeline, so + # a character `ylim` set here would reach lim_args() unparsed. The flag is + # also idempotent (so it cannot double-reverse if something upstream has + # asked for the same thing) and flip_datapoints() knows to swap it under + # `flip = TRUE`. + if (isTRUE(settings$null_ylim)) settings$rev_y = TRUE + } + return(fun) +} + + +## Rescale `by` within each level of `g`, either to the unit interval +## (method = "rescale") or as a z-score (method = "zscore"). Both divide by a +## measure of spread, so a group with no spread (all values identical, or a +## single observation) would produce NaN. That is much worse than it sounds: +## `range()` of a vector containing one NaN is NaN, so the draw loop's colour +## indices all become NA and tiles blank out across the *whole* plot, not just +## the offending group. Map such groups to the midpoint of the target range +## instead, and report them back so the caller can warn -- a silently flattened +## group otherwise reads as a genuine mid-scale value. +## +## Lives here rather than next to its call site in data_tile(), since scaling is +## a heatmap concern: data_tile()'s `scale` branch is only ever reached via +## type_heatmap(), type_tile() having no `scale` argument to trigger it. +scale_by_group = function(by, g, method = "zscore") { + gi = if (is.factor(g)) g else factor(g) + mid = if (identical(method, "zscore")) 0 else 0.5 + flat = character(0) + out = unsplit( + lapply(split(seq_along(by), gi), function(ix) { + v = by[ix] + if (identical(method, "zscore")) { + s = sd(v, na.rm = TRUE) + if (!is.finite(s) || s == 0) { + flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) + return(rep.int(mid, length(v))) + } + return((v - mean(v, na.rm = TRUE)) / s) + } + # rescale_num()'s default `from` is range(x), which propagates an NA to + # every element, so compute the range with na.rm explicitly. + rng = range(v, na.rm = TRUE) + if (!all(is.finite(rng)) || diff(rng) == 0) { + flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) + return(rep.int(mid, length(v))) + } + rescale_num(v, from = rng, to = c(0, 1)) + }), + gi + ) + attr(out, "flat") = flat + out +} diff --git a/R/type_tile.R b/R/type_tile.R index e402ef13..e95e86bc 100644 --- a/R/type_tile.R +++ b/R/type_tile.R @@ -160,6 +160,7 @@ #' `type_tile()` builds on, and [`tinytheme()`] for the companion `"heatmap"` #' theme. #' +#' @order 1 #' @export type_tile = function(width = 1, height = 1) { assert_numeric(width) @@ -174,130 +175,10 @@ type_tile = function(width = 1, height = 1) { } -#' @rdname type_tile -#' @param scale Character. Should the `by` (fill) values be scaled *within* -#' each category of one axis? One of `"none"` (default, i.e. the raw values -#' are used), `"x"`, or `"y"`. Scaling is what makes a raw matrix legible -#' when its variables span very different magnitudes: left alone, the -#' largest-magnitude column monopolises the entire colour ramp. See Examples. -#' -#' Note that `"x"` and `"y"` refer to the axes *as written in the formula*, -#' i.e. before any `flip = TRUE` is applied. We deliberately avoid base R's -#' `"row"`/`"column"` wording, since a tile's position depends on which -#' variable the user placed where in the formula, so there is no fixed matrix -#' orientation to refer to. -#' -#' Rescaling is computed independently per facet; pooling across facets would -#' pin a panel on a different scale to one end of the ramp and lose its -#' internal structure. Since rescaled values are no longer in the units of the -#' `by` variable, the legend title is annotated accordingly. -#' @param method Character. How should the values be rescaled, if `scale` is not -#' `"none"`? Either `"zscore"` (default) to centre each group and divide by its -#' standard deviation, or `"rescale"` to map each group onto the unit interval -#' \[0, 1\]. Ignored when `scale = "none"`. -#' -#' `"zscore"` matches base R's \code{\link[stats]{heatmap}} and keeps values -#' comparable across groups, since `-1` means "one standard deviation below -#' this group's mean" everywhere. `"rescale"` instead pins every group's -#' minimum and maximum to the ends of the colour ramp, which uses the palette -#' more fully but makes the endpoints an artefact of the transform rather than -#' a feature of the data. -#' -#' Groups with no spread---a constant column, or a single tile---would divide -#' by zero, so they are set to the midpoint of the target range (`0.5` and `0` -#' respectively) and a warning is emitted. -#' -#' @importFrom stats sd -#' @export -type_heatmap = function( - width = 1, - height = 1, - scale = c("none", "x", "y"), - method = c("zscore", "rescale")) { - assert_numeric(width) - assert_numeric(height) - if (length(scale) > 1L) scale = scale[1L] - assert_choice(scale, c("none", "x", "y")) - if (length(method) > 1L) method = method[1L] - assert_choice(method, c("zscore", "rescale")) - out = list( - draw = draw_rect(), - data = data_heatmap( - width = width, height = height, scale = scale, method = method - ), - # Deliberately reports "tile": the two types are interchangeable as far as - # the rest of the pipeline is concerned, and nothing downstream needs to - # tell them apart. Keeps the option of diverging later. - name = "tile" - ) - class(out) = "tinyplot_type" - return(out) -} - - -## Rescale `by` within each level of `g`, either to the unit interval -## (method = "rescale") or as a z-score (method = "zscore"). Both divide by a -## measure of spread, so a group with no spread (all values identical, or a -## single observation) would produce NaN. That is much worse than it sounds: -## `range()` of a vector containing one NaN is NaN, so the draw loop's colour -## indices all become NA and tiles blank out across the *whole* plot, not just -## the offending group. Map such groups to the midpoint of the target range -## instead, and report them back so the caller can warn -- a silently flattened -## group otherwise reads as a genuine mid-scale value. -scale_by_group = function(by, g, method = "zscore") { - gi = if (is.factor(g)) g else factor(g) - mid = if (identical(method, "zscore")) 0 else 0.5 - flat = character(0) - out = unsplit( - lapply(split(seq_along(by), gi), function(ix) { - v = by[ix] - if (identical(method, "zscore")) { - s = sd(v, na.rm = TRUE) - if (!is.finite(s) || s == 0) { - flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) - return(rep.int(mid, length(v))) - } - return((v - mean(v, na.rm = TRUE)) / s) - } - # rescale_num()'s default `from` is range(x), which propagates an NA to - # every element, so compute the range with na.rm explicitly. - rng = range(v, na.rm = TRUE) - if (!all(is.finite(rng)) || diff(rng) == 0) { - flat[[length(flat) + 1L]] <<- as.character(gi[ix][1L]) - return(rep.int(mid, length(v))) - } - rescale_num(v, from = rng, to = c(0, 1)) - }), - gi - ) - attr(out, "flat") = flat - out -} - - -## type_heatmap() is data_tile() plus one extra convention: the first row sits -## at the *top*, matching how one reads a matrix (cf. `heatmap()`, `image()`). -## Kept separate from data_tile() so that type_tile() keeps drawing values -## exactly as supplied. -data_heatmap = function( - width = 1, height = 1, scale = "none", method = "zscore") { - tile_fun = data_tile( - width = width, height = height, scale = scale, method = method - ) - fun = function(settings, ...) { - tile_fun(settings, ...) - # Only default the reversal when the user has left `ylim` alone: an explicit - # `ylim` is a direct instruction about axis direction and must win. We set - # the already-parsed `rev_y` flag rather than `ylim = "reverse"`, because - # sanitize_lim_rev() resolves that keyword much earlier in the pipeline, so - # a character `ylim` set here would reach lim_args() unparsed. The flag is - # also idempotent (so it cannot double-reverse if something upstream has - # asked for the same thing) and flip_datapoints() knows to swap it under - # `flip = TRUE`. - if (isTRUE(settings$null_ylim)) settings$rev_y = TRUE - } - return(fun) -} +## type_heatmap(), a specialised case of type_tile(), lives in type_heatmap.R +## but documents itself onto this same help page via `@rdname type_tile`. +## data_tile()'s `scale`/`method` arguments below exist to serve it; type_tile() +## itself has no `scale` argument and always draws the values as supplied. data_tile = function( diff --git a/man/type_tile.Rd b/man/type_tile.Rd index 7e62dc90..41400695 100644 --- a/man/type_tile.Rd +++ b/man/type_tile.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/type_tile.R +% Please edit documentation in R/type_tile.R, R/type_heatmap.R \name{type_tile} \alias{type_tile} \alias{type_heatmap}