Skip to content

Stacked area - #688

Merged
grantmcdermott merged 12 commits into
mainfrom
stacked-area
Aug 23, 2026
Merged

Stacked area#688
grantmcdermott merged 12 commits into
mainfrom
stacked-area

Conversation

@grantmcdermott

@grantmcdermott grantmcdermott commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Closes #632

Examples for the updated ?type_area docs.

pkgload::load_all("~/Documents/Projects/tinyplot/")
#> ℹ Loading tinyplot

#
## Stacked area plots

# Grouped area plots can be stacked cumulatively, rather than being drawn
# from a common zero baseline.

# Group B is small and steady; A and C are larger and wobblier.
dat = expand.grid(year = 2000:2020, grp = factor(c("A", "B", "C")))
dat$val = as.integer(dat$grp) +
  c(1.2, 0.1, 1.8)[dat$grp] * sin(dat$year / 3) +
  c(0.06, 0.02, 0.10)[dat$grp] * (dat$year - 2000)

tinyplot(val ~ year | grp, data = dat, type = type_area(stack = TRUE))

# Use `byord` to control which group stacks where. Here we stack by their
# largest end value.

tinyplot(
  val ~ year | grp, data = dat,
  type = type_area(stack = TRUE, byord = "end")
)

# `"minvar"` instead puts the *least variable* group on the baseline. Every
# band inherits the movement of the ones below it, so a steady bottom layer
# keeps the whole chart legible. Here that picks group B, which the default
# level order leaves in the middle and `"end"`/`"total"` push to the top.

tinyplot(
  val ~ year | grp, data = dat,
  type = type_area(stack = TRUE, byord = "minvar")
)

# `"rev"` simply flips the existing level order, which is the one thing a
# ranking function cannot do (it never sees which group it was handed).

tinyplot(
  val ~ year | grp, data = dat,
  type = type_area(stack = TRUE, byord = "rev")
)

# Custom ranking functions are also accepted. Name an argument `x` and it
# receives the group's x values too, which is what a slope needs.

tinyplot(
  val ~ year | grp, data = dat,
  type = type_area(stack = TRUE, byord = function(y, x) coef(lm(y ~ x))[2])
)

# Stacking expects a single `y` value per group per `x` value. Any repeats
# are collapsed for us first, using `FUN` (`mean` by default). Here, for
# instance, ChickWeight records many chicks per diet at each timepoint.

tinyplot(
  weight ~ Time | Diet, data = ChickWeight,
  type = type_area(stack = TRUE, FUN = median)
)

# (Illustrative purposes aside, we leave it to the reader to decide whether
# stacking separate diets on top of one another makes any sense...)

Created on 2026-08-23 with reprex v2.1.1

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Facet completion, ordered-factor handling, and legend reversal contain correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds stacked area plots to type_area(), addressing #632.

Changes:

  • Adds stacking, group ordering, aggregation, and categorical-axis support.
  • Reverses stacked-area legends to match geometry.
  • Adds documentation and snapshot coverage.
File summaries
File Description
vignettes/types.qmd Documents the legend hint.
R/type_ribbon.R Documents stacked-area options.
R/type_area.R Implements aggregation and stacking.
R/sanitize_bylevels.R Adds group-order sanitization.
R/legend.R Adds legend-key reversal.
R/assertions.R Registers the new type hint.
NEWS.md Announces the feature and axis fix.
man/type_ribbon.Rd Updates generated reference documentation.
inst/tinytest/test-type_area.R Adds area-plot snapshot tests.
inst/tinytest/_tinysnapshot/area_stack.svg Captures default stacking.
inst/tinytest/_tinysnapshot/area_stack_flip.svg Captures flipped stacking.
inst/tinytest/_tinysnapshot/area_stack_facet.svg Captures faceted stacking.
inst/tinytest/_tinysnapshot/area_stack_bylevels_end.svg Captures reordered stacking.
inst/tinytest/_tinysnapshot/area_stack_bylevels_aggregated.svg Captures aggregated ordering.
inst/tinytest/_tinysnapshot/area_stack_alpha.svg Captures explicit transparency.
inst/tinytest/_tinysnapshot/area_grouped.svg Captures unstacked grouped areas.
inst/tinytest/_tinysnapshot/area_factor_x.svg Captures categorical-axis labels.
Review details

Files not reviewed (1)

  • man/type_ribbon.Rd: Generated file

Suppressed comments (2)

R/sanitize_bylevels.R:73

  • The data-dependent path also recreates by as an unordered factor, causing ordered groups to switch from the sequential palette to a qualitative one whenever "start", "end", "total", or a function is used. Carry the original ordered flag into the new factor.
  factor(by, levels = levels(by)[ord])

R/legend.R:755

  • Reversing only atomic values whose length exactly equals n can detach keys from labels. Expression/plotmath labels are non-atomic and remain in their original order while colours reverse; recycled aesthetics also fail (for example, two fill colours across four groups need their displayed sequence reversed). Reverse expression labels too, and expand positional aesthetics to the displayed group count before reversing.
    if (is.atomic(val) && length(val) == n) legend_args[[key]] = rev(val)
  • Files reviewed: 8/17 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread R/sanitize_bylevels.R Outdated
Comment thread R/type_area.R Outdated
Comment thread R/legend.R Outdated
Comment thread R/type_ribbon.R Outdated
stack_area() completed its grid from the global cross product of x, by
and facet, so every facet received zero-valued cells at x positions that
only occurred in other facets. Facets with different x domains drew
artificial ramps to zero, and free-scale facets inherited the union of
all x ranges rather than their own. Cross `by` against the (facet, x)
pairs actually observed instead; ragged groups within a facet are still
zero-filled, which is all the completion was ever for.

Also move the legend_reversed key flip ahead of the horizontal padding
block. The padding appends a space to every label but the rightmost, so
flipping afterwards stranded it on the wrong end: the new leftmost label
lost its inter-label gap and the new rightmost carried dead width.

Both paths picked up regression snapshots, neither of which was covered
before. Reported by Copilot on #688.
`bylevels` did two unrelated jobs: respecify factor levels literally
(character names, numeric indexes) and derive an order from the data
("start", "end", "total", a function). The name describes the first and
fits the second badly -- `bylevels = "total"` reads as nonsense.

Split the vocabularies. `type_area()` now exposes `byord`, which accepts
only the computed forms: "asis", "start", "end", "total", or a function.
Explicit level order is rejected with a message pointing at
factor(levels = ), which is what it was always delegating to.

Keyword-only on purpose. The wider cleanup -- restricting xlevels /
ylevels back to their released contract and giving the other seven types
an `*ord` -- needs `type_barplot` to validate the composition rule and
the aggregate-before-rank ordering, and that belongs in its own PR. A
`bylevels` for `type_area` can be added there; adding an argument is not
breaking, whereas shipping the overloaded name would have been.

None of the keyword vocabulary has ever been released, so there is
nothing to deprecate. Snapshots are byte-identical -- only the labels
move.

Spec for the remaining work: SCRATCH/spec-ord-family.md
@grantmcdermott
grantmcdermott merged commit 9017c51 into main Aug 23, 2026
3 checks passed
@grantmcdermott
grantmcdermott deleted the stacked-area branch August 23, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support stacked area plots

2 participants