diff --git a/R/facet.R b/R/facet.R
index 744360d3..a5f74b27 100644
--- a/R/facet.R
+++ b/R/facet.R
@@ -152,7 +152,7 @@ draw_facet_window = function(grid, ...) {
if (!(nfacet_rows == 2 && nfacet_cols == 2)) fmar = fmar * .75
}
# Extra reduction if no plot frame to reduce whitespace
- if (isFALSE(frame.plot)) {
+ if (isFALSE(frame.plot) && !isTRUE(facet.args[["free"]])) {
fmar = fmar - 0.5
}
@@ -236,9 +236,54 @@ draw_facet_window = function(grid, ...) {
yside = 2
}
+
# axes, frame.plot and grid
- if (isTRUE(axes)) {
- if (isTRUE(frame.plot)) {
+ if (isTRUE(axes) || isTRUE(facet.args[["free"]])) {
+
+ if (isTRUE(facet.args[["free"]]) && (par("xlog") || par("ylog"))) {
+ warning(
+ "\nFree scale axes for faceted plots are currently not supported if the axes are logged. Reverting back to fixed scales.",
+ "\nIf support for this feature is important to you, please raise an issue on our GitHub repo:",
+ "\nhttps://github.com/grantmcdermott/tinyplot/issues\n"
+ )
+ facet.args[["free"]] = FALSE
+ }
+
+ # Special logic if facets are free...
+ if (isTRUE(facet.args[["free"]])) {
+ # First, we need to calculate the plot extent and axes range of each
+ # individual facet.
+ xfree = split(c(x, xmin, xmax), facet)[[ii]]
+ yfree = split(c(y, ymin, ymax), facet)[[ii]]
+ xlim = range(xfree, na.rm = TRUE)
+ ylim = range(yfree, na.rm = TRUE)
+ xext = extendrange(xlim, f = 0.04)
+ yext = extendrange(ylim, f = 0.04)
+ # We'll save this in a special .fusr env var (list) that we'll re-use
+ # when it comes to plotting the actual elements later
+ if (ii==1) {
+ fusr = replicate(4, vector("double", length = nfacets), simplify = FALSE)
+ assign(".fusr", fusr, envir = get(".tinyplot_env", envir = parent.env(environment())))
+ }
+ fusr = get(".fusr", envir = get(".tinyplot_env", envir = parent.env(environment())))
+ fusr[[ii]] = c(xext, yext)
+ assign(".fusr", fusr, envir = get(".tinyplot_env", envir = parent.env(environment())))
+ # 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("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(xlabs)) {
+ tinyAxis(xfree, side = xside, at = xlabs, labels = names(xlabs), type = xaxt)
+ } else {
+ tinyAxis(xfree, side = xside, type = xaxt)
+ }
+ if (isTRUE(flip) && type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(ylabs)) {
+ tinyAxis(yfree, side = yside, at = ylabs, labels = names(ylabs), type = yaxt)
+ } else {
+ tinyAxis(yfree, side = yside, type = yaxt)
+ }
+
+ # For fixed facets we can just reuse the same plot extent and axes limits
+ } else if (isTRUE(frame.plot)) {
# if plot frame is true then print axes per normal...
if (type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(xlabs)) {
tinyAxis(x, side = xside, at = xlabs, labels = names(xlabs), type = xaxt)
diff --git a/R/tinyplot.R b/R/tinyplot.R
index 444bbb6b..228edba7 100644
--- a/R/tinyplot.R
+++ b/R/tinyplot.R
@@ -49,6 +49,11 @@
#' precedence if both are specified together. Ignored if a two-sided formula
#' is passed to the main `facet` argument, since the layout is arranged in a
#' fixed grid.
+#' - `free` a logical value indicating whether the axis limits (scales) for
+#' each individual facet should adjust independently to match the range of
+#' the data within that facet. Default is `FALSE`. Separate free scaling of
+#' the x- or y-axis (i.e., whilst holding the other axis fixed) is not
+#' currently supported.
#' - `fmar` a vector of form `c(b,l,t,r)` for controlling the base margin
#' between facets in terms of lines. Defaults to the value of `tpar("fmar")`,
#' which should be `c(1,1,1,1)`, i.e. a single line of padding around each
@@ -1096,6 +1101,14 @@ tinyplot.default = function(
mfgj = ii %% nfacet_cols
if (mfgj == 0) mfgj = nfacet_cols
par(mfg = c(mfgi, mfgj))
+
+ # For free facets, we need to reset par(usr) based extent of that
+ # particular facet... which we calculated and saved to the .fusr env var
+ # (list) back in draw_facet_window()
+ if (isTRUE(facet.args[["free"]])) {
+ fusr = get(".fusr", envir = get(".tinyplot_env", envir = parent.env(environment())))
+ par(usr = fusr[[ii]])
+ }
}
# empty plot flag
diff --git a/inst/tinytest/_tinysnapshot/facet_free.svg b/inst/tinytest/_tinysnapshot/facet_free.svg
new file mode 100644
index 00000000..67ecd0c1
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/facet_free.svg
@@ -0,0 +1,193 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/facet_free_grid.svg b/inst/tinytest/_tinysnapshot/facet_free_grid.svg
new file mode 100644
index 00000000..47b9b803
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/facet_free_grid.svg
@@ -0,0 +1,193 @@
+
+
diff --git a/inst/tinytest/test-facet.R b/inst/tinytest/test-facet.R
index a4277630..c1f69d2e 100644
--- a/inst/tinytest/test-facet.R
+++ b/inst/tinytest/test-facet.R
@@ -480,6 +480,33 @@ f = function() {
}
expect_snapshot_plot(f, label = "facet_density_3x2")
+
+#
+# Free facet scales
+#
+
+f = function() {
+ tinyplot(
+ ~ Ozone, aq,
+ type = "density",
+ facet = ~hot:windy,
+ facet.args = list(free = TRUE),
+ main = "Free facet scales"
+ )
+}
+expect_snapshot_plot(f, label = "facet_free")
+
+f = function() {
+ tinyplot(
+ ~ Ozone, aq,
+ type = "density",
+ facet = windy ~ hot,
+ facet.args = list(free = TRUE),
+ main = "Free facet scales (grid)"
+ )
+}
+expect_snapshot_plot(f, label = "facet_free_grid")
+
#
# restore original par settings
#
diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd
index 8b792975..d76e4e68 100644
--- a/man/tinyplot.Rd
+++ b/man/tinyplot.Rd
@@ -166,6 +166,11 @@ arrangement. Only one of these should be specified, but \code{nrow} will take
precedence if both are specified together. Ignored if a two-sided formula
is passed to the main \code{facet} argument, since the layout is arranged in a
fixed grid.
+\item \code{free} a logical value indicating whether the axis limits (scales) for
+each individual facet should adjust independently to match the range of
+the data within that facet. Default is \code{FALSE}. Separate free scaling of
+the x- or y-axis (i.e., whilst holding the other axis fixed) is not
+currently supported.
\item \code{fmar} a vector of form \code{c(b,l,t,r)} for controlling the base margin
between facets in terms of lines. Defaults to the value of \code{tpar("fmar")},
which should be \code{c(1,1,1,1)}, i.e. a single line of padding around each
diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd
index 02aeac87..4f9821e5 100644
--- a/vignettes/introduction.qmd
+++ b/vignettes/introduction.qmd
@@ -393,12 +393,13 @@ tinyplot(
```
To customize facets, simply pass a list of named arguments through the companion
-`facet.args` argument. For example, users can override the default "square"
-facet window arrangement by supplying explicit `nrow` or `ncol` values. They can
-also adjust the padding (margin) between individual facets, change the facet
-title text and background, etc., etc. Here's a simple example where we (1)
-arrange the facets in a single row, (2) add some background fill to the facet
-text, and (3) and reduce axis redundancy by turning off the plot frame.
+`facet.args` argument. Customization options include: override the default
+"square" facet window arrangement; allow free-scaled axes so that the limits of
+each individual facet are drawn independently; adjust the padding (margin)
+between individual facets; change the facet title text and background; etc.
+Here is a simple example where we (1) arrange the facets in a single row, (2)
+add some background fill to the facet text, and (3) and reduce axis redundancy
+by turning off the plot frame.
```{r facet_nrow}
tinyplot(
@@ -412,7 +413,7 @@ tinyplot(
```
The `facet.args` customizations can also be set globally via the `tpar()`
-function. We will revisit this idea in the @sec-themes section below.
+function. We will revisit this idea in the [Themes](#themes) section below.
Finally, the `facet` argument also accepts a _two-sided_ formula for arranging
facets in a fixed grid layout. Here's a simple (if contrived) example.