Skip to content

Commit

Permalink
infer dodge width from barplots
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumbiegel committed Sep 16, 2024
1 parent 2b3e485 commit 9737169
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 3 deletions.
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ authors = ["Pietro Vertechi", "Julius Krumbiegel"]
version = "0.8.7"

[deps]
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
Expand Down
1 change: 1 addition & 0 deletions src/AlgebraOfGraphics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ using Dictionaries: AbstractDictionary, Dictionary, Indices, getindices, set!, d
using KernelDensity: kde, pdf
using StatsBase: fit, histrange, Histogram, normalize, sturges, StatsBase

import Accessors
import GLM, Loess
import FileIO
import RelocatableFolders
Expand Down
82 changes: 80 additions & 2 deletions src/algebra/layers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ function compute_axes_grid(d::AbstractDrawable, scales::Scales = scales(); axis=
map!(fitscale, scaledict, scaledict)
end

set_dodge_width_default!(categoricalscales, processedlayers)

pls_grid = compute_processedlayers_grid(processedlayers, categoricalscales)
entries_grid, continuousscales_grid, merged_continuousscales =
compute_entries_continuousscales(pls_grid, categoricalscales, scale_props)
Expand Down Expand Up @@ -504,6 +506,8 @@ function Base.show(io::IO, layers::Layers; indent = 0)
end
end

scale_setting_name(scale_id, aes::Type{<:Aesthetic}) = scale_id !== nothing ? scale_id : string(nameof(aes))[4:end]

function compute_dodge(data, key::Symbol, dodgevalues, scale_mapping, categoricalscales, dodge_aes)
scale_id = get(scale_mapping, key, nothing)
scale = categoricalscales[dodge_aes][scale_id]
Expand All @@ -516,9 +520,83 @@ function compute_dodge(data, key::Symbol, dodgevalues, scale_mapping, categorica
width = if props.width !== nothing
props.width
else
error("Tried to compute dodging offsets but the `width` attribute of the dodging scale was `nothing`. This happens if only plots participate in the dodge that do not have an inherent width. For example, a scatter plot has no width but a barplot does. You can pass a width manually via the settings for `$(scale_id !== nothing ? scale_id : string(nameof(dodge_aes))[4:end])` in the `scales()` function.")
error("Tried to compute dodging offsets but the `width` attribute of the dodging scale was `nothing`. This happens if only plots participate in the dodge that do not have an inherent width. For example, a scatter plot has no width but a barplot does. You can pass a width manually like `draw(..., scales($(scale_setting_name(scale_id, dodge_aes)) = (; width = 0.6))`.")
end
# scale to 0-1, center around 0, shrink to width (like centers of bins that added together result in width)
offsets = ((indices .- 1) ./ (n - 1) .- 0.5) .* width * (n-1) / n
return data .+ offsets
end
end

function set_dodge_width_default!(categoricalscales, processedlayers)
for dodgetype in (AesDodgeX, AesDodgeY)
haskey(categoricalscales, dodgetype) || continue
scales = categoricalscales[dodgetype]
for (scale_id, scale) in pairs(scales)
props = scale.props.aesprops
props.width === nothing || continue
n_dodge = length(datavalues(scale))
width::Union{Float64,Nothing} = nothing
for p in processedlayers
_width = determine_dodge_width(p, dodgetype, n_dodge)
if width === nothing
width = _width
elseif _width !== nothing
width == _width || error("Determined at least two different auto-widths for the `$(scale_setting_name(scale_id, dodgetype))` scale, $width and $_width. AlgebraOfGraphics tried to determine dodge with because you specified that a width-less plot type such as Scatter or Errorbars should be dodged. Some plot types like Barplot may have an inherent width for dodging which can often be auto-determined, so AlgebraOfGraphics looked for such widths in all other plot types in this plot. Because multiple such widths were detected, AlgebraOfGraphics gives up and you have to specify the dodge width for your width-less plots manually, like `draw(..., scales($(scale_setting_name(scale_id, dodgetype)) = (; width = 0.5))`")
end
end
if width !== nothing
scales[scale_id] = update_width(scale, width)
end
end
end
return
end

function update_width(scale::CategoricalScale, width)
return Accessors.@set scale.props.aesprops.width = width
end

function determine_dodge_width(p::ProcessedLayer, dodgetype, n_dodge)::Union{Float64,Nothing}
aes_mapping = aesthetic_mapping(p)
for key in keys(p.primary)
aes = hardcoded_or_mapped_aes(p, key, aes_mapping)
# check that processedlayer participates in this dodge
aes == dodgetype || continue
return determine_dodge_width(p.plottype, p, aes_mapping, dodgetype, n_dodge)
end
return nothing
end

determine_dodge_width(anyplot, p::ProcessedLayer, aes_mapping, dodgetype, n_dodge) = nothing

attribute_or_plot_default(plottype, attributes, key) = get(attributes, key) do
to_value(Makie.default_theme(nothing, plottype)[key])
end

function determine_dodge_width(T::Type{BarPlot}, p::ProcessedLayer, aes_mapping, dodgetype, n_dodge)
width = attribute_or_plot_default(T, p.attributes, :width)
gap = attribute_or_plot_default(T, p.attributes, :gap)
dodge_gap = attribute_or_plot_default(T, p.attributes, :dodge_gap)
gap_scaler = dodge_gap / (n_dodge - 1) * 1.5 # why 1.5?
if width === Makie.automatic
corresponding_aes(::Type{AesDodgeX}) = AesX
corresponding_aes(::Type{AesDodgeY}) = AesY
if length(p.positional) == 1 # Makie goes 1:n automatically if only one arg is given
return (1.0 - gap + gap_scaler)
end
for key in eachindex(p.positional)
if aes_mapping[key] === corresponding_aes(dodgetype)
return resolution(p.positional[key]) * (1 - gap + gap_scaler)
end
end
elseif width isa Real
return width * (1 - gap + gap_scaler)
end
return
end

function resolution(vec_of_vecs)::Float64
iscategoricalcontainer(vec_of_vecs) && return 1.0
s = unique(sort(reduce(vcat, vec_of_vecs)))
return minimum((b - a for (a, b) in @views zip((s[begin:end-1]), s[begin+1:end])))
end
45 changes: 44 additions & 1 deletion test/reference_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -900,5 +900,48 @@ reftest("title subtitle footnotes fontsize inherit") do
),
axis = (; width = 100, height = 100)
)
end

reftest("dodge barplot with errorbars") do
f = Figure()
df = (
x = [1, 1, 2, 2],
y = [1, 2, 5, 6],
err = [0.5, 0.4, 0.7, 0.6],
group = ["A", "B", "A", "C"],
)

function spec(; kwargs...)
xdir = get(kwargs, :direction, :y) == :x
dodge_map = xdir ? mapping(dodge_y = :group) : mapping(dodge_x = :group)
return data(df) * (
mapping(:x, :y, dodge = :group, color = :group) * visual(BarPlot; kwargs...) +
mapping(xdir ? :y : :x, xdir ? :x : :y, :err) * dodge_map * visual(Errorbars; direction = xdir ? :x : :y)
)
end

draw!(f[1, 1], spec())
draw!(f[1, 2], spec(; width = 0.7, dodge_gap = 0.2))
draw!(f[2, 1], spec(; direction = :x))
draw!(f[2, 2], spec(; direction = :x, width = 0.7, gap = 0.3, dodge_gap = 0.2))
f
end

reftest("dodge scatter with rangebars") do
df = (
x = repeat(1:10, inner = 2),
y = cos.(range(0, 2pi, length = 20)),
ylow = cos.(range(0, 2pi, length = 20)) .- 0.2,
yhigh = cos.(range(0, 2pi, length = 20)) .+ 0.3,
dodge = repeat(["A", "B"], 10)
)

end
f = Figure()
spec1 = data(df) * (mapping(:x, :y, dodge_x = :dodge, color = :dodge) * visual(Scatter) + mapping(:x, :ylow, :yhigh, dodge_x = :dodge, color = :dodge) * visual(Rangebars))
spec2 = data(df) * (mapping(:y, :x, dodge_y = :dodge, color = :dodge) * visual(Scatter) + mapping(:x, :ylow, :yhigh, dodge_y = :dodge, color = :dodge) * visual(Rangebars, direction = :x))
draw!(f[1, 1], spec1, scales(DodgeX = (; width = 0.5)))
draw!(f[1, 2], spec2, scales(DodgeY = (; width = 0.5)))
draw!(f[2, 1], spec1, scales(DodgeX = (; width = 1.0)))
draw!(f[2, 2], spec2, scales(DodgeY = (; width = 1.0)))
f
end
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 9737169

Please sign in to comment.