Skip to content

Commit

Permalink
Compute pagination after applying scales (#593)
Browse files Browse the repository at this point in the history
This PR changes the pagination mechanism. Before, layers were converted
to processed layers, and those were then split apart based on their
input data. The problem with that approach was that it made each page
independent of the others. This can be seen in this MWE:

```julia
df = (x = 1:4, y = 1:4, page = [1, 1, 2, 2], color = ["A", "B", "C", "D"])
spec = data(df) *
    mapping(:x, :y, color = :color, layout = :page => nonnumeric) *
    visual(Scatter, markersize = 30)
draw(spec)
```

<img width="533" alt="image"
src="https://github.com/user-attachments/assets/9e633871-e245-4259-a693-33f31f28dd38"
/>

When we split this into two pages, we get this on master:

<img width="531" alt="image"
src="https://github.com/user-attachments/assets/98734dc4-350b-4fba-86c7-d50095f9174f"
/>

<img width="534" alt="image"
src="https://github.com/user-attachments/assets/0e5eabfc-1ee4-4496-b9fe-55dc8dbf4a46"
/>

Both A & C and B & D share the same color in the legend because each
page has separately computed categorical scales. (This problem does not
appear if all pages have the same categories, but that's not something
to generally rely on.)

With this PR, the pages reflect the original colors from the full plot,
and the legend also lists all categories on both pages.

<img width="528" alt="image"
src="https://github.com/user-attachments/assets/8bfa44e2-ec70-4be1-bc0a-c51b16bde1b8"
/>

<img width="542" alt="image"
src="https://github.com/user-attachments/assets/8d3ce90d-113d-4aab-a144-7a077ae70c84"
/>

In case there are lots of categories that crowd the legend, it might be
possible in the future to remove the legend entries for categories that
don't appear on a given page, while still keeping the overall
colorscheme intact.
  • Loading branch information
jkrumbiegel authored Jan 30, 2025
1 parent 4a289d6 commit 98a216b
Show file tree
Hide file tree
Showing 25 changed files with 326 additions and 137 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- **Breaking**: `paginate` now splits facet plots into pages _after_ fitting scales and not _before_ [#593](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/593). This means that, e.g., categorical color mappings are consistent across pages where before each page could have a different mapping if some groups were not represented on a given page. This change also makes pagination work with the split X and Y scales feature enabled by version 0.8.14. `paginate`'s return type changes from `PaginatedLayers` to `Pagination` because no layers are stored in that type anymore. The interface to use `Pagination` with `draw` and other functions doesn't change compared to `PaginatedLayers`. `paginate` now also accepts an optional second positional argument which are the scales that are normally passed to `draw` when not paginating, but which must be available prior to pagination to fit all scales accordingly.

## v0.8.14 - 2025-01-16

- Added automatic `alpha` forwarding to all legend elements which will have an effect from Makie 0.22.1 on [#588](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/588).
Expand Down
28 changes: 19 additions & 9 deletions docs/gallery/gallery/layout/faceting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,27 @@ draw(plt)

# ## Pagination
#
# If you have too many facets for one figure, you can use `paginate` to split the data into several subsets
# If you have too many facets for one figure, you can use [`paginate`](@ref) to split the data into several subsets
# given a maximum number of plots per layout, row or column.
#
# We start with a normal facet plot, in this case a wrapped layout:

df = (
x = repeat(1:10, 36),
y = cumsum(sin.(range(0, 10pi, 360))),
group = repeat(string.("Group ", 1:36), inner = 10),
color = 1:360,
)
plt = data(df) * mapping(:x, :y, color = :color, layout=:group) * visual(Lines)
draw(plt)

# Note that pagination is considered an experimental feature.
# In the current implementation, scales and layouts are not synchronized across pages.
# This means that, e.g., linked limits on one page are not influenced by limits of other pages.
# The exact synchronization behavior can be subject to change in non-breaking versions.
# Scales are synchronized across pages.
# Note, however, that linked axis limits are currently not synchronized across pages.
# The exact synchronization behavior may be subject to change in non-breaking versions.

df = (x=rand(500), y=rand(500), l=rand(["a", "b", "c", "d", "e", "f", "g", "h"], 500))
plt = data(df) * mapping(:x, :y, layout=:l)
pag = paginate(plt, layout = 4)


pag = paginate(plt, layout = 9)

# The object returned from `draw` will be a `Vector{FigureGrid}`.

Expand All @@ -94,7 +104,7 @@ figuregrids[1]

# or use `draw` with an optional second argument specifying the index of the page to draw.

draw(pag, 2)
draw(pag, 4)

# save cover image #src
mkpath("assets") #src
Expand Down
3 changes: 3 additions & 0 deletions src/algebra/layer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ function continuousscales(processedlayer::ProcessedLayer, scale_props)

continuousscales = similar(keys(continuous), ContinuousScale)
map!(continuousscales, keys(continuous), continuous) do key, val
if hardcoded_mapping(key) !== nothing
error("The `$key` mapping was used with continuous data but can only be used with categorical data. Consider using the `=> nonnumeric` modifier to turn numerical data into categorical.")
end
aes = aes_mapping[key]
scale_id = get(processedlayer.scale_mapping, key, nothing)
props = get_scale_props(scale_props, aes, scale_id)
Expand Down
13 changes: 12 additions & 1 deletion src/algebra/layers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,22 @@ function compute_axes_grid(d::AbstractDrawable, scales::Scales = scales(); axis=

indices = CartesianIndices(pls_grid)
axes_grid = map(indices) do c

# for the subsequent logic, the x,y,z scales are kept per-facet, while
# for all other scales the merged versions are picked (for example to keep
# colorbars synchronized in pagination)
mixed_continuousscales = copy(merged_continuousscales)
for aes in (AesX, AesY, AesZ)
if haskey(continuousscales_grid[c], aes)
mixed_continuousscales[aes] = continuousscales_grid[c][aes]
end
end

return AxisSpecEntries(
AxisSpec(c, axis),
entries_grid[c],
categoricalscales,
continuousscales_grid[c],
mixed_continuousscales,
pls_grid[c],
)
end
Expand Down
9 changes: 8 additions & 1 deletion src/draw.jl
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,19 @@ end
function _draw(d::AbstractDrawable, scales::Scales;
axis, figure, facet, legend, colorbar)

ae = compute_axes_grid(d, scales; axis)
_draw(ae; figure, facet, legend, colorbar)
end
function _draw(ae::Matrix{AxisSpecEntries}; figure = (;), facet = (;), legend = (;), colorbar = (;))

fs, remaining_figure_kw = figure_settings(; pairs(figure)...)

_filter_nothings(; kwargs...) = (key => value for (key, value) in kwargs if value !== nothing)

return update(Figure(; pairs(remaining_figure_kw)...)) do f
grid = plot!(f, d, scales; axis)
grid = map(x -> AxisEntries(x, f), ae)
foreach(plot!, grid)

fg = FigureGrid(f, grid)
facet!(fg; facet)
if get(colorbar, :show, true)
Expand Down
Loading

0 comments on commit 98a216b

Please sign in to comment.