Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support incomplete list for sorter and renamer #384

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/gallery/gallery/scales/custom_scales.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
5 changes: 5 additions & 0 deletions docs/src/layers/mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".
12 changes: 9 additions & 3 deletions src/algebra/layer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions src/algebra/layers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
60 changes: 40 additions & 20 deletions src/helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,15 +39,15 @@ 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)
for i in keys(r.uniquevalues)
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))
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
10 changes: 7 additions & 3 deletions src/scales.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions test/helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down