diff --git a/src/args.jl b/src/args.jl index 68354760..6616dc1c 100644 --- a/src/args.jl +++ b/src/args.jl @@ -1450,18 +1450,18 @@ end # converts a symbol or string into a colorant (Colors.RGB), and assigns a color automatically -function getSeriesRGBColor(c, α, sp::Subplot, n::Int) +function getSeriesRGBColor(c, sp::Subplot, n::Int) if c == :auto c = autopick(sp[:color_palette], n) elseif isa(c, Int) c = autopick(sp[:color_palette], c) end - plot_color(c, α) + plot_color(c) end function ensure_gradient!(d::KW, csym::Symbol, asym::Symbol) if !isa(d[csym], ColorGradient) - d[csym] = cgrad(alpha = d[asym]) + d[csym] = typeof(d[asym]) <: AbstractVector ? cgrad() : cgrad(alpha = d[asym]) end end @@ -1508,21 +1508,21 @@ function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int) end # update series color - d[:seriescolor] = getSeriesRGBColor(d[:seriescolor], d[:seriesalpha], sp, plotIndex) + d[:seriescolor] = getSeriesRGBColor.(d[:seriescolor], sp, plotIndex) # update other colors for s in (:line, :marker, :fill) csym, asym = Symbol(s,:color), Symbol(s,:alpha) d[csym] = if d[csym] == :auto - plot_color(if has_black_border_for_default(d[:seriestype]) && s == :line + plot_color.(if has_black_border_for_default(d[:seriestype]) && s == :line sp[:foreground_color_subplot] else d[:seriescolor] - end, d[asym]) + end) elseif d[csym] == :match - plot_color(d[:seriescolor], d[asym]) + plot_color.(d[:seriescolor]) else - getSeriesRGBColor(d[csym], d[asym], sp, plotIndex) + getSeriesRGBColor.(d[csym], sp, plotIndex) end end @@ -1530,7 +1530,7 @@ function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int) d[:markerstrokecolor] = if d[:markerstrokecolor] == :match plot_color(sp[:foreground_color_subplot], d[:markerstrokealpha]) else - getSeriesRGBColor(d[:markerstrokecolor], d[:markerstrokealpha], sp, plotIndex) + getSeriesRGBColor(plot_color(d[:markerstrokecolor], d[:markerstrokealpha]), sp, plotIndex) end # if marker_z, fill_z or line_z are set, ensure we have a gradient diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 2ab89c5d..cf369f64 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -135,7 +135,7 @@ const gr_font_family = Dict( # -------------------------------------------------------------------------------------- function gr_getcolorind(c) - GR.settransparency(float(alpha(c))) + gr_set_transparency(float(alpha(c))) convert(Int, GR.inqcolorfromrgb(red(c), green(c), blue(c))) end @@ -143,6 +143,8 @@ gr_set_linecolor(c) = GR.setlinecolorind(gr_getcolorind(_cycle(c,1))) gr_set_fillcolor(c) = GR.setfillcolorind(gr_getcolorind(_cycle(c,1))) gr_set_markercolor(c) = GR.setmarkercolorind(gr_getcolorind(_cycle(c,1))) gr_set_textcolor(c) = GR.settextcolorind(gr_getcolorind(_cycle(c,1))) +gr_set_transparency(α::Real) = GR.settransparency(clamp(α, 0, 1)) +function gr_set_transparency(::Void) end # -------------------------------------------------------------------------------------- @@ -230,7 +232,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) #draw angular grid if xaxis[:grid] gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) - GR.settransparency(xaxis[:gridalpha]) + gr_set_transparency(xaxis[:gridalpha]) for i in 1:length(α) GR.polyline([sinf[i], 0], [cosf[i], 0]) end @@ -239,7 +241,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) #draw radial grid if yaxis[:grid] gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) - GR.settransparency(yaxis[:gridalpha]) + gr_set_transparency(yaxis[:gridalpha]) for i in 1:length(rtick_values) r = (rtick_values[i] - rmin) / (rmax - rmin) if r <= 1.0 && r >= 0.0 @@ -250,7 +252,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) end #prepare to draw ticks - GR.settransparency(1) + gr_set_transparency(1) GR.setlinecolorind(90) GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_HALF) @@ -319,9 +321,6 @@ function normalize_zvals(zv::AVec, clims::NTuple{2, <:Real}) end end -gr_alpha(α::Void) = 1 -gr_alpha(α::Real) = α - # --------------------------------------------------------- # draw ONE Shape @@ -369,7 +368,7 @@ function gr_draw_markers(series::Series, x, y, msize, mz) # pick a color from the pre-loaded gradient ci = round(Int, 1000 + _cycle(mz, i) * 255) cfuncind(ci) - GR.settransparency(_gr_gradient_alpha[ci-999]) + gr_set_transparency(_gr_gradient_alpha[ci-999]) end # don't draw filled area if marker shape is 1D if !(shape in (:hline, :vline, :+, :x)) @@ -794,21 +793,21 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if xaxis[:grid] gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) - GR.settransparency(xaxis[:gridalpha]) + gr_set_transparency(xaxis[:gridalpha]) GR.grid3d(xtick, 0, 0, xmin, ymax, zmin, 2, 0, 0) end if yaxis[:grid] gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) - GR.settransparency(yaxis[:gridalpha]) + gr_set_transparency(yaxis[:gridalpha]) GR.grid3d(0, ytick, 0, xmin, ymax, zmin, 0, 2, 0) end if zaxis[:grid] gr_set_line(zaxis[:gridlinewidth], zaxis[:gridstyle], zaxis[:foreground_color_grid]) - GR.settransparency(zaxis[:gridalpha]) + gr_set_transparency(zaxis[:gridalpha]) GR.grid3d(0, 0, ztick, xmin, ymax, zmin, 0, 0, 2) end gr_set_line(1, :solid, xaxis[:foreground_color_axis]) - GR.settransparency(1) + gr_set_transparency(1) GR.axes3d(xtick, 0, ztick, xmin, ymin, zmin, 2, 0, 2, -ticksize) GR.axes3d(0, ytick, 0, xmax, ymin, zmin, 0, 2, 0, ticksize) @@ -831,15 +830,15 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # gr_set_linecolor(sp[:foreground_color_grid]) # GR.grid(xtick, ytick, 0, 0, majorx, majory) gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) - GR.settransparency(xaxis[:gridalpha]) + gr_set_transparency(xaxis[:gridalpha]) gr_polyline(coords(xgrid_segs)...) end if yaxis[:grid] gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) - GR.settransparency(yaxis[:gridalpha]) + gr_set_transparency(yaxis[:gridalpha]) gr_polyline(coords(ygrid_segs)...) end - GR.settransparency(1.0) + gr_set_transparency(1.0) # axis lines if xaxis[:showaxis] @@ -858,7 +857,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if xaxis[:showaxis] if sp[:framestyle] in (:zerolines, :grid) gr_set_line(1, :solid, xaxis[:foreground_color_grid]) - GR.settransparency(xaxis[:gridalpha]) + gr_set_transparency(xaxis[:gridalpha]) else gr_set_line(1, :solid, xaxis[:foreground_color_axis]) end @@ -868,7 +867,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if yaxis[:showaxis] if sp[:framestyle] in (:zerolines, :grid) gr_set_line(1, :solid, yaxis[:foreground_color_grid]) - GR.settransparency(yaxis[:gridalpha]) + gr_set_transparency(yaxis[:gridalpha]) else gr_set_line(1, :solid, yaxis[:foreground_color_axis]) end @@ -920,10 +919,10 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) intensity = sp[:framestyle] == :semi ? 0.5 : 1.0 if sp[:framestyle] in (:box, :semi) gr_set_line(intensity, :solid, xaxis[:foreground_color_border]) - GR.settransparency(intensity) + gr_set_transparency(intensity) gr_polyline(coords(xborder_segs)...) gr_set_line(intensity, :solid, yaxis[:foreground_color_border]) - GR.settransparency(intensity) + gr_set_transparency(intensity) gr_polyline(coords(yborder_segs)...) end end @@ -1021,28 +1020,25 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if st in (:path, :scatter, :straightline) if length(x) > 1 lz = series[:line_z] - segments_iterator = if lz != nothing && length(lz) > 1 - [i:(i + 1) for i in 1:(length(x) - 1)] - else - iter_segments(x, y) - end + segments = iter_segments(series) # do area fill if frng != nothing GR.setfillintstyle(GR.INTSTYLE_SOLID) fr_from, fr_to = (is_2tuple(frng) ? frng : (y, frng)) - for (i, rng) in enumerate(segments_iterator) - gr_set_fillcolor(get_fillcolor(sp, series, i)) + for (i, rng) in enumerate(segments) + gr_set_fillcolor(get_fillcolor(series, i)) fx = _cycle(x, vcat(rng, reverse(rng))) fy = vcat(_cycle(fr_from,rng), _cycle(fr_to,reverse(rng))) - series[:fillalpha] != nothing && GR.settransparency(series[:fillalpha]) + gr_set_transparency(get_fillalpha(series, i)) GR.fillarea(fx, fy) end end # draw the line(s) if st in (:path, :straightline) - for (i, rng) in enumerate(segments_iterator) - gr_set_line(series[:linewidth], series[:linestyle], get_linecolor(sp, series, i)) #, series[:linealpha]) + for (i, rng) in enumerate(segments) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) #, series[:linealpha]) + gr_set_transparency(get_linealpha(series, i)) arrowside = isa(series[:arrow], Arrow) ? series[:arrow].side : :none gr_polyline(x[rng], y[rng]; arrowside = arrowside) end @@ -1068,8 +1064,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if series[:fillrange] != nothing GR.surface(x, y, z, GR.OPTION_CELL_ARRAY) else - GR.setlinetype(gr_linetype[series[:linestyle]]) - GR.setlinewidth(max(0, series[:linewidth] / (sum(gr_plot_size) * 0.001))) + GR.setlinetype(gr_linetype[get_linestyle(series)]) + GR.setlinewidth(max(0, get_linewidth(series) / (sum(gr_plot_size) * 0.001))) if plot_color(series[:linecolor]) == [plot_color(:black)] GR.contour(x, y, h, z, 0 + (series[:contour_labels] == true ? 1 : 0)) else @@ -1119,13 +1115,10 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if st == :path3d if length(x) > 1 lz = series[:line_z] - segments_iterator = if lz != nothing && length(lz) > 1 - [i:(i + 1) for i in 1:(length(x) - 1)] - else - iter_segments(x, y, z) - end - for (i, rng) in enumerate(segments_iterator) - gr_set_line(series[:linewidth], series[:linestyle], get_linecolor(sp, series, i)) #, series[:linealpha]) + segments = iter_segments(series) + for (i, rng) in enumerate(segments) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) #, series[:linealpha]) + gr_set_transparency(get_linealpha(series, i)) GR.polyline3d(x[rng], y[rng], z[rng]) end end @@ -1196,11 +1189,13 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) xseg, yseg = x[rng], y[rng] # draw the interior - gr_set_fill(get_fillcolor(sp, series, i)) + gr_set_fill(get_fillcolor(series, i)) + gr_set_transparency(get_fillalpha(series, i)) GR.fillarea(xseg, yseg) # draw the shapes - gr_set_line(series[:linewidth], :solid, get_linecolor(sp, series, i)) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) + gr_set_transparency(get_linealpha(series, i)) GR.polyline(xseg, yseg) end end @@ -1232,7 +1227,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # draw the colorbar if cmap && st != :contour # special colorbar with steps is drawn for contours gr_set_line(1, :solid, yaxis[:foreground_color_axis]) - GR.settransparency(1) + gr_set_transparency(1) gr_colorbar(sp, clims) end @@ -1279,29 +1274,30 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if sp[:legendtitle] != nothing GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_HALF) gr_set_textcolor(sp[:legendfontcolor]) - GR.settransparency(1) + gr_set_transparency(1) gr_text(xpos - 0.03 + 0.5*w, ypos, string(sp[:legendtitle])) ypos -= dy end for series in series_list(sp) should_add_to_legend(series) || continue st = series[:seriestype] - gr_set_line(series[:linewidth], series[:linestyle], get_linecolor(sp, series)) #, series[:linealpha]) + gr_set_line(get_linewidth(series), get_linestyle(series), get_linecolor(series)) #, series[:linealpha]) if (st == :shape || series[:fillrange] != nothing) && series[:ribbon] == nothing - gr_set_fill(get_fillcolor(sp, series)) #, series[:fillalpha]) + gr_set_fill(get_fillcolor(series)) #, series[:fillalpha]) l, r = xpos-0.07, xpos-0.01 b, t = ypos-0.4dy, ypos+0.4dy x = [l, r, r, l, l] y = [b, b, t, t, b] - GR.settransparency(gr_alpha(series[:fillalpha])) + gr_set_transparency(get_fillalpha(series)) gr_polyline(x, y, GR.fillarea) - GR.settransparency(gr_alpha(series[:linealpha])) + gr_set_transparency(get_linealpha(series)) + gr_set_line(get_linewidth(series), get_linestyle(series), get_linecolor(series)) st == :shape && gr_polyline(x, y) end if st in (:path, :straightline) - GR.settransparency(gr_alpha(series[:linealpha])) + gr_set_transparency(get_linealpha(series)) if series[:fillrange] == nothing || series[:ribbon] != nothing GR.polyline([xpos - 0.07, xpos - 0.01], [ypos, ypos]) else diff --git a/src/backends/pgfplots.jl b/src/backends/pgfplots.jl index bda7ac16..1dd2b272 100644 --- a/src/backends/pgfplots.jl +++ b/src/backends/pgfplots.jl @@ -27,7 +27,7 @@ const _pgfplots_attr = merge_with_base_supported([ :tickfont, :guidefont, :legendfont, :grid, :legend, :colorbar, - :marker_z, #:levels, + :fill_z, :line_z, :marker_z, #:levels, # :ribbon, :quiver, :arrow, # :orientation, # :overwrite_figure, @@ -148,33 +148,47 @@ function pgf_colormap(grad::ColorGradient) end,", ") end -function pgf_fillstyle(d::KW) - cstr,a = pgf_color(d[:fillcolor]) +function pgf_fillstyle(d, i = 1) + cstr,a = pgf_color(get_fillcolor(d, i)) + fa = get_fillalpha(d, i) + if fa != nothing + a = fa + end "fill = $cstr, fill opacity=$a" end -function pgf_linestyle(d::KW) - cstr,a = pgf_color(d[:linecolor]) +function pgf_linestyle(d, i = 1) + cstr,a = pgf_color(get_linecolor(d, i)) + la = get_linealpha(d, i) + if la != nothing + a = la + end """ color = $cstr, draw opacity=$a, - line width=$(d[:linewidth]), - $(get(_pgfplots_linestyles, d[:linestyle], "solid"))""" + line width=$(get_linewidth(d, i)), + $(get(_pgfplots_linestyles, get_linestyle(d, i), "solid"))""" end -function pgf_marker(d::KW) - shape = d[:markershape] - cstr, a = pgf_color(d[:markercolor]) - cstr_stroke, a_stroke = pgf_color(d[:markerstrokecolor]) +function pgf_marker(d, i = 1) + shape = _cycle(d[:markershape], i) + cstr, a = pgf_color(_cycle(d[:markercolor], i)) + if d[:markeralpha] != nothing + a = _cycle(d[:markeralpha], i) + end + cstr_stroke, a_stroke = pgf_color(_cycle(d[:markerstrokecolor], i)) + if d[:markerstrokealpha] != nothing + a_stroke = _cycle(d[:markerstrokealpha], i) + end """ mark = $(get(_pgfplots_markers, shape, "*")), - mark size = $(0.5 * d[:markersize]), + mark size = $(0.5 * _cycle(d[:markersize], i)), mark options = { color = $cstr_stroke, draw opacity = $a_stroke, fill = $cstr, fill opacity = $a, - line width = $(d[:markerstrokewidth]), + line width = $(_cycle(d[:markerstrokewidth], i)), rotate = $(shape == :dtriangle ? 180 : 0), - $(get(_pgfplots_linestyles, d[:markerstrokestyle], "solid")) + $(get(_pgfplots_linestyles, _cycle(d[:markerstrokestyle], i), "solid")) }""" end @@ -196,27 +210,10 @@ end function pgf_series(sp::Subplot, series::Series) d = series.d st = d[:seriestype] - style = [] - kw = KW() - push!(style, pgf_linestyle(d)) - push!(style, pgf_marker(d)) - - if d[:fillrange] != nothing || st in (:shape,) - push!(style, pgf_fillstyle(d)) - end - - # add to legend? - if sp[:legend] != :none && should_add_to_legend(series) - kw[:legendentry] = d[:label] - if st == :shape || d[:fillrange] != nothing - push!(style, "area legend") - end - else - push!(style, "forget plot") - end + series_collection = PGFPlots.Plot[] # function args - args = if st == :contour + args = if st == :contour d[:z].surf, d[:x], d[:y] elseif is3d(st) d[:x], d[:y], d[:z] @@ -241,31 +238,102 @@ function pgf_series(sp::Subplot, series::Series) else a end, args) - # for (i,a) in enumerate(args) - # if typeof(a) <: AbstractVector && typeof(a) != Vector - # args[i] = collect(a) - # end - # end - # include additional style, then add to the kw + if st in (:contour, :histogram2d) + style = [] + kw = KW() + push!(style, pgf_linestyle(d)) + push!(style, pgf_marker(d)) + push!(style, "forget plot") + + kw[:style] = join(style, ',') + func = if st == :histogram2d + PGFPlots.Histogram2 + else + PGFPlots.Contour + end + push!(series_collection, func(args...; kw...)) + + else + # series segments + segments = iter_segments(series) + for (i, rng) in enumerate(segments) + style = [] + kw = KW() + push!(style, pgf_linestyle(d, i)) + push!(style, pgf_marker(d, i)) + + if st == :shape + push!(style, pgf_fillstyle(d, i)) + end + + # add to legend? + if i == 1 && sp[:legend] != :none && should_add_to_legend(series) + kw[:legendentry] = d[:label] + if st == :shape # || d[:fillrange] != nothing + push!(style, "area legend") + end + else + push!(style, "forget plot") + end + + seg_args = (arg[rng] for arg in args) + + # include additional style, then add to the kw + if haskey(_pgf_series_extrastyle, st) + push!(style, _pgf_series_extrastyle[st]) + end + kw[:style] = join(style, ',') + + # add fillrange + if series[:fillrange] != nothing && st != :shape + push!(series_collection, pgf_fillrange_series(series, i, _cycle(series[:fillrange], rng), seg_args...)) + end + + # build/return the series object + func = if st == :path3d + PGFPlots.Linear3 + elseif st == :scatter + PGFPlots.Scatter + else + PGFPlots.Linear + end + push!(series_collection, func(seg_args...; kw...)) + end + end + series_collection +end + +function pgf_fillrange_series(series, i, fillrange, args...) + st = series[:seriestype] + style = [] + kw = KW() + push!(style, "line width = 0") + push!(style, "draw opacity = 0") + push!(style, pgf_fillstyle(series, i)) + push!(style, pgf_marker(series, i)) + push!(style, "forget plot") if haskey(_pgf_series_extrastyle, st) push!(style, _pgf_series_extrastyle[st]) end kw[:style] = join(style, ',') + func = is3d(series) ? PGFPlots.Linear3 : PGFPlots.Linear + return func(pgf_fillrange_args(fillrange, args...)...; kw...) +end - # build/return the series object - func = if st == :path3d - PGFPlots.Linear3 - elseif st == :scatter - PGFPlots.Scatter - elseif st == :histogram2d - PGFPlots.Histogram2 - elseif st == :contour - PGFPlots.Contour - else - PGFPlots.Linear - end - func(args...; kw...) +function pgf_fillrange_args(fillrange, x, y) + n = length(x) + x_fill = [x; x[n:-1:1]; x[1]] + y_fill = [y; _cycle(fillrange, n:-1:1); y[1]] + return x_fill, y_fill +end + +function pgf_fillrange_args(fillrange, x, y, z) + n = length(x) + x_fill = [x; x[n:-1:1]; x[1]] + y_fill = [y; y[n:-1:1]; x[1]] + z_fill = [z; _cycle(fillrange, n:-1:1); z[1]] + return x_fill, y_fill, z_fill end @@ -438,7 +506,7 @@ function _update_plot_object(plt::Plot{PGFPlotsBackend}) # As it is likely that all series within the same axis use the same # colormap this should not cause any problem. for series in series_list(sp) - for col in (:markercolor, :fillcolor) + for col in (:markercolor, :fillcolor, :linecolor) if typeof(series.d[col]) == ColorGradient push!(style,"colormap={plots}{$(pgf_colormap(series.d[col]))}") @@ -458,7 +526,7 @@ function _update_plot_object(plt::Plot{PGFPlotsBackend}) # add the series object to the PGFPlots.Axis for series in series_list(sp) - push!(o, pgf_series(sp, series)) + push!.(o, pgf_series(sp, series)) # add series annotations anns = series[:series_annotations] diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 392b73e0..9d7126b0 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -30,7 +30,7 @@ const _plotly_attr = merge_with_base_supported([ :tickfont, :guidefont, :legendfont, :grid, :gridalpha, :gridlinewidth, :legend, :colorbar, :colorbar_title, - :marker_z, :fill_z, :levels, + :marker_z, :fill_z, :line_z, :levels, :ribbon, :quiver, :orientation, # :overwrite_figure, @@ -543,23 +543,8 @@ function plotly_series(plt::Plot, series::Series) end # set the "type" - if st in (:path, :scatter, :scattergl, :straightline) - d_out[:type] = st==:scattergl ? "scattergl" : "scatter" - d_out[:mode] = if hasmarker - hasline ? "lines+markers" : "markers" - else - hasline ? "lines" : "none" - end - if series[:fillrange] == true || series[:fillrange] == 0 || isa(series[:fillrange], Tuple) - d_out[:fill] = "tozeroy" - d_out[:fillcolor] = rgba_string(series[:fillcolor]) - elseif typeof(series[:fillrange]) <: Union{AbstractVector{<:Real}, Real} - d_out[:fill] = "tonexty" - d_out[:fillcolor] = rgba_string(series[:fillcolor]) - elseif !(series[:fillrange] in (false, nothing)) - warn("fillrange ignored... plotly only supports filling to zero and to a vector of values. fillrange: $(series[:fillrange])") - end - d_out[:x], d_out[:y] = x, y + if st in (:path, :scatter, :scattergl, :straightline, :path3d, :scatter3d) + return plotly_series_segments(series, d_out, x, y, z) elseif st == :heatmap d_out[:type] = "heatmap" @@ -583,7 +568,7 @@ function plotly_series(plt::Plot, series::Series) d_out[:hidesurface] = true wirelines = KW( :show => true, - :color => rgba_string(series[:linecolor]), + :color => rgba_string(plot_color(series[:linecolor], series[:linealpha])), :highlightwidth => series[:linewidth], ) d_out[:contours] = KW(:x => wirelines, :y => wirelines, :z => wirelines) @@ -603,15 +588,6 @@ function plotly_series(plt::Plot, series::Series) d_out[:values] = y d_out[:hoverinfo] = "label+percent+name" - elseif st in (:path3d, :scatter3d) - d_out[:type] = "scatter3d" - d_out[:mode] = if hasmarker - hasline ? "lines+markers" : "markers" - else - hasline ? "lines" : "none" - end - d_out[:x], d_out[:y], d_out[:z] = x, y, z - else warn("Plotly: seriestype $st isn't supported.") return KW() @@ -646,61 +622,15 @@ function plotly_series(plt::Plot, series::Series) end end - # add "line" - if hasline - d_out[:line] = KW( - :color => rgba_string(series[:linecolor]), - :width => series[:linewidth], - :shape => if st == :steppre - "vh" - elseif st == :steppost - "hv" - else - "linear" - end, - :dash => string(series[:linestyle]), - # :dash => "solid", - ) - end - plotly_polar!(d_out, series) plotly_hover!(d_out, series[:hover]) - if hasfillrange - # if hasfillrange is true, return two dictionaries (one for original - # series, one for series being filled to) instead of one - d_out_fillrange = deepcopy(d_out) - d_out_fillrange[:showlegend] = false - # if fillrange is provided as real or tuple of real, expand to array - if typeof(series[:fillrange]) <: Real - series[:fillrange] = fill(series[:fillrange], length(series[:x])) - elseif typeof(series[:fillrange]) <: Tuple - f1 = typeof(series[:fillrange][1]) <: Real ? fill(series[:fillrange][1], length(series[:x])) : series[:fillrange][1] - f2 = typeof(series[:fillrange][2]) <: Real ? fill(series[:fillrange][2], length(series[:x])) : series[:fillrange][2] - series[:fillrange] = (f1, f2) - end - if isa(series[:fillrange], AbstractVector) - d_out_fillrange[:y] = series[:fillrange] - delete!(d_out_fillrange, :fill) - delete!(d_out_fillrange, :fillcolor) - else - # if fillrange is a tuple with upper and lower limit, d_out_fillrange - # is the series that will do the filling - d_out_fillrange[:x], d_out_fillrange[:y] = - concatenate_fillrange(series[:x], series[:fillrange]) - d_out_fillrange[:line][:width] = 0 - delete!(d_out, :fill) - delete!(d_out, :fillcolor) - end - - return [d_out_fillrange, d_out] - else - return [d_out] - end + return [d_out] end function plotly_series_shapes(plt::Plot, series::Series) - d_outs = [] + segments = iter_segments(series) + d_outs = Vector{KW}(length(segments)) # TODO: create a d_out for each polygon # x, y = series[:x], series[:y] @@ -714,7 +644,7 @@ function plotly_series_shapes(plt::Plot, series::Series) # base_d[:legendgroup] = series[:label] x, y = shape_data(series) - for (i,rng) in enumerate(iter_segments(x,y)) + for (i,rng) in enumerate(segments) length(rng) < 2 && continue # to draw polygons, we actually draw lines with fill @@ -724,23 +654,186 @@ function plotly_series_shapes(plt::Plot, series::Series) :x => vcat(x[rng], x[rng[1]]), :y => vcat(y[rng], y[rng[1]]), :fill => "tozeroy", - :fillcolor => rgba_string(_cycle(series[:fillcolor], i)), + :fillcolor => rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))), )) if series[:markerstrokewidth] > 0 d_out[:line] = KW( - :color => rgba_string(_cycle(series[:linecolor], i)), - :width => series[:linewidth], - :dash => string(series[:linestyle]), + :color => rgba_string(plot_color(get_linecolor(series, i), get_linealpha(series, i))), + :width => get_linewidth(series, i), + :dash => string(get_linestyle(series, i)), ) end d_out[:showlegend] = i==1 ? should_add_to_legend(series) : false plotly_polar!(d_out, series) plotly_hover!(d_out, _cycle(series[:hover], i)) - push!(d_outs, d_out) + d_outs[i] = d_out + end + if series[:fill_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, base_d, :line)) end d_outs end +function plotly_series_segments(series::Series, d_base::KW, x, y, z) + st = series[:seriestype] + sp = series[:subplot] + isscatter = st in (:scatter, :scatter3d, :scattergl) + hasmarker = isscatter || series[:markershape] != :none + hasline = st in (:path, :path3d, :straightline) + hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && + (isa(series[:fillrange], AbstractVector) || isa(series[:fillrange], Tuple)) + + segments = iter_segments(series) + d_outs = Vector{KW}((hasfillrange ? 2 : 1 ) * length(segments)) + + for (i,rng) in enumerate(segments) + length(rng) < 2 && continue + + d_out = deepcopy(d_base) + d_out[:showlegend] = i==1 ? should_add_to_legend(series) : false + + # set the type + if st in (:path, :scatter, :scattergl, :straightline) + d_out[:type] = st==:scattergl ? "scattergl" : "scatter" + d_out[:mode] = if hasmarker + hasline ? "lines+markers" : "markers" + else + hasline ? "lines" : "none" + end + if series[:fillrange] == true || series[:fillrange] == 0 || isa(series[:fillrange], Tuple) + d_out[:fill] = "tozeroy" + d_out[:fillcolor] = rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))) + elseif typeof(series[:fillrange]) <: Union{AbstractVector{<:Real}, Real} + d_out[:fill] = "tonexty" + d_out[:fillcolor] = rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))) + elseif !(series[:fillrange] in (false, nothing)) + warn("fillrange ignored... plotly only supports filling to zero and to a vector of values. fillrange: $(series[:fillrange])") + end + d_out[:x], d_out[:y] = x[rng], y[rng] + + elseif st in (:path3d, :scatter3d) + d_out[:type] = "scatter3d" + d_out[:mode] = if hasmarker + hasline ? "lines+markers" : "markers" + else + hasline ? "lines" : "none" + end + d_out[:x], d_out[:y], d_out[:z] = x[rng], y[rng], z[rng] + end + + # add "marker" + if hasmarker + d_out[:marker] = KW( + :symbol => get(_plotly_markers, series[:markershape], string(series[:markershape])), + # :opacity => series[:markeralpha], + :size => 2 * series[:markersize], + # :color => rgba_string(series[:markercolor]), + :line => KW( + :color => _cycle(rgba_string.(series[:markerstrokecolor]), eachindex(rng)), + :width => series[:markerstrokewidth], + ), + ) + + # gotta hack this (for now?) since plotly can't handle rgba values inside the gradient + if series[:marker_z] == nothing + d_out[:marker][:color] = _cycle(rgba_string.(series[:markercolor]), eachindex(rng)) + else + # grad = ColorGradient(series[:markercolor], alpha=series[:markeralpha]) + # grad = as_gradient(series[:markercolor], series[:markeralpha]) + cmin, cmax = get_clims(sp) + # zrange = zmax == zmin ? 1 : zmax - zmin # if all marker_z values are the same, plot all markers same color (avoids division by zero in next line) + d_out[:marker][:color] = [clamp(zi, cmin, cmax) for zi in _cycle(series[:marker_z], rng)] + d_out[:marker][:cmin] = cmin + d_out[:marker][:cmax] = cmax + d_out[:marker][:colorscale] = plotly_colorscale(series[:markercolor], series[:markeralpha]) + d_out[:marker][:showscale] = hascolorbar(sp) + end + end + + # add "line" + if hasline + d_out[:line] = KW( + :color => rgba_string(plot_color(get_linecolor(series, i), get_linealpha(series, i))), + :width => get_linewidth(series, i), + :shape => if st == :steppre + "vh" + elseif st == :steppost + "hv" + else + "linear" + end, + :dash => string(get_linestyle(series, i)), + ) + end + + plotly_polar!(d_out, series) + plotly_hover!(d_out, series[:hover]) + + if hasfillrange + # if hasfillrange is true, return two dictionaries (one for original + # series, one for series being filled to) instead of one + d_out_fillrange = deepcopy(d_out) + d_out_fillrange[:showlegend] = false + # if fillrange is provided as real or tuple of real, expand to array + if typeof(series[:fillrange]) <: Real + series[:fillrange] = fill(series[:fillrange], length(rng)) + elseif typeof(series[:fillrange]) <: Tuple + f1 = typeof(series[:fillrange][1]) <: Real ? fill(series[:fillrange][1], length(rng)) : series[:fillrange][1][rng] + f2 = typeof(series[:fillrange][2]) <: Real ? fill(series[:fillrange][2], length(rng)) : series[:fillrange][2][rng] + series[:fillrange] = (f1, f2) + end + if isa(series[:fillrange], AbstractVector) + d_out_fillrange[:y] = series[:fillrange] + delete!(d_out_fillrange, :fill) + delete!(d_out_fillrange, :fillcolor) + else + # if fillrange is a tuple with upper and lower limit, d_out_fillrange + # is the series that will do the filling + d_out_fillrange[:x], d_out_fillrange[:y] = + concatenate_fillrange(x[rng], series[:fillrange][rng]) + d_out_fillrange[:line][:width] = 0 + delete!(d_out, :fill) + delete!(d_out, :fillcolor) + end + + d_outs[(2 * i - 1):(2 * i)] = [d_out_fillrange, d_out] + else + d_outs[i] = d_out + end + end + + if series[:line_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :line)) + elseif series[:fill_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :fill)) + end + + d_outs +end + +function plotly_colorbar_hack(series::Series, d_base::KW, sym::Symbol) + d_out = deepcopy(d_base) + cmin, cmax = get_clims(series[:subplot]) + d_out[:showlegend] = false + d_out[:type] = is3d(series) ? :scatter3d : :scatter + d_out[:mode] = :markers + d_out[:x], d_out[:y] = [series[:x][1]], [series[:y][1]] + if is3d(series) + d_out[:z] = [series[:z][1]] + end + # zrange = zmax == zmin ? 1 : zmax - zmin # if all marker_z values are the same, plot all markers same color (avoids division by zero in next line) + d_out[:marker] = KW( + :size => 0, + :color => [0.5], + :cmin => cmin, + :cmax => cmax, + :colorscale => plotly_colorscale(series[Symbol("$(sym)color")], 1), + :showscale => hascolorbar(series[:subplot]), + ) + return d_out +end + + function plotly_polar!(d_out::KW, series::Series) # convert polar plots x/y to theta/radius if ispolar(series[:subplot]) diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index fe67ac7e..cdf63b46 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -136,6 +136,7 @@ py_color(s) = py_color(parse(Colorant, string(s))) py_color(c::Colorant) = (red(c), green(c), blue(c), alpha(c)) py_color(cs::AVec) = map(py_color, cs) py_color(grad::ColorGradient) = py_color(grad.colors) +py_color(c::Colorant, α) = py_color(plot_color(c, α)) function py_colormap(grad::ColorGradient) pyvals = [(z, py_color(grad[z])) for z in grad.values] @@ -492,61 +493,51 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # line plot if st in (:path, :path3d, :steppre, :steppost, :straightline) - if series[:linewidth] > 0 - if series[:line_z] == nothing - handle = ax[:plot](xyargs...; - label = series[:label], - zorder = series[:series_plotindex], - color = py_linecolor(series), - linewidth = py_dpi_scale(plt, series[:linewidth]), - linestyle = py_linestyle(st, series[:linestyle]), - solid_capstyle = "round", - drawstyle = py_stepstyle(st) - )[1] - push!(handles, handle) - - else - # multicolored line segments - n = length(x) - 1 - # segments = Array(Any,n) - segments = [] - kw = KW( - :label => series[:label], - :zorder => plt.n, - :cmap => py_linecolormap(series), - :linewidth => py_dpi_scale(plt, series[:linewidth]), - :linestyle => py_linestyle(st, series[:linestyle]), - :norm => pycolors["Normalize"](; extrakw...) - ) - lz = _cycle(series[:line_z], 1:n) - handle = if is3d(st) - for rng in iter_segments(x, y, z) - length(rng) < 2 && continue - for i in rng[1:end-1] - push!(segments, [(_cycle(x,i),_cycle(y,i),_cycle(z,i)), - (_cycle(x,i+1),_cycle(y,i+1),_cycle(z,i+1))]) - end - end - - lc = pyart3d["Line3DCollection"](segments; kw...) - lc[:set_array](lz) - ax[:add_collection3d](lc, zs=z) #, zdir='y') - lc - else - for rng in iter_segments(x, y) - length(rng) < 2 && continue - for i in rng[1:end-1] - push!(segments, [(_cycle(x,i),_cycle(y,i)), (_cycle(x,i+1),_cycle(y,i+1))]) - end - end - - lc = pycollections["LineCollection"](segments; kw...) - lc[:set_array](lz) - ax[:add_collection](lc) - lc + if maximum(series[:linewidth]) > 0 + segments = iter_segments(series) + # TODO: check LineCollection alternative for speed + # if length(segments) > 1 && (any(typeof(series[attr]) <: AbstractVector for attr in (:fillcolor, :fillalpha)) || series[:fill_z] != nothing) && !(typeof(series[:linestyle]) <: AbstractVector) + # # multicolored line segments + # n = length(segments) + # # segments = Array(Any,n) + # segments = [] + # kw = KW( + # :label => series[:label], + # :zorder => plt.n, + # :cmap => py_linecolormap(series), + # :linewidths => py_dpi_scale(plt, get_linewidth.(series, 1:n)), + # :linestyle => py_linestyle(st, get_linestyle.(series)), + # :norm => pycolors["Normalize"](; extrakw...) + # ) + # lz = _cycle(series[:line_z], 1:n) + # handle = if is3d(st) + # line_segments = [[(x[j], y[j], z[j]) for j in rng] for rng in segments] + # lc = pyart3d["Line3DCollection"](line_segments; kw...) + # lc[:set_array](lz) + # ax[:add_collection3d](lc, zs=z) #, zdir='y') + # lc + # else + # line_segments = [[(x[j], y[j]) for j in rng] for rng in segments] + # lc = pycollections["LineCollection"](line_segments; kw...) + # lc[:set_array](lz) + # ax[:add_collection](lc) + # lc + # end + # push!(handles, handle) + # else + for (i, rng) in enumerate(iter_segments(series)) + handle = ax[:plot]((arg[rng] for arg in xyargs)...; + label = i == 1 ? series[:label] : "", + zorder = series[:series_plotindex], + color = py_color(get_linecolor(series, i), get_linealpha(series, i)), + linewidth = py_dpi_scale(plt, get_linewidth(series, i)), + linestyle = py_linestyle(st, get_linestyle(series, i)), + solid_capstyle = "round", + drawstyle = py_stepstyle(st) + )[1] + push!(handles, handle) end - push!(handles, handle) - end + # end a = series[:arrow] if a != nothing && !is3d(st) # TODO: handle 3d later @@ -559,8 +550,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) :shrinkB => 0, :edgecolor => py_linecolor(series), :facecolor => py_linecolor(series), - :linewidth => py_dpi_scale(plt, series[:linewidth]), - :linestyle => py_linestyle(st, series[:linestyle]), + :linewidth => py_dpi_scale(plt, get_linewidth(series)), + :linestyle => py_linestyle(st, get_linestyle(series)), ) add_arrows(x, y) do xyprev, xy ax[:annotate]("", @@ -794,16 +785,17 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) if st == :shape handle = [] - for (i,rng) in enumerate(iter_segments(x, y)) + for (i, rng) in enumerate(iter_segments(series)) if length(rng) > 1 path = pypath["Path"](hcat(x[rng], y[rng])) patches = pypatches["PathPatch"]( path; label = series[:label], zorder = series[:series_plotindex], - edgecolor = py_color(_cycle(series[:linecolor], i)), - facecolor = py_color(_cycle(series[:fillcolor], i)), - linewidth = py_dpi_scale(plt, series[:linewidth]), + edgecolor = py_color(get_linecolor(series, i), get_linealpha(series, i)), + facecolor = py_color(get_fillcolor(series, i), get_fillalpha(series, i)), + linewidth = py_dpi_scale(plt, get_linewidth(series, i)), + linestyle = py_linestyle(st, get_linestyle(series, i)), fill = true ) push!(handle, ax[:add_patch](patches)) @@ -835,24 +827,26 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # handle area filling fillrange = series[:fillrange] if fillrange != nothing && st != :contour - f, dim1, dim2 = if isvertical(series) - :fill_between, x, y - else - :fill_betweenx, y, x - end - n = length(dim1) - args = if typeof(fillrange) <: Union{Real, AVec} - dim1, expand_data(fillrange, n), dim2 - elseif is_2tuple(fillrange) - dim1, expand_data(fillrange[1], n), expand_data(fillrange[2], n) - end + for (i, rng) in enumerate(iter_segments(series)) + f, dim1, dim2 = if isvertical(series) + :fill_between, x[rng], y[rng] + else + :fill_betweenx, y[rng], x[rng] + end + n = length(dim1) + args = if typeof(fillrange) <: Union{Real, AVec} + dim1, expand_data(fillrange, n), dim2 + elseif is_2tuple(fillrange) + dim1, expand_data(fillrange[1], n), expand_data(fillrange[2], n) + end - handle = ax[f](args..., trues(n), false, py_fillstepstyle(st); - zorder = series[:series_plotindex], - facecolor = py_fillcolor(series), - linewidths = 0 - ) - push!(handles, handle) + handle = ax[f](args..., trues(n), false, py_fillstepstyle(st); + zorder = series[:series_plotindex], + facecolor = py_color(get_fillcolor(series, i), get_fillalpha(series, i)), + linewidths = 0 + ) + push!(handles, handle) + end end # this is all we need to add the series_annotations text @@ -1020,6 +1014,13 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) kw[:ticks] = locator kw[:format] = formatter kw[:boundaries] = vcat(0, kw[:values] + 0.5) + elseif any(colorbar_series[attr] != nothing for attr in (:line_z, :fill_z)) + cmin, cmax = get_clims(sp) + norm = pycolors[:Normalize](vmin = cmin, vmax = cmax) + f = colorbar_series[:line_z] != nothing ? py_linecolormap : py_fillcolormap + cmap = pycmap[:ScalarMappable](norm = norm, cmap = f(colorbar_series)) + cmap[:set_array]([]) + handle = cmap end # create and store the colorbar object (handle) and the axis that it is drawn on. @@ -1250,15 +1251,16 @@ function py_add_legend(plt::Plot, sp::Subplot, ax) # add a line/marker and a label push!(handles, if series[:seriestype] == :shape || series[:fillrange] != nothing pypatches[:Patch]( - edgecolor = py_color(_cycle(series[:linecolor],1)), - facecolor = py_color(_cycle(series[:fillcolor],1)), - linewidth = py_dpi_scale(plt, clamp(series[:linewidth], 0, 5)), + edgecolor = py_color(get_linecolor(series), get_linealpha(series)), + facecolor = py_color(get_fillcolor(series), get_fillalpha(series)), + linewidth = py_dpi_scale(plt, clamp(get_linewidth(series), 0, 5)), + linestyle = py_linestyle(series[:seriestype], get_linestyle(series)) ) elseif series[:seriestype] in (:path, :straightline) PyPlot.plt[:Line2D]((0,1),(0,0), - color = py_color(_cycle(series[:linecolor],1)), - linewidth = py_dpi_scale(plt, clamp(series[:linewidth], 0, 5)), - linestyle = py_linestyle(:path,series[:linestyle]), + color = py_color(get_linecolor(series), get_linealpha(series)), + linewidth = py_dpi_scale(plt, clamp(get_linewidth(series), 0, 5)), + linestyle = py_linestyle(:path, get_linestyle(series)), marker = py_marker(series[:markershape]), markeredgecolor = py_markerstrokecolor(series), markerfacecolor = series[:marker_z] == nothing ? py_markercolor(series) : py_color(series[:markercolor][0.5]) diff --git a/src/utils.jl b/src/utils.jl index e8ded4ec..80d5487f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -192,6 +192,20 @@ function iter_segments(args...) SegmentsIterator(tup, n) end +function iter_segments(series::Series) + x, y, z = series[:x], series[:y], series[:z] + if has_attribute_segments(series) + return [i:(i + 1) for i in 1:(length(y) - 1)] + else + segs = UnitRange{Int64}[] + args = is3d(series) ? (x, y, z) : (x, y) + for seg in iter_segments(args...) + push!(segs, seg) + end + return segs + end +end + # helpers to figure out if there are NaN values in a list of array types anynan(i::Int, args::Tuple) = any(a -> !isfinite(_cycle(a,i)), args) anynan(istart::Int, iend::Int, args::Tuple) = any(i -> anynan(i, args), istart:iend) @@ -602,26 +616,38 @@ function hascolorbar(sp::Subplot) hascbar end -function get_linecolor(sp::Subplot, series::Series, i::Int = 1) +function get_linecolor(series, i::Int = 1) lc = series[:linecolor] lz = series[:line_z] if lz == nothing isa(lc, ColorGradient) ? lc : _cycle(lc, i) else - cmin, cmax = get_clims(sp) + cmin, cmax = get_clims(series[:subplot]) grad = isa(lc, ColorGradient) ? lc : cgrad() grad[clamp((_cycle(lz, i) - cmin) / (cmax - cmin), 0, 1)] end end -function get_fillcolor(sp::Subplot, series::Series, i::Int = 1) +function get_linealpha(series, i::Int = 1) + _cycle(series[:linealpha], i) +end + +function get_linewidth(series, i::Int = 1) + _cycle(series[:linewidth], i) +end + +function get_linestyle(series, i::Int = 1) + _cycle(series[:linestyle], i) +end + +function get_fillcolor(series, i::Int = 1) fc = series[:fillcolor] fz = series[:fill_z] lz = series[:line_z] if fz == nothing && lz == nothing isa(fc, ColorGradient) ? fc : _cycle(fc, i) else - cmin, cmax = get_clims(sp) + cmin, cmax = get_clims(series[:subplot]) grad = isa(fc, ColorGradient) ? fc : cgrad() if fz != nothing grad[clamp((_cycle(fz, i) - cmin) / (cmax - cmin), 0, 1)] @@ -631,6 +657,23 @@ function get_fillcolor(sp::Subplot, series::Series, i::Int = 1) end end +function get_fillalpha(series, i::Int = 1) + _cycle(series[:fillalpha], i) +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 + return any((typeof(series[attr]) <: AbstractVector && length(series[attr]) > 1) for attr in [:seriescolor, :seriesalpha, :linecolor, :linealpha, :linewidth, :fillcolor, :fillalpha]) || any(typeof(series[attr]) <: AbstractArray{<:Real} for attr in (:line_z, :fill_z)) +end + # --------------------------------------------------------------- makekw(; kw...) = KW(kw)