Skip to content

feat(analyses): compute analyses in transformed scale space#773

Merged
jkrumbiegel merged 8 commits into
masterfrom
jk/analyses-scale-space
Jun 25, 2026
Merged

feat(analyses): compute analyses in transformed scale space#773
jkrumbiegel merged 8 commits into
masterfrom
jk/analyses-scale-space

Conversation

@jkrumbiegel

Copy link
Copy Markdown
Member

People often plot data on a log y axis and want a linear or smooth fit on top, but if you treat the data as gaussian in log space the fit has to be computed in log space too. Setting axis = (; yscale = log10) doesn't do that, it's a Makie axis attribute that warps the coordinates at draw time, after the analysis has already run in data space, so the fit is computed on the raw values and bends away from the data on the log axis.

This PR makes the scale set via scales actually affect analyses. scales(Y = (; scale = log10)) fits in the transformed space and back-transforms the result, so the fit lines up with the log-scaled display. It's a first-class scale, distinct from the display-only axis attribute.

That's the same split ggplot2 makes between scale_y_log10() (transforms before the stat) and coord_trans(y = "log10") (display only), so this is well-established practice. Quick check that AoG with scales(Y = (; scale = log10)) matches ggplot2's scale_y_log10() on identical data (left AoG, right ggplot2; lm top, loess bottom):

AoG vs ggplot2 fits in log-y space
t = repeat(0:8, inner = 3)
conc = 100 .* 10 .^ (-0.11 .* t .+ 0.08 .* randn(length(t)))
spec = data((; t, conc)) * mapping(:t, :conc) * (visual(Scatter) + linear())

draw(spec, scales(Y = (; scale = log10)))
ggplot(df, aes(t, conc)) +
    geom_point() +
    geom_smooth(method = "lm") +
    scale_y_log10()

Covers linear, smooth, density, histogram, expectation and filled_contours. The transform sits inside the existing unit-stripping bracket so units still compose, and data outside the scale's domain (log of zero or a negative) errors early, naming the analysis and the offending value.

One consequence worth calling out: the positional aesthetics an analysis maps to (which arg is x, which is y) were already mostly assumed fixed in the existing code. This just turns that into a hard requirement, because the transform has to be chosen up front based on which aesthetic each positional lands on. So if a downstream visual changes the plot type or orientation such that data moves onto or off a scaled aesthetic the analysis didn't fit for, that's now an error instead of a silent mismatch.

This only kicks in when such a scale is actually set. Without one (or with identity), no pretransform happens, so there's no possible mismatch and nothing is checked: plots that worked before this PR behave exactly as before.

histogram also gains a direction keyword for 1D histograms, which sets the bar orientation and picks the aesthetic (so the scale space) the bins are computed in. Previously you'd flip orientation with a downstream visual(direction = :x), which can't carry the binning into the right scale space, so it lives on the analysis instead, like density's direction.

jkrumbiegel and others added 8 commits June 24, 2026 10:30
Setting a scale function on a positional aesthetic via `scales` (e.g.
`scales(Y = (; scale = log10))`) now makes analyses fit in the transformed
space and back-transform their output, so a fit on log-scaled data is
computed in log space and lines up with the display. This is distinct from
`axis = (; yscale = ...)`, which stays display-only and leaves the fit in
data space. Covers linear, smooth, density, histogram, expectation and
filled_contours.

The transform sits inside the existing unit-stripping bracket, so units
compose. Data outside the scale's domain (e.g. log of zero) errors early,
naming the analysis and the offending value. Because the transform now has
to happen up front, the positional aesthetics an analysis assumes are a
hard requirement: a guard catches a downstream `visual` that flips data
onto or off a scaled aesthetic the analysis did not fit for.

`histogram` gains a `direction` keyword for 1D histograms, selecting the
aesthetic (and thus the scale space) the bins are computed in.
Replace the vague `all(> 0)` positivity check with a concrete assertion
that the fit equals the exponential the data was generated from.
Key the transform dict by aesthetic type with a `Base.Callable` forward
value instead of a `(; forward, inverse, sym)` tuple. The inverse comes
from `Makie.inverse_transform` at use, and the aesthetic for a domain
error is recovered from the dict key.
`positional_transform` now returns the matching `aes => forward` dict
entry, so the aesthetic travels to the domain error with the forward
instead of being reverse-looked-up (which was ambiguous when the same
scale function is set on more than one aesthetic, e.g. a log/log plot).
Match the axis_transforms key type; the values are aesthetic types.
Move the scale-vs-axis explanation and the linear-fit-in-log-y example
from a standalone section in continuous-scales.md into the existing
`scales` X/Y/Z options section in draw.md, where `ticks`/`tickformat`/`scale`
are documented.
Recipes like visual(Density)/visual(Hist) compute their statistic on the
raw data inside the recipe, so the scale does not reach them; use the
AlgebraOfGraphics analysis to compute in the scaled space.
@jkrumbiegel
jkrumbiegel merged commit 14c8202 into master Jun 25, 2026
8 checks passed
@jkrumbiegel
jkrumbiegel deleted the jk/analyses-scale-space branch June 25, 2026 08:48
@jkrumbiegel jkrumbiegel mentioned this pull request Jun 25, 2026
jkrumbiegel added a commit that referenced this pull request Jun 25, 2026
Minor release. Highlights:

- Analyses (`linear`, `smooth`, `density`, `histogram`, `expectation`,
`filled_contours`) now fit in transformed space and back-transform when
the aesthetic carries a `scale` function via `scales` (e.g. `scales(Y =
(; scale = log10))`)
[#773](#773).
- `histogram` now accepts a `direction` (`:x` or `:y`) keyword for 1D
histograms
[#773](#773).
- `smooth` with the default `degree = 2` no longer collapses on `Date`
(and other large-magnitude) x axes
[#774](#774).

The transformed-space analyses feature warrants the minor (`0.12.13` →
`0.13.0`) bump.
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.

1 participant