From 42b3c5625f0de2c1012d0478ca007e83a51284a9 Mon Sep 17 00:00:00 2001 From: yha Date: Thu, 25 Feb 2021 02:33:11 +0200 Subject: [PATCH] Fix for "segmented" attributes with NaNs --- src/backends.jl | 1 + src/backends/gr.jl | 25 ++++++++++-------- src/backends/pgfplotsx.jl | 5 ++-- src/backends/plotly.jl | 17 +++++++----- src/backends/pyplot.jl | 14 ++++++---- src/examples.jl | 17 ++++++++++-- src/utils.jl | 54 +++++++++++++++++++-------------------- test/runtests.jl | 10 ++------ 8 files changed, 82 insertions(+), 61 deletions(-) diff --git a/src/backends.jl b/src/backends.jl index bd280f0f..6164441d 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -842,6 +842,7 @@ const _pgfplotsx_marker = [ :rtriangle, :cross, :xcross, + :x, :star5, :pentagon, :hline, diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 56693c00..1cc51624 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -1561,7 +1561,7 @@ function gr_add_series(sp, series) gr_draw_markers(series, x, y, clims) end elseif st === :shape - gr_draw_shapes(series, x, y, clims) + gr_draw_shapes(series, clims) elseif st in (:path3d, :scatter3d) gr_draw_segments_3d(series, x, y, z, clims) if st === :scatter3d || series[:markershape] !== :none @@ -1613,12 +1613,13 @@ end function gr_draw_segments(series, x, y, fillrange, clims) st = series[:seriestype] if x !== nothing && length(x) > 1 - segments = iter_segments(series, st) + segments = series_segments(series, st) # do area fill if fillrange !== nothing GR.setfillintstyle(GR.INTSTYLE_SOLID) fr_from, fr_to = (is_2tuple(fillrange) ? fillrange : (y, fillrange)) - for (i, rng) in enumerate(segments) + for segment in segments + i, rng = segment.attr_index, segment.range fc = get_fillcolor(series, clims, i) gr_set_fillcolor(fc) fx = _cycle(x, vcat(rng, reverse(rng))) @@ -1630,7 +1631,8 @@ function gr_draw_segments(series, x, y, fillrange, clims) # draw the line(s) if st in (:path, :straightline) - for (i, rng) in enumerate(segments) + for segment in segments + i, rng = segment.attr_index, segment.range lc = get_linecolor(series, clims, i) gr_set_line( get_linewidth(series, i), get_linestyle(series, i), lc, series @@ -1648,8 +1650,9 @@ end function gr_draw_segments_3d(series, x, y, z, clims) if series[:seriestype] === :path3d && length(x) > 1 lz = series[:line_z] - segments = iter_segments(series, :path3d) - for (i, rng) in enumerate(segments) + segments = series_segments(series, :path3d) + for segment in segments + i, rng = segment.attr_index, segment.range lc = get_linecolor(series, clims, i) gr_set_line( get_linewidth(series, i), get_linestyle(series, i), lc, series @@ -1674,8 +1677,9 @@ function gr_draw_markers( shapes = series[:markershape] if shapes != :none - for (i, rng) in enumerate(iter_segments(series, :scatter)) - rng = intersect(eachindex(x), rng) + 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) @@ -1688,9 +1692,10 @@ function gr_draw_markers( end end -function gr_draw_shapes(series, x, y, clims) +function gr_draw_shapes(series, clims) x, y = shape_data(series) - for (i,rng) in enumerate(iter_segments(x, y)) + for segment in series_segments(series, :shape) + i, rng = segment.attr_index, segment.range if length(rng) > 1 # connect to the beginning rng = vcat(rng, rng[1]) diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl index f2e09073..a34c0065 100644 --- a/src/backends/pgfplotsx.jl +++ b/src/backends/pgfplotsx.jl @@ -339,9 +339,10 @@ end function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, opt) # treat segments - segments = iter_segments(series, series[:seriestype]) + segments = collect(series_segments(series, series[:seriestype])) sf = opt[:fillrange] - for (i, rng) in enumerate(segments) + for segment in segments + i, rng = segment.attr_index, segment.range segment_opt = PGFPlotsX.Options() segment_opt = merge(segment_opt, pgfx_linestyle(opt, i)) if opt[:markershape] != :none diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index ce2b704c..597413f2 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -643,7 +643,7 @@ function plotly_series(plt::Plot, series::Series) end function plotly_series_shapes(plt::Plot, series::Series, clims) - segments = iter_segments(series) + segments = collect(series_segments(series)) plotattributes_outs = Vector{KW}(undef, length(segments)) # TODO: create a plotattributes_out for each polygon @@ -662,7 +662,8 @@ function plotly_series_shapes(plt::Plot, series::Series, clims) for (letter, data) in zip((:x, :y), shape_data(series, 100)) ) - for (i,rng) in enumerate(segments) + for segment in segments + i, rng = segment.attr_index, segment.range length(rng) < 2 && continue # to draw polygons, we actually draw lines with fill @@ -705,14 +706,16 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && (isa(series[:fillrange], AbstractVector) || isa(series[:fillrange], Tuple)) - segments = iter_segments(series, st) + segments = collect(series_segments(series, st)) plotattributes_outs = fill(KW(), (hasfillrange ? 2 : 1 ) * length(segments)) needs_scatter_fix = !isscatter && hasmarker && !any(isnan,y) && length(segments) > 1 - for (i,rng) in enumerate(segments) + for (k, segment) in enumerate(segments) + i, rng = segment.attr_index, segment.range + plotattributes_out = deepcopy(plotattributes_base) - plotattributes_out[:showlegend] = i==1 ? should_add_to_legend(series) : false + plotattributes_out[:showlegend] = k==1 ? should_add_to_legend(series) : false plotattributes_out[:legendgroup] = series[:label] # set the type @@ -805,9 +808,9 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z delete!(plotattributes_out, :fillcolor) end - plotattributes_outs[(2 * i - 1):(2 * i)] = [plotattributes_out_fillrange, plotattributes_out] + plotattributes_outs[(2k-1):(2k)] = [plotattributes_out_fillrange, plotattributes_out] else - plotattributes_outs[i] = plotattributes_out + plotattributes_outs[k] = plotattributes_out end end diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 83899620..4c8ed18a 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -440,9 +440,10 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # end # push!(handles, handle) # else - for (i, rng) in enumerate(iter_segments(series, st)) + for (k, segment) in enumerate(series_segments(series, st)) + i, rng = segment.attr_index, segment.range handle = ax."plot"((arg[rng] for arg in xyargs)...; - label = i == 1 ? series[:label] : "", + label = k == 1 ? series[:label] : "", zorder = series[:series_plotindex], color = py_color(single_color(get_linecolor(series, clims, i)), get_linealpha(series, i)), linewidth = py_thickness_scale(plt, get_linewidth(series, i)), @@ -486,7 +487,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) if series[:markershape] != :none && st in ( :path, :scatter, :path3d, :scatter3d, :steppre, :steppost, :bar ) - for (i, rng) in enumerate(iter_segments(series, :scatter)) + for segment in series_segments(series, :scatter) + i, rng = segment.attr_index, segment.range xyargs = if st == :bar && !isvertical(series) if RecipesPipeline.is3d(sp) y[rng], x[rng], z[rng] @@ -679,7 +681,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) if st == :shape handle = [] - for (i, rng) in enumerate(iter_segments(series)) + for segment in series_segments(series) + i, rng = segment.attr_index, segment.range if length(rng) > 1 path = pypath."Path"(hcat(x[rng], y[rng])) patches = pypatches."PathPatch"( @@ -706,7 +709,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # handle area filling fillrange = series[:fillrange] if fillrange !== nothing && st != :contour - for (i, rng) in enumerate(iter_segments(series)) + for segment in series_segments(series) + i, rng = segment.attr_index, segment.range f, dim1, dim2 = if isvertical(series) :fill_between, x[rng], y[rng] else diff --git a/src/examples.jl b/src/examples.jl index b81f5789..1d5daecc 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -1031,13 +1031,26 @@ const _examples = PlotExample[ [quote yv = ones(9) ys = [1; 1; NaN; ones(6)] - plot( - 5 .- [yv 2ys 3yv 4ys], + y = 5 .- [yv 2ys 3yv 4ys] + + plt_color_rows = plot( + y, seriestype = [:path :path :scatter :scatter], markershape = [:utriangle, :rect], markersize = 8, color = [:red, :black], ) + + plt_z_cols = plot( + y, + markershape = [:utriangle :x :circle :square], + markersize = [5 10 10 5], + marker_z = [5 4 3 2], + line_z = [1 3 3 1], + linewidth = [1 10 5 1] + ) + + plot(plt_color_rows, plt_z_cols) end] ), PlotExample( # 49 diff --git a/src/utils.jl b/src/utils.jl index fc85923a..67fe5d61 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -53,10 +53,16 @@ function Base.push!(segments::Segments{T}, vs::AVec) where T end +struct SeriesSegment + # indexes of this segement in series data vectors + range::UnitRange + # index into vector-valued attributes corresponding to this segment + attr_index::Int +end + # ----------------------------------------------------- # helper to manage NaN-separated segments - -mutable struct SegmentsIterator +struct NaNSegmentsIterator args::Tuple n1::Int n2::Int @@ -66,28 +72,26 @@ function iter_segments(args...) tup = Plots.wraptuple(args) n1 = minimum(map(firstindex, tup)) n2 = maximum(map(lastindex, tup)) - SegmentsIterator(tup, n1, n2) + NaNSegmentsIterator(tup, n1, n2) end -function iter_segments(series::Series, seriestype::Symbol = :path) +function series_segments(series::Series, seriestype::Symbol = :path) x, y, z = series[:x], series[:y], series[:z] - if x === nothing - return UnitRange{Int}[] - elseif has_attribute_segments(series) - if any(isnan,y) - return [iter_segments(y)...] - elseif seriestype in (:scatter, :scatter3d) - return [[i] for i in eachindex(y)] - else - return [i:(i + 1) for i in firstindex(y):lastindex(y)-1] - end + x === nothing && return UnitRange{Int}[] + + args = RecipesPipeline.is3d(series) ? (x, y, z) : (x, y) + nan_segments = collect(iter_segments(args...)) + + if has_attribute_segments(series) + return Iterators.flatten(map(nan_segments) do r + if seriestype in (:scatter, :scatter3d) + (SeriesSegment(i:i, i) for i in r) + else + (SeriesSegment(i:i+1, i) for i in first(r):last(r)-1) + end + end) else - segs = UnitRange{Int}[] - args = RecipesPipeline.is3d(series) ? (x, y, z) : (x, y) - for seg in iter_segments(args...) - push!(segs, seg) - end - return segs + return (SeriesSegment(r, 1) for r in nan_segments) end end @@ -97,7 +101,7 @@ anynan(args::Tuple) = i -> anynan(i,args) anynan(istart::Int, iend::Int, args::Tuple) = any(anynan(args), istart:iend) allnan(istart::Int, iend::Int, args::Tuple) = all(anynan(args), istart:iend) -function Base.iterate(itr::SegmentsIterator, nextidx::Int = itr.n1) +function Base.iterate(itr::NaNSegmentsIterator, nextidx::Int = itr.n1) i = findfirst(!anynan(itr.args), nextidx:itr.n2) i === nothing && return nothing nextval = nextidx + i - 1 @@ -107,6 +111,7 @@ function Base.iterate(itr::SegmentsIterator, nextidx::Int = itr.n1) nextval:nextnan-1, nextnan end +Base.IteratorSize(::NaNSegmentsIterator) = Base.SizeUnknown() # Find minimal type that can contain NaN and x # To allow use of NaN separated segments with categorical x axis @@ -575,13 +580,8 @@ end function has_attribute_segments(series::Series) # we want to check if a series needs to be split into segments just because # of its attributes - for letter in (:x, :y, :z) - # If we have NaNs in the data they define the segments and - # SegmentsIterator is used - series[letter] !== nothing && NaN in collect(series[letter]) && return false - end series[:seriestype] == :shape && return false - # ... else we check relevant attributes if they have multiple inputs + # check relevant attributes if they have multiple inputs return any( (typeof(series[attr]) <: AbstractVector && length(series[attr]) > 1) for diff --git a/test/runtests.jl b/test/runtests.jl index 09ec3a31..26797405 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -175,14 +175,8 @@ end @test_throws ArgumentError gif(anim) end -@testset "Segments" begin - function segments(args...) - segs = UnitRange{Int}[] - for seg in iter_segments(args...) - push!(segs,seg) - end - segs - end +@testset "NaN-separated Segments" begin + segments(args...) = collect(iter_segments(args...)) nan10 = fill(NaN,10) @test segments(11:20) == [1:10]