diff --git a/NAMESPACE b/NAMESPACE index b089312d..c8f3c7d4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ S3method(tinyplot,data.frame) S3method(tinyplot,default) S3method(tinyplot,density) S3method(tinyplot,formula) +S3method(tinyplot,matrix) S3method(tinyplot,ts) export(draw_legend) export(get_saved_par) diff --git a/NEWS.md b/NEWS.md index 0bfdec0e..04c68f3c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -162,10 +162,9 @@ Theme fixes: #### New `tinyplot.*` methods -- A top-level `tinypairs()` function, together with a dedicated - `tinyplot.data.frame()` method, now supports direct plotting of data frames, - with or without a formula. Combining with a formula is mostly useful insofar - as it facilitates piping, e.g. +- `tinyplot.data.frame()`: Supports direct plotting of data frames, alongside + the new top-level function `tinypairs()`. Can be called with or without a + formula. One benefit of the former is that it facilitates piping, e.g. ```r iris |> plt(Sepal.Length ~ Petal.Width | Species) @@ -176,7 +175,16 @@ Theme fixes: variables will yield a `pairs()`-style grid of all variable combinations. Thanks to @mthulin for the suggestion and original implementation idea. (#613, #640 @zeileis @grantmcdermott) -- New dedicated `tinyplot.ts()` method for `ts` time series, e.g. +- `tinyplot.matrix()`: for `matrix` objects, e.g. + + ```r + plt(VADeaths, type = "b") + ``` + + The output largely mimics the base `matplot`/`matlines` equivalents, but with + additional **tinyplot** functionality related to automatic legends, options + for faceting, etc. (#649 @grantmcdermott) +- `tinyplot.ts()`: for `ts` time series, e.g. ```r plt(EuStockMarkets) @@ -299,6 +307,10 @@ Theme fixes: `sub`, `cap`, `xlab`, and `ylab` being evaluated instead of coerced to plotmath expressions, e.g. `plt(0, 0, main = bquote(foo == .(pi)))`. Thanks (again) to @bastistician for the report. (#642 @grantmcdermott) +- Line plots (`type = "l"`, and relatives like `"b"`/`"o"`) with a factor or + character `x` variable now draw the category labels on the x-axis, matching + the behaviour of point plots. Previously these tick labels were only shown + when dodging was active. (#648 @grantmcdermott) ## v0.6.1 diff --git a/R/facet.R b/R/facet.R index a9b73a37..55b77e6c 100644 --- a/R/facet.R +++ b/R/facet.R @@ -329,7 +329,9 @@ draw_facet_window = function( ) if (!is.null(xaxb)) args_x$at = xaxb if (!is.null(yaxb)) args_y$at = yaxb - type_range_x = type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "p", "violin") && !is.null(xlabs) + # `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. + 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"))) if (type_range_x) { args_x = modifyList(args_x, list(at = xlabs, labels = names(xlabs))) @@ -369,7 +371,7 @@ draw_facet_window = function( # Explicitly set (override) the current facet extent par(usr = fusr[[ii]]) # if plot frame is true then print axes per normal... - if (type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "p", "violin") && !is.null(xlabs)) { + if (!is.null(xlabs)) { tinyAxis(xfree, side = xside, at = xlabs, labels = names(xlabs), type = xaxt, labeller = xaxl) } else { tinyAxis(xfree, side = xside, type = xaxt, labeller = xaxl) diff --git a/R/sanitize_type.R b/R/sanitize_type.R index 5b2cba4b..b0a1cff6 100644 --- a/R/sanitize_type.R +++ b/R/sanitize_type.R @@ -88,8 +88,6 @@ sanitize_type = function(settings) { "hline" = type_hline, "j" = type_jitter, "jitter" = type_jitter, - "l" = type_lines, - "lines" = type_lines, "lm" = type_lm, "loess" = type_loess, "p" = type_points, @@ -110,15 +108,22 @@ sanitize_type = function(settings) { "text" = type_text, "violin" = type_violin, "vline" = type_vline, - type # default case + type # default case (incl. line-family chars, handled below) ) } -# browser() + if (is.function(type)) { args = intersect(names(formals(type)), names(dots)) args = if (length(args) >= 1L) dots[args] else list() type = do.call(type, args) type$dots = dots[setdiff(names(dots), names(args))] + } else if (is.character(type) && type %in% c("l", "lines", "o", "b", "c", "h", "s", "S")) { + # Line-family convenience characters route through type_lines(), preserving + # the requested plot character (so e.g. "b" stays points + lines, rather than + # defaulting to "l"). + args = dots[intersect(names(formals(type_lines)), names(dots))] + args[["type"]] = if (type == "lines") "l" else type + type = do.call(type_lines, args) } if (inherits(type, "tinyplot_type")) { diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R new file mode 100644 index 00000000..b2fc29c1 --- /dev/null +++ b/R/tinyplot.matrix.R @@ -0,0 +1,92 @@ +#' tinyplot Method for Plotting Matrices +#' +#' @description Convenience interface for visualizing +#' \code{\link[base]{matrix}} objects with tinyplot. +#' +#' @details Internally the matrix is converted to long form and visualized as a +#' scatter (or other `type`) of each column's values against their row index. +#' Each column is mapped to a separate `by` category, so a matrix with +#' multiple columns produces a grouped plot. Optionally, it can also be +#' faceted via `facet = "by"`. This mirrors the base R +#' \code{\link[graphics]{matplot}} convention of plotting the columns of a +#' 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. +#' +#' @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. +#' @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. +#' @param ... further arguments passed to `tinyplot`. +#' +#' @returns No return value, called for the side effect of producing a plot. +#' +#' @seealso \code{\link[graphics]{matplot}} +#' +#' @examples +#' # basic use +#' tinyplot(VADeaths) +#' 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))) +#' +#' @export +tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = NULL, ylab = NULL, ...) { + assert_choice(facet, "by", null.ok = TRUE) + ## Default to points. We set this explicitly (rather than relying on + ## tinyplot's auto-inference) because the x-axis row labels are passed as a + ## factor, which would otherwise be inferred as a boxplot. + if (is.null(type)) type = "p" + dep_x = deparse1(substitute(x)) + dims = dim(x) + if (dims[2] == 1L) { + bby = NULL + legend = FALSE + } else { + nms = colnames(x) + if (!is.null(nms)) { + bby = factor(rep(nms, each = dims[1]), levels = nms) + if (is.null(legend)) legend = list(title = NULL) + } else { + bby = factor(rep(seq_len(dims[2]), each = dims[1])) + legend = FALSE + } + } + ## If the matrix has row names, use them for the x-axis tick labels via an + ## ordered factor (preserving row order). Otherwise fall back to a plain + ## numeric index. + rnms = rownames(x) + dim(x) = dims[1] * dims[2] + y = x + if (is.null(rnms)) { + x = rep(seq_len(dims[1]), times = dims[2]) + ## no row names: x is a plain numeric index, so label it as such + if (is.null(xlab)) xlab = "Index" + } else { + x = factor(rep(rnms, times = dims[2]), levels = rnms, ordered = TRUE) + ## row names already label the ticks, so an "Index" title is redundant + if (is.null(xlab)) xlab = NA + } + if (is.null(ylab)) ylab = dep_x + tinyplot.default( + x = x, y = y, + type = type, + by = bby, + facet = facet, + legend = legend, + xlab = xlab, + ylab = ylab, + ... + ) +} diff --git a/R/type_lines.R b/R/type_lines.R index c5f3b251..a76155be 100644 --- a/R/type_lines.R +++ b/R/type_lines.R @@ -45,7 +45,6 @@ type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE) { data_lines = function(dodge = 0, fixed.dodge = FALSE) { - if (is.null(dodge) || dodge == 0) return(NULL) fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "xlabs")) @@ -53,7 +52,8 @@ data_lines = function(dodge = 0, fixed.dodge = FALSE) { datapoints$x = as.factor(datapoints$x) } if (is.factor(datapoints$x)) { - xlvls = unique(datapoints$x) + # honour pre-ordered factors; otherwise fall back to first-appearance order + xlvls = if (is.ordered(datapoints$x)) levels(datapoints$x) else unique(datapoints$x) datapoints$x = factor(datapoints$x, levels = xlvls) xlabs = seq_along(xlvls) names(xlabs) = xlvls diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index 0349cfa4..d3a55ff9 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -2,7 +2,7 @@ altdoc: 0.7.2 pandoc: '3.10' pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-06-23T18:52:00+0000 +last_built: 2026-06-26T05:13:18+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index c6ce09b2..7450962b 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -160,6 +160,8 @@ website: contents: - text: tinyplot.data.frame file: man/tinyplot.data.frame.qmd + - text: tinyplot.matrix + file: man/tinyplot.matrix.qmd - text: tinyplot.ts file: man/tinyplot.ts.qmd diff --git a/inst/tinytest/_tinysnapshot/matrix_basic.svg b/inst/tinytest/_tinysnapshot/matrix_basic.svg new file mode 100644 index 00000000..e8389c55 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/matrix_basic.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + +Rural Male +Rural Female +Urban Male +Urban Female + + + + + + + +VADeaths + + + + + + + + +50-54 +55-59 +60-64 +65-69 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/matrix_facet.svg b/inst/tinytest/_tinysnapshot/matrix_facet.svg new file mode 100644 index 00000000..1103ac61 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/matrix_facet.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + +Rural Male +Rural Female +Urban Male +Urban Female + + + + + + + +VADeaths + + + + + + + + + + + + + + + +50-54 +60-64 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + +Rural Male + + + + + + + + + + + + + + + + +50-54 +60-64 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + +Rural Female + + + + + + + + + + + + + + + + +50-54 +60-64 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + +Urban Male + + + + + + + + + + + + + + + + +50-54 +60-64 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + +Urban Female + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/matrix_type_b.svg b/inst/tinytest/_tinysnapshot/matrix_type_b.svg new file mode 100644 index 00000000..d9d39d30 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/matrix_type_b.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + +Rural Male +Rural Female +Urban Male +Urban Female + + + + + + + +VADeaths + + + + + + + + +50-54 +55-59 +60-64 +65-69 +70-74 + + + + + + + + +10 +20 +30 +40 +50 +60 +70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-matrix.R b/inst/tinytest/test-matrix.R new file mode 100644 index 00000000..11f54670 --- /dev/null +++ b/inst/tinytest/test-matrix.R @@ -0,0 +1,17 @@ +source("helpers.R") +using("tinysnapshot") + +# tinyplot.matrix() method + +# basic: named columns -> grouped, named rows -> x-axis tick labels +f = function() tinyplot(VADeaths) +expect_snapshot_plot(f, label = "matrix_basic") + +# row names should label the x-axis for line types too (not just points), and +# the "Index" title is dropped when row names are present (#645-adjacent fix) +f = function() tinyplot(VADeaths, type = "b") +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") diff --git a/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd new file mode 100644 index 00000000..2aae1d0b --- /dev/null +++ b/man/tinyplot.matrix.Rd @@ -0,0 +1,68 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tinyplot.matrix.R +\name{tinyplot.matrix} +\alias{tinyplot.matrix} +\title{tinyplot Method for Plotting Matrices} +\usage{ +\method{tinyplot}{matrix}( + x, + type = NULL, + legend = NULL, + facet = NULL, + xlab = NULL, + ylab = NULL, + ... +) +} +\arguments{ +\item{x}{an object of class \code{"matrix"}.} + +\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.} + +\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 +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.} + +\item{...}{further arguments passed to \code{tinyplot}.} +} +\value{ +No return value, called for the side effect of producing a plot. +} +\description{ +Convenience interface for visualizing +\code{\link[base]{matrix}} objects with tinyplot. +} +\details{ +Internally the matrix is converted to long form and visualized as a +scatter (or other \code{type}) of each column's values against their row index. +Each column is mapped to a separate \code{by} category, so a matrix with +multiple columns produces a grouped plot. Optionally, it can also be +faceted via \code{facet = "by"}. This mirrors the base R +\code{\link[graphics]{matplot}} convention of plotting the columns of a +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. +} +\examples{ +# basic use +tinyplot(VADeaths) +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))) + +} +\seealso{ +\code{\link[graphics]{matplot}} +}