diff --git a/docs/gallery/gallery/scales/custom_scales.jl b/docs/gallery/gallery/scales/custom_scales.jl index 72ca0a20e..25d9323f3 100644 --- a/docs/gallery/gallery/scales/custom_scales.jl +++ b/docs/gallery/gallery/scales/custom_scales.jl @@ -42,15 +42,15 @@ plt = data(df) * visual(Arrows, arrowsize=10, lengthscale=0.3) draw(plt; palettes=(arrowcolor=colors, arrowhead=heads)) -# To associate specific attribute values to specific data values, use pairs. -# Missing keys will cycle over values that are not pairs. +# To associate specific attribute values to specific data values, use `sorter` or `renamer` x = rand(100) y = rand(100) z = rand(["a", "b", "c", "d"], 100) df = (; x, y, z) -plt = data(df) * mapping(:x, :y, color=:z) -colors = ["a" => colorant"#E24A33", "c" => colorant"#348ABD", colorant"#988ED5", colorant"#777777"] +plt = data(df) * mapping(:x, :y, color=:z => sorter("a", "c")) +# "a" will get color #E24A33 and "c" will get #348ABD +colors = [colorant"#E24A33", colorant"#348ABD", colorant"#988ED5", colorant"#777777"] draw(plt; palettes=(color=colors,)) # Categorical color gradients can also be passed to `palettes`. diff --git a/docs/src/layers/mapping.md b/docs/src/layers/mapping.md index dc74a1ad3..95da14ce2 100644 --- a/docs/src/layers/mapping.md +++ b/docs/src/layers/mapping.md @@ -82,3 +82,8 @@ The complete API of helper functions is available at [Mapping helpers](@ref). # column `labels` is expressed in strings and we do not want to treat it as categorical :labels => verbatim ``` + +!!! note + + Renaming/reordering helpers need not mention all values; e.g. `sorter(["medium", + "high"])` will order the unspecified value ("low") after "medimum" and "high". diff --git a/src/algebra/layer.jl b/src/algebra/layer.jl index 9e42f0aa6..2e69f3761 100644 --- a/src/algebra/layer.jl +++ b/src/algebra/layer.jl @@ -159,11 +159,17 @@ end function rescale(p::ProcessedLayer, categoricalscales::MixedArguments) primary = map(keys(p.primary), p.primary) do key, values scale = get(categoricalscales, key, nothing) - return rescale(values, scale) + result, scale = rescale(values, scale) + categoricalscales = set(categoricalscales, key => scale) + return result end positional = map(keys(p.positional), p.positional) do key, values scale = get(categoricalscales, key, nothing) - return rescale.(values, Ref(scale)) + return map(values) do value + value, scale = rescale(value, scale) + categoricalscales = set(categoricalscales, key => scale) + return value + end end # compute dodging information @@ -201,7 +207,7 @@ function concatenate(pls::AbstractVector{ProcessedLayer}) end function append_processedlayers!(pls_grid, processedlayer::ProcessedLayer, categoricalscales::MixedArguments) - processedlayer = rescale(processedlayer, categoricalscales) + processedlayer, categoricalscales = rescale(processedlayer, categoricalscales) tmp_pls_grid = map(_ -> ProcessedLayer[], pls_grid) for c in CartesianIndices(shape(processedlayer)) pl = slice(processedlayer, c) diff --git a/src/algebra/layers.jl b/src/algebra/layers.jl index d72800882..5120673aa 100644 --- a/src/algebra/layers.jl +++ b/src/algebra/layers.jl @@ -32,9 +32,9 @@ function compute_processedlayers_grid(processedlayers, categoricalscales) indices = CartesianIndices(compute_grid_positions(categoricalscales)) pls_grid = map(_ -> ProcessedLayer[], indices) for processedlayer in processedlayers - append_processedlayers!(pls_grid, processedlayer, categoricalscales) + categoricalscales = append_processedlayers!(pls_grid, processedlayer, categoricalscales) end - return pls_grid + return pls_grid, categoricalscales end function compute_entries_continuousscales(pls_grid, categoricalscales) @@ -109,7 +109,7 @@ function compute_axes_grid(s::OneOrMoreLayers; # fit categorical scales (compute plot values using all data values) map!(fitscale, categoricalscales, categoricalscales) - pls_grid = compute_processedlayers_grid(processedlayers, categoricalscales) + pls_grid, categoricalscales = compute_processedlayers_grid(processedlayers, categoricalscales) entries_grid, continuousscales_grid, merged_continuousscales = compute_entries_continuousscales(pls_grid, categoricalscales) diff --git a/src/helpers.jl b/src/helpers.jl index 0f06bb6d3..fa3fbf320 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -3,17 +3,34 @@ to_string(s) = string(s) to_string(s::AbstractString) = s -struct Sorted{T} +struct Renamed{S, T} idx::UInt32 - value::T + original::S + value::Union{T, Nothing} +end +Renamed(idx::Integer, original, value) = Renamed(convert(UInt32, idx), original, value) +value(x::Renamed) = isnothing(x.value) ? x.original : s.value + +Base.hash(s::Renamed, h::UInt) = hash(value(s), hash(s.idx, hash(:Renamed, h))) +function Base.:(==)(s1::Renamed, s2::Renamed) + val_equal = isequal(s1.idx, s2.idx) && isequal(s1.value, s2.value) + if val_equal && isnothing(s1.value) + return s1.original == s2.original + else + return val_equal + end end -Sorted(idx::Integer, value) = Sorted(convert(UInt32, idx), value) - -Base.hash(s::Sorted, h::UInt) = hash(s.value, hash(s.idx, hash(:Sorted, h))) -Base.:(==)(s1::Sorted, s2::Sorted) = isequal(s1.idx, s2.idx) && isequal(s1.value, s2.value) -Base.print(io::IO, s::Sorted) = print(io, s.value) -Base.isless(s1::Sorted, s2::Sorted) = isless((s1.idx, s1.value), (s2.idx, s2.value)) +Base.print(io::IO, s::Renamed) = print(io, value(s)) +function Base.isless(s1::Renamed, s2::Renamed) + if isnothing(s1.value) == isnothing(s1.value) + return isless((s1.idx, value(s1)), (s2.idx, value(s2))) + else + # sort any renamed values before values that keep + # their original value + return isless(!isnothing(s1.value), !isnothing(s2.value)) + end +end struct Renamer{U, L} uniquevalues::U @@ -22,7 +39,7 @@ end function (r::Renamer{Nothing})(x) i = LinearIndices(r.labels)[x] - return Sorted(i, r.labels[i]) + return Renamed(i, x, r.labels[i]) end function (r::Renamer)(x) @@ -30,7 +47,7 @@ function (r::Renamer)(x) cand = @inbounds r.uniquevalues[i] if isequal(cand, x) label = r.labels[i] - return Sorted(i, label) + return Renamed(i, x, label) end end throw(KeyError(x)) @@ -41,10 +58,11 @@ renamer(args::Pair...) = renamer(args) """ renamer(arr::Union{AbstractArray, Tuple}) -Utility to rename a categorical variable, as in `renamer([value1 => label1, value2 => label2])`. -The keys of all pairs should be all the unique values of the categorical variable and -the values should be the corresponding labels. The order of `arr` is respected in -the legend. +Utility to rename a categorical variable, as in `renamer([value1 => label1, value2 => +label2])`. The order of `arr` is respected in the legend. The renamer need not specify all +values of the sequence it renames: if the renamer is missing one of the values from a +sequence it is applied to, the unspecified values are sorted after those that are specified (in the +order returned by `unique`) and are not renamed. # Examples ```jldoctest @@ -64,8 +82,8 @@ Class One ``` If `arr` does not contain `Pair`s, elements of `arr` are assumed to be labels, and the -unique values of the categorical variable are taken to be the indices of the array. -This is particularly useful for `dims` mappings. +unique values of the categorical variable are taken to be the indices of the array. This is +particularly useful for `dims` mappings. # Examples ```jldoctest @@ -90,10 +108,12 @@ end """ sorter(ks) -Utility to reorder a categorical variable, as in `sorter(["low", "medium", "high"])`. -A vararg method `sorter("low", "medium", "high")` is also supported. -`ks` should include all the unique values of the categorical variable. -The order of `ks` is respected in the legend. +Utility to reorder a categorical variable, as in `sorter(["low", "medium", "high"])`. A +vararg method `sorter("low", "medium", "high")` is also supported. The order of `ks` is +respected in the legend. The sorter need not specify all values (e.g. `sorter(["low", +"medium"])` will work for an array that includes `"high"``); the unspecified values will be +sorted after the specified values and will occur in the order returned by `unique`. + """ function sorter(ks::Union{AbstractArray, Tuple}) vs = map(to_string, ks) diff --git a/src/scales.jl b/src/scales.jl index ecb8b3887..b99728fae 100644 --- a/src/scales.jl +++ b/src/scales.jl @@ -61,6 +61,9 @@ end datavalues(c::CategoricalScale) = c.data plotvalues(c::CategoricalScale) = c.plot getlabel(c::CategoricalScale) = something(c.label, "") +function addvalue(c::CategoricalScale, values) + return CategoricalScale(vcat(c.data, values), c.plot, c.palette, c.label) +end ## Continuous Scales @@ -104,13 +107,14 @@ elementwise_rescale(value) = value contextfree_rescale(values) = map(elementwise_rescale, values) -rescale(values, ::Nothing) = values +rescale(values, ::Nothing) = values, nothing function rescale(values, c::CategoricalScale) # Do not rescale continuous data with categorical scale scientific_eltype(values) === categorical || return values - idxs = indexin(values, datavalues(c)) - return plotvalues(c)[idxs] + scalevalues = vcat(datavalues(c), setdiff(values, datavalues(c))) + idxs = indexin(values, scalevalues) + return plotvalues(c)[idxs], addvalues(c, scalevalues) end Base.length(c::CategoricalScale) = length(datavalues(c)) diff --git a/test/helpers.jl b/test/helpers.jl index 6f259c6a9..a9753c767 100644 --- a/test/helpers.jl +++ b/test/helpers.jl @@ -10,6 +10,8 @@ @test r("a") < r("b") < r("c") @test r("a") == r("a") @test r("a") != r("b") + @test map(r, ["a", "a", "b", "c", "d"]) == [Sorted(1, "A"), Sorted(1, "A"), Sorted(2, "B"), Sorted(3, "C"), Sorted(4, "d")] + r̂ = renamer(["a" => "A", "b" => "B", "c" => "C"]) @test r̂("a") == Sorted(1, "A") @test r̂("b") == Sorted(2, "B") @@ -33,6 +35,8 @@ @test s("b") < s("c") < s("a") @test s("a") == s("a") @test s("a") != s("b") + @test map(s, ["a", "b", "c", "d"]) == [Sorted(3, "a"), Sorted(1, "b"), Sorted(2, "c"), Sorted(4, "d")] + ŝ = sorter(["b", "c", "a"]) @test ŝ("a") == Sorted(3, "a") @test ŝ("b") == Sorted(1, "b")