Odd behaviour of custom labels in Plot recipes

Hello everyone,

I am very new to this forum as this is my first question here. I try to follow the guidelines for posting questions, but please point out any mistakes I do in this regard.

To my question: I want to plot a custom type which has fields times (a vector of numbers) and data, which is a Vector whose elements are either vectors or vectors of vectors. My goal is to have times on the x-axis, have a plotline with a custom label for each (nested) element in data. However, doing this by using @recipe and @series somehow mixes up the order of my labels (and I really don’t understand why).
Here is the code of my minimal working example:

using Plots

struct MyData
    times
    data
end

@recipe function f(mydata::MyData)
    for (i,d) in enumerate(mydata.data)
        @series begin
            i==1 && (label --> "Constant")
            i==2 && (label --> ["Straight" "Small Random" "Big Random"])
            mydata.times, d
        end
    end
end
times = 1:10
vec = fill(2, 10)
vecofvec = [1:10, rand(10), 10*rand(10)]
mydata = MyData(times, [vec, vecofvec])
plot(mydata)

Which results in this plot

As you can see the order of the labels is mixed up compared to what I want. The first label is still correctly labelling the constant vector vec, but the labels for vecofvec are set off by one.

I figured out that changing vec to a vector of vectors of length two leads to a ‘label offset’ of two and that changing --> to := does not help. However, putting the labels in the function call like

plot(mydata, label=["Constant" "Straight" "Small Random" "Big Random"])

works (but this is not what I want in the end!).

I would be very grateful if someone could explain to me why this happens and how I can circumvent it.

Hello, please check the code below which seems to work fine.

using Plots

struct MyData
    times
    data
end

@recipe function f(mydata::MyData)
    labels = ["Constant" "Straight" "Small Random" "Big Random"]
    label --> labels
    mydata.times, mydata.data
end

times = 1:10
vc = fill(2, 10)
vecofvec = [1:10, rand(10), 10*rand(10)]
mydata = MyData(times, [vc, vecofvec])
plot(mydata)

Thank you for the very quick answer; your code indeed does work. However, my actual use case is more complicated as the labels are created by external functions and data transformations performed. Additionally I would like to flexibly change the series type of single data entries.
So I think I would have more flexibility with the @series construction.

Also for more complicated recipes I would like to understand why my example doesn’t work as expected.

Can you help me with that?

I don’t know why, but it seems that the labels are issued in reverse order compared to the series.

Just changing this line in your recipe to iterate in reverse, seems to fix it:

for (i,d) in Iterators.reverse(pairs(mydata.data))
...

Hmm … this seems to work in this case, because the labels of vecofvec offset the labels of vc by three, but that doesn’t matter since the length of the labels for vc is one. So nothing is changed.

In a little more complex example like below the ‘offset’ appears again

@recipe function f(mydata::MyData)
    for (i,d) in Iterators.reverse(pairs(mydata.data))
        @series begin
            i==1 && (label --> ["Constant = 2" "Big Random"])
            i==2 && (label --> ["Straight" "Small Random" "Constant = 10"])
            mydata.times, d
        end
    end
end
times = 1:10
vecofvec1 = [fill(2, 10), 10*rand(10)]
vecofvec2 = [1:10, rand(10), fill(10, 10)]
mydata = MyData(1:10, [vecofvec1, vecofvec2])

Note that it works if one changes vecofvec2 to a vector of only two vectors, since the ‘offset’ due to the vecofvec labels would ‘iterate’ the labels of vecofvec2 to its beginning again.

I think you should preprocess the labels first and then call the @series macro. Try this:

@recipe function f(mydata::MyData)
    labels = String[]
    for (i,d) in pairs(mydata.data)
        i==1 && push!(labels, "Constant = 2", "Big Random")
        i==2 && push!(labels, "Straight", "Small, Random", "Constant = 10")
    end
    @series begin
        label --> permutedims(labels)
        mydata.times, mydata.data
    end
end
1 Like

Yes, thank you , this works well. Event though I still believe there should be a nicer way to do this. Especially since (what I just figured out) this behaviour does not only affect labels but all plotattributes that I want to set in @series (so for example linewidth). Of course I can preprocess all of them in the way you showed, but there seems to be something I conceptually don’t understand about this macro.

But this is a nice workaround, so thanks!

Just to explain why this happens:
Plots sees 4 series and a 3 element label vector and thus cycles the vector to match the 4 series, so you get ["Straight" "Small Random" "Big Random" "Straight"] and then it takes the values 2:4 from that vector and that gives you the order you are seeing.

3 Likes

In any case, it seems that simplifying the data structure input to the @series macro is helpful here.
Say converting Vector{Vector{AbstractVector}} to Vector{AbstractVector}

I’d probably write this as

using Plots

struct MyData
    times
    data
end

@recipe function f(mydata::MyData)
    @series begin
        label := "Constant"
        mydata.times, first(mydata.data)
    end
    for (d, l) in zip(mydata.data[2],["Straight" "Small Random" "Big Random"])
        @series begin
            label := l
            mydata.times, d
        end
    end
end
times = 1:10
vec = fill(2, 10)
vecofvec = [1:10, rand(10), 10*rand(10)]
mydata = MyData(times, [vec, vecofvec])
plot(mydata)
1 Like

Thanks for the answer, but I still don’t quite understand. I thought the @series macro works by making a kind of local copy of the plotattributes and alters them individually for each series. Then it takes the seriesdata provided and basically appends the resulting series to the plot (kind of like plots!).
So I believed that for i==2 I more or less write

plot!(times, vecofvec, label = ["Straight" "Small Random" "Big Random"]

Apparently that’s not the case.
So when you say that plots sees 4 series I guess you mean the one series from vec and three from vecofvec but why does it then not see a 4 element label vector ["Constant" "Straight" "Small Random" "Big Random"]? I guess that has something to do with the fact that I change the labels during each series?

I think your expectation is reasonable. Its just that you can do stuff in recipes, that you can’t through the plot/plot! interface like in this example (you’d need two calls).
So this wasn’t considered up to this point and it might be fixable, but until that you’d need to take one of the other routes.