diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml index b6044f31..8bd71439 100644 --- a/.github/workflows/format_pr.yml +++ b/.github/workflows/format_pr.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - name: Install JuliaFormatter and format run: | - julia -e 'import Pkg; pkg"add JuliaFormatter CSTParser#master"' + julia -e 'using Pkg; pkg"add JuliaFormatter CSTParser#master"' julia -e 'using JuliaFormatter; [format(["src", "test"]) for _ in 1:2]' git diff --exit-code diff --git a/Project.toml b/Project.toml index a8c9360e..c233c068 100644 --- a/Project.toml +++ b/Project.toml @@ -31,6 +31,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" [compat] Contour = "0.5" diff --git a/src/Plots.jl b/src/Plots.jl index bb0ea855..cea12a03 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -31,6 +31,7 @@ using Base.Meta @reexport using PlotThemes import Showoff import StatsBase +import Downloads import JSON using Requires 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/gaston.jl b/src/backends/gaston.jl index 223ce4b7..cd0ccc4a 100644 --- a/src/backends/gaston.jl +++ b/src/backends/gaston.jl @@ -225,13 +225,13 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) gsp = sp.o x, y, z = series[:x], series[:y], series[:z] st = series[:seriestype] - curves = [] if gsp.dims == 2 && z === nothing for (n, seg) in enumerate(series_segments(series, st; check = true)) i, rng = seg.attr_index, seg.range + fr =_cycle(series[:fillrange], 1:length(x[rng])) for sc in gaston_seriesconf!(sp, series, i, n == 1) - push!(curves, Gaston.Curve(x[rng], y[rng], nothing, nothing, sc)) + push!(curves, Gaston.Curve(x[rng], y[rng], nothing, fr, sc)) end end else @@ -303,9 +303,13 @@ function gaston_seriesconf!( lc, dt, lw = gaston_lc_ls_lw(series, clims, i) pt, ps, mc = gaston_mk_ms_mc(series, clims, i) push!(curveconf, "w points pt $pt ps $ps lc $mc") - elseif st ∈ (:path, :straightline, :path3d) + elseif st ∈ (:path, :straightline, :path3d) + fr = series[:fillrange] + fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i)) lc, dt, lw = gaston_lc_ls_lw(series, clims, i) - if series[:markershape] == :none # simplepath + if fr !== nothing # filled curves, but not filled curves with markers + push!(curveconf, "w filledcurves fc $fc fs solid border lc $lc lw $lw dt $dt,'' w lines lc $lc lw $lw dt $dt") + elseif series[:markershape] == :none # simplepath push!(curveconf, "w lines lc $lc dt $dt lw $lw") else pt, ps, mc = gaston_mk_ms_mc(series, clims, i) 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/examples.jl b/src/examples.jl index 6cd1e9b3..0f740b32 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -125,7 +125,8 @@ const _examples = PlotExample[ :( begin import FileIO - path = download( + import Downloads + path = Downloads.download( "http://juliaplots.org/PlotReferenceImages.jl/Plots/pyplot/0.7.0/ref1.png", ) img = FileIO.load(path) diff --git a/src/init.jl b/src/init.jl index 8f8303a6..b1da67f2 100644 --- a/src/init.jl +++ b/src/init.jl @@ -98,7 +98,7 @@ function __init__() global plotly_local_file_path[] = joinpath(@get_scratch!("plotly"), _plotly_min_js_filename) if !isfile(plotly_local_file_path[]) - download( + Downloads.download( "https://cdn.plot.ly/$(_plotly_min_js_filename)", plotly_local_file_path[], ) 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, diff --git a/test/runtests.jl b/test/runtests.jl index 7e4150ff..fa8d9099 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,6 +12,11 @@ using LibGit2 import GeometryBasics using Dates using RecipesBase +using JSON + +@testset "Infrastructure" begin + @test_nowarn JSON.Parser.parse(String(read(joinpath(dirname(pathof(Plots)), "..", ".zenodo.json")))) +end @testset "Plotly standalone" begin @test_nowarn Plots._init_ijulia_plotting()