When using the GR backend for Plots.jl, I noticed that for a series with a marker, the marker will not show up in the legend of a plot if the first entry in the series is NaN.
MWE:
using Plots; gr()
plot([NaN,2,3], markershape=:circle)
This issue happens specifically when the first value in the series is NaN. If we make only the second value NaN, the marker will appear in the legend (no plot attached because I am a new user and limited to only one image per topic):
using Plots; gr()
plot([1,NaN,3], markershape=:circle)
This issue does not occur in when using the PlotlyJS backend:
using Plots; plotlyjs()
plot([NaN,2,3], markershape=:circle)
Looking into the source code of the GR backend, I found the function gr_add_legend
in the file Plots/src/backends/gr.jl
. Lines 1110 through 1120 read:
if series[:markershape] !== :none
ms = first(series[:markersize])
msw = first(series[:markerstrokewidth])
s, sw = if ms > 0
0.8 * sp[:legend_font_pointsize],
0.8 * sp[:legend_font_pointsize] * msw / ms
else
0, 0.8 * sp[:legend_font_pointsize] * msw / 8
end
gr_draw_markers(series, xpos - leg.width_factor * 2, ypos, clims, s, sw)
end
After some debugging, I found that the issue is in gr_draw_markers
:
function gr_draw_markers(
series::Series,
x,
y,
clims,
msize = series[:markersize],
strokewidth = series[:markerstrokewidth],
)
isempty(x) && return
GR.setfillintstyle(GR.INTSTYLE_SOLID)
shapes = series[:markershape]
if shapes !== :none
for segment in series_segments(series, :scatter)
i = segment.attr_index
rng = intersect(eachindex(x), segment.range)
if !isempty(rng)
ms = get_thickness_scaling(series) * _cycle(msize, i)
msw = get_thickness_scaling(series) * _cycle(strokewidth, i)
shape = _cycle(shapes, i)
for j in rng
gr_draw_marker(
series,
_cycle(x, j),
_cycle(y, j),
clims,
i,
ms,
msw,
shape,
)
end
end
end
end
end
Namely, rng
will be empty, I think due to some of the work Julia does to avoid NaN values in the data, and the marker will not be drawn. But I think it would be more useful and appropriate to include the marker in the legend, since in this case it is more a property of the series than of each data point. I.e. this series of data is represented by a blue line whose markers are blue circles.
I replaced lines 1110 through 1120 with the following code:
if series[:markershape] !== :none
ms = first(series[:markersize])
msw = first(series[:markerstrokewidth])
shape = Plots._cycle(series[:markershape], 1)
s, sw = if ms > 0
0.8 * sp[:legend_font_pointsize],
0.8 * sp[:legend_font_pointsize] * msw / ms
else
0, 0.8 * sp[:legend_font_pointsize] * msw / 8
end
gr_draw_marker(series, xpos - leg.width_factor * 2, ypos, clims, 1, s, sw, shape)
end
Now the marker associated with the first entry of the series is drawn in the legend regardless of whether the first entry is NaN.
Re-running the MWE, the marker is drawn in the legend.
using Plots; gr()
plot([NaN,2,3], markershape=:circle)
It works for multiple marker shapes in the same series as well (e.g. when the arg markershape
is a vector of symbols):
using Plots; gr()
plot([1,2,3], markershape=[:circle, :square, :diamond])
The only case I have found where this change creates possibly confusing behavior is when a series has multiple marker shapes, but the first entry in the series is NaN:
using Plots; gr()
plot([NaN,2,3], markershape=[:circle, :square, :diamond])
The marker in the legend is a circle, although no circle marker appears in the series. Still, in the original version there would be no marker in the legend at all. Perhaps it would be best to draw the marker of the first NaN entry in the series in the legend, but personally I am satisfied with this implementation.
Any thoughts on this fix? Is there some context I am missing that makes the NaN-behavior a feature and not a bug?
Personally I have found that this behavior frequently made plotting frustrating. For example, when plotting the error of several numerical methods as the number of timesteps increased, the markers for each method would not show up in the legend because the methods were unstable for a small number of timesteps, and I had to do some workarounds to plot each series starting from the first NaN entry.
I put the revised repo on github at GitHub - leespen1/Plots.jl: Powerful convenience for Julia visualizations and data analysis, and I’ll submit a pull request if people think this is an appropriate change to the package.