diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e46a09f92..559cb19f9 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,7 +3,7 @@ name: benchmarks on: pull_request: -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -14,11 +14,17 @@ jobs: steps: - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@latest - + - name: Ubuntu TESTCMD run: echo "TESTCMD=xvfb-run --auto-servernum julia" >> $GITHUB_ENV - name: Install Plots dependencies - uses: julia-actions/julia-buildpkg@latest + run: | + julia -e ' + using Pkg + foreach(("RecipesBase", "RecipesPipeline")) do name + Pkg.develop(path=name) + end + ' - name: Install Benchmarking dependencies run: julia -e 'using Pkg; pkg"add PkgBenchmark BenchmarkCI"' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5d639098..341d73963 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: with: version: ${{ matrix.version }} - uses: julia-actions/cache@v1 - - uses: julia-actions/julia-buildpkg@latest + # - uses: julia-actions/julia-buildpkg@latest - name: Run upstream RecipesBase & RecipesPipeline tests shell: julia --project=@. --color=yes {0} @@ -99,10 +99,28 @@ jobs: CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) CondaPkg.status() - - uses: julia-actions/julia-runtest@latest - timeout-minutes: 60 - with: - prefix: ${{ matrix.prefix }} # for `xvfb-run` + # - uses: julia-actions/julia-runtest@latest + # timeout-minutes: 60 + # with: + # prefix: ${{ matrix.prefix }} # for `xvfb-run` + - name: Run Plots.jl tests + if: startsWith(matrix.os, 'ubuntu') + shell: xvfb-run julia --project=@. --color=yes {0} + run: | + using Pkg + foreach(("RecipesBase", "RecipesPipeline")) do name + Pkg.develop(path=name) + end + Pkg.test(; coverage=true) + - name: Run Plots.jl tests + if: "!startsWith(matrix.os, 'ubuntu')" + shell: julia --project=@. --color=yes {0} + run: | + using Pkg + foreach(("RecipesBase", "RecipesPipeline")) do name + Pkg.develop(path=name) + end + Pkg.test(; coverage=true) - name: Run downstream tests if: startsWith(matrix.os, 'ubuntu') diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index deedd8895..06bb0cb51 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -4,8 +4,8 @@ on: pull_request: push: branches: [master] - -concurrency: + +concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -22,6 +22,9 @@ jobs: PackageSpec("JuliaFormatter"), PackageSpec(url = "https://github.com/tkf/JuliaProjectFormatter.jl.git"), ]) + foreach(("RecipesBase", "RecipesPipeline")) do name + Pkg.develop(path=name) + end shell: julia --color=yes {0} - name: Format Julia files diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml index 4d51d55c9..d5dac254c 100644 --- a/.github/workflows/invalidations.yml +++ b/.github/workflows/invalidations.yml @@ -4,7 +4,7 @@ on: push: branches: [master] -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -16,16 +16,20 @@ jobs: with: version: '1' - uses: actions/checkout@v3 - - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-invalidations@v1 id: invs_pr + with: + test_script: | + "using Pkg; foreach((\"RecipesBase\", \"RecipesPipeline\")) do name; Pkg.develop(path=name); end; using Plots" - uses: actions/checkout@v3 with: ref: 'master' - - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-invalidations@v1 id: invs_master + with: + test_script: | + "using Pkg; foreach((\"RecipesBase\", \"RecipesPipeline\")) do name; Pkg.develop(path=name); end; using Plots" - name: Report invalidation counts run: | diff --git a/Project.toml b/Project.toml index c24aa6a01..ecf35f48d 100644 --- a/Project.toml +++ b/Project.toml @@ -104,7 +104,7 @@ Preferences = "1" FFMPEG = "0.2 - 0.4" Measures = "0.3" julia = "1.6" -RecipesBase = "1.3.1" +RecipesBase = "1.4" UnicodeFun = "0.4" UnicodePlots = "3.4" PlotThemes = "2, 3" @@ -122,7 +122,7 @@ ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" [targets] -test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "Gtk", "ImageMagick", "Images", "InspectDR", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyBase", "PyPlot", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "StatsPlots", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] +test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "Gtk", "ImageMagick", "Images", "InspectDR", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyBase", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "StatsPlots", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] [extensions] FileIOExt = "FileIO" diff --git a/RecipesBase/Project.toml b/RecipesBase/Project.toml index 284647765..4948530da 100644 --- a/RecipesBase/Project.toml +++ b/RecipesBase/Project.toml @@ -1,7 +1,7 @@ name = "RecipesBase" uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" author = ["Tom Breloff (@tbreloff)"] -version = "1.3.4" +version = "1.4.0" [deps] PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" diff --git a/RecipesBase/src/RecipesBase.jl b/RecipesBase/src/RecipesBase.jl index 5f3410aff..e3edf1932 100644 --- a/RecipesBase/src/RecipesBase.jl +++ b/RecipesBase/src/RecipesBase.jl @@ -35,6 +35,8 @@ function animate end # a placeholder to establish the name so that other packages (Plots.jl for example) # can add their own definition of RecipesBase.is_key_supported(k::Symbol) function is_key_supported end +# funciton to determine canonical form of key in presence of aliases +canonical_key(key) = key function grid end @@ -161,6 +163,7 @@ function process_recipe_body!(expr::Expr) e = e.args[1] end + # the unused operator `:=` will mean force: `x := 5` is equivalent to `x --> 5, force` # note: this means "x is defined as 5" if e.head === :(:=) @@ -168,10 +171,19 @@ function process_recipe_body!(expr::Expr) e.head = :(-->) end + # `$` will be the recommended way to retrieve values + if e.head === :call && e.args[1] === :(<--) # binary use + target, attribute = e.args[2], e.args[3] + expr.args[i] = Expr(:(=), target, :(plotattributes[$RecipesBase.canonical_key($(attribute))])) + elseif e.head === :$ # unary use + expr.args[i] = :(plotattributes[$RecipesBase.canonical_key($(QuoteNode(only(e.args))))]) + end + # we are going to recursively swap out `a --> b, flags...` commands # note: this means "x may become 5" if e.head === :(-->) k, v = e.args + k = canonical_key(k) if isa(k, Symbol) k = QuoteNode(k) end @@ -204,12 +216,9 @@ function process_recipe_body!(expr::Expr) elseif e.head ≡ :return # To allow `return` in recipes just extract the returned arguments. expr.args[i] = first(e.args) - - elseif e.head ≢ :call - # we want to recursively replace the arrows, but not inside function calls - # as this might include things like Dict(1=>2) - process_recipe_body!(e) end + + process_recipe_body!(expr.args[i]) end end end diff --git a/RecipesBase/test/runtests.jl b/RecipesBase/test/runtests.jl index 2dae4af67..4a4d00b4a 100644 --- a/RecipesBase/test/runtests.jl +++ b/RecipesBase/test/runtests.jl @@ -5,9 +5,13 @@ import RecipesBase as RB using StableRNGs using Test + const KW = Dict{Symbol,Any} RB.is_key_supported(k::Symbol) = true +# Reset method table so tests can be rerun (could be more robust) +recipe_methods = methods(RB.apply_recipe) +length(recipe_methods) > 2 && Base.delete_method.(methods(RB.apply_recipe)[setdiff(1:end, [1,8])]) for t in map(i -> Symbol(:T, i), 1:5) @eval struct $t end @@ -139,9 +143,11 @@ end :markershape --> :auto, :require :markercolor --> customcolor, :force :xrotation --> 5 - :zrotation --> 6, :quiet - plotattributes[:hello] = "hi" - plotattributes[:world] = "world" + :zrotation --> $xrotation, :quiet + var = $markercolor + war <-- :markershape + plotattributes[:hello] = "$var" + plotattributes[:world] = "$war" rand(StableRNG(1), 10, n) end check_apply_recipe( @@ -151,9 +157,9 @@ end :markershape => :auto, :markercolor => :red, :xrotation => 5, - :zrotation => 6, - :hello => "hi", - :world => "world", + :zrotation => 5, + :hello => "red", + :world => "auto", ), ) end diff --git a/src/Plots.jl b/src/Plots.jl index 3c8e6b942..869d7e369 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -122,6 +122,7 @@ export BezierCurve, plotattr, + getattr, scalefontsize, scalefontsizes, resetfontsizes diff --git a/src/args.jl b/src/args.jl index d60fc74cf..2c8173433 100644 --- a/src/args.jl +++ b/src/args.jl @@ -1520,6 +1520,7 @@ function preprocess_attributes!(plotattributes::AKW) end RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = Plots.preprocess_attributes!(plotattributes) +RecipesBase.canonical_key(key::Symbol) = get(_keyAliases, key, key) # ----------------------------------------------------------------------------- diff --git a/src/pipeline.jl b/src/pipeline.jl index 3babfb5ab..1e7fb58c6 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -456,7 +456,7 @@ function _add_the_series(plt, sp, plotattributes) throw(ArgumentError("Unsupported type for extra keyword arguments")) end warn_on_unsupported(plt.backend, plotattributes) - series = Series(plotattributes) + series = Series(length(plt.series_list) + 1, sp, plotattributes) push!(plt.series_list, series) if (z_order = plotattributes[:z_order]) === :front push!(sp.series_list, series) diff --git a/src/plotattr.jl b/src/plotattr.jl index 69018dc64..3164bfbca 100644 --- a/src/plotattr.jl +++ b/src/plotattr.jl @@ -99,3 +99,75 @@ function plotattr(attrtype::Symbol, attribute::Symbol) def == "" ? "" : ", defaults to `$def`.", ) end + +function getattr(plt::Plot, s::Symbol) + attribute = get(_keyAliases, s, s) + _getattr(plt, plt.subplots, plt.series_list, attribute) +end +function getattr(sp::Subplot, s::Symbol) + attribute = get(_keyAliases, s, s) + _getattr(sp.plt, [sp], sp.series_list, attribute) +end +function getattr(axis::Axis, s::Symbol) + attribute = get(_keyAliases, s, s) + if attribute in _axis_args + attribute = get_attr_symbol(axis[:letter], attribute) + end + _getattr(only(axis.sps).plt, axis.sps, only(axis.sps).series_list, attribute) +end + +function getattr(series::Series, s::Symbol) + attribute = get(_keyAliases, s, s) + _getattr(series.subplot.plt, [series.subplot], [series], attribute) +end + +function _getattr( + plt::Plot, + subplots::Vector{<:Subplot}, + serieses::Vector{<:Series}, + attribute::Symbol, +) + if attribute ∈ _all_plot_args + return plt[attribute] + elseif attribute ∈ _all_subplot_args && attribute ∉ _magic_subplot_args + return reduce(hcat, getindex.(subplots, attribute)) + elseif (attribute ∈ _all_axis_args || attribute ∈ _lettered_all_axis_args) && + attribute ∉ _magic_axis_args + if attribute ∈ _lettered_all_axis_args + letters = collect(String(attribute)) + letter = Symbol(first(letters)) + attribute = Symbol(letters[2:end]...) + axis = get_attr_symbol(letter, :axis) + reduce(hcat, getindex.(getindex.(subplots, axis), attribute)) + else + axes = (:xaxis, :yaxis, :zaxis) + return map(subplots) do sp + return NamedTuple(axis => sp[axis][attribute] for axis in axes) + end + end + elseif attribute ∈ _all_series_args && attribute ∉ _magic_series_args + return reduce(hcat, map(serieses) do series + series[attribute] + end) + else + if attribute in _all_magic_args + @info "$attribute is a magic argument. These are not present in the Plot object. Please use the more specific attribute, such as `linestyle` instead of `line`." + return missing + end + extra_kwargs = Dict( + :plot => + haskey(plt[:extra_plot_kwargs], attribute) ? + plt[:extra_plot_kwargs][attribute] : [], + :subplots => [ + i => sp[:extra_kwargs][attribute] for + (i, sp) in enumerate(subplots) if haskey(sp[:extra_kwargs], attribute) + ], + :series => [ + series.id => series[:extra_kwargs][attribute] for + series in serieses if haskey(series[:extra_kwargs], attribute) + ], + ) + !all(isempty, values(extra_kwargs)) && return extra_kwargs + throw(ArgumentError("Attribute not found.")) + end +end diff --git a/src/types.jl b/src/types.jl index 6dc6067e2..e73050fbb 100644 --- a/src/types.jl +++ b/src/types.jl @@ -15,7 +15,9 @@ struct InputWrapper{T} obj::T end -mutable struct Series +mutable struct Series{S} + id::Int + subplot::S plotattributes::DefaultsDict end diff --git a/test/test_args.jl b/test/test_args.jl index 30e337a02..404ed9763 100644 --- a/test/test_args.jl +++ b/test/test_args.jl @@ -90,18 +90,14 @@ end @test_throws ArgumentError png(plot(1:2; aspect_ratio = :invalid_ar), fn) end -@testset "aliases" begin - @test :legend in aliases(:legend_position) - Plots.add_non_underscore_aliases!(Plots._typeAliases) - Plots.add_axes_aliases(:ticks, :tick) -end - @userplot MatrixHeatmap @recipe function f(A::MatrixHeatmap) mat = A.args[1] margin --> (0, :mm) seriestype := :heatmap + c --> :red + foreground_color := $color x := axes(mat, 2) y := axes(mat, 1) z := Surface(mat) @@ -113,6 +109,14 @@ end @test show(devnull, matrixheatmap(reshape(1:12, 3, 4))) isa Nothing end +@testset "aliases" begin + @test :legend in aliases(:legend_position) + Plots.add_non_underscore_aliases!(Plots._typeAliases) + Plots.add_axes_aliases(:ticks, :tick) + @test getattr(matrixheatmap(reshape(1:12, 3, 4))[1][1], :foreground_color) == + RGBA(colorant"red") +end + @testset "Formatters" begin ts = range(DateTime(today()), step = Hour(1), length = 24) p1 = plot(ts, 100randn(24)) diff --git a/test/test_plotattr.jl b/test/test_plotattr.jl new file mode 100644 index 000000000..ea1182952 --- /dev/null +++ b/test/test_plotattr.jl @@ -0,0 +1,71 @@ +using Plots, Test + +tplot = plot( + repeat([1:5, 2:6], inner = 3), + layout = @layout([a b; c]), + this = :that, + line = (5, :dash), + title = ["A" "B"], + xlims = [:auto (0, Inf)], +) +@testset "Get attributes" begin + @testset "From Plot" begin + @test getattr(tplot, :size) == default(:size) == getattr(tplot, :sizes) + @test getattr(tplot, :linestyle) == permutedims(fill(:dash, 6)) + @test getattr(tplot, :title) == ["A" "B" "A"] + @test getattr(tplot, :xlims) == [:auto (0, Inf) :auto] #Note: this is different from Plots.xlims.(tplot.subplots) + @test getattr(tplot, :lims) == [ + (xaxis = :auto, yaxis = :auto, zaxis = :auto), + (xaxis = (0, Inf), yaxis = :auto, zaxis = :auto), + (xaxis = :auto, yaxis = :auto, zaxis = :auto), + ] + @test getattr(tplot, :this) == Dict( + :series => + [1 => :that, 2 => :that, 3 => :that, 4 => :that, 5 => :that, 6 => :that], + :subplots => Any[], + :plot => Any[], + ) + @test (@test_logs (:info, r"line is a magic argument") getattr(tplot, :line)) === + missing + @test_throws ArgumentError getattr(tplot, :nothere) + end + @testset "From Sublot" begin + sp = tplot[2] + @test getattr(sp, :size) == default(:size) == getattr(sp, :sizes) + @test getattr(sp, :linestyle) == permutedims(fill(:dash, 2)) + @test getattr(sp, :title) == "B" + @test getattr(sp, :xlims) == (0, Inf) + @test getattr(sp, :lims) == [(xaxis = (0, Inf), yaxis = :auto, zaxis = :auto)] + @test getattr(sp, :this) == + Dict(:series => [2 => :that, 5 => :that], :subplots => Any[], :plot => Any[]) + @test (@test_logs (:info, r"line is a magic argument") getattr(sp, :line)) === + missing + @test_throws ArgumentError getattr(sp, :nothere) + end + @testset "From Axis" begin + axis = tplot[3][:yaxis] + @test getattr(axis, :size) == default(:size) == getattr(axis, :sizes) + @test getattr(axis, :linestyle) == permutedims(fill(:dash, 2)) + @test getattr(axis, :title) == "A" + @test getattr(axis, :xlims) === :auto # TODO: is this expected? + @test getattr(axis, :lims) == :auto + @test getattr(axis, :this) == + Dict(:series => [3 => :that, 6 => :that], :subplots => Any[], :plot => Any[]) + @test (@test_logs (:info, r"line is a magic argument") getattr(axis, :line)) === + missing + @test_throws ArgumentError getattr(axis, :nothere) + end + @testset "From Series" begin + series = tplot[1][1] + @test getattr(series, :size) == default(:size) == getattr(series, :sizes) + @test getattr(series, :linestyle) == :dash + @test getattr(series, :title) == "A" + @test getattr(series, :xlims) == :auto + @test getattr(series, :lims) == [(xaxis = :auto, yaxis = :auto, zaxis = :auto)] + @test getattr(series, :this) == + Dict(:series => [1 => :that], :subplots => Any[], :plot => Any[]) + @test (@test_logs (:info, r"line is a magic argument") getattr(series, :line)) === + missing + @test_throws ArgumentError getattr(series, :nothere) + end +end diff --git a/test/test_recipes.jl b/test/test_recipes.jl index 1079306dc..e4cf1862c 100644 --- a/test/test_recipes.jl +++ b/test/test_recipes.jl @@ -1,3 +1,4 @@ +using Plots, Test using OffsetArrays @testset "User recipes" begin @@ -107,35 +108,35 @@ with(:gr) do end @testset "parametric" begin - @test plot(sin, sin, cos, 0, 2π) isa Plot - @test plot(sin, sin, cos, collect((-2π):(π / 4):(2π))) isa Plot + @test plot(sin, sin, cos, 0, 2π) isa Plots.Plot + @test plot(sin, sin, cos, collect((-2π):(π / 4):(2π))) isa Plots.Plot end @testset "dict" begin - show(devnull, plot(Dict(1 => 2, 3 => -1))) + @test_nowarn show(devnull, plot(Dict(1 => 2, 3 => -1))) end @testset "gray image" begin - show(devnull, plot(rand(Gray, 2, 2))) + @test_nowarn show(devnull, plot(rand(Gray, 2, 2))) end @testset "plots_heatmap" begin - show(devnull, plots_heatmap(rand(RGBA, 2, 2))) + @test_nowarn show(devnull, plots_heatmap(rand(RGBA, 2, 2))) end @testset "scatter3d" begin - show(devnull, scatter3d(1:2, 1:2, 1:2)) + @test_nowarn show(devnull, scatter3d(1:2, 1:2, 1:2)) end @testset "sticks" begin - show(devnull, sticks(1:2, marker = :circle)) + @test_nowarn show(devnull, sticks(1:2, marker = :circle)) end @testset "stephist" begin - show(devnull, stephist([1, 2], marker = :circle)) + @test_nowarn show(devnull, stephist([1, 2], marker = :circle)) end @testset "bar with logscales" begin - show(devnull, bar([1 2 3], [0.02 125 10_000]; yscale = :log10)) + @test_nowarn show(devnull, bar([1 2 3], [0.02 125 10_000]; yscale = :log10)) end end