Order of legend entries in AlgebraOfGraphics

Hi everyone,

does anyone know of a way to specify the order of the legend entries created by AlgebraOfGraphics?
In the following, the labels get reordered alphabetically but i’d like them to stay in the original order:

d = (;y = rand(3), x = string.('a':'c'), c = ["x", "z", "y"])
p = data(d) * mapping(:x, :y, color=:c) * visual(BarPlot)


I think you can just apply a sorter to your mapping:

using AlgebraOfGraphics, CairoMakie

labels = ["x", "z", "y"]
d = (x='a':'c', y=[1, 2, 3], c=labels)
p = data(d) * mapping(:x, :y, color=:c=>sorter(labels)) * visual(BarPlot)

Thanks, yes sorter indeed works. But it also changes the color mapping, which doesn’t play well with color palettes:

labels = ["x", "z", "y"]
d = (x='a':'c', y=[1, 2, 3], c=labels)
pal = ["x" => "red", "y" => "blue", "z" => "green"]
p = data(d) * mapping(:x, :y, color=:c => sorter(d.c)) * visual(BarPlot)
draw(p, palettes = (;color=pal))

This throws the following error:

ArgumentError: Key AlgebraOfGraphics.Sorted{String}(0x00000001, "x") not found and no default values are present

    (::AlgebraOfGraphics.Cycler{String, String})(::AlgebraOfGraphics.Sorted{String})@scales.jl:20
    collect(::Base.Generator{Vector{AlgebraOfGraphics.Sorted{String}}, AlgebraOfGraphics.Cycler{String, String}})@array.jl:724
    apply_palette(::Vector{Pair{String, String}}, ::Vector{AlgebraOfGraphics.Sorted{String}})@scales.jl:28
    fitscale(::AlgebraOfGraphics.CategoricalScale{Vector{AlgebraOfGraphics.Sorted{String}}, Nothing, Vector{Pair{String, String}}})@scales.jl:57
    map!(::typeof(AlgebraOfGraphics.fitscale), ::Dictionaries.Dictionary{Union{Int64, Symbol}, Any}, ::Dictionaries.Dictionary{Union{Int64, Symbol}, Any})@map.jl:57
    var"#compute_axes_grid#116"(::NamedTuple{(), Tuple{}}, ::NamedTuple{(:color,), Tuple{Vector{Pair{String, String}}}}, ::typeof(AlgebraOfGraphics.compute_axes_grid), ::AlgebraOfGraphics.Layer)@layers.jl:143
    var"#compute_axes_grid#113"(::NamedTuple{(), Tuple{}}, ::NamedTuple{(:color,), Tuple{Vector{Pair{String, String}}}}, ::typeof(AlgebraOfGraphics.compute_axes_grid), ::Makie.Figure, ::AlgebraOfGraphics.Layer)@layers.jl:117
    var"#plot!#225"(::NamedTuple{(), Tuple{}}, ::NamedTuple{(:color,), Tuple{Vector{Pair{String, String}}}}, ::typeof(MakieCore.plot!), ::Makie.Figure, ::AlgebraOfGraphics.Layer)@draw.jl:21
    (::AlgebraOfGraphics.var"#230#231"{NamedTuple{(), Tuple{}}, NamedTuple{(:color,), Tuple{Vector{Pair{String, String}}}}, NamedTuple{(), Tuple{}}, NamedTuple{(), Tuple{}}, NamedTuple{(), Tuple{}}, AlgebraOfGraphics.Layer})(::Makie.Figure)@draw.jl:46
    top-level scope@Local: 6[inlined]

I don’t think I’ve seen a dictionary be used for a palette before. Could you just pass your vector of colors directly?

draw(p, palettes=(; color=[:red, :green, :blue]))

The pairs syntax for associating specific labels with a fixed color is from the documentation (second example) and is really nice if you need to create many different plots with the same grouping but possibly varying subsets of labels. Maybe there’s a ‘hack’ to rearrange the legend labels after drawing.

That part is being reworked (see this github comment and the discussion there in general).

There is a conceptual issue with the current approach where the keys of the dictionary in pal do not match the transformed entries for your categorical variable (which are no longer just strings but strings + sorting information).

A “hack” around it would be to pass the transformed values to the palette, e.g.

labels = ["x", "z", "y"]
d = (x='a':'c', y=[1, 2, 3], c=labels)
transf = sorter(d.c)
pal = [transf("x") => "red", transf("y") => "blue", transf("z") => "green"]
p = data(d) * mapping(:x, :y, color=:c => transf) * visual(BarPlot)
draw(p, palettes = (; color=pal))

But the long term fix is probably to decouple the palette (which really should just be a list of color) and the “categorization of the variable” (convert from strings or other types to integres) in AlgebraOfGraphics.jl

EDIT: but yes, definitely do file an issue about this, it’s a valid use case to keep in mind when reworking the palette system


The pairs syntax for associating specific labels with a fixed color is from …

Oh that’s really handy, thanks for the example! It looks like things work until sorter is introduced:

colors = ["x" => "red", "y" => "blue", "z" => "green"]
p = data(d) * mapping(:x, :y; color=:c) * visual(BarPlot) # Work's
p = data(d) * mapping(:x, :y; color=:c=>sorter(d.c)) * visual(BarPlot) # Doesn't work
draw(p; palettes=(; color=colors))

This might be worth raising an issue?

haha, @piever beat me to it

I’ve filed an issue. Thanks for the help!

I was actually thinking about the following. Could it make sense to improve the sorter helper (maybe with a better name to subsume most of the various helpers we use) so that the pair syntax in the palette is not needed?

Essentially, one could use :c => sorter("y", "x", "z") to establish levels: "y" is 1, "x" is 2 and "z" is 3, regardless of what data is actually present. That way, passing the same palette will always yield consistent plots (regardless of data filtering), but now the palette is strictly a “theme component” (not data specific).

I like that. What would happen if I don’t pass all the names present in the data? If it would only include the specified names in the plot in the given order it would be similar to select for a DataFrame, wouldn’t it? Maybe it could be named accordingly.

I was actually thinking that the other ones would just be attributed the following levels. This way, we’d keep supporting the “mixed approach”, ie only some entries get “fixed” attribute values.

For instance the old

# `a` and `b` entries get specified colors, for the other entries it's data-dependent
palette = (color=["a" => "red", "b" => "blue", "green", "black"],)

could be achieved with

mapping(color=v => sorter(["a", "b"])
palette = (color=["red", "blue", "green", "black"],)