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.