Fix for "segmented" attributes with NaNs
This commit is contained in:
parent
cd851df28a
commit
42b3c5625f
@ -842,6 +842,7 @@ const _pgfplotsx_marker = [
|
||||
:rtriangle,
|
||||
:cross,
|
||||
:xcross,
|
||||
:x,
|
||||
:star5,
|
||||
:pentagon,
|
||||
:hline,
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
54
src/utils.jl
54
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
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user