diff --git a/src/arg_desc.jl b/src/arg_desc.jl index af85b46d..5ec9a4d6 100644 --- a/src/arg_desc.jl +++ b/src/arg_desc.jl @@ -14,6 +14,7 @@ const _arg_desc = KW( :fillcolor => "Color Type. Color of the filled area of path or bar types. `:match` will take the value from `:seriescolor`.", :fillalpha => "Number in [0,1]. The alpha/opacity override for the fill area. `nothing` (the default) means it will take the alpha value of fillcolor.", :markershape => "Symbol, Shape, or AbstractVector. Choose from $(_allMarkers).", + :fillstyle => "Symbol. Style of the fill area. `nothing` (the default) means solid fill. Choose from :/, :\\, :|, :-, :+, :x", :markercolor => "Color Type. Color of the interior of the marker or shape. `:match` will take the value from `:seriescolor`.", :markeralpha => "Number in [0,1]. The alpha/opacity override for the marker interior. `nothing` (the default) means it will take the alpha value of markercolor.", :markersize => "Number or AbstractVector. Size (radius pixels) of the markers", diff --git a/src/args.jl b/src/args.jl index f2eeb4c5..461c07f1 100644 --- a/src/args.jl +++ b/src/args.jl @@ -348,6 +348,7 @@ const _series_defaults = KW( :fillrange => nothing, # ribbons, areas, etc :fillcolor => :match, :fillalpha => nothing, + :fillstyle => nothing, :markershape => :none, :markercolor => :match, :markeralpha => nothing, @@ -1117,6 +1118,7 @@ function processLineArg(plotattributes::AKW, arg) arg.color == :auto ? :auto : plot_color(arg.color) ) arg.alpha === nothing || (plotattributes[:fillalpha] = arg.alpha) + arg.style === nothing || (plotattributes[:fillstyle] = arg.style) elseif typeof(arg) <: Arrow || arg in (:arrow, :arrows) plotattributes[:arrow] = arg @@ -1188,6 +1190,7 @@ function processFillArg(plotattributes::AKW, arg) arg.color == :auto ? :auto : plot_color(arg.color) ) arg.alpha === nothing || (plotattributes[:fillalpha] = arg.alpha) + arg.style === nothing || (plotattributes[:fillstyle] = arg.style) elseif typeof(arg) <: Bool plotattributes[:fillrange] = arg ? 0 : nothing diff --git a/src/backends/gr.jl b/src/backends/gr.jl index cdd15d5b..42dcffc3 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -136,6 +136,23 @@ gr_set_arrowstyle(s::Symbol) = GR.setarrowstyle( ), ) +gr_set_fillstyle(::Nothing) = GR.setfillintstyle(GR.INTSTYLE_SOLID) +function gr_set_fillstyle(s::Symbol) + GR.setfillintstyle(GR.INTSTYLE_HATCH) + GR.setfillstyle(get( + ( + (/) = 9, + (\) = 10, + (|) = 7, + (-) = 8, + (+) = 11, + (x) = 6, + ), + s, + 9), + ) +end + # -------------------------------------------------------------------------------------- # draw line segments, splitting x/y into contiguous/finite segments @@ -1058,7 +1075,9 @@ function gr_add_legend(sp, leg, viewport_plotarea) series[:ribbon] === nothing ) fc = get_fillcolor(series, clims) - gr_set_fill(fc) #, series[:fillalpha]) + gr_set_fill(fc) + fs = get_fillstyle(series, i) + gr_set_fillstyle(fs) l, r = xpos - leg.width_factor * 3.5, xpos - leg.width_factor / 2 b, t = ypos - 0.4 * leg.dy, ypos + 0.4 * leg.dy x = [l, r, r, l, l] @@ -1824,6 +1843,8 @@ function gr_draw_segments(series, x, y, fillrange, clims) i, rng = segment.attr_index, segment.range fc = get_fillcolor(series, clims, i) gr_set_fillcolor(fc) + fs = get_fillstyle(series, i) + gr_set_fillstyle(fs) fx = _cycle(x, vcat(rng, reverse(rng))) fy = vcat(_cycle(fr_from, rng), _cycle(fr_to, reverse(rng))) gr_set_transparency(fc, get_fillalpha(series, i)) @@ -1912,6 +1933,8 @@ function gr_draw_shapes(series, clims) # draw the interior fc = get_fillcolor(series, clims, i) gr_set_fill(fc) + fs = get_fillstyle(series, i) + gr_set_fillstyle(fs) gr_set_transparency(fc, get_fillalpha(series, i)) GR.fillarea(xseg, yseg) diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index d0d237d3..0df6a3a0 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -162,6 +162,9 @@ function py_fillstepstyle(seriestype::Symbol) return nothing end +py_fillstyle(::Nothing) = nothing +py_fillstyle(fillstyle::Symbol) = string(fillstyle) + # # untested... return a FontProperties object from a Plots.Font # function py_font(font::Font) # pyfont["FontProperties"]( @@ -709,24 +712,45 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) for segment in series_segments(series) i, rng = segment.attr_index, segment.range if length(rng) > 1 + lc = get_linecolor(series, clims, i) + la = get_linealpha(series, i) + ls = get_linestyle(series, i) + fc = get_fillcolor(series, clims, i) + fa = get_fillalpha(series, i) + fs = get_fillstyle(series, i) + has_fs = !isnothing(fs) + path = pypath."Path"(hcat(x[rng], y[rng])) + + # shape outline (and potentially solid fill) patches = pypatches."PathPatch"( path; label = series[:label], zorder = series[:series_plotindex], - edgecolor = py_color( - get_linecolor(series, clims, i), - get_linealpha(series, i), - ), - facecolor = py_color( - get_fillcolor(series, clims, i), - get_fillalpha(series, i), - ), + edgecolor = py_color(lc, la), + facecolor = py_color(fc, has_fs ? 0 : fa), linewidth = py_thickness_scale(plt, get_linewidth(series, i)), - linestyle = py_linestyle(st, get_linestyle(series, i)), - fill = true, + linestyle = py_linestyle(st, ls), + fill = !has_fs, ) push!(handle, ax."add_patch"(patches)) + + # shape hatched fill + # hatch color/alpha are controlled by edge (not face) color/alpha + if has_fs + patches = pypatches."PathPatch"( + path; + label = "", + zorder = series[:series_plotindex], + edgecolor = py_color(fc, fa), + facecolor = py_color(fc, 0), # don't fill with solid background + hatch = py_fillstyle(fs), + linewidth = 0, # don't replot shape outline (doesn't affect hatch linewidth) + linestyle = py_linestyle(st, ls), + fill = false, + ) + push!(handle, ax."add_patch"(patches)) + end end end push!(handles, handle) @@ -754,17 +778,24 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) dim1, _cycle(fillrange[1], rng), _cycle(fillrange[2], rng) end + la = get_linealpha(series, i) + fc = get_fillcolor(series, clims, i) + fa = get_fillalpha(series, i) + fs = get_fillstyle(series, i) + has_fs = !isnothing(fs) + handle = getproperty(ax, f)( args..., trues(n), false, py_fillstepstyle(st); zorder = series[:series_plotindex], - facecolor = py_color( - get_fillcolor(series, clims, i), - get_fillalpha(series, i), - ), - linewidths = 0, + # hatch color/alpha are controlled by edge (not face) color/alpha + # if has_fs, set edge color/alpha <- fill color/alpha and face alpha <- 0 + edgecolor = py_color(fc, has_fs ? fa : la), + facecolor = py_color(fc, has_fs ? 0 : fa), + hatch = py_fillstyle(fs), + linewidths = 0 ) push!(handles, handle) end @@ -1455,67 +1486,82 @@ function py_add_legend(plt::Plot, sp::Subplot, ax) if should_add_to_legend(series) clims = get_clims(sp, series) # add a line/marker and a label - push!( - handles, - if series[:seriestype] == :shape || series[:fillrange] !== nothing - pypatches."Patch"( - edgecolor = py_color( - single_color(get_linecolor(series, clims)), - get_linealpha(series), - ), - facecolor = py_color( - single_color(get_fillcolor(series, clims)), - get_fillalpha(series), - ), - linewidth = py_thickness_scale( - plt, - clamp(get_linewidth(series), 0, 5), - ), - linestyle = py_linestyle( - series[:seriestype], - get_linestyle(series), - ), + if series[:seriestype] == :shape || series[:fillrange] !== nothing + lc = get_linecolor(series, clims) + la = get_linealpha(series) + ls = get_linestyle(series) + fc = get_fillcolor(series, clims) + fa = get_fillalpha(series) + fs = get_fillstyle(series) + has_fs = !isnothing(fs) + + # line (and potentially solid fill) + line_handle = pypatches."Patch"( + edgecolor = py_color(single_color(lc), la), + facecolor = py_color(single_color(fc), has_fs ? 0 : fa), + linewidth = py_thickness_scale(plt, clamp(get_linewidth(series), 0, 5)), + linestyle = py_linestyle(series[:seriestype], ls), + capstyle = "butt", + ) + + # hatched fill + # hatch color/alpha are controlled by edge (not face) color/alpha + if has_fs + fill_handle = pypatches."Patch"( + edgecolor = py_color(single_color(fc), fa), + facecolor = py_color(single_color(fc), 0), # don't fill with solid background + hatch = py_fillstyle(fs), + linewidth = 0, # don't replot shape outline (doesn't affect hatch linewidth) + linestyle = py_linestyle(series[:seriestype], ls), capstyle = "butt", ) - elseif series[:seriestype] in - (:path, :straightline, :scatter, :steppre, :stepmid, :steppost) - hasline = get_linewidth(series) > 0 - PyPlot.plt."Line2D"( - (0, 1), - (0, 0), - color = py_color( - single_color(get_linecolor(series, clims)), - get_linealpha(series), - ), - linewidth = py_thickness_scale( - plt, - hasline * sp[:legendfontsize] / 8, - ), - linestyle = py_linestyle(:path, get_linestyle(series)), - solid_capstyle = "butt", - solid_joinstyle = "miter", - dash_capstyle = "butt", - dash_joinstyle = "miter", - marker = py_marker(_cycle(series[:markershape], 1)), - markersize = py_thickness_scale(plt, 0.8 * sp[:legendfontsize]), - markeredgecolor = py_color( - single_color(get_markerstrokecolor(series)), - get_markerstrokealpha(series), - ), - markerfacecolor = py_color( - single_color(get_markercolor(series, clims)), - get_markeralpha(series), - ), - markeredgewidth = py_thickness_scale( - plt, - 0.8 * get_markerstrokewidth(series) * sp[:legendfontsize] / - first(series[:markersize]), - ), # retain the markersize/markerstroke ratio from the markers on the plot - ) + + # plot two handles on top of each other by passing in a tuple + # https://matplotlib.org/stable/tutorials/intermediate/legend_guide.html + push!(handles, (line_handle, fill_handle)) else - series[:serieshandle][1] - end, - ) + # plot line handle (which includes solid fill) only + push!(handles, line_handle) + end + elseif series[:seriestype] in + (:path, :straightline, :scatter, :steppre, :stepmid, :steppost) + hasline = get_linewidth(series) > 0 + handle = PyPlot.plt."Line2D"( + (0, 1), + (0, 0), + color = py_color( + single_color(get_linecolor(series, clims)), + get_linealpha(series), + ), + linewidth = py_thickness_scale( + plt, + hasline * sp[:legendfontsize] / 8, + ), + linestyle = py_linestyle(:path, get_linestyle(series)), + solid_capstyle = "butt", + solid_joinstyle = "miter", + dash_capstyle = "butt", + dash_joinstyle = "miter", + marker = py_marker(_cycle(series[:markershape], 1)), + markersize = py_thickness_scale(plt, 0.8 * sp[:legendfontsize]), + markeredgecolor = py_color( + single_color(get_markerstrokecolor(series)), + get_markerstrokealpha(series), + ), + markerfacecolor = py_color( + single_color(get_markercolor(series, clims)), + get_markeralpha(series), + ), + markeredgewidth = py_thickness_scale( + plt, + 0.8 * get_markerstrokewidth(series) * sp[:legendfontsize] / + first(series[:markersize]), + ), # retain the markersize/markerstroke ratio from the markers on the plot + ) + push!(handles, handle) + else + push!(handles, series[:serieshandle][1]) + end push!(labels, series[:label]) end end diff --git a/src/utils.jl b/src/utils.jl index 244a14a0..0d69f3a5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -538,6 +538,7 @@ get_gradient(cp::ColorPalette) = cgrad(cp, categorical = true) get_linewidth(series, i::Int = 1) = _cycle(series[:linewidth], i) get_linestyle(series, i::Int = 1) = _cycle(series[:linestyle], i) +get_fillstyle(series, i::Int = 1) = _cycle(series[:fillstyle], i) function get_markerstrokecolor(series, i::Int = 1) msc = series[:markerstrokecolor] @@ -556,6 +557,7 @@ const _segmenting_vector_attributes = ( :linestyle, :fillcolor, :fillalpha, + :fillstyle, :markercolor, :markeralpha, :markersize,