diff --git a/CHANGELOG.md b/CHANGELOG.md index afdc539a1ed..4d048fbda5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index f903dc42a3c..430d4c81b4c 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -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) diff --git a/docs/src/reference/blocks/legend.md b/docs/src/reference/blocks/legend.md index 565a86b1406..227069060b9 100644 --- a/docs/src/reference/blocks/legend.md +++ b/docs/src/reference/blocks/legend.md @@ -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 diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index cf30b0cb3eb..b0c48d4bfe3 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -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) @@ -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 @@ -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)) @@ -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]