Fixing missing marker in plot legend when first value in series is NaN

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)

NaN_legend_marker_issue

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.

2 Likes

Thanks for the PR Fixed missing marker in legend when plotting a series whose first entry is NaN by leespen1 · Pull Request #4328 · JuliaPlots/Plots.jl · GitHub.

We have some trouble fixing Plots CI since a few days, but it’s close to being resolved.
EDIT: fixed.

Once that is settled, we can review your contribution :+1: .

1 Like

Awesome, thanks!