Skip to content

Commit

Permalink
Separate X/Y scales within facet layout (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumbiegel authored Jan 15, 2025
1 parent 10099b0 commit a5e9aa5
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Added the ability to use multiple different X and Y scales within one facet layout. The requirement is that not more than one X and Y scale is used per facet. `Row`, `Col` and `Layout` scales got the ability to set `show_labels = false` in `scales`. Also added the `zerolayer` function which can be used as a basis to build up the required mappings iteratively [#586](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/586).
- Increased compat to Makie 0.22 and GeometryBasics 0.5 [#587](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/587).

## v0.8.13 - 2024-10-21
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/gallery/scales/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"description": "Some advanced keywords to tweak the plot",
"order": ["discrete_scales.jl", "continuous_scales.jl", "custom_scales.jl", "secondary_scales.jl", "multiple_color_scales.jl", "prescaled_data.jl", "legend_merging.jl", "dodging.jl"]
"order": ["discrete_scales.jl", "continuous_scales.jl", "custom_scales.jl", "split_scales_facet.jl", "secondary_scales.jl", "multiple_color_scales.jl", "prescaled_data.jl", "legend_merging.jl", "dodging.jl"]
}
72 changes: 72 additions & 0 deletions docs/gallery/gallery/scales/split_scales_facet.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ---
# title: Split scales across facets
# cover: assets/split_scales_facet.png
# description: Using different categorical and continuous scales across a facet layout.
# author: "[Julius Krumbiegel](https://github.com/jkrumbiegel)"
# ---

# Usually, facet layout plots use the same scale for all x and y axes, respectively, of every facet.
# Sometimes you might want to break with this convention, for example, to show how different
# categorical and continuous variables interact with each other.

# AlgebraOfGraphics allows you to use multiple different scale ids for the X and Y aesthetics in a plot,
# as long as only one scale id for x and y axis, respectively, appears in a given facet.

# Currently, this scenario requires building the plot in multiple separate layers rather
# than using wide input data with the `dims` selector for `layout`, `row` or `col` mappings.
# You need one layer for each set of scale ids. Multiple facets are allowed to share scale ids,
# but a single facet may not have multiple scale ids for x or y, respectively.

using AlgebraOfGraphics, CairoMakie
set_aog_theme!() #src
using Random; Random.seed!(123) #src

dat = data((;
fruit = rand(["Apple", "Orange", "Pear"], 150),
taste = randn(150) .* repeat(1:3, inner = 50),
weight = repeat(["Heavy", "Light", "Medium"], inner = 50),
cost = randn(150) .+ repeat([10, 20, 30], inner = 50),
))

fruit = :fruit => "Fruit" => scale(:X1)
weights = :weight => "Weight" => scale(:Y1)
taste = :taste => "Taste Score" => scale(:X2)
cost = :cost => "Cost" => scale(:Y2)

layer1 = mapping(
fruit,
weights,
col = direct("col1"), # this controls what facet this mapping belongs to
row = direct("row1")
) * frequency()

layer2 = mapping(
fruit,
cost,
col = direct("col1"),
row = direct("row2")
) * visual(Violin)

layer3 = mapping(
weights, # note X and Y are flipped here for a horizontal violin
taste,
col = direct("col2"),
row = direct("row1")
) * visual(Violin, orientation = :horizontal)

layer4 = mapping(
taste,
cost,
col = direct("col2"),
row = direct("row2")
) * visual(Scatter)

spec = dat * (layer1 + layer2 + layer3 + layer4)

fg = draw(spec, scales(Row = (; show_labels = false), Col = (; show_labels = false)))

#

# save cover image #src
mkpath("assets") #src
save("assets/split_scales_facet.png", fg) #src
1 change: 1 addition & 0 deletions docs/src/API/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ AlgebraOfGraphics.AbstractDrawable
AlgebraOfGraphics.AbstractAlgebraic
AlgebraOfGraphics.Layer
AlgebraOfGraphics.Layers
AlgebraOfGraphics.zerolayer
AlgebraOfGraphics.ProcessedLayer
AlgebraOfGraphics.ProcessedLayers
AlgebraOfGraphics.Entry
Expand Down
25 changes: 23 additions & 2 deletions docs/src/layers/draw.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Colorbar and legend, should they be necessary, can be added separately with the
All properties that decide how scales are visualized can be modified by passing scale options (using the `scales` function) as the second argument of `draw` .
The properties that are accepted differ depending on the scale aesthetic type (for example `Color`, `Marker`, `LineStyle`) and whether the scale is categorical or continuous.

### Categorical scale options
### Shared categorical scale options

All categorical scales take the same properties, independent of aesthetic type.
The `palette` and `categories` options are implemented across all categorical scale types.

#### `palette`

Expand Down Expand Up @@ -207,6 +207,27 @@ draw(spec, scales(Col = (;
)))
```

### Special categorical scale options

#### Row, Col & Layout

All three facetting scales have the option `show_labels` which is `true` by default and can be set to `false` to hide the facet labels.

This example shows the behavior for `Col` only:

```@example
using AlgebraOfGraphics
using CairoMakie
spec = data((;
x = 1:16,
y = 17:32,
group1 = repeat(["A", "B"], inner = 8),
group2 = repeat(["C", "D"], 8))
) * mapping(:x, :y, row = :group1, col = :group2) * visual(Scatter)
draw(spec, scales(Col = (; show_labels = false)))
```

### Continuous scale options

Expand Down
2 changes: 1 addition & 1 deletion src/AlgebraOfGraphics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Isoband
import NaturalSort

export hideinnerdecorations!, deleteemptyaxes!
export Layer, Layers, ProcessedLayer, ProcessedLayers
export Layer, Layers, ProcessedLayer, ProcessedLayers, zerolayer
export Entry, AxisEntries
export renamer, sorter, nonnumeric, verbatim, presorted
export density, histogram, linear, smooth, expectation, frequency, contours, filled_contours
Expand Down
53 changes: 43 additions & 10 deletions src/algebra/layers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ function Base.:*(a::AbstractAlgebraic, a′::AbstractAlgebraic)
return Layers([layer * layer′ for layer in layers for layer′ in layers′])
end

"""
zerolayer()
Returns a `Layers` with an empty layer list which can act as a zero
in the layer algebra.
```
layer * zerolayer() ~ zerolayer()
layer + zerolayer() ~ layer
```
"""
zerolayer() = Layers(Layer[])

"""
ProcessedLayers(layers::Vector{ProcessedLayer})
Expand Down Expand Up @@ -298,25 +311,31 @@ function compute_axes_grid(d::AbstractDrawable, scales::Scales = scales(); axis=
ndims = isaxis2d(ae) ? 2 : 3
aesthetics = [AesX, AesY, AesZ]
for (aes, var) in zip(aesthetics[1:ndims], (:x, :y, :z)[1:ndims])
if haskey(ae.categoricalscales, aes)
# Determine which scales of type AesX, AesY, AesZ have actually been
# used by the processed layers in the current AxisSpecEntries.
# We can allow the usage of multiple of these scales in a facetted figure
# as long as each facet only uses one kind. That makes it possible to
# create facet plots in which adjacent facets don't share X and Y scales at all,
# like completely disjoint categories or categorical next to continuous data.
used_scale_ids = get_used_scale_ids(ae, aes)
isempty(used_scale_ids) && continue
if length(used_scale_ids) > 1
error("Found more than two scales of type $aes used in one AxesSpecGrid, this is currently not supported. Scales were: $used_scale_ids")
end
used_scale_id = only(used_scale_ids)
if haskey(ae.categoricalscales, aes) && haskey(ae.categoricalscales[aes], used_scale_id)
catscales = ae.categoricalscales[aes]
if length(keys(catscales)) != 1 || only(keys(catscales)) !== nothing
error("There should only be one $aes, found keys $(keys(catscales))")
end
scale = catscales[nothing]
scale = catscales[used_scale_id]
elseif haskey(ae.continuousscales, aes)
conscales = ae.continuousscales[aes]
if length(keys(conscales)) != 1 || only(keys(conscales)) !== nothing
error("There should only be one $aes, found keys $(keys(conscales))")
end
scale = conscales[nothing]
scale = conscales[used_scale_id]
else
continue
end
label = getlabel(scale)
# Use global scales for ticks for now
# TODO: requires a nicer mechanism that takes into account axis linking
(scale isa ContinuousScale) && (scale = merged_continuousscales[aes][nothing])
(scale isa ContinuousScale) && (scale = merged_continuousscales[aes][used_scale_id])
for (k, v) in pairs((label=label, ticks=ticks(scale)))
keyword = Symbol(var, k)
# Only set attribute if it was not present beforehand
Expand All @@ -328,6 +347,20 @@ function compute_axes_grid(d::AbstractDrawable, scales::Scales = scales(); axis=
return axes_grid
end

function get_used_scale_ids(ae::AxisSpecEntries, aestype)
scale_ids = Set{Union{Nothing,Symbol}}()
for p in ae.processedlayers
aes_map = aesthetic_mapping(p)
for (key, value) in pairs(aes_map)
if value === aestype
scale_id = get(p.scale_mapping, key, nothing)
push!(scale_ids, scale_id)
end
end
end
return scale_ids
end

function to_entry(p::ProcessedLayer, categoricalscales::Dictionary, continuousscales::Dictionary)
entry = to_entry(p.plottype, p, categoricalscales, continuousscales)
insert!(entry.named, :cycle, nothing)
Expand Down
14 changes: 7 additions & 7 deletions src/facet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function facet_wrap!(fig, aes::AbstractMatrix{AxisEntries}; facet)
deleteemptyaxes!(aes)

# add facet labels
panel_labels!(fig, aes, scale)
scale.props.aesprops.show_labels && panel_labels!(fig, aes, scale)

# span axis labels if appropriate
is2d = all(isaxis2d, nonemptyaxes(aes))
Expand Down Expand Up @@ -151,13 +151,13 @@ function facet_grid!(fig, aes::AbstractMatrix{AxisEntries}; facet)
# span axis labels if appropriate
is2d = all(isaxis2d, nonemptyaxes(aes))

if !isnothing(row_scale) && consistent_ylabels(aes)
is2d && span_ylabel!(fig, aes)
row_labels!(fig, aes, row_scale)
if !isnothing(row_scale)
is2d && consistent_ylabels(aes) && span_ylabel!(fig, aes)
row_scale.props.aesprops.show_labels && row_labels!(fig, aes, row_scale)
end
if !isnothing(col_scale) && consistent_xlabels(aes)
is2d && span_xlabel!(fig, aes)
col_labels!(fig, aes, col_scale)
if !isnothing(col_scale)
is2d && consistent_xlabels(aes) && span_xlabel!(fig, aes)
col_scale.props.aesprops.show_labels && col_labels!(fig, aes, col_scale)
end
return
end
Expand Down
12 changes: 12 additions & 0 deletions src/scales.jl
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,21 @@ end
Base.@kwdef struct AesDodgeYCategoricalProps <: CategoricalAesProps
width::Union{Nothing,Float64} = nothing
end
Base.@kwdef struct AesRowCategoricalProps <: CategoricalAesProps
show_labels::Bool = true
end
Base.@kwdef struct AesColCategoricalProps <: CategoricalAesProps
show_labels::Bool = true
end
Base.@kwdef struct AesLayoutCategoricalProps <: CategoricalAesProps
show_labels::Bool = true
end

categorical_aes_props_type(::Type{AesDodgeX}) = AesDodgeXCategoricalProps
categorical_aes_props_type(::Type{AesDodgeY}) = AesDodgeYCategoricalProps
categorical_aes_props_type(::Type{AesRow}) = AesRowCategoricalProps
categorical_aes_props_type(::Type{AesCol}) = AesColCategoricalProps
categorical_aes_props_type(::Type{AesLayout}) = AesLayoutCategoricalProps


function CategoricalScale(aestype::Type{<:Aesthetic}, data, label::Union{AbstractString, Nothing}, props)
Expand Down
61 changes: 61 additions & 0 deletions test/reference_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1018,3 +1018,64 @@ reftest("stairs") do
visual(Stairs)
draw(spec)
end

reftest("split x scales across facet layout") do
dat = data((;
cat1 = ["Apple", "Orange", "Pear"],
cat2 = ["Blue", "Green", "Red"],
cat3 = ["Heavy", "Light", "Medium"],
cont = [4.5, 7.6, 9.3],
y1 = [3.4, 5.2, 6],
y2 = [0.3, 0.2, 0.3],
y3 = [123, 82, 71],
y4 = [-10, 10, 0.4],
))

cat_mappings = mapping(:cat1 => scale(:X1), :y1, layout = direct("A")) +
mapping(:cat2 => "Cat 2" => scale(:X2), :y2, layout = direct("B")) +
mapping(:cat3 => scale(:X3), :y3, layout = direct("C"))

cont_mapping = mapping(:cont => scale(:X4), :y4, layout = direct("D"))

spec = dat * (cat_mappings * visual(Scatter) + cont_mapping * visual(Lines))

draw(spec, scales(X3 = (; label = "Third Categorical")))
end

reftest("split x and y scales row col layout") do
dat = data((;
cat1 = ["Apple", "Orange", "Pear"],
cont2 = [1.4, 5.1, 2.5],
cat3 = ["Heavy", "Light", "Medium"],
cont4 = [2.5, -0.2, 1.2],
))

mappings = zerolayer()
for x in [:cat1, :cont2]
for y in [:cat3, :cont4]
mappings += mapping(x => scale(x), y => scale(y), col = direct("$x"), row = direct("$y"))
end
end

spec = dat * mappings * visual(Scatter)

draw(spec)
end

reftest("hide row col and layout labels") do
f = Figure(size = (600, 600))
d = data((;
x = 1:16,
y = 17:32,
group1 = repeat(["A", "B"], inner = 8),
group2 = repeat(["C", "D"], 8))
)
spec1 = d * mapping(:x, :y, row = :group1, col = :group2) * visual(Scatter)
spec2 = d * mapping(:x, :y, layout = (:group1, :group2) => tuple) * visual(Scatter)

draw!(f[1, 1], spec1, scales(Row = (; show_labels = false), Col = (; show_labels = false)))
draw!(f[1, 2], spec1, scales(Row = (; show_labels = true), Col = (; show_labels = true)))
draw!(f[2, 1], spec2, scales(Layout = (; show_labels = false)))
draw!(f[2, 2], spec2, scales(Layout = (; show_labels = true)))
f
end
Binary file modified test/reference_tests/barplot layout ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a5e9aa5

Please sign in to comment.