Fix for "segmented" attributes with NaNs

This commit is contained in:
yha 2021-02-25 02:33:11 +02:00
parent cd851df28a
commit 42b3c5625f
8 changed files with 82 additions and 61 deletions

View File

@ -842,6 +842,7 @@ const _pgfplotsx_marker = [
:rtriangle,
:cross,
:xcross,
:x,
:star5,
:pentagon,
:hline,

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]