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 @@
+
+
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 @@
+
+
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 @@
+
+
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}}
+}