diff --git a/NEWS.md b/NEWS.md
index c57d083a..9e6a43eb 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -183,6 +183,9 @@ related to plot layering. See "Bug fixes" below.
with a flat bar along the baseline) or left undrawn. The default `NULL`
lets `FUN` decide; see the new "Implicit zeros and empty cells" section of
`?type_barplot`. (#718 @grantmcdermott)
+ - `type_histogram()` now (correctly) supports mapping `by` grouping along the
+ `x` variable, e.g. `tinyplot(~mpg | mpg, data = mtcars, type = "hist")`.
+ Bar colours map to mean bin values. (#727 @grantmcdermott)
- Themes:
- `"heatmap"` provides a dedicated companion theme to the new `type_tile()`
and `type_heatmap()` types (see above). The theme removes all axis padding,
diff --git a/R/type_histogram.R b/R/type_histogram.R
index ae32476f..67cc56f0 100644
--- a/R/type_histogram.R
+++ b/R/type_histogram.R
@@ -41,6 +41,14 @@
#' data = iris
#' )
#'
+#' # Special case: `by`==`x`.
+#' # Here we colour each bar (bin) according to its position along the x-axis
+#' tinyplot(
+#' ~ Petal.Width | Petal.Width,
+#' type = "histogram",
+#' data = iris
+#' )
+#'
#' # Faceted version
#' tinyplot(
#' ~Petal.Width,
@@ -101,21 +109,35 @@ data_histogram = function(breaks = "Sturges",
hright = right
fun = function(settings, .breaks = hbreaks, .freebreaks = hfree.breaks, .freq = hfreq, .right = hright, .drop.zeros = hdrop.zeros, ...) {
- env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by", "null_by", "null_palette"))
+ env2env(
+ settings,
+ environment(),
+ c(
+ "bg", "by", "col", "datapoints", "facet", "freq",
+ "null_by", "null_palette", "palette", "plot", "ribbon.alpha",
+ "x_by", "xlab", "xmax", "xmin", "ylab", "ymax", "ymin"
+ )
+ )
has_weights = !is.null(datapoints[["weights"]])
if (has_weights) settings$weights_used = TRUE
+ # `by == x`: colour each bin by its own position along the x-axis.
+ if (x_by) datapoints$by = ""
+
hbreaks = ifelse(!sapply(.breaks, is.null), .breaks, "Sturges")
# Multi-group displays fill from the palette at `ribbon.alpha`
- # transparency via the `ribbon.alpha` keyword. For single-group displays
- # with a theme palette active we leave `bg = NULL` so the fill tracks the
+ # transparency via the `ribbon.alpha` keyword. The transparency is there
+ # so that overlapping groups show through one another, which `x_by` bins
+ # never do (one group per bin) -- it would only wash out the gradient, so
+ # those fill from the palette outright. For single-group displays with a
+ # theme palette active we leave `bg = NULL` so the fill tracks the
# resolved border colour (see by_bg), which honours `col.default`. With
# no theme palette, single-group uses the neutral "lightgray" shared by
# all single-group area fills (matches base R hist()).
if (is.null(bg) && !null_by) {
- bg = ribbon.alpha
+ bg = if (x_by) "by" else ribbon.alpha
} else if (is.null(bg) && null_by && null_palette && is.null(get_tpar("palette.qualitative", default = NULL))) {
bg = "lightgray"
}
@@ -147,7 +169,9 @@ data_histogram = function(breaks = "Sturges",
}
freq = if (!is.null(.freq)) .freq else is.null(.freq) && h$equidist
out = data.frame(
- by = k$by[1], # already split
+ # `x_by`: one group per bin, valued at the bin's midpoint, so
+ # that the colours track the x-axis (see above)
+ by = if (x_by) h$mids else k$by[1], # already split
facet = k$facet[1], # already split
ymin = 0,
ymax = if (freq) h$counts else h$density,
@@ -169,7 +193,9 @@ data_histogram = function(breaks = "Sturges",
ymax = datapoints$ymax
xmin = datapoints$xmin
xmax = datapoints$xmax
- by = if (length(unique(datapoints$by)) == 1) by else datapoints$by
+ # `x_by` keeps the per-bin values even if a single bin survives, since
+ # the original `by` vector is no longer the right length (or scale).
+ by = if (!x_by && length(unique(datapoints$by)) == 1) by else datapoints$by
facet = if (length(unique(datapoints$facet)) == 1) facet else datapoints$facet
# legend customizations
diff --git a/inst/tinytest/_tinysnapshot/hist_by_equals_x.svg b/inst/tinytest/_tinysnapshot/hist_by_equals_x.svg
new file mode 100644
index 00000000..caea6b42
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/hist_by_equals_x.svg
@@ -0,0 +1,97 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/hist_by_equals_x_faceted.svg b/inst/tinytest/_tinysnapshot/hist_by_equals_x_faceted.svg
new file mode 100644
index 00000000..af7ee211
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/hist_by_equals_x_faceted.svg
@@ -0,0 +1,211 @@
+
+
diff --git a/inst/tinytest/test-type_histogram.R b/inst/tinytest/test-type_histogram.R
index 7edb10fd..6dd2c347 100644
--- a/inst/tinytest/test-type_histogram.R
+++ b/inst/tinytest/test-type_histogram.R
@@ -100,3 +100,22 @@ f = function() {
tinyplot(~Sepal.Length, data = iris, type = "histogram", weights = Petal.Width)
}
expect_snapshot_plot(f, label = "hist_weights")
+
+
+# `by == x`: each bin is coloured by its own position along the x-axis, rather
+# than every distinct x value being binned (and overplotted) as a group of its
+# own. (#725)
+f = function() {
+ tinyplot(~mpg | mpg, data = mtcars, type = "hist")
+}
+expect_snapshot_plot(f, label = "hist_by_equals_x")
+
+f = function() {
+ tinyplot(
+ ~Petal.Width | Petal.Width, data = iris,
+ facet = ~Species, facet.args = list(nrow = 1),
+ type = "hist"
+ )
+}
+expect_snapshot_plot(f, label = "hist_by_equals_x_faceted")
+
diff --git a/man/type_histogram.Rd b/man/type_histogram.Rd
index 02115fbf..aabe0113 100644
--- a/man/type_histogram.Rd
+++ b/man/type_histogram.Rd
@@ -78,6 +78,14 @@ tinyplot(
data = iris
)
+# Special case: `by`==`x`.
+# Here we colour each bar (bin) according to its position along the x-axis
+tinyplot(
+ ~ Petal.Width | Petal.Width,
+ type = "histogram",
+ data = iris
+)
+
# Faceted version
tinyplot(
~Petal.Width,