Skip to content

Conversation

@jkrumbiegel
Copy link
Member

The highlight transformation has a similar effect as the gghighlight extension from the ggplot ecosystem.

You can pass predicates which are then evaluated on the subgroups created by the given grouping variables in mapping.
For example, this plot highlights all series with standard deviation of positional arg 2 (the y data) over some threshold:

using Statistics
using Random

Random.seed!(5)

nx = 100
ngroup = 8
df = (;
    x = repeat(1:nx, ngroup),
    y = reduce(vcat, [randn(nx) .* rand(range(0.01, 0.3, length = 20)) .+ i for i in 1:ngroup]),
    group = repeat(Char.(Int('A') .+ (0:ngroup-1)), inner = nx)
)
fg = data(df) *
    mapping(:x, :y, color = :group, layout = :group => x -> x > 'D' ? "Upper" : "Lower") *
    visual(Lines) *
    highlight(2 => y -> std(y) > 0.2) |> draw
image

You can also repeat the data split over facets in each facet. Whatever transformation has been done previously on that data in the facetted context will remain (so for example the histogram binning when using histogram()):

data((; x = randn(900) .+ repeat(0:3:6, inner = 300), group = repeat('A':'C', inner = 300))) *
    mapping(:x, color = :group, col = :group) *
    histogram(bins = 30) *
    highlight(:color => Returns(true), repeat_facets = true) |> draw
image

This PR adds the `aggregate` analysis which can aggregate one or more
mapped columns grouped by one or more other columns. The aggregation
functions can be chosen freely and the plotting function should just be
picked as usual with `visual`, so it's a very flexible analysis layer.
The reason for this is that it's usually a bit annoying having to add
data wrangling just for some simple visualizations that should be done
on the fly, where you are not interested in keeping the aggregated data
around. This way you don't have to come up with a variable name for it,
plus it works with all table inputs and not just the typical
`DataFrame`.

For example, let's say we have some categories and associated
measurements. We can plot these as a normal scatter:

```julia
using AlgebraOfGraphics
using CairoMakie
using Statistics

df = (
    cats = repeat(["low", "mid", "high"], 30),
    vals = repeat([1, 2, 3], 30) .+ randn.(),
)

base = data(df) * mapping(:cats, :vals)
scat = base * visual(Scatter)
draw(scat)
```

<img width="606" height="438" alt="image"
src="https://github.com/user-attachments/assets/94a1523a-924f-4bd9-ad0b-06044a2f9108"
/>

Let's say we want to show the median of each group. We can do this with
`aggregate`. Every mapped column needs to be either a grouping column or
an aggregated column. Grouping columns are denoted by a `:`.

```julia
med = base * aggregate(:, median) * visual(Scatter, markersize = 20, color = :red)
draw(scat + med)
```

<img width="592" height="440" alt="image"
src="https://github.com/user-attachments/assets/75396a20-7f81-4dd5-a340-34b65eae7545"
/>


Each column can only have one function applied, but this function may
return multiple values per group, for example as a tuple. There can then
be multiple functions that are applied on the result, each of which can
be assigned to a different output mapping. This can be used, for
example, to draw error bars or confidence intervals. Let's compute the
25th and 75th percentiles and draw the interval.

```julia
interval = base * aggregate(
    :,
    (x -> quantile(x, [0.25, 0.75])) => [
        first => 2,
        last => 3,
    ]
) * visual(Rangebars, linewidth = 3, color = :red)

draw(scat + med + interval)
```

<img width="591" height="438" alt="image"
src="https://github.com/user-attachments/assets/44a703b3-7000-42af-9f53-231a6a09585a"
/>

With `=> 2` and `=> 3` we assign the first and second quantile to
positional mappings 2 and 3 for `Rangebars`. If you don't specify a
remapping, the initial mapping is kept, but there can only be one output
assigned to a mapping.

In this case, it might look nice to apply a dodge, so both components
can be discriminated better.

```julia
with_dodge = scat * mapping(dodge_x = direct("A")) +
    (med + interval) * mapping(dodge_x = direct("B"))

draw(with_dodge, scales(DodgeX = (; width = 0.2)))
```

<img width="592" height="437" alt="image"
src="https://github.com/user-attachments/assets/3c640481-435c-4c00-960d-61f09994b5d1"
/>

Grouping by multiple mappings also works, for example to compute a
heatmap by summing all values of a given group (the empty cells are
combinations of x and y that don't exist by chance):

```julia
df = (;
    x = rand(1:5, 100),
    y = rand(1:5, 100),
    z = randn(100)
)
data(df) * mapping(:x, :y, :z) * aggregate(:, :, sum) *
    visual(Heatmap) |> draw
```

<img width="585" height="451" alt="image"
src="https://github.com/user-attachments/assets/3f7ab201-9084-407a-8860-6d76ece8525b"
/>


Additionally, the outputs can be renamed with the pair syntax, plus you
can assign a scale id like with a normal `mapping`, because `aggregate`
freely creates new mapped columns:

```julia
data(df) * mapping(:x, :y, :z) *
    aggregate(
        :,
        :,
        sum => "Sum of all z values" => scale(:sumcolor)
    ) *
    visual(Heatmap) |>
    draw(scales(sumcolor = (; colormap = :plasma)))
```

<img width="591" height="435" alt="image"
src="https://github.com/user-attachments/assets/899cc4d0-73b6-4615-a26a-d06e3ec0b313"
/>
@jkrumbiegel jkrumbiegel changed the base branch from master to jk/0.12 November 6, 2025 13:47
Base automatically changed from jk/0.12 to master November 28, 2025 11:30
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.

2 participants