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 @@ + + + + + + + + + + + + +Free facet scales +N = [35, 73, 5, ...] Joint Bandwidth = 5.693 +Density + + + + + + + + + + + + + + +0 +20 +40 +60 + + + + + +0.00 +0.01 +0.02 +0.03 + +cold:calm + + + + + + + + + + + + + + + +0 +50 +100 +150 + + + + + + + + +0.000 +0.004 +0.008 +0.012 + +hot:calm + + + + + + + + + + + + + + + + + + +-10 +0 +10 +20 +30 +40 +50 + + + + + + + +0.00 +0.01 +0.02 +0.03 +0.04 +0.05 + +cold:windy + + + + + + + + + + + + + + + + +10 +20 +30 +40 +50 + + + + + + + +0.00 +0.01 +0.02 +0.03 +0.04 +0.05 + +hot:windy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + +Free facet scales (grid) +N = [35, 73, 5, ...] Joint Bandwidth = 5.693 +Density + + + + + + + + + + + + + + +0 +20 +40 +60 + + + + + +0.00 +0.01 +0.02 +0.03 + +cold + + + + + + + + + + + + + + + +0 +50 +100 +150 + + + + + + + + +0.000 +0.004 +0.008 +0.012 + +hot + +calm + + + + + + + + + + + + + + + + + + +-10 +0 +10 +20 +30 +40 +50 + + + + + + + +0.00 +0.01 +0.02 +0.03 +0.04 +0.05 + + + + + + + + + + + + + + + + +10 +20 +30 +40 +50 + + + + + + + +0.00 +0.01 +0.02 +0.03 +0.04 +0.05 + +windy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.