diff --git a/Project.toml b/Project.toml index ced799e81..d79173055 100644 --- a/Project.toml +++ b/Project.toml @@ -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" diff --git a/src/AlgebraOfGraphics.jl b/src/AlgebraOfGraphics.jl index b97975b8c..4388a052a 100644 --- a/src/AlgebraOfGraphics.jl +++ b/src/AlgebraOfGraphics.jl @@ -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 diff --git a/src/algebra/layers.jl b/src/algebra/layers.jl index 6a8256b23..454d7912b 100644 --- a/src/algebra/layers.jl +++ b/src/algebra/layers.jl @@ -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) @@ -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] @@ -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 \ No newline at end of file +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 diff --git a/test/reference_tests.jl b/test/reference_tests.jl index 756051f61..d916d4042 100644 --- a/test/reference_tests.jl +++ b/test/reference_tests.jl @@ -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 \ No newline at end of file + 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 diff --git a/test/reference_tests/dodge barplot with errorbars ref.png b/test/reference_tests/dodge barplot with errorbars ref.png new file mode 100644 index 000000000..926ce6a29 Binary files /dev/null and b/test/reference_tests/dodge barplot with errorbars ref.png differ diff --git a/test/reference_tests/dodge scatter with rangebars ref.png b/test/reference_tests/dodge scatter with rangebars ref.png new file mode 100644 index 000000000..82b45826f Binary files /dev/null and b/test/reference_tests/dodge scatter with rangebars ref.png differ