How to plot with manually created legend entries?

I created my own legend entries following the docs, now I’d like to plot with them.

MWE:

using GLMakie
legendentries = Dict("A" => LineElement(linestyle = nothing, linewidth = 0.3, color = :red),
                     "B" => MarkerElement(color = :white, marker = '⋆', strokecolor = :black, markerstrokewidth = 0.5, strokewidth = 0.5, markersize = 10px))
fig = Figure()
ax = fig[1, 1] = Axis(fig, aspect = DataAspect(), xlabel = "X (cm)", ylabel = "Y (cm)")
lines!(ax, rand(Point2f0, 10)) # Iwant the line here to be red and have a thickness of 0.3
scatter!(ax, rand(Point2f0, 10)) # I want the markers here to be stars etc...
fig[0,1] = Legend(fig, collect(values(legendentries)), collect(keys(legendentries)))
fig

Basically, I suspect I need a method to convert LineElements, MarkerElements and PolyElements to a named tuple for this to work smoothly, or perhaps for the lines! function to accept LineElement as a specifier for the properties of the plotted line. But perhaps there is a better way to do this…?

1 Like

Maybe I’m missing something, but why don’t you start with the named tuple instead? I.e., plot with the named tuple of the series’ style, and use the same named tuple again for the legend (edited and added the plot output):

using GLMakie
seriestype = Dict("A" => (linestyle = nothing, linewidth = 0.3, color = :red),
                  "B" => (color = :white, marker = '⋆', strokecolor = :black, markerstrokewidth = 0.5, strokewidth = 0.5, markersize = 10px))
legendentries = Dict("A" => LineElement(; seriestype["A"]...),
                     "B" => MarkerElement(; seriestype["B"]...))

fig = Figure(resolution = (1200, 700))
ax = fig[1, 1] = Axis(fig, xlabel = "X (cm)", ylabel = "Y (cm)", aspect = DataAspect(), width=500)
lines!(ax, rand(Point2f0, 10); seriestype["A"]...) # Iwant the line here to be red and have a thickness of 0.3
scatter!(ax, rand(Point2f0, 10); seriestype["B"]...) # I want the markers here to be stars etc...
leg = Legend(fig, collect(values(legendentries)), collect(keys(legendentries)), tellheight=true)
fig[2, 1] = leg
leg.tellheight = true
leg.orientation = :horizontal
fig

gives me

1 Like

Why are you creating the legend elements before the plots? It’s meant to be the other way around

2 Likes

Hi @jules! The reason is that this plot is part of a whole series of other plots (elsewhere) and I want the styles to be consistent between plots. So by defining the styles first, I can use the styles everywhere and know that they are the same.
So yea, @briochemc, I could define the named tuples first, and use those as-is in the plots, and then wrap them appropriately (i.e. with LineElement) in the Legend call, but I was wondering if I was missing something simpler here. Like, why should there be a discrepancy between the argument types that Legend accepts and the types lines! and scatter! accept when it comes to styling the objects?

@briochemc I tried to implement what you said, but I can’t even get a MWE to work. Can you show me what exactly you mean? I might have misunderstood you, but I thought you meant:

  1. define the styles in named tuples
  2. feed those to the plots
  3. wrap these nt with the appropriate LineElement, MarkerElement etc
  4. feed that to the Legend

Is that what you meant?

Yes, you’re right! I’m sorry, I’m an idiot, I removed the relevant part when I edited my post… :sweat_smile:

I edited it again and it should work as a MWE to produce the plot I showed now.

1 Like

Thank you so much @briochemc ! Yes, it works perfectly and solves my issue.

But I think the real solution I’m looking for is using plot recipes. That would be the correct way to avoid defining the styles both as a named tuple and as a LineElement , MarkerElement, etc.

I’m not sure you need a recipe for that. I assumed you wanted to separate the series named tuples and the legend for fine control, but if they are supposed to be the same, you should be able to just do things like @jules suggested, e.g. following the tutorial on layouting, defining the series first and then reusing them for the legend:

mylines = lines!(ax, rand(Point2f0, 10); seriestype["A"]...) # Iwant the line here to be red and have a thickness of 0.3
myscatter = scatter!(ax, rand(Point2f0, 10); seriestype["B"]...) # I want the markers here to be stars etc...

and then use mylines and myscatter to define the legend via

leg = Legend(fig, [mylines, myscatter], ["A", "B"])
fig[2,1] = leg

Could you give this a try?

Yea, that’s one way, my problem with that though has been the fact that I plot multiple similar lines inside a for-loop, so it’s a pain to store the line-object outside the loop just for the sake of a legend. Importantly, the legend does not detail every single line plotted in the loop, just one of them (cause they all are the same “thing”). Let me know if you need to see a MWE to understand my pain-point.

1 Like

You can also do something like this:

f = Figure()

ax = Axis(f[1, 1])

for i in 1:100
    l = lines!(cumsum(randn(1000)), color = (:blue, 0.3))
    if i == 1
        l.label = "one line out of many"
    end
end

Legend(f[1, 2], ax)

f

I have actually thought about adding a unique = true option to this legend constructor, so you can label all your plots but only one line per label will be added. That saves you some if else logic in loops.

3 Likes

That looks pretty ideal. Yea, this saves me all sorts. Great way of doing it.

I’m using IterTools, so I can:

for (isfirst, x) in flagfirst(xs)
    l = lines!(cumsum(randn(1000)), color = (:blue, 0.3))
    if isfirst
        l.label = "one line out of many"
    end
end

But your unique would be even more convenient. Thanks a lot!!!

1 Like

@jules, I encountered a detail I thought would be worth mentioning:

As we discussed here, when plotting multiple lines, we might want to have a single legend entry as a “representative” for those lines.

One way to help the reader distinguish between the individual lines is to color them separately (e.g. from red to dark red, say with range(colorant"red", stop=colorant"black", length=5)[1:end-1]). This works very well when the number of lines is limited (impossible to differentiate more than say 10 lines with slightly darker shades), and especially well when the plot includes groups of multiple lines, where each group is colored differently (e.g. group 1 has 4 lines colored red to dark red, group 2 has 3 lines colored green to dark green, group 3 has 7 lines colored blue to dark blue, etc).

Your current method with assigning a label to the first line is problematic in that context: the legend marker will receive the color of the first (or whatever) line. It might have been better to have the color black for that legend marker. Especially if more groups (i.e. color ranges) are included.

I’m not necessarily suggesting what is best here, but only highlighting some aspects/possible issues. Maybe the best way here is to be able to deal with series/groups of lines or things in a more rigorous way. This might fall into the realms of the promising GitHub - JuliaPlots/AlgebraOfGraphics.jl: Combine ingredients for a plot

Yeah I never got into the weeds of what to do with continuous attributes of any kind when creating legends. Basically, all the convenience constructors only work for scalar attributes, as I can’t guess what is the best choice for a legend for all possible combinations of array attributes. So if you have a color range, you are a bit on your own :slight_smile: And that’s exactly what a meta-package like AlgebraOfGraphics is for

1 Like

Totally agree. I’m just gonna go ahead and ping @piever so he reads this if he likes. Pietro, I don’t mean for you to “solve” this, just to bring this to your attention. Thanks!

I also think that this type of subtle distinctions (categorical versus ordinal versus continuous, different kinds of groupings) should be handled outside of Makie. Legend and colorbar support is planned in the short term in “AlgebraOfGraphicsNext”: there’s already a WIP PR for legends Support Legends by greimel · Pull Request #32 · piever/SplitApplyPlot.jl (github.com)

(That being said, even with an opinionated approach like AlgebraOfGraphics it may well be that some usecases are very hard to cover.)

1 Like