Skip to content

Commit

Permalink
Add type_density() function (#284)
Browse files Browse the repository at this point in the history
* type_density

* add alpha support for type_area while we're at it

* docs

* clean up

* revamped tinyplot.density method

* namespace and docs

* docs

* update tests

* readme

* switch to individual bandwidths

- add joint.bw arg for override

* news

- reword other sections while we're at it

* don't set xlim and ylim

* namespace and fix tests
  • Loading branch information
grantmcdermott authored Jan 9, 2025
1 parent cba67f0 commit c376b59
Show file tree
Hide file tree
Showing 36 changed files with 2,144 additions and 1,922 deletions.
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ Depends:
Imports:
graphics,
grDevices,
methods,
stats,
tools,
utils
Expand Down
9 changes: 7 additions & 2 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export(tpar)
export(type_abline)
export(type_area)
export(type_boxplot)
export(type_density)
export(type_errorbar)
export(type_function)
export(type_glm)
Expand Down Expand Up @@ -88,10 +89,14 @@ importFrom(graphics,segments)
importFrom(graphics,strwidth)
importFrom(graphics,text)
importFrom(graphics,title)
importFrom(methods,as)
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,density)
importFrom(stats,dnorm)
importFrom(stats,glm)
Expand All @@ -108,7 +113,7 @@ importFrom(stats,qt)
importFrom(stats,quantile)
importFrom(stats,spline)
importFrom(stats,terms)
importFrom(stats,update)
importFrom(stats,weighted.mean)
importFrom(tools,file_ext)
importFrom(utils,globalVariables)
importFrom(utils,head)
Expand Down
86 changes: 49 additions & 37 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,34 @@ where the formatting is also better._
## 0.2.1.99 (dev version)

**tinyplot** v0.3.0 is a big release with many new features, both internal and
user-facing. Below we have tried to group alike news items together, starting
with the new plot type processing system.

New plot `type` processing system:

- In previous versions of `tinyplot` the plot `type` was specified by character
arguments, i.e., either the standard character shortcuts (such `"p"`, `"l"`,
etc.) or labels (such as `"hist"`, `"boxplot"`, etc.). In addition, the
`type` argument now accepts functional `type_*()` equivalents (e.g.,
`type_hist()`) which enable a variety of additional features, as well as a
more disciplined approach to explicit argument passing for customized type
behavior. (#222 @vincentarelbundock)
- All plot types provided in the package can be specified either by a character
label or the corresponding function. Thus, the following two are equivalent:
`tinyplot(Nile, type = "hist")` and `tinyplot(Nile, type = type_hist())`.
- The main advantage of the function specification is that many more plot types
can be supported (see list below) and users can define their own custom types
by creating `type_<typename>()` functions.
- Enabling these new features comes at the cost of a different approach for
specifying ancilliary arguments of the type function. It is recommended to
pass such arguments explicitly to the `type_*()` function call, e.g., as in
`tinyplot(Nile, type = type_hist(breaks = 30))`. In many situations it is
still possible to use the character specification plus extra arguments
instead, e.g., as in `tinyplot(Nile, type = "hist", breaks = 30)`. However,
this only works if the ancilliary type arguments do not match or even
partially match any of the arguments of the `tinyplot()` function itself.
This is why the former approach is recommended (unless using only the
default type).
- Due to this change in the processing of the ancilliary type arguments there
are a few breaking changes but we have tried to minimize them. One argument
that was deprecated explicitly is `ribbon.alpha` in `tinyplot()`. Use the
`alpha` argument of the `type_ribbon()` function instead:
`tinyplot(..., type = type_ribbon(alpha = 0.5))`. Note that it is not
equivalent to use `tinyplot(..., type = "ribbon", alpha = 0.5)` because the
latter matches the `alpha` argument of `tinyplot()` (rather than of
`type_ribbon()`) and modifies the `palette` rather than the ribbon only.
- More details are provided in the dedicated
user-facing. Related updates are grouped below for easier navigation.

New plot `type` processing system (#222 @vincentarelbundock):

- In addition to the standard character labels (e.g., `"p"`, `"density"`), the
`type` argument now supports _functional_ equivalents (e.g., `type_points()`,
`type_density()`. These new functional types all take the form `type_*()`.
- The character and functional types are interchangeable. For example,
`tinyplot(Nile, type = "hist")` and `tinyplot(Nile, type = type_hist())`
produce exactly the same result.
- The main advantage of the functional `type_*()` variants is that they offer
much more flexibility and control beyond the default case(s). Users can pass
appropriate arguments to existing types for customization and can even define
their own `type_<typename>()` functions.
- On the development side, overhauling the `type` system has also allowed us to
introduce a number of new plot types and features (see list below). We have
also simplified our internal codebase, since explicit argument passing
requires less guesswork on our end.
- Speaking of which, we now recommended that users explicitly pass ancillary
type-specific arguments as part of the relevant `type_*()` call. For example:
`tinyplot(Nile, type = type_hist(breaks = 30))` is preferable to
`tinyplot(Nile, type = "hist", breaks = 30)`. While the latter option will
still work in this particular case, we cannot guarantee that it will for other
cases. (Reason: Passing ancillary type-specific arguments at the top level of
the plot call only works if these do not conflict with the main arguments of
the `tinyplot()` function itself; see #267.)
- Some minor breaking changes were unavoidable; see further below.
- For more details on the new `type` system, please see the dedicated
[Plot types vignette](https://grantmcdermott.com/tinyplot/vignettes/types.html)
on the website.

Expand Down Expand Up @@ -107,6 +98,27 @@ dedicated
[Themes vignette](https://grantmcdermott.com/tinyplot/vignettes/themes.html)
on the website. (#258 @vincentarelbundock and @grantmcdermott)

Breaking changes:

- There are a few breaking changes to grouped density plots. (#284 @grantmcdermott)
- The default smoothing bandwidth is now computed independently for each data
subgroup, rather than being computed from the joint density. Users can still
opt into using a joint bandwidth by invoking the
`type_density(joint.bw = <option>)` argument. See the function documentation
for details.
- Grouped and/or faceted plots are no longer possible on density objects
(i.e., via the `tinyplot.density()` method). Instead, please rather call
`tinyplot(..., type = "density")` or `tinyplot(..., type = type_density())`
on the raw data and pass grouping or facet arguments as needed.
- The `ribbon.alpha` argument in `tinyplot()` has been deprecated. Use the
`alpha` argument in `type_ribbon()` (and equivalents) instead: e.g.,
`tinyplot(..., type = type_ribbon(alpha = 0.5))`.
- Aside: Please note that this is _not_ equivalent to using
`tinyplot(..., type = "ribbon", alpha = 0.5)` because the latter matches the
top-level `alpha` argument of `tinyplot()` itself (and thus modifies the
entire `palette`, rather than just the ribbon). See our warning about passing
ancillary type-specific arguments above.

Bug fixes:

- Better preserve facet attributes, thus avoiding misarrangement of facet grids
Expand Down
1 change: 1 addition & 0 deletions R/sanitize.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ sanitize_type = function(type, x, y, dots) {
"abline" = type_abline,
"area" = type_area,
"boxplot" = type_boxplot,
"density" = type_density,
"errorbar" = type_errorbar,
"function" = type_function,
"glm" = type_glm,
Expand Down
96 changes: 83 additions & 13 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
#' - `"text"` / [`type_text()`]: Add text annotations.
#' - Visualizations:
#' - `"boxplot"` / [`type_boxplot()`]: Creates a box-and-whisker plot.
#' - `"density"`: Plots the density estimate of a variable.
#' - `"density"` / [`type_density()`]: Plots the density estimate of a variable.
#' - `"histogram"` / [`type_histogram()`]: Creates a histogram of a single variable.
#' - `"jitter"` / [`type_jitter()`]: Jittered points.
#' - `"qq"` / [`type_qq()`]: Creates a quantile-quantile plot.
Expand Down Expand Up @@ -602,11 +602,15 @@ tinyplot.default = function(
# type factories vs. strings
type = sanitize_type(type, x, y, dots)
if ("dots" %in% names(type)) dots = type$dots

# retrieve type-specific data and drawing functions
type_data = type$data
type_draw = type$draw
type = type$name

# area flag (mostly for legend)
was_area_type = identical(type, "area")

# check flip flag is logical
assert_flag(flip)

palette = substitute(palette)
Expand Down Expand Up @@ -719,12 +723,14 @@ tinyplot.default = function(
ymax_dep = deparse(substitute(ymax))
y_dep = paste0("[", ymin_dep, ", ", ymax_dep, "]")
y = rep(NA, length(x))
} else if (!type %in% c("density", "histogram", "function")) {
} else if (type == "density") {
if (is.null(ylab)) ylab = "Density"
} else if (type %in% c("histogram", "function")) {
if (is.null(ylab)) ylab = "Frequency"
} else {
y = x
x = seq_along(x)
if (is.null(xlab)) xlab = "Index"
} else {
if (is.null(ylab)) ylab = "Frequency"
}
}

Expand All @@ -734,13 +740,6 @@ tinyplot.default = function(
# alias
if (is.null(bg) && !is.null(fill)) bg = fill

# type-specific settings and arguments
if (isTRUE(type == "density")) {
fargs = mget(ls(environment(), sorted = FALSE))
fargs = density_args(fargs = fargs, dots = dots, by_dep = by_dep)
return(do.call(tinyplot.density, args = fargs))
}

datapoints = list(x = x, y = y, xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, ygroup = ygroup)
datapoints = Filter(function(z) length(z) > 0, datapoints)
datapoints = data.frame(datapoints)
Expand Down Expand Up @@ -1354,8 +1353,12 @@ tinyplot.formula = function(
}

## nice axis and legend labels
dens_type = (is.atomic(type) && identical(type, "density")) || (!is.atomic(type) && identical(type$name, "density"))
hist_type = (is.atomic(type) && type %in% c("hist", "histogram")) || (!is.atomic(type) && identical(type$name, "histogram"))
if (!is.null(type) && hist_type) {
if (!is.null(type) && dens_type) {
if (is.null(ylab)) ylab = "Density"
if (is.null(xlab)) xlab = xnam
} else if (!is.null(type) && hist_type) {
if (is.null(ylab)) ylab = "Frequency"
if (is.null(xlab)) xlab = xnam
} else if (is.null(y)) {
Expand Down Expand Up @@ -1396,6 +1399,73 @@ tinyplot.formula = function(
)
}

#' @rdname tinyplot
#' @export
tinyplot.density = function(
x = NULL,
type = c("l", "area"),
...) {

dots = list(...)

if (!is.null(dots[["by"]]) || !is.null(dots[["facet"]])) {
stop(
'\nGrouped and/or faceted plots are no longer supported with the tinyplot.density() method. ',
'\nPlease use the dedicated type argument instead, e.g. `tinyplot(..., type = "density")`. See `?type_density` for details.',
'\n\nThis breaking change was introduced in tinyplot v0.3.0.'
)
}

type = match.arg(type)

## override if bg = "by"
if (!is.null(dots[["bg"]]) || !is.null(dots[["fill"]])) type = "area"

if (inherits(x, "density")) {
object = x
# legend_args = list(x = NULL)
# # Grab by label to pass on legend title to tinyplot.default
# legend_args[["title"]] = deparse(substitute(by))
} else {
## An internal catch for non-density objects that were forcibly
## passed to tinyplot.density (e.g., via a one-side formula)
if (anyNA(x)) {
x = na.omit(x)
x = as.numeric(x)
}
object = density(x)
}

x = object$x
y = object$y

if (type == "area") {
ymin = rep(0, length(y))
ymax = y
# # set extra legend params to get bordered boxes with fill
# legend_args[["x.intersp"]] = 1.25
# legend_args[["lty"]] = 0
# legend_args[["pt.lwd"]] = 1
}

# splice in change arguments
dots[["x"]] = x
dots[["y"]] = y
dots[["type"]] = type

## axes range
if (is.null(dots[["xlim"]])) dots[["xlim"]] = range(x)
if (is.null(dots[["ylim"]])) dots[["ylim"]] = range(y)

## nice labels and titles
if (is.null(dots[["ylab"]])) dots[["ylab"]] = "Density"
if (is.null(dots[["xlab"]])) dots[["xlab"]] = paste0("N = ", object$n, " Bandwidth = ", sprintf("%.4g", object$bw))
if (is.null(dots[["main"]])) dots[["main"]] = paste0(paste(object$call, collapse = "(x = "), ")")

do.call(tinyplot.default, args = dots)

}


#' @export
#' @name plt
Expand Down
10 changes: 6 additions & 4 deletions R/type_area.R
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
#' @rdname type_ribbon
#' @export
type_area = function() {
type_area = function(alpha = NULL) {
out = list(
draw = NULL,
data = data_area(),
data = data_area(alpha = alpha),
name = "area"
)
class(out) = "tinyplot_type"
return(out)
}


data_area = function() {
data_area = function(alpha = alpha) {
ribbon.alpha = if (is.null(alpha)) .tpar[["ribbon.alpha"]] else (alpha)
fun = function(datapoints, ...) {
datapoints$ymax = datapoints$y
datapoints$ymin = rep.int(0, nrow(datapoints))
out = list(
datapoints = datapoints,
ymax = datapoints$ymax,
ymin = datapoints$ymin,
type = "ribbon"
type = "ribbon",
ribbon.alpha = ribbon.alpha
)
return(out)
}
Expand Down
Loading

0 comments on commit c376b59

Please sign in to comment.