Skip to content

Commit

Permalink
Legend overrides (#4427)
Browse files Browse the repository at this point in the history
* start implementing legend override mechanism

* fix two scenarios

* add reference test

* enable pair on vector

* update testcase

* overrides for poly elements

* add docs section

* add changelog entry

* unexport LegendOverride

* enable any key-value convertible to Attributes

* remove LegendOverride from docs, use NamedTuples, add  all possible attributes

* remove LegendOverride from tests
  • Loading branch information
jkrumbiegel authored Oct 1, 2024
1 parent f1c1918 commit ad75383
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Fixed issue with CairoMakie rendering scene backgrounds at the wrong position [#4425](https://github.com/MakieOrg/Makie.jl/pull/4425)
- Fix incorrect inverse transformation in `position_on_plot` for lines, causing incorrect tooltip placement in DataInspector [#4402](https://github.com/MakieOrg/Makie.jl/pull/4402)
- Added ability to override legend element attributes by pairing labels or plots with override attributes [#4427](https://github.com/MakieOrg/Makie.jl/pull/4427).
- Added threshold before a drag starts which improves false negative rates for clicks. `Button` can now trigger on click and not mouse-down which is the canonical behavior in other GUI systems [#4336](https://github.com/MakieOrg/Makie.jl/pull/4336).
- `PolarAxis` font size now defaults to global figure `fontsize` in the absence of specific `Axis` theming [#4314](https://github.com/MakieOrg/Makie.jl/pull/4314)
- `MultiplesTicks` accepts new option `strip_zero=true`, allowing labels of the form `0x` to be `0` [#4372](https://github.com/MakieOrg/Makie.jl/pull/4372)
Expand Down
33 changes: 33 additions & 0 deletions ReferenceTests/src/tests/figures_and_makielayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,39 @@ end
f
end

@reference_test "Legend overrides" begin
f = Figure()
ax = Axis(f[1, 1])

li = lines!(
1:10,
label = "Line" => (; linewidth = 4, color = :gray60, linestyle = :dot),
)
sc = scatter!(
1:10,
2:11,
color = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1],
colorrange = (1, 3),
marker = :utriangle,
markersize = 20,
label = [
label => (; markersize = 30, color = i) for (i, label) in enumerate(["blue", "green", "yellow"])
]
)
Legend(f[1, 2], ax)
Legend(
f[1, 3],
[
sc => (; markersize = 30),
[li => (; color = :red), sc => (; color = :cyan)],
[li, sc] => Dict(:color => :cyan),
],
["Scatter", "Line and Scatter", "Another"],
patchsize = (40, 20)
)
f
end

@reference_test "LaTeXStrings in Axis3 plots" begin
xs = LinRange(-10, 10, 100)
ys = LinRange(0, 15, 100)
Expand Down
71 changes: 71 additions & 0 deletions docs/src/reference/blocks/legend.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,77 @@ f
```


## Overriding legend entry attributes

By default, legends inherit the visual attributes of the plots they belong to.
Sometimes, it is necessary to override some of these attributes to make the legend more legible.
You can pair a key-value object like a `NamedTuple` or a `Dict{Symbol}` to a plot's `label` to override its automatic legend entry, for example to increase the marker size of a `Scatter`:

```@figure
f, ax, sc = scatter(
cos.(range(0, 7pi, 100)),
color = :black,
markersize = 8,
label = "cos" => (; markersize = 15)
)
scatter!(
sin.(range(0, 7pi, 100)),
color = :black,
marker = :utriangle,
markersize = 8,
label = "sin" => (; markersize = 15)
)
Legend(f[1, 2], ax)
f
```

These are the attributes you can override (note that some of them have convenience aliases like `color` which applies to all elements while `polycolor` only applies to `PolyElement`s):

- `MarkerElement`
- `[marker]points`, `markersize`, `[marker]strokewidth`, `[marker]color`, `[marker]strokecolor`, `[marker]colorrange`, `[marker]colormap`
- `LineElement`
- `[line]points`, `linewidth`, `[line]color`, `linestyle`, `[line]colorrange`, `[line]colormap`
- `PolyElement`
- `[poly]points`, `[poly]strokewidth`, `[poly]color`, `[poly]strokecolor`, `[poly]colorrange`, `[poly]colormap`

Another common case is when you want to create a legend for a plot with a categorical colormap.
By passing a vector of labels paired with overrides, you can create multiple entries with the correct colors:

```@figure
f, ax, bp = barplot(
1:5,
[1, 3, 2, 5, 4],
color = 1:5,
colorrange = (1, 5),
colormap = :Set1_5,
label = [label => (; color = i)
for (i, label) in enumerate(["red", "blue", "green", "purple", "orange"])]
)
Legend(f[1, 2], ax)
f
```

You may also override plots in the `Legend` constructor itself, in this case, you pair the overrides with the plots whose legend entries you want to override:

```@figure
f = Figure()
ax = Axis(f[1, 1])
li = lines!(ax, 1:5, linestyle = :dot)
sc = scatter!(ax, 1:5, markersize = 10)
Legend(
f[1, 2],
[
sc => (; markersize = 20),
li => (; linewidth = 3),
[li, sc] => (; color = :red),
[li => (; linewidth = 3), sc => (; markersize = 20)],
],
["Scatter", "Line", "Both", "Both 2"],
patchsize = (40, 20),
)
f
```

## Multi-Group Legends

Sometimes a legend consists of multiple groups, for example in a plot where both
Expand Down
100 changes: 93 additions & 7 deletions src/makielayout/blocks/legend.jl
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ function initialize_block!(leg::Legend; entrygroups)
return
end

struct LegendOverride
overrides::Attributes
LegendOverride(attrs::Attributes) = new(attrs)
LegendOverride(l::LegendOverride) = l
LegendOverride(attrs) = new(Attributes(attrs))
end

LegendOverride(; kwargs...) = LegendOverride(Attributes(; kwargs...))

function connect_block_layoutobservables!(leg::Legend, layout_width, layout_height, layout_tellwidth, layout_tellheight, layout_halign, layout_valign, layout_alignmode)
connect!(layout_width, leg.width)
connect!(layout_height, leg.height)
Expand Down Expand Up @@ -329,26 +338,76 @@ end
legendelements(le::LegendElement, legend) = LegendElement[le]
legendelements(les::AbstractArray{<:LegendElement}, legend) = LegendElement[les...]

legendelements(p::Pair, legend) = legendelements(p[1], legend, LegendOverride(p[2]))

function LegendEntry(label, contentelements::AbstractArray, legend; kwargs...)
attrs = Attributes(label = label)
function legendelements(any, legend, override::LegendOverride)
les = legendelements(any, legend)
for le in les
apply_legend_override!(le, override)
end
return les
end

function apply_legend_override!(le::MarkerElement, override::LegendOverride)
renamed_attrs = _rename_attributes!(MarkerElement, copy(override.overrides))
for sym in (:markerpoints, :markersize, :markercolor, :markerstrokewidth, :markerstrokecolor, :markercolormap, :markercolorrange)
if haskey(renamed_attrs, sym)
le.attributes[sym] = renamed_attrs[sym]
end
end
end

function apply_legend_override!(le::LineElement, override::LegendOverride)
renamed_attrs = _rename_attributes!(LineElement, copy(override.overrides))
for sym in (:linepoints, :linewidth, :linecolor, :linecolormap, :linecolorrange, :linestyle)
if haskey(renamed_attrs, sym)
le.attributes[sym] = renamed_attrs[sym]
end
end
end

function apply_legend_override!(le::PolyElement, override::LegendOverride)
renamed_attrs = _rename_attributes!(PolyElement, copy(override.overrides))
for sym in (:polypoints, :polycolor, :polystrokewidth, :polystrokecolor, :polycolormap, :polycolorrange, :polystrokestyle)
if haskey(renamed_attrs, sym)
le.attributes[sym] = renamed_attrs[sym]
end
end
end

function LegendEntry(label, contentelement, override::Attributes, legend; kwargs...)
attrs = Attributes(; label)

kwargattrs = Attributes(kwargs)
merge!(attrs, kwargattrs)

elems = vcat(legendelements.(contentelements, Ref(legend))...)
elems = legendelements(contentelement, legend, override)
if isempty(elems)
error("`legendelements` returned an empty list for content element of type $(typeof(contentelement)). That could mean that neither this object nor any possible child objects had a method for `legendelements` defined that returned a non-empty result.")
end
LegendEntry(elems, attrs)
end

function LegendEntry(label, contentelement, legend; kwargs...)

function LegendEntry(label, content, legend; kwargs...)
attrs = Attributes(label = label)

kwargattrs = Attributes(kwargs)
merge!(attrs, kwargattrs)

elems = legendelements(contentelement, legend)
if content isa AbstractArray
elems = vcat(legendelements.(content, Ref(legend))...)
elseif content isa Pair
if content[1] isa AbstractArray
elems = vcat(legendelements.(content[1] .=> Ref(content[2]), Ref(legend))...)
else
elems = legendelements(content, legend)
end
else
elems = legendelements(content, legend)
end
if isempty(elems)
error("`legendelements` returned an empty list for content element of type $(typeof(contentelement)). That could mean that neither this object nor any possible child objects had a method for `legendelements` defined that returned a non-empty result.")
error("`legendelements` returned an empty list for content element of type $(typeof(content)). That could mean that neither this object nor any possible child objects had a method for `legendelements` defined that returned a non-empty result.")
end
LegendEntry(elems, attrs)
end
Expand Down Expand Up @@ -611,6 +670,24 @@ function get_labeled_plots(ax; merge::Bool, unique::Bool)
l.label[]
end

if any(x -> x isa AbstractVector, labels)
_lplots = []
_labels = []
for (lplot, label) in zip(lplots, labels)
if label isa AbstractVector
for lab in label
push!(_lplots, lplot)
push!(_labels, lab)
end
else
push!(_lplots, lplot)
push!(_labels, label)
end
end
lplots = _lplots
labels = _labels
end

# filter out plots with same plot type and label
if unique
plots_labels = Base.unique(((p, l),) -> (typeof(p), l), zip(lplots, labels))
Expand All @@ -626,7 +703,16 @@ function get_labeled_plots(ax; merge::Bool, unique::Bool)
lplots, labels = mergedplots, ulabels
end

lplots, labels
lplots_with_overrides = map(lplots, labels) do plots, label
if label isa Pair
plots => LegendOverride(label[2])
else
plots
end
end
labels = [label isa Pair ? label[1] : label for label in labels]

lplots_with_overrides, labels
end

get_plots(p::AbstractPlot) = [p]
Expand Down

0 comments on commit ad75383

Please sign in to comment.