From c91872bc46a66bab72d936d19b2f84b2d3292114 Mon Sep 17 00:00:00 2001 From: Zhanibek Date: Tue, 12 Mar 2024 23:53:38 +0900 Subject: [PATCH 01/85] Road to plots 2.0 (#4866) * remove deprecated backends * move GR to extension * make plot3d work * make heatmap work * factor components into modules * finish Surface module * export is_horizontal from Fonts * start refactoring Plot, Subplot, etc. components * change version * continue module shuffling * continued refactoring * remove orientation * minimal usable state * format * remove makekw * anything up to Segments * move series stuff from utils * use Arrows * create Colorbars module * move axes stuff (precompiling state) * let's relax deps and add them back later if needed (macOS issue) * only 2 tests fail now * restructure extension logic * remove unicode for now * GR extension * update deps * init step of gr * proper init of GR * for now manually activate the backend * road to fixing gr * revert some changes * fix import exports * fix measures * clarify name space * might need to be reverted, shape->Shape (too confusing_ * return lost wrap * types and modules * fix more things * add gr function * remove stale and fix naming * added unicode backend * unicodeplots works * update runtests * only use explicitly imported names in backends * rename wrap to protect * remove pre post imports, move to alignment jl * alignment jl * remove comments * fix typo * finally fix InputWrapper, misc changes * import overloaded, namespace fix * fixing per backend exceptions * alost all tests are passing * fix exports * trying to fix project toml * try to resolve deps * move other backends to the extensions * start pgfplotsx * add backends to weakdeps * recover * already defined * fix GR plot area method overload * plotarea methods are overloaded in layouts.jl * more fixes * fix test * namespace fixes * fix namespaces * revert later * rm pyplot * remove requires * add PGFPlotsX imports * bump julia compat * create runtime functions for all backends * improve error handling * pgfplotsx loading * get tests running * fix names, skip StatsPlots examples * test on 1.10 * remove pyplot remainings * don't test julia < 1.9 * run julia formatter * add pythonplotbackend * fix init python plot * gaston working * reorganize plotly backends * update kaleido compat * format * remove PlotlyBase compat * make default backend functional * make plotly functional * naming and PlotlyJS * get gaston running * run more tests * move dev in ci * dev both at the same time * fix syntax * don't dev twice * only load Gtk if not on CI * add missing using statement for PlotlyJS * more imports * move merge_with_base_supported to Commons * make Plotly module * skip non-working tests * fix filtering * format * invert filter * fix pgfplotsx warning * don't test Gaston * fix cgrad missing * format --------- Co-authored-by: Simon Christ Co-authored-by: Simon Christ Co-authored-by: = <=> --- .github/workflows/ci.yml | 29 +- NEWS.md | 16 + Project.toml | 195 +- RecipesPipeline/Project.toml | 4 +- RecipesPipeline/src/RecipesPipeline.jl | 20 +- RecipesPipeline/src/api.jl | 16 +- RecipesPipeline/src/group.jl | 6 +- RecipesPipeline/src/series_recipe.jl | 4 +- RecipesPipeline/src/type_recipe.jl | 12 +- benchmark/benchmarks.jl | 13 +- ext/FileIOExt.jl | 16 +- ext/ImageInTerminalExt.jl | 1 - ext/PlotsGRExt/PlotsGRExt.jl | 54 + {src/backends => ext/PlotsGRExt}/gr.jl | 19 +- ext/PlotsGRExt/initialization.jl | 200 ++ ext/PlotsGastonExt/PlotsGastonExt.jl | 16 + .../backends => ext/PlotsGastonExt}/gaston.jl | 33 +- ext/PlotsGastonExt/initialization.jl | 145 ++ ext/PlotsHDF5Ext/PlotsHDF5Ext.jl | 534 ++++ {src/backends => ext/PlotsHDF5Ext}/hdf5.jl | 60 +- ext/PlotsInspectDR/PlotsInspectDR.jl | 1 + .../PlotsInspectDR}/inspectdr.jl | 2 +- ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl | 60 + ext/PlotsPGFPlotsXExt/initialization.jl | 218 ++ .../PlotsPGFPlotsXExt}/pgfplotsx.jl | 6 +- ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl | 12 + ext/PlotsPlotlyJSExt/initialization.jl | 53 + .../PlotsPlotlyJSExt}/plotlyjs.jl | 2 - .../PlotsPlotlyKaleidoExt.jl | 31 + ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl | 84 + ext/PlotsPythonPlotExt/initialization.jl | 192 ++ .../PlotsPythonPlotExt}/pythonplot.jl | 29 +- .../PlotsUnicodePlotsExt.jl | 41 + ext/PlotsUnicodePlotsExt/initialization.jl | 118 + .../PlotsUnicodePlotsExt}/unicodeplots.jl | 12 +- ext/UnitfulExt.jl | 8 +- src/Annotations.jl | 254 ++ src/Arrows.jl | 62 + src/Axes.jl | 463 ++++ src/BezierCurves.jl | 22 + src/{colorbars.jl => Colorbars.jl} | 27 +- src/Commons/Commons.jl | 294 +++ src/Commons/aliases.jl | 422 ++++ src/Commons/attrs.jl | 1276 ++++++++++ src/Commons/postprocess_attrs.jl | 23 + src/Fonts.jl | 177 ++ src/{plotmeasures.jl => PlotMeasures.jl} | 19 + src/Plots.jl | 117 +- src/PlotsPlots.jl | 293 +++ src/Series.jl | 331 +++ src/Shapes.jl | 228 ++ src/Strokes.jl | 82 + src/Subplots.jl | 295 +++ src/Surfaces.jl | 22 + src/Ticks.jl | 100 + src/abstract_backend.jl | 181 ++ src/alignment.jl | 65 + src/arg_desc.jl | 23 +- src/args.jl | 2220 ----------------- src/axes.jl | 1096 -------- src/axes_utils.jl | 553 ++++ src/backends.jl | 1788 ------------- src/backends/deprecated/pgfplots.jl | 739 ------ src/backends/deprecated/pyplot.jl | 1640 ------------ src/backends/nobackend.jl | 15 + src/backends/plotly.jl | 212 +- src/backends/plotlybase.jl | 27 - src/components.jl | 814 ------ src/consts.jl | 96 - src/examples.jl | 11 +- src/init.jl | 112 +- src/layouts.jl | 44 +- src/legend.jl | 26 + src/output.jl | 9 +- src/pipeline.jl | 60 +- src/plot.jl | 43 +- src/plotattr.jl | 20 +- src/recipes.jl | 61 +- src/themes.jl | 10 +- src/types.jl | 186 -- src/users.jl | 4 + src/utils.jl | 922 +++---- test/runtests.jl | 60 +- test/test_args.jl | 6 +- test/test_axes.jl | 25 +- test/test_backends.jl | 36 +- test/test_components.jl | 22 +- test/test_contours.jl | 26 +- test/test_layouts.jl | 2 +- test/test_misc.jl | 31 +- test/test_output.jl | 20 +- test/test_pgfplotsx.jl | 5 +- test/test_preferences.jl | 59 - test/test_recipes.jl | 9 +- test/test_utils.jl | 68 +- 95 files changed, 8177 insertions(+), 9938 deletions(-) create mode 100644 ext/PlotsGRExt/PlotsGRExt.jl rename {src/backends => ext/PlotsGRExt}/gr.jl (99%) create mode 100644 ext/PlotsGRExt/initialization.jl create mode 100644 ext/PlotsGastonExt/PlotsGastonExt.jl rename {src/backends => ext/PlotsGastonExt}/gaston.jl (97%) create mode 100644 ext/PlotsGastonExt/initialization.jl create mode 100644 ext/PlotsHDF5Ext/PlotsHDF5Ext.jl rename {src/backends => ext/PlotsHDF5Ext}/hdf5.jl (90%) create mode 100644 ext/PlotsInspectDR/PlotsInspectDR.jl rename {src/backends => ext/PlotsInspectDR}/inspectdr.jl (99%) create mode 100644 ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl create mode 100644 ext/PlotsPGFPlotsXExt/initialization.jl rename {src/backends => ext/PlotsPGFPlotsXExt}/pgfplotsx.jl (99%) create mode 100644 ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl create mode 100644 ext/PlotsPlotlyJSExt/initialization.jl rename {src/backends => ext/PlotsPlotlyJSExt}/plotlyjs.jl (98%) create mode 100644 ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl create mode 100644 ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl create mode 100644 ext/PlotsPythonPlotExt/initialization.jl rename {src/backends => ext/PlotsPythonPlotExt}/pythonplot.jl (98%) create mode 100644 ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl create mode 100644 ext/PlotsUnicodePlotsExt/initialization.jl rename {src/backends => ext/PlotsUnicodePlotsExt}/unicodeplots.jl (96%) create mode 100644 src/Annotations.jl create mode 100644 src/Arrows.jl create mode 100644 src/Axes.jl create mode 100644 src/BezierCurves.jl rename src/{colorbars.jl => Colorbars.jl} (88%) create mode 100644 src/Commons/Commons.jl create mode 100644 src/Commons/aliases.jl create mode 100644 src/Commons/attrs.jl create mode 100644 src/Commons/postprocess_attrs.jl create mode 100644 src/Fonts.jl rename src/{plotmeasures.jl => PlotMeasures.jl} (52%) create mode 100644 src/PlotsPlots.jl create mode 100644 src/Series.jl create mode 100644 src/Shapes.jl create mode 100644 src/Strokes.jl create mode 100644 src/Subplots.jl create mode 100644 src/Surfaces.jl create mode 100644 src/Ticks.jl create mode 100644 src/abstract_backend.jl create mode 100644 src/alignment.jl delete mode 100644 src/args.jl delete mode 100644 src/axes.jl create mode 100644 src/axes_utils.jl delete mode 100644 src/backends.jl delete mode 100644 src/backends/deprecated/pgfplots.jl delete mode 100644 src/backends/deprecated/pyplot.jl create mode 100644 src/backends/nobackend.jl delete mode 100644 src/backends/plotlybase.jl delete mode 100644 src/components.jl delete mode 100644 src/consts.jl delete mode 100644 src/types.jl create mode 100644 src/users.jl delete mode 100644 test/test_preferences.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7193eeda4..18cffdc97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,22 +28,17 @@ jobs: fail-fast: false matrix: version: - - '1.6' # LTS (minimal declared julia compat in `Project.toml`) - - '1.9' # latest stable + - '1.9' # (minimal declared julia compat in `Project.toml`) + - '1.10' # latest stable os: [ubuntu-latest, windows-latest, macos-latest] arch: [x64] include: - - os: ubuntu-latest - prefix: xvfb-run # julia-actions/julia-runtest/blob/master/README.md - - os: ubuntu-latest - prefix: xvfb-run - version: '1.7' # only test intermediate release on `ubuntu` to spare resources - - os: ubuntu-latest - prefix: xvfb-run - version: '1.8' # only test intermediate release on `ubuntu` to spare resources - - os: ubuntu-latest - prefix: xvfb-run - version: '~1.10.0-0' # upcoming julia version, next `rc` + # - os: ubuntu-latest + # prefix: xvfb-run + # version: '1.9' # only test intermediate release on `ubuntu` to spare resources + # - os: ubuntu-latest + # prefix: xvfb-run + # version: '~1.11.0-0' # upcoming julia version, next `rc` - os: ubuntu-latest prefix: xvfb-run version: 'nightly' @@ -67,6 +62,11 @@ jobs: with: version: ${{ matrix.version }} - uses: julia-actions/cache@v1 + - name: Use local RecipesBase/RecipesPipeline + shell: julia --project=@. --color=yes {0} + run: | + using Pkg + Pkg.develop([(; path="./RecipesBase"), (; path="./RecipesPipeline")]) - uses: julia-actions/julia-buildpkg@latest - name: Run upstream RecipesBase & RecipesPipeline tests @@ -74,7 +74,7 @@ jobs: run: | using Pkg foreach(("RecipesBase", "RecipesPipeline")) do name - Pkg.develop(path=name); Pkg.test(name; coverage=true) + Pkg.test(name; coverage=true) end - name: Install conda based matplotlib @@ -98,7 +98,6 @@ jobs: end CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) CondaPkg.status() - - uses: julia-actions/julia-runtest@latest timeout-minutes: 60 with: diff --git a/NEWS.md b/NEWS.md index fcb363c42..dc901e858 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,22 @@ # Plots.jl NEWS +## Breaking changes +--- + +## v2 + +- deprecated backends `pgfplots` and `pyplot` removed +- deprecated keyword `orientation` removed +- backends are extensions now so the backend code must be explicitly loaded using `import` with the backend package, e.g. ```julia +using Plots +import GR # loads backend code + +``` +- Types are no longer part of the Plots API this affects + - `Shape`, which is now `shape` + +--- #### notes on release changes, ongoing development, and future planned work ## NOTE: this file is deprecated, see the [TagBot](https://github.com/marketplace/actions/julia-tagbot) auto-generated changelogs instead diff --git a/Project.toml b/Project.toml index b73284b1d..9cef1f37e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,135 +1,138 @@ name = "Plots" -uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" author = ["Tom Breloff (@tbreloff)"] -version = "1.39.0-dev" +uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +version = "2.0.0-dev" [deps] -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" JLFzf = "1019f520-868f-41f5-a6de-eb00f4b6a39c" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" -NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" -PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -Preferences = "21216c6a-2e73-6563-6e65-726566657250" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" -Requires = "ae029012-a4dd-5104-9daa-d747884805df" +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Scratch = "6c6a2e73-6563-6170-7368-637461726353" -Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +Preferences = "21216c6a-2e73-6563-6e65-726566657250" +FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1" -UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" -Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" +PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" -[weakdeps] -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +[extras] +PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Gaston = "4b11ee91-296f-5714-9832-002c20994614" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" +PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" +RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" +TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" -ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[extensions] -FileIOExt = "FileIO" -GeometryBasicsExt = "GeometryBasics" -IJuliaExt = "IJulia" -ImageInTerminalExt = "ImageInTerminal" -UnitfulExt = "Unitful" +FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" +PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" +InspectDR = "d0351b0e-4b05-5898-87b3-e2a8edfddd1d" +SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" [compat] -Aqua = "0.8" -Contour = "0.5 - 0.6" -Downloads = "1" -FFMPEG = "0.2 - 0.4" -FixedPointNumbers = "0.6 - 0.8" +NaNMath = "0.3, 1" +Showoff = "0.3.1, 1" GR = "0.69.5 - 0.73" Gaston = "1" -HDF5 = "0.16" -InspectDR = "0.4" +FixedPointNumbers = "0.6 - 0.8" JLFzf = "0.1" -JSON = "0.21, 1" -LaTeXStrings = "1" -Latexify = "0.14 - 0.15, 0.16" -Measures = "0.3" -NaNMath = "0.3, 1" -PGFPlots = "3" -PGFPlotsX = "1" -PlotThemes = "2, 3" -PlotUtils = "1" -PlotlyBase = "0.7 - 0.8" -PlotlyJS = "0.18" -PlotlyKaleido = "1" PrecompileTools = "1" -Preferences = "1" -PyPlot = "2" +PGFPlotsX = "1" +Unzip = "0.1 - 0.2" PythonPlot = "1 - 1.0.2" -RecipesBase = "1.3.1" -RecipesPipeline = "0.6.10" -Reexport = "0.2, 1" +UnitfulLatexify = "1" +RecipesPipeline = "1" +LaTeXStrings = "1" +PlotUtils = "1" +JSON = "0.21, 1" +StatsBase = "0.33, 0.34" +HDF5 = "0.16" RelocatableFolders = "0.3, 1" -Requires = "1" Scratch = "1" -Showoff = "0.3.1, 1" -Statistics = "1" -StatsBase = "0.33, 0.34" +Latexify = "0.14 - 0.15, 0.16" +Preferences = "1" +FFMPEG = "0.2 - 0.4" +Measures = "0.3" +julia = "1.9" +RecipesBase = "1.3.1" UnicodeFun = "0.4" UnicodePlots = "3.4" -UnitfulLatexify = "1" -Unzip = "0.1 - 0.2" -julia = "1.6" +PlotThemes = "2, 3" +Contour = "0.5 - 0.6" +PlotlyJS = "0.18" +PlotlyKaleido = "2.2.2" +Reexport = "0.2, 1" -[extras] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +[weakdeps] FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" -FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" +InspectDR = "d0351b0e-4b05-5898-87b3-e2a8edfddd1d" Gaston = "4b11ee91-296f-5714-9832-002c20994614" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" -HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" -InspectDR = "d0351b0e-4b05-5898-87b3-e2a8edfddd1d" -LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -PGFPlots = "3b7a836e-365b-5785-a47d-02c71176b4aa" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" -PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5" PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" -PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" -RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" -SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c" -StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" -VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" [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", "GR", "Images", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] + +[extensions] +FileIOExt = "FileIO" +UnitfulExt = "Unitful" +GeometryBasicsExt = "GeometryBasics" +IJuliaExt = "IJulia" +ImageInTerminalExt = "ImageInTerminal" +PlotsGRExt = "GR" +PlotsUnicodePlotsExt = "UnicodePlots" +PlotsPGFPlotsXExt = "PGFPlotsX" +PlotsPythonPlotExt = "PythonPlot" +PlotsPlotlyJSExt = "PlotlyJS" +PlotsPlotlyKaleidoExt = "PlotlyKaleido" +PlotsInspectDRExt = "InspectDR" +PlotsGastonExt = "Gaston" diff --git a/RecipesPipeline/Project.toml b/RecipesPipeline/Project.toml index 8c3062174..bba45bff5 100644 --- a/RecipesPipeline/Project.toml +++ b/RecipesPipeline/Project.toml @@ -1,7 +1,7 @@ name = "RecipesPipeline" uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c" authors = ["Michael Krabbe Borregaard "] -version = "0.6.12" +version = "1.0.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -15,7 +15,7 @@ NaNMath = "0.3, 1" PlotUtils = "0.6.5, 1" RecipesBase = "1.3.1" PrecompileTools = "1" -julia = "1.6" +julia = "1.9" [extras] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" diff --git a/RecipesPipeline/src/RecipesPipeline.jl b/RecipesPipeline/src/RecipesPipeline.jl index d2cf52588..2a2342cbb 100644 --- a/RecipesPipeline/src/RecipesPipeline.jl +++ b/RecipesPipeline/src/RecipesPipeline.jl @@ -127,22 +127,22 @@ using PrecompileTools mats = (Int[1 2; 3 4], Float64[1 2; 3 4]) surfs = Surface.(mats) vols = Volume(ones(Int, 1, 2, 3)), Volume(ones(Float64, 1, 2, 3)) - for pl_attr in plotattributes - _series_data_vector(1, pl_attr) - _series_data_vector([1], pl_attr) - _series_data_vector(["a"], pl_attr) - _series_data_vector([1 2], pl_attr) - _series_data_vector(["a" "b"], pl_attr) - _series_data_vector.(surfs, Ref(pl_attr)) - _apply_type_recipe.(Ref(pl_attr), surfs, Ref(:x)) - _apply_type_recipe.(Ref(pl_attr), mats, Ref(:x)) + for pl_attrs in plotattributes + _series_data_vector(1, pl_attrs) + _series_data_vector([1], pl_attrs) + _series_data_vector(["a"], pl_attrs) + _series_data_vector([1 2], pl_attrs) + _series_data_vector(["a" "b"], pl_attrs) + _series_data_vector.(surfs, Ref(pl_attrs)) + _apply_type_recipe.(Ref(pl_attrs), surfs, Ref(:x)) + _apply_type_recipe.(Ref(pl_attrs), mats, Ref(:x)) _map_funcs(identity, [1, 2]) _map_funcs([identity, identity], [1, 2]) unzip([(1.0, 1.0)]) unzip([(1, 1)]) unzip([(1, 1.0)]) unzip([([1.0], [2.0])]) - # _process_seriesrecipe(nothing, pl_attr) + # _process_seriesrecipe(nothing, pl_attrs) # recipe_pipeline!(plt, [1, 2], ["foo", "bar"]) end end diff --git a/RecipesPipeline/src/api.jl b/RecipesPipeline/src/api.jl index ff0e1897b..f79ee9a23 100644 --- a/RecipesPipeline/src/api.jl +++ b/RecipesPipeline/src/api.jl @@ -87,12 +87,12 @@ is_axis_attribute(plt, attr) = false # ### processing of axis args # axis args before type recipes should still be mapped to all axes """ - preprocess_axis_args!(plt, plotattributes) + preprocess_axis_attrs!(plt, plotattributes) Preprocessing of axis attributes. Prepends the axis letter to axis attributes by default. """ -function preprocess_axis_args!(plt, plotattributes) +function preprocess_axis_attrs!(plt, plotattributes) for (k, v) in plotattributes is_axis_attribute(plt, k) || continue pop!(plotattributes, k) @@ -103,22 +103,22 @@ function preprocess_axis_args!(plt, plotattributes) end """ - preprocess_axis_args!(plt, plotattributes, letter) + preprocess_axis_attrs!(plt, plotattributes, letter) This version additionally stores the letter name in `plotattributes[:letter]`. """ -function preprocess_axis_args!(plt, plotattributes, letter) +function preprocess_axis_attrs!(plt, plotattributes, letter) plotattributes[:letter] = letter - preprocess_axis_args!(plt, plotattributes) + preprocess_axis_attrs!(plt, plotattributes) end # axis args in type recipes should only be applied to the current axis """ - postprocess_axis_args!(plt, plotattributes, letter) + postprocess_axis_attrs!(plt, plotattributes, letter) -Removes the `:letter` key from `plotattributes` and does the same prepending of the letters as `preprocess_axis_args!`. +Removes the `:letter` key from `plotattributes` and does the same prepending of the letters as `preprocess_axis_attrs!`. """ -function postprocess_axis_args!(plt, plotattributes, letter) +function postprocess_axis_attrs!(plt, plotattributes, letter) pop!(plotattributes, :letter) letter in (:x, :y, :z) || return for (k, v) in plotattributes diff --git a/RecipesPipeline/src/group.jl b/RecipesPipeline/src/group.jl index cfb5c5bf5..ede1641c8 100644 --- a/RecipesPipeline/src/group.jl +++ b/RecipesPipeline/src/group.jl @@ -104,9 +104,9 @@ group_as_matrix(t) = false # used in `StatsPlots` for indexes in groupby.group_indices x[indexes] = eachindex(indexes) end - last_args = g.args + last_attrs = g.args else - x, last_args... = g.args + x, last_attrs... = g.args end x_u = unique(sort(x)) x_ind = Dict(zip(x_u, eachindex(x_u))) @@ -118,7 +118,7 @@ group_as_matrix(t) = false # used in `StatsPlots` label --> reshape(groupby.group_labels, 1, :) typeof(g)(( x_u, - (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_args)..., + (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_attrs)..., )) end end diff --git a/RecipesPipeline/src/series_recipe.jl b/RecipesPipeline/src/series_recipe.jl index bb4275803..5c770c3d3 100644 --- a/RecipesPipeline/src/series_recipe.jl +++ b/RecipesPipeline/src/series_recipe.jl @@ -15,7 +15,7 @@ function _process_seriesrecipes!(plt, kw_list) end process_sliced_series_attributes!(plt, kw_list) for kw in kw_list - series_attr = DefaultsDict(kw, series_defaults(plt)) + series_attrs = DefaultsDict(kw, series_defaults(plt)) # now we have a fully specified series, with colors chosen. we must recursively # handle series recipes, which dispatch on seriestype. If a backend does not # natively support a seriestype, we check for a recipe that will convert that @@ -24,7 +24,7 @@ function _process_seriesrecipes!(plt, kw_list) # really a filled step plot, and a step plot is really just a path. So any backend # that supports drawing a path will implicitly be able to support step, bar, and # histogram plots (and any recipes that use those components). - _process_seriesrecipe(plt, series_attr) + _process_seriesrecipe(plt, series_attrs) end end diff --git a/RecipesPipeline/src/type_recipe.jl b/RecipesPipeline/src/type_recipe.jl index c1de00afc..da05656d3 100644 --- a/RecipesPipeline/src/type_recipe.jl +++ b/RecipesPipeline/src/type_recipe.jl @@ -17,10 +17,10 @@ Apply the type recipe with signature `(::Type{T}, ::T)`. """ function _apply_type_recipe(plotattributes, v, letter) plt = plotattributes[:plot_object] - preprocess_axis_args!(plt, plotattributes, letter) + preprocess_axis_attrs!(plt, plotattributes, letter) rdvec = RecipesBase.apply_recipe(plotattributes, typeof(v), v) warn_on_recipe_aliases!(plotattributes[:plot_object], plotattributes, :type, v) - postprocess_axis_args!(plt, plotattributes, letter) + postprocess_axis_attrs!(plt, plotattributes, letter) rdvec[1].args[1] end @@ -29,20 +29,20 @@ end # and one to format tick values. function _apply_type_recipe(plotattributes, v::AbstractArray, letter) plt = plotattributes[:plot_object] - preprocess_axis_args!(plt, plotattributes, letter) + preprocess_axis_attrs!(plt, plotattributes, letter) # First we try to apply an array type recipe. w = RecipesBase.apply_recipe(plotattributes, typeof(v), v)[1].args[1] warn_on_recipe_aliases!(plt, plotattributes, :type, v) # If the type did not change try it element-wise if typeof(v) == typeof(w) if (smv = skipmissing(v)) |> isempty - postprocess_axis_args!(plt, plotattributes, letter) + postprocess_axis_attrs!(plt, plotattributes, letter) return Float64[] end x = first(smv) args = RecipesBase.apply_recipe(plotattributes, typeof(x), x)[1].args warn_on_recipe_aliases!(plt, plotattributes, :type, x) - postprocess_axis_args!(plt, plotattributes, letter) + postprocess_axis_attrs!(plt, plotattributes, letter) return if length(args) == 2 && all(arg -> arg isa Function, args) numfunc, formatter = args Formatted(map(numfunc, v), formatter) @@ -50,7 +50,7 @@ function _apply_type_recipe(plotattributes, v::AbstractArray, letter) v end end - postprocess_axis_args!(plt, plotattributes, letter) + postprocess_axis_attrs!(plt, plotattributes, letter) w end diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index a57b19cdf..89da81b96 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -4,7 +4,12 @@ using Plots const SUITE = BenchmarkGroup() julia_cmd = split(get(ENV, "TESTCMD", unsafe_string(Base.JLOptions().julia_bin))) -SUITE["load_plot_display"] = @benchmarkable run(`$julia_cmd --startup-file=no --project=$(Base.active_project()) -e 'using Plots; display(plot(1:0.1:10, sin))'`) -SUITE["load"] = @benchmarkable run(`$julia_cmd --startup-file=no --project=$(Base.active_project()) -e 'using Plots'`) -SUITE["plot"] = @benchmarkable p = plot(1:0.1:10, sin) samples=1 evals=1 -SUITE["display"] = @benchmarkable display(p) setup=(p = plot(1:0.1:10, sin)) samples=1 evals=1 +SUITE["load_plot_display"] = @benchmarkable run( + `$julia_cmd --startup-file=no --project=$(Base.active_project()) -e 'using Plots; display(plot(1:0.1:10, sin))'`, +) +SUITE["load"] = @benchmarkable run( + `$julia_cmd --startup-file=no --project=$(Base.active_project()) -e 'using Plots'`, +) +SUITE["plot"] = @benchmarkable p = plot(1:0.1:10, sin) samples = 1 evals = 1 +SUITE["display"] = + @benchmarkable display(p) setup = (p = plot(1:0.1:10, sin)) samples = 1 evals = 1 diff --git a/ext/FileIOExt.jl b/ext/FileIOExt.jl index 2aba42921..d70ab80d5 100644 --- a/ext/FileIOExt.jl +++ b/ext/FileIOExt.jl @@ -25,12 +25,14 @@ function _show_pdfbackends(io::IO, ::MIME"image/png", plt::Plot) write(io, read(open(pngfn), String)) end -for be in ( - Plots.PGFPlotsBackend, # NOTE: I guess this can be removed in Plots@2.0 -) - showable(MIME"image/png"(), Plot{be}) && continue - @eval Plots._show(io::IO, mime::MIME"image/png", plt::Plot{$be}) = - _show_pdfbackends(io, mime, plt) -end +# Possibly need to create another extension that has both pgfplotsx and showio +# delete for now, as testing for pgfplotsx is hard; TODO restore later at @2.0 +# for be in ( +# Plots.PGFPlotsBackend, # NOTE: I guess this can be removed in Plots@2.0 +# ) +# showable(MIME"image/png"(), Plot{be}) && continue +# @eval Plots._show(io::IO, mime::MIME"image/png", plt::Plot{$be}) = +# _show_pdfbackends(io, mime, plt) +# end end # module diff --git a/ext/ImageInTerminalExt.jl b/ext/ImageInTerminalExt.jl index d03dc4aea..5010c9b1a 100644 --- a/ext/ImageInTerminalExt.jl +++ b/ext/ImageInTerminalExt.jl @@ -7,7 +7,6 @@ if ImageInTerminal.ENCODER_BACKEND[] == :Sixel get!(ENV, "GKSwstype", "nul") # disable `gr` output, we display in the terminal instead for be in ( Plots.GRBackend, - Plots.PyPlotBackend, Plots.PythonPlotBackend, # Plots.UnicodePlotsBackend, # better and faster as MIME("text/plain") in terminal Plots.PGFPlotsXBackend, diff --git a/ext/PlotsGRExt/PlotsGRExt.jl b/ext/PlotsGRExt/PlotsGRExt.jl new file mode 100644 index 000000000..f1a4c9f72 --- /dev/null +++ b/ext/PlotsGRExt/PlotsGRExt.jl @@ -0,0 +1,54 @@ +module PlotsGRExt + +using GR: GR +using Plots: Plots +# TODO: eliminate this list +using Plots: + bbox, + left, + right, + bottom, + top, + plotarea, + axis_drawing_info, + axis_drawing_info_3d, + _guess_best_legend_position, + labelfunc_tex, + _cycle, + isortho, + isautop, + heatmap_edges, + is_uniformly_spaced, + DPI, + shape_data, + is_2tuple, + is3d, + straightline_data, + convert_to_polar + +using RecipesPipeline: RecipesPipeline +using NaNMath: NaNMath +using Plots.Arrows +using Plots.Axes +using Plots.Annotations +using Plots.Colorbars +using Plots.Colorbars: cbar_gradient, cbar_fill, cbar_lines +using Plots.Colors +using Plots.Commons +using Plots.Fonts +using Plots.Fonts: Font, PlotText +using Plots.PlotMeasures +using Plots.PlotsPlots +using Plots.PlotsSeries +using Plots.Subplots +using Plots.Shapes +using Plots.Shapes: Shape +using Plots.Ticks + +# These are overriden by GR +import Plots: labelfunc, _update_min_padding!, _show, _display, closeall + +include("initialization.jl") +include("gr.jl") + +end # module diff --git a/src/backends/gr.jl b/ext/PlotsGRExt/gr.jl similarity index 99% rename from src/backends/gr.jl rename to ext/PlotsGRExt/gr.jl index 896e978fb..238ba31e5 100644 --- a/src/backends/gr.jl +++ b/ext/PlotsGRExt/gr.jl @@ -1,3 +1,4 @@ + # https://github.com/jheinen/GR.jl - significant contributions by @jheinen const gr_projections = (auto = 1, ortho = 1, orthographic = 1, persp = 2, perspective = 2) @@ -608,7 +609,7 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport) if _has_ticks(sp[:colorbar_ticks]) z_tick = 0.5GR.tick(z_min, z_max) gr_set_line(1, :solid, plot_color(:black), sp) - (yscale = sp[:colorbar_scale]) ∈ _logScales && GR.setscale(gr_y_log_scales[yscale]) + (yscale = sp[:colorbar_scale]) ∈ _log_scales && GR.setscale(gr_y_log_scales[yscale]) # signature: gr.axes(x_tick, y_tick, x_org, y_org, major_x, major_y, tick_size) GR.axes(0, z_tick, x_max, z_min, 0, 1, gr_colorbar_tick_size[]) end @@ -1096,7 +1097,7 @@ function gr_add_legend(sp, leg, viewport_area) 1, min(max_markersize, mfac * msz), min(max_markersize, mfac * msw), - Plots._cycle(msh, 1), + _cycle(msh, 1), ) end @@ -1364,13 +1365,13 @@ gr_set_window(sp, vp) = end if x_max > x_min && y_max > y_min && zok scaleop = 0 - if (xscale = sp[:xaxis][:scale]) ∈ _logScales + if (xscale = sp[:xaxis][:scale]) ∈ _log_scales scaleop |= gr_x_log_scales[xscale] end - if (yscale = sp[:yaxis][:scale]) ∈ _logScales + if (yscale = sp[:yaxis][:scale]) ∈ _log_scales scaleop |= gr_y_log_scales[yscale] end - if needs_3d && (zscale = sp[:zaxis][:scale] ∈ _logScales) + if needs_3d && (zscale = sp[:zaxis][:scale] ∈ _log_scales) scaleop |= gr_z_log_scales[zscale] end sp[:xaxis][:flip] && (scaleop |= GR.OPTION_FLIP_X) @@ -1998,7 +1999,7 @@ function gr_draw_heatmap(series, x, y, z, clims) # even on log scales, where it is visually non-uniform. _z, colors = if (scale = sp[:colorbar_scale]) === :identity z, plot_color.(get(fillgrad, z, clims), series[:fillalpha]) - elseif scale ∈ _logScales + elseif scale ∈ _log_scales z_log, z_normalized = gr_z_normalized_log_scaled(scale, z, clims) z_log, plot_color.(map(z -> get(fillgrad, z), z_normalized), series[:fillalpha]) end @@ -2012,7 +2013,7 @@ function gr_draw_heatmap(series, x, y, z, clims) end _z, z_normalized = if (scale = sp[:colorbar_scale]) === :identity z, get_z_normalized.(z, clims...) - elseif scale ∈ _logScales + elseif scale ∈ _log_scales gr_z_normalized_log_scaled(scale, z, clims) end rgba = map(x -> round(Int32, 1_000 + 255x), z_normalized) @@ -2049,7 +2050,7 @@ for (mime, fmt) in ( "image/svg+xml" => "svg", ) @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) - dpi_factor = $fmt == "png" ? plt[:dpi] / Plots.DPI : 1 + dpi_factor = $fmt == "png" ? plt[:dpi] / DPI : 1 filepath = tempname() * "." * $fmt # workaround windows bug github.com/JuliaLang/julia/issues/46989 touch(filepath) @@ -2067,7 +2068,7 @@ for (mime, fmt) in ( end end -function _display(plt::Plot{GRBackend}) +function Plots._display(plt::Plot{GRBackend}) if plt[:display_type] === :inline filepath = tempname() * ".pdf" GR.emergencyclosegks() diff --git a/ext/PlotsGRExt/initialization.jl b/ext/PlotsGRExt/initialization.jl new file mode 100644 index 000000000..603497da4 --- /dev/null +++ b/ext/PlotsGRExt/initialization.jl @@ -0,0 +1,200 @@ +import Plots: backend_name, backend_package_name, is_marker_supported + +# unrolling the old # init_backend macro by hand case by case +const package_str = "GR" +const str = "gr" +const sym = :gr + +struct GRBackend <: Plots.AbstractBackend end + +get_concrete_backend() = GRBackend # opposite to abstract + +function __init__() + @info "Initializing GR backend in Plots; run `gr()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[GRBackend] = sym + + push!(Plots._initialized_backends, sym) +end +# Make GR know to Plots +backend_name(::GRBackend) = sym +backend_package_name(::GRBackend) = backend_package_name(sym) + +const _gr_attrs = Plots.merge_with_base_supported([ + :annotations, + :annotationrotation, + :annotationhalign, + :annotationfontsize, + :annotationfontfamily, + :annotationcolor, + :annotationvalign, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :legend_foreground_color, + :foreground_color_grid, + :foreground_color_axis, + :foreground_color_text, + :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :fillstyle, + :bins, + :layout, + :title, + :window_title, + :guide, + :widen, + :lims, + :ticks, + :scale, + :flip, + :titlefontfamily, + :titlefontsize, + :titlefonthalign, + :titlefontvalign, + :titlefontrotation, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_halign, + :legend_font_valign, + :legend_font_rotation, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfonthalign, + :tickfontvalign, + :tickfontrotation, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefonthalign, + :guidefontvalign, + :guidefontrotation, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_titlefont, + :colorbar_titlefontsize, + :colorbar_titlefontrotation, + :colorbar_titlefontcolor, + :colorbar_entry, + :colorbar_scale, + :clims, + :fill, + :fill_z, + :fontfamily, + :fontfamily_subplot, + :line_z, + :marker_z, + :legend_column, + :legend_font, + :legend_title, + :legend_title_font_color, + :legend_title_font_family, + :legend_title_font_rotation, + :legend_title_font_pointsize, + :legend_title_font_valigm, + :levels, + :line, + :ribbon, + :quiver, + :overwrite_figure, + :plot_title, + :plot_titlefontcolor, + :plot_titlefontfamily, + :plot_titlefontrotation, + :plot_titlefontsize, + :plot_titlelocation, + :plot_titlevspan, + :polar, + :aspect_ratio, + :normalize, + :weights, + :inset_subplots, + :bar_width, + :arrow, + :framestyle, + :tick_direction, + :camera, + :contour_labels, + :connections, + :axis, + :thickness_scaling, + :minorgrid, + :minorgridalpha, + :minorgridlinewidth, + :minorgridstyle, + :minorticks, + :mirror, + :rotation, + :showaxis, + :tickfonthalign, + :formatter, + :mirror, + :guidefont, +]) +const _gr_seriestypes = [ + :path, + :scatter, + :straightline, + :heatmap, + :image, + :contour, + :path3d, + :scatter3d, + :surface, + :wireframe, + :mesh3d, + :volume, + :shape, +] +const _gr_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _gr_markers = vcat(Commons._all_markers, :pixel) +const _gr_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_gr_", s, "s") + eval(quote + Plots.$f1(::GRBackend, $s::Symbol) = $s in $v + Plots.$f2(::GRBackend) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + +is_marker_supported(::GRBackend, shape::Shape) = true diff --git a/ext/PlotsGastonExt/PlotsGastonExt.jl b/ext/PlotsGastonExt/PlotsGastonExt.jl new file mode 100644 index 000000000..d1a135804 --- /dev/null +++ b/ext/PlotsGastonExt/PlotsGastonExt.jl @@ -0,0 +1,16 @@ +module PlotsGastonExt + +using Gaston +using Plots: Plots, mesh3d_triangles +import Plots: _show, _display +using Plots.Commons +using Plots.PlotsPlots +using Plots.Subplots +using Plots.PlotsSeries +using Plots.Fonts +using Plots.PlotUtils: alphacolor, hex + +include("initialization.jl") +include("gaston.jl") + +end # module diff --git a/src/backends/gaston.jl b/ext/PlotsGastonExt/gaston.jl similarity index 97% rename from src/backends/gaston.jl rename to ext/PlotsGastonExt/gaston.jl index 10b055ca7..c6eeb3878 100644 --- a/src/backends/gaston.jl +++ b/ext/PlotsGastonExt/gaston.jl @@ -61,18 +61,19 @@ for (mime, term) in ( @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend}) term = String($term) tmpfile = tempname() * ".$term" - - ret = Gaston.save(; - saveopts = gaston_saveopts(plt), - handle = plt.o.handle, - output = tmpfile, - term, - ) - if ret === nothing || ret - while !isfile(tmpfile) - end # avoid race condition with read in next line - write(io, read(tmpfile)) - rm(tmpfile, force = true) + if plt.o !== nothing + ret = Gaston.save(; + saveopts = gaston_saveopts(plt), + handle = plt.o.handle, + output = tmpfile, + term, + ) + if ret === nothing || ret + while !isfile(tmpfile) + end # avoid race condition with read in next line + write(io, read(tmpfile)) + rm(tmpfile, force = true) + end end nothing end @@ -155,7 +156,7 @@ function gaston_init_subplot( end any_label |= should_add_to_legend(series) end - axesconf = gaston_parse_axes_args(plt, sp, dims, any_label) + axesconf = gaston_parse_axes_attrs(plt, sp, dims, any_label) sp.o = Gaston.Plot(; dims, curves = [], axesconf) end push!(plt.o.subplots, obj) @@ -392,7 +393,7 @@ gaston_fillstyle(x) = "solid" end -function gaston_parse_axes_args( +function gaston_parse_axes_attrs( plt::Plot{GastonBackend}, sp::Subplot{GastonBackend}, dims::Int, @@ -556,7 +557,7 @@ function gaston_parse_axes_args( tmin, tmax = axis_limits(sp, :x, false, false) rmin, rmax = axis_limits(sp, :y, false, false) rticks = get_ticks(sp, :y) - gaston_ticks = if (ttype = ticksType(rticks)) === :ticks + gaston_ticks = if (ttype = ticks_type(rticks)) === :ticks string.(rticks) elseif ttype === :ticks_and_labels ["'$l' $t" for (t, l) in zip(rticks...)] @@ -593,7 +594,7 @@ function gaston_set_ticks!(axesconf, ticks, letter, I, maj_min, add) push!(axesconf, "unset $(maj_min)$(letter)tics") return end - gaston_ticks = if (ttype = ticksType(ticks)) === :ticks + gaston_ticks = if (ttype = ticks_type(ticks)) === :ticks tics = gaston_fix_ticks_overflow(ticks) if maj_min == "m" map(t -> "'' $t 1", tics) # see gnuplot manual 'Mxtics' diff --git a/ext/PlotsGastonExt/initialization.jl b/ext/PlotsGastonExt/initialization.jl new file mode 100644 index 000000000..999dea35e --- /dev/null +++ b/ext/PlotsGastonExt/initialization.jl @@ -0,0 +1,145 @@ +# unrolling the old # init_backend macro by hand case by case +# this is not a macro for the backend maintainers and explicit control + +const package_str = "Gaston" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct GastonBackend <: Plots.AbstractBackend end +const T = GastonBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @info "Initializing $package_str backend in Plots; run `$str()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[T] = sym + + push!(Plots._initialized_backends, sym) + + # Additional setup required by the backend: + +end + +Plots.backend_name(::T) = sym +Plots.backend_package_name(::T) = Plots.backend_package_name(sym) + +const _gaston_attrs = Plots.merge_with_base_supported([ + :annotations, + # :background_color_legend, + # :background_color_inside, + # :background_color_outside, + # :foreground_color_legend, + # :foreground_color_grid, :foreground_color_axis, + # :foreground_color_text, :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + # :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :markerstrokestyle, + # :fillrange, :fillcolor, :fillalpha, + # :bins, + # :bar_width, :bar_edges, + :title, + :window_title, + :guide, + :guide_position, + :widen, + :lims, + :ticks, + :scale, + :flip, + :rotation, + :tickfont, + :guidefont, + :legendfont, + :grid, + :legend, + # :colorbar, :colorbar_title, + # :fill_z, :line_z, :marker_z, :levels, + # :ribbon, + :quiver, + :arrow, + # :orientation, :overwrite_figure, + :polar, + # :normalize, :weights, :contours, + :aspect_ratio, + :tick_direction, + # :framestyle, + # :camera, + # :contour_labels, + :connections, +]) + +const _gaston_seriestypes = [ + :path, + :path3d, + :scatter, + :steppre, + :stepmid, + :steppost, + :ysticks, + :xsticks, + :contour, + :shape, + :straightline, + :scatter3d, + :contour3d, + :wireframe, + :heatmap, + :surface, + :mesh3d, + :image, +] + +const _gaston_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] + +const _gaston_markers = [ + :none, + :auto, + :pixel, + :cross, + :xcross, + :+, + :x, + :star5, + :rect, + :circle, + :utriangle, + :dtriangle, + :diamond, + :pentagon, + # :hline, + # :vline, +] + +const _gaston_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + eval(quote + Plots.$f1(::T, $s::Symbol) = $s in $v + Plots.$f2(::T) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- diff --git a/ext/PlotsHDF5Ext/PlotsHDF5Ext.jl b/ext/PlotsHDF5Ext/PlotsHDF5Ext.jl new file mode 100644 index 000000000..a9e886799 --- /dev/null +++ b/ext/PlotsHDF5Ext/PlotsHDF5Ext.jl @@ -0,0 +1,534 @@ +module PlotsHDF5Ext + +import Plots: Plot, HDF5Backend, _display, _show, closeall + +#= + +# HDF5 Plots: Save/replay plots to/from HDF5 + +# Usage +Write to .hdf5 file using: + p = plot(...) + Plots.hdf5plot_write(p, "plotsave.hdf5") + +Read from .hdf5 file using: + pyplot() # Must first select backend + pread = Plots.hdf5plot_read("plotsave.hdf5") + display(pread) + +# TODO + 1. Support more features. + - GridLayout known not to be working. + 2. Improve error handling. + - Will likely crash if file format is off. + 3. Save data in a folder parallel to "plot". + - Will make it easier for users to locate data. + - Use HDF5 reference to link data? + 4. Develop an actual versioned file format. + - Should have some form of backward compatibility. + - Should be reliable for archival purposes. + 5. Fix construction of plot object with hdf5plot_read. + - Layout doesn't seem to get transferred well (ex: `Plots._examples[40]`). + - Not building object correctly when backends do not natively support + a certain feature (ex: :steppre) + - No support for CategoricalArrays.* structures. But they appear to be + brought into `Plots._examples[25,30]` through DataFrames.jl - so we can't + really reference them in this code. +=# + +""" + _hdf5_implementation + +Create module (namespace) for implementing HDF5 "plots". +(Avoid name collisions, while keeping names short) +""" +module _hdf5_implementation # Tools required to implements HDF5 "plots" + +import Dates + +# Plots.jl imports HDF5 to main: +import ..HDF5 +import ..HDF5: Group, Dataset + +import ..Colors, ..Colorant +import ..PlotUtils.ColorSchemes.ColorScheme + +import ..HDF5Backend, .._current_plots_version +import ..HDF5PLOT_MAP_STR2TELEM, ..HDF5PLOT_MAP_TELEM2STR +import ..HDF5Plot_PlotRef, ..HDF5PLOT_PLOTREF +import ..BoundingBox, ..Extrema, ..Length +import ..RecipesPipeline.datetimeformatter +import ..PlotUtils.ColorPalette, + ..PlotUtils.CategoricalColorGradient, ..PlotUtils.ContinuousColorGradient +import ..Surface, ..Shape, ..Arrow +import ..GridLayout, ..RootLayout +import ..Font, ..PlotText, ..SeriesAnnotations +import ..Axis, ..Subplot, ..Plot +import ..AKW, ..KW, ..DefaultsDict +import .._axis_defaults +import ..plot, ..plot! + +# Types that already have built-in HDF5 support (just write out natively): +const HDF5_SupportedTypes = Union{Number,String} + +# Dispatch types: +struct CplxTuple end # Identifies a "complex" tuple structure (not merely numbers) + +# HDF5 reader will auto-detect type correctly: +struct HDF5_AutoDetect end # See HDF5_SupportedTypes + +if length(HDF5PLOT_MAP_TELEM2STR) < 1 + # Possible element types of high-level data types: + # (Used to add type information as an HDF5 string attribute) + # (Also used to dispatch appropriate read function through _read_typed()) + _telem2str = Dict{String,Type}( + "NOTHING" => Nothing, + "SYMBOL" => Symbol, + "RGBA" => Colorant, # Write out any Colorant to an #RRGGBBAA string + "TUPLE" => Tuple, + "CTUPLE" => CplxTuple, + "EXTREMA" => Extrema, + "LENGTH" => Length, + "ARRAY" => Array, # Array{Any} (because Array{T<:Union{Number, String}} natively supported by HDF5) + + # Sub-structure types: + "T_DATETIMEFORMATTER" => typeof(datetimeformatter), + + # Sub-structure types: + "DEFAULTSDICT" => DefaultsDict, + "FONT" => Font, + "BOUNDINGBOX" => BoundingBox, + "GRIDLAYOUT" => GridLayout, + "ROOTLAYOUT" => RootLayout, + "SERIESANNOTATIONS" => SeriesAnnotations, + "PLOTTEXT" => PlotText, + "SHAPE" => Shape, + "ARROW" => Arrow, + "COLORSCHEME" => ColorScheme, + "COLORPALETTE" => ColorPalette, + "CONT_COLORGRADIENT" => ContinuousColorGradient, + "CAT_COLORGRADIENT" => CategoricalColorGradient, + "AXIS" => Axis, + "SURFACE" => Surface, + "SUBPLOT" => Subplot, + ) + merge!(HDF5PLOT_MAP_STR2TELEM, _telem2str) # Faster to create than push!()?? + merge!( + HDF5PLOT_MAP_TELEM2STR, + Dict{Type,String}(v => k for (k, v) in HDF5PLOT_MAP_STR2TELEM), + ) +end + +# Helper functions + +h5plotpath(plotname::String) = "plots/$plotname" + +_hdf5_merge!(dest::AKW, src::AKW) = + for (k, v) in src + if isa(v, Axis) + _hdf5_merge!(dest[k].plotattributes, v.plotattributes) + else + dest[k] = v + end + end + +# _type_for_map returns the type to use with HDF5PLOT_MAP_TELEM2STR[], in case it is not concrete: +_type_for_map(::Type{T}) where {T} = T # Catch-all +_type_for_map(::Type{T}) where {T<:BoundingBox} = BoundingBox +_type_for_map(::Type{T}) where {T<:ColorScheme} = ColorScheme +_type_for_map(::Type{T}) where {T<:Surface} = Surface + +# Read/write things like type name in attributes +_write_datatype_attrs(ds::Union{Group,Dataset}, ::Type{T}) where {T} = + HDF5.attributes(ds)["TYPE"] = HDF5PLOT_MAP_TELEM2STR[T] + +function _read_datatype_attrs(ds::Union{Group,Dataset}) + Base.haskey(HDF5.attributes(ds), "TYPE") || return HDF5_AutoDetect + HDF5PLOT_MAP_STR2TELEM[HDF5.read(HDF5.attributes(ds)["TYPE"])] +end + +# Type parameter attributes: +_write_typeparam_attrs(ds::Dataset, v::Length{T}) where {T} = + HDF5.attributes(ds)["TYPEPARAM"] = string(T) # Need to add units for Length + +_read_typeparam_attrs(ds::Dataset) = HDF5.read(HDF5.attributes(ds)["TYPEPARAM"]) + +_write_length_attrs(grp::Group, v::Vector) = HDF5.attributes(grp)["LENGTH"] = length(v) +_read_length_attrs(::Type{Vector}, grp::Group) = HDF5.read(HDF5.attributes(grp)["LENGTH"]) + +_write_size_attrs(grp::Group, v::Array) = HDF5.attributes(grp)["SIZE"] = [size(v)...] + +_read_size_attrs(::Type{Array}, grp::Group) = + tuple(HDF5.read(HDF5.attributes(grp)["SIZE"])...) + +# _write_typed(): Simple (leaf) datatypes. (Labels with type name.) + +set_value!(grp::Group, name::String, v) = (grp[name] = v; grp[name]) + +# Default behaviour: Assumes value is supported by HDF5 format +_write_typed(grp::Group, name::String, v::HDF5_SupportedTypes) = + (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr + +_write_typed(grp::Group, name::String, v::Nothing) = + _write_datatype_attrs(set_value!(grp, name, "nothing"), Nothing) # Redundancy check/easier to read HDF5 file + +_write_typed(grp::Group, name::String, v::Symbol) = + _write_datatype_attrs(set_value!(grp, name, string(v)), Symbol) + +_write_typed(grp::Group, name::String, v::Colorant) = + _write_datatype_attrs(set_value!(grp, name, "#" * Colors.hex(v, :RRGGBBAA)), Colorant) + +_write_typed(grp::Group, name::String, v::Extrema) = + _write_datatype_attrs(set_value!(grp, name, [v.emin, v.emax]), Extrema) # More compact than writing struct + +function _write_typed(grp::Group, name::String, v::Length) + grp[name] = v.value + _write_datatype_attrs(grp[name], Length) + _write_typeparam_attrs(grp[name], v) +end + +_write_typed(grp::Group, name::String, v::typeof(datetimeformatter)) = + _write_datatype_attrs(set_value!(grp, name, string(v)), typeof(datetimeformatter)) # Just write something that helps reader + +_write_typed(grp::Group, name::String, v::Array{T}) where {T<:Number} = + (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr + +_write_typed(grp::Group, name::String, v::AbstractRange) = + _write_typed(grp, name, collect(v)) # For now + +# Helper functions for writing complex data structures + +# Write an array using HDF5 hierarchy (when not using simple numeric eltype): +function _write_harray(grp::Group, name::String, v::Array) + sgrp = HDF5.create_group(grp, name) + lidx = LinearIndices(size(v)) + + for iter in eachindex(v) + coord = lidx[iter] + elem = v[iter] + idxstr = join(coord, "_") + _write_typed(sgrp, "v$idxstr", elem) + end + + _write_size_attrs(sgrp, v) +end + +# Write Dict without tagging with type: +_write(grp::Group, name::String, d::AbstractDict) = + let sgrp = HDF5.create_group(grp, name) + for (k, v) in d + kstr = string(k) + _write_typed(sgrp, kstr, v) + end + end + +# Write out arbitrary `struct`s: +_writestructgeneric(grp::Group, obj::T) where {T} = + for fname in fieldnames(T) + v = getfield(obj, fname) + _write_typed(grp, String(fname), v) + end + +# _write_typed(): More complex structures. (Labels with type name.) + +# Catch-all (default behaviour for `struct`s): +function _write_typed(grp::Group, name::String, v::T) where {T} + # NOTE: need "name" parameter so that call signature is same with built-ins + MT = _type_for_map(T) + try # Check to see if type is supported + typestr = HDF5PLOT_MAP_TELEM2STR[MT] + catch + @warn "HDF5Plots does not yet support structs of type `$MT`\n\n$grp" + return + end + + # If attribute is supported and no writer is defined, then this should work: + objgrp = HDF5.create_group(grp, name) + _write_datatype_attrs(objgrp, MT) + _writestructgeneric(objgrp, v) +end + +function _write_typed(grp::Group, name::String, v::Array{T}) where {T} + _write_harray(grp, name, v) + _write_datatype_attrs(grp[name], Array) # Any +end + +function _write_typed(grp::Group, name::String, v::Tuple, ::Type{ELT}) where {ELT<:Number} # Basic Tuple + _write_typed(grp, name, [v...]) + _write_datatype_attrs(grp[name], Tuple) +end +function _write_typed(grp::Group, name::String, v::Tuple, ::Type) # CplxTuple + _write_harray(grp, name, [v...]) + _write_datatype_attrs(grp[name], CplxTuple) +end +_write_typed(grp::Group, name::String, v::Tuple) = _write_typed(grp, name, v, eltype(v)) + +_write_typed(grp::Group, name::String, v::Dict) = nothing + +function _write_typed(grp::Group, name::String, d::DefaultsDict) # Typically for plot attributes + _write(grp, name, d) + _write_datatype_attrs(grp[name], DefaultsDict) +end + +function _write_typed(grp::Group, name::String, v::Axis) + sgrp = HDF5.create_group(grp, name) + # Ignore: sps::Vector{Subplot} + _write_typed(sgrp, "plotattributes", v.plotattributes) + _write_datatype_attrs(sgrp, Axis) +end + +function _write_typed(grp::Group, name::String, v::Subplot) + # Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. + sgrp = HDF5.create_group(grp, name) + _write_typed(sgrp, "index", v[:subplot_index]) + _write_datatype_attrs(sgrp, Subplot) + return +end + +_write_typed(grp::Group, name::String, v::Plot) = nothing # Don't write plot references + +# _write(): Write out more complex structures +# NOTE: No need to write out type information (inferred from hierarchy) + +function _write(grp::Group, sp::Subplot{HDF5Backend}) + _write_typed(grp, "attr", sp.attr) + + listgrp = HDF5.create_group(grp, "series_list") + _write_length_attrs(listgrp, sp.series_list) + for (i, series) in enumerate(sp.series_list) + # Just write .plotattributes part: + _write(listgrp, "$i", series.plotattributes) + end +end + +function _write(grp::Group, plt::Plot{HDF5Backend}) + _write_typed(grp, "attr", plt.attr) + + listgrp = HDF5.create_group(grp, "subplots") + _write_length_attrs(listgrp, plt.subplots) + for (i, sp) in enumerate(plt.subplots) + sgrp = HDF5.create_group(listgrp, "$i") + _write(sgrp, sp) + end +end + +function hdf5plot_write( + plt::Plot{HDF5Backend}, + path::AbstractString; + name::String = "_unnamed", +) + HDF5.h5open(path, "w") do file + HDF5.write_dataset(file, "VERSION_INFO", string(_current_plots_version)) + grp = HDF5.create_group(file, h5plotpath(name)) + _write(grp, plt) + end +end + +# _read(): Read data, but not type information. + +# Types with built-in HDF5 support: +_read(::Type{HDF5_AutoDetect}, ds::Dataset) = HDF5.read(ds) + +function _read(::Type{Nothing}, ds::Dataset) + nstr = "nothing" + v = HDF5.read(ds) + nstr == v || throw( + Meta.ParseError("_read(::Nothing, ::Group): Read $v != $nstr:\n$(HDF5.name(ds))"), + ) + return +end +_read(::Type{Symbol}, ds::Dataset) = Symbol(HDF5.read(ds)) +_read(::Type{Colorant}, ds::Dataset) = parse(Colorant, HDF5.read(ds)) +_read(::Type{Tuple}, ds::Dataset) = tuple(HDF5.read(ds)...) +_read(::Type{Extrema}, ds::Dataset) = + let v = HDF5.read(ds) + Extrema(v[1], v[2]) + end +function _read(::Type{Length}, ds::Dataset) + TUNIT = Symbol(_read_typeparam_attrs(ds)) + v = HDF5.read(ds) + Length{TUNIT,typeof(v)}(v) +end +_read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter + +# Helper functions for reading in complex data structures + +# When type is unknown, _read_typed() figures it out: +function _read_typed(grp::Group, name::String) + ds = grp[name] + _read(_read_datatype_attrs(ds), ds) +end + +# _readstructgeneric: Needs object values to be written out with _write_typed(): +function _readstructgeneric(::Type{T}, grp::Group) where {T} + vlist = Array{Any}(nothing, fieldcount(T)) + for (i, fname) in enumerate(fieldnames(T)) + vlist[i] = _read_typed(grp, String(fname)) + end + T(vlist...) +end + +# Read KW from group: +function _read(::Type{KW}, grp::Group) + d = KW() + gkeys = keys(grp) + for k in gkeys + try + v = _read_typed(grp, k) + d[Symbol(k)] = v + catch e + @warn "Could not read field $k" e grp + end + end + d +end + +# _read(): More complex structures. + +# Catch-all (default behaviour for `struct`s): +_read(T::Type, grp::Group) = _readstructgeneric(T, grp) + +function _read(::Type{Array}, grp::Group) # Array{Any} + sz = _read_size_attrs(Array, grp) + tuple(0) == sz && return [] + result = Array{Any}(undef, sz) + lidx = LinearIndices(sz) + + for iter in eachindex(result) + coord = lidx[iter] + idxstr = join(coord, "_") + result[iter] = _read_typed(grp, "v$idxstr") + end + + # Hack: Implicitly make Julia detect element type. + # (Should probably write it explicitly to file) + result = [elem for elem in result] # Potentially make more specific + reshape(result, sz) +end + +_read(::Type{CplxTuple}, grp::Group) = tuple(_read(Array, grp)...) + +function _read(::Type{GridLayout}, grp::Group) + # parent = _read_typed(grp, "parent") # Can't use generic reader + parent = RootLayout() # TODO: support parent??? + minpad = _read_typed(grp, "minpad") + bbox = _read_typed(grp, "bbox") + grid = _read_typed(grp, "grid") + widths = _read_typed(grp, "widths") + heights = _read_typed(grp, "heights") + attr = KW() # TODO support attr: _read_typed(grp, "attr") + + GridLayout(parent, minpad, bbox, grid, widths, heights, attr) +end +# Defaults depends on context. So: user must constructs with defaults, then read. +function _read(::Type{DefaultsDict}, grp::Group) + # User should set DefaultsDict.defaults to one of: + # _plot_defaults, _subplot_defaults, _axis_defaults, _series_defaults + path = HDF5.name(ds) + @warn "Cannot yet read DefaultsDict using _read_typed():\n $path\nCannot fully reconstruct plot." +end + +# 1st arg appears to be ref to subplots. Seems to work without it. +_read(::Type{Axis}, grp::Group) = + Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), _axis_defaults)) + +# Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. +_read(::Type{Subplot}, grp::Group) = + HDF5PLOT_PLOTREF.ref.subplots[_read_typed(grp, "index")] + +# _read(): Main plot structures + +function _read(grp::Group, sp::Subplot) + listgrp = HDF5.open_group(grp, "series_list") + nseries = _read_length_attrs(Vector, listgrp) + + for i in 1:nseries + sgrp = HDF5.open_group(listgrp, "$i") + seriesinfo = _read(KW, sgrp) + + plot!(sp, seriesinfo[:x], seriesinfo[:y]) # Add data & create data structures + _hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo) + end + + # Perform after adding series... otherwise values get overwritten: + agrp = HDF5.open_group(grp, "attr") + _hdf5_merge!(sp.attr, _read(KW, agrp)) + + return sp +end + +function _read_plot(grp::Group) + listgrp = HDF5.open_group(grp, "subplots") + n = _read_length_attrs(Vector, listgrp) + + # Construct new plot, +allocate subplots: + plt = plot(layout = n) + HDF5PLOT_PLOTREF.ref = plt # Used when reading "layout" + + agrp = HDF5.open_group(grp, "attr") + _hdf5_merge!(plt.attr, _read(KW, agrp)) + + for (i, sp) in enumerate(plt.subplots) + sgrp = HDF5.open_group(listgrp, "$i") + _read(sgrp, sp) + end + + plt +end + +hdf5plot_read(path::AbstractString; name::String = "_unnamed") = + HDF5.h5open(path, "r") do file + grp = HDF5.open_group(file, h5plotpath("_unnamed")) + return _read_plot(grp) + end + +end # module _hdf5_implementation + +# Implement Plots.jl backend interface for HDF5Backend + +is_marker_supported(::HDF5Backend, shape::Shape) = true + +# Create the window/figure for this backend. +function _create_backend_figure(plt::Plot{HDF5Backend}) end + +# Set up the subplot within the backend object. +function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end + +# Add one series to the underlying backend object. +# Called once per series +# NOTE: Seems to be called when user calls plot()... even if backend +# plot, sp.o has not yet been constructed... +function _series_added(plt::Plot{HDF5Backend}, series::Series) end + +# When series data is added/changed, this callback can do dynamic updates to the backend object. +# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here. +function _series_updated(plt::Plot{HDF5Backend}, series::Series) end + +# called just before updating layout bounding boxes... in case you need to prep +# for the calcs +function _before_layout_calcs(plt::Plot{HDF5Backend}) end + +# Set the (left, top, right, bottom) minimum padding around the plot area +# to fit ticks, tick labels, guides, colorbars, etc. +function _update_min_padding!(sp::Subplot{HDF5Backend}) end + +# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations]) +function _update_plot_object(plt::Plot{HDF5Backend}) end + +# ---------------------------------------------------------------- + +# Display/show the plot (open a GUI window, or browser page, for example). +function _display(plt::Plot{HDF5Backend}) + msg = "HDF5 interface does not support `display()` function." + msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." + @warn msg + return +end + +# Interface actually required to use HDF5Backend + +hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString) = + _hdf5_implementation.hdf5plot_write(plt, path) +hdf5plot_write(path::AbstractString) = _hdf5_implementation.hdf5plot_write(current(), path) +hdf5plot_read(path::AbstractString) = _hdf5_implementation.hdf5plot_read(path) +end # module diff --git a/src/backends/hdf5.jl b/ext/PlotsHDF5Ext/hdf5.jl similarity index 90% rename from src/backends/hdf5.jl rename to ext/PlotsHDF5Ext/hdf5.jl index c81c5690b..e2fd571a2 100644 --- a/src/backends/hdf5.jl +++ b/ext/PlotsHDF5Ext/hdf5.jl @@ -135,26 +135,26 @@ _type_for_map(::Type{T}) where {T<:ColorScheme} = ColorScheme _type_for_map(::Type{T}) where {T<:Surface} = Surface # Read/write things like type name in attributes -_write_datatype_attr(ds::Union{Group,Dataset}, ::Type{T}) where {T} = +_write_datatype_attrs(ds::Union{Group,Dataset}, ::Type{T}) where {T} = HDF5.attributes(ds)["TYPE"] = HDF5PLOT_MAP_TELEM2STR[T] -function _read_datatype_attr(ds::Union{Group,Dataset}) +function _read_datatype_attrs(ds::Union{Group,Dataset}) Base.haskey(HDF5.attributes(ds), "TYPE") || return HDF5_AutoDetect HDF5PLOT_MAP_STR2TELEM[HDF5.read(HDF5.attributes(ds)["TYPE"])] end # Type parameter attributes: -_write_typeparam_attr(ds::Dataset, v::Length{T}) where {T} = +_write_typeparam_attrs(ds::Dataset, v::Length{T}) where {T} = HDF5.attributes(ds)["TYPEPARAM"] = string(T) # Need to add units for Length -_read_typeparam_attr(ds::Dataset) = HDF5.read(HDF5.attributes(ds)["TYPEPARAM"]) +_read_typeparam_attrs(ds::Dataset) = HDF5.read(HDF5.attributes(ds)["TYPEPARAM"]) -_write_length_attr(grp::Group, v::Vector) = HDF5.attributes(grp)["LENGTH"] = length(v) -_read_length_attr(::Type{Vector}, grp::Group) = HDF5.read(HDF5.attributes(grp)["LENGTH"]) +_write_length_attrs(grp::Group, v::Vector) = HDF5.attributes(grp)["LENGTH"] = length(v) +_read_length_attrs(::Type{Vector}, grp::Group) = HDF5.read(HDF5.attributes(grp)["LENGTH"]) -_write_size_attr(grp::Group, v::Array) = HDF5.attributes(grp)["SIZE"] = [size(v)...] +_write_size_attrs(grp::Group, v::Array) = HDF5.attributes(grp)["SIZE"] = [size(v)...] -_read_size_attr(::Type{Array}, grp::Group) = +_read_size_attrs(::Type{Array}, grp::Group) = tuple(HDF5.read(HDF5.attributes(grp)["SIZE"])...) # _write_typed(): Simple (leaf) datatypes. (Labels with type name.) @@ -166,25 +166,25 @@ _write_typed(grp::Group, name::String, v::HDF5_SupportedTypes) = (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr _write_typed(grp::Group, name::String, v::Nothing) = - _write_datatype_attr(set_value!(grp, name, "nothing"), Nothing) # Redundancy check/easier to read HDF5 file + _write_datatype_attrs(set_value!(grp, name, "nothing"), Nothing) # Redundancy check/easier to read HDF5 file _write_typed(grp::Group, name::String, v::Symbol) = - _write_datatype_attr(set_value!(grp, name, string(v)), Symbol) + _write_datatype_attrs(set_value!(grp, name, string(v)), Symbol) _write_typed(grp::Group, name::String, v::Colorant) = - _write_datatype_attr(set_value!(grp, name, "#" * Colors.hex(v, :RRGGBBAA)), Colorant) + _write_datatype_attrs(set_value!(grp, name, "#" * Colors.hex(v, :RRGGBBAA)), Colorant) _write_typed(grp::Group, name::String, v::Extrema) = - _write_datatype_attr(set_value!(grp, name, [v.emin, v.emax]), Extrema) # More compact than writing struct + _write_datatype_attrs(set_value!(grp, name, [v.emin, v.emax]), Extrema) # More compact than writing struct function _write_typed(grp::Group, name::String, v::Length) grp[name] = v.value - _write_datatype_attr(grp[name], Length) - _write_typeparam_attr(grp[name], v) + _write_datatype_attrs(grp[name], Length) + _write_typeparam_attrs(grp[name], v) end _write_typed(grp::Group, name::String, v::typeof(datetimeformatter)) = - _write_datatype_attr(set_value!(grp, name, string(v)), typeof(datetimeformatter)) # Just write something that helps reader + _write_datatype_attrs(set_value!(grp, name, string(v)), typeof(datetimeformatter)) # Just write something that helps reader _write_typed(grp::Group, name::String, v::Array{T}) where {T<:Number} = (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr @@ -206,7 +206,7 @@ function _write_harray(grp::Group, name::String, v::Array) _write_typed(sgrp, "v$idxstr", elem) end - _write_size_attr(sgrp, v) + _write_size_attrs(sgrp, v) end # Write Dict without tagging with type: @@ -240,22 +240,22 @@ function _write_typed(grp::Group, name::String, v::T) where {T} # If attribute is supported and no writer is defined, then this should work: objgrp = HDF5.create_group(grp, name) - _write_datatype_attr(objgrp, MT) + _write_datatype_attrs(objgrp, MT) _writestructgeneric(objgrp, v) end function _write_typed(grp::Group, name::String, v::Array{T}) where {T} _write_harray(grp, name, v) - _write_datatype_attr(grp[name], Array) # Any + _write_datatype_attrs(grp[name], Array) # Any end function _write_typed(grp::Group, name::String, v::Tuple, ::Type{ELT}) where {ELT<:Number} # Basic Tuple _write_typed(grp, name, [v...]) - _write_datatype_attr(grp[name], Tuple) + _write_datatype_attrs(grp[name], Tuple) end function _write_typed(grp::Group, name::String, v::Tuple, ::Type) # CplxTuple _write_harray(grp, name, [v...]) - _write_datatype_attr(grp[name], CplxTuple) + _write_datatype_attrs(grp[name], CplxTuple) end _write_typed(grp::Group, name::String, v::Tuple) = _write_typed(grp, name, v, eltype(v)) @@ -263,21 +263,21 @@ _write_typed(grp::Group, name::String, v::Dict) = nothing function _write_typed(grp::Group, name::String, d::DefaultsDict) # Typically for plot attributes _write(grp, name, d) - _write_datatype_attr(grp[name], DefaultsDict) + _write_datatype_attrs(grp[name], DefaultsDict) end function _write_typed(grp::Group, name::String, v::Axis) sgrp = HDF5.create_group(grp, name) # Ignore: sps::Vector{Subplot} _write_typed(sgrp, "plotattributes", v.plotattributes) - _write_datatype_attr(sgrp, Axis) + _write_datatype_attrs(sgrp, Axis) end function _write_typed(grp::Group, name::String, v::Subplot) # Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. sgrp = HDF5.create_group(grp, name) _write_typed(sgrp, "index", v[:subplot_index]) - _write_datatype_attr(sgrp, Subplot) + _write_datatype_attrs(sgrp, Subplot) return end @@ -290,7 +290,7 @@ function _write(grp::Group, sp::Subplot{HDF5Backend}) _write_typed(grp, "attr", sp.attr) listgrp = HDF5.create_group(grp, "series_list") - _write_length_attr(listgrp, sp.series_list) + _write_length_attrs(listgrp, sp.series_list) for (i, series) in enumerate(sp.series_list) # Just write .plotattributes part: _write(listgrp, "$i", series.plotattributes) @@ -301,7 +301,7 @@ function _write(grp::Group, plt::Plot{HDF5Backend}) _write_typed(grp, "attr", plt.attr) listgrp = HDF5.create_group(grp, "subplots") - _write_length_attr(listgrp, plt.subplots) + _write_length_attrs(listgrp, plt.subplots) for (i, sp) in enumerate(plt.subplots) sgrp = HDF5.create_group(listgrp, "$i") _write(sgrp, sp) @@ -341,7 +341,7 @@ _read(::Type{Extrema}, ds::Dataset) = Extrema(v[1], v[2]) end function _read(::Type{Length}, ds::Dataset) - TUNIT = Symbol(_read_typeparam_attr(ds)) + TUNIT = Symbol(_read_typeparam_attrs(ds)) v = HDF5.read(ds) Length{TUNIT,typeof(v)}(v) end @@ -352,7 +352,7 @@ _read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter # When type is unknown, _read_typed() figures it out: function _read_typed(grp::Group, name::String) ds = grp[name] - _read(_read_datatype_attr(ds), ds) + _read(_read_datatype_attrs(ds), ds) end # _readstructgeneric: Needs object values to be written out with _write_typed(): @@ -385,7 +385,7 @@ end _read(T::Type, grp::Group) = _readstructgeneric(T, grp) function _read(::Type{Array}, grp::Group) # Array{Any} - sz = _read_size_attr(Array, grp) + sz = _read_size_attrs(Array, grp) tuple(0) == sz && return [] result = Array{Any}(undef, sz) lidx = LinearIndices(sz) @@ -436,7 +436,7 @@ _read(::Type{Subplot}, grp::Group) = function _read(grp::Group, sp::Subplot) listgrp = HDF5.open_group(grp, "series_list") - nseries = _read_length_attr(Vector, listgrp) + nseries = _read_length_attrs(Vector, listgrp) for i in 1:nseries sgrp = HDF5.open_group(listgrp, "$i") @@ -455,7 +455,7 @@ end function _read_plot(grp::Group) listgrp = HDF5.open_group(grp, "subplots") - n = _read_length_attr(Vector, listgrp) + n = _read_length_attrs(Vector, listgrp) # Construct new plot, +allocate subplots: plt = plot(layout = n) diff --git a/ext/PlotsInspectDR/PlotsInspectDR.jl b/ext/PlotsInspectDR/PlotsInspectDR.jl new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ext/PlotsInspectDR/PlotsInspectDR.jl @@ -0,0 +1 @@ + diff --git a/src/backends/inspectdr.jl b/ext/PlotsInspectDR/inspectdr.jl similarity index 99% rename from src/backends/inspectdr.jl rename to ext/PlotsInspectDR/inspectdr.jl index 07a435fa0..b80b71952 100644 --- a/src/backends/inspectdr.jl +++ b/ext/PlotsInspectDR/inspectdr.jl @@ -96,7 +96,7 @@ function _inspectdr_getaxisticks(ticks, gridlines, xfrm) TickCustom = InspectDR.TickCustom _xfrm(coord) = InspectDR.axis2aloc(Float64(coord), xfrm.spec) #Ensure Float64 - in case - ttype = ticksType(ticks) + ttype = ticks_type(ticks) if ticks === :native # keep current elseif ttype === :ticks_and_labels diff --git a/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl b/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl new file mode 100644 index 000000000..db64fb1c0 --- /dev/null +++ b/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl @@ -0,0 +1,60 @@ +module PlotsPGFPlotsXExt + +using PGFPlotsX: PGFPlotsX +using LaTeXStrings: LaTeXString +using UUIDs: uuid4 +using Latexify: Latexify +using Contour: Contour # TODO: this could become its own extensionoo +using PlotUtils: PlotUtils, ColorGradient, color_list +using Printf: @sprintf + +using Plots: Plots, straightline_data, shape_data +# TODO: eliminate this list +using Plots: + bbox, + left, + right, + bottom, + width, + height, + labelfunc_tex, + top, + plotarea, + axis_drawing_info, + _guess_best_legend_position, + prepare_output, + current +using Plots: GridLayout +using RecipesPipeline: RecipesPipeline +using Plots.Arrows +using Plots.Axes +using Plots.Axes: has_ticks +using Plots.Annotations +using Plots.Colorbars +using Plots.Colors +using Plots.Commons +using Plots.Fonts +using Plots.Fonts: Font, PlotText +using Plots.PlotMeasures +using Plots.PlotsPlots +using Plots.PlotsSeries +using Plots.Subplots +using Plots.Surfaces +using Plots.Shapes +using Plots.Shapes: Shape +using Plots.Ticks + +import Plots: + _display, + _show, + _update_min_padding!, + labelfunc, + _create_backend_figure, + _series_added, + _update_plot_object, + pgfx_sanitize_string + +include("initialization.jl") +include("pgfplotsx.jl") + +end # module diff --git a/ext/PlotsPGFPlotsXExt/initialization.jl b/ext/PlotsPGFPlotsXExt/initialization.jl new file mode 100644 index 000000000..749a5c20c --- /dev/null +++ b/ext/PlotsPGFPlotsXExt/initialization.jl @@ -0,0 +1,218 @@ +# unrolling the old # init_backend macro by hand case by case +# this is not a macro for the backend maintainers and explicit control + +const package_str = "PGFPlotsX" +const str = "pgfplotsx" +const sym = :pgfplotsx + +struct PGFPlotsXBackend <: Plots.AbstractBackend end +const T = PGFPlotsXBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @info "Initializing $package_str backend in Plots; run `$str()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[T] = sym + + push!(Plots._initialized_backends, sym) + + # Additional setup required by the backend: + +end + +Plots.backend_name(::T) = sym +Plots.backend_package_name(::T) = Plots.backend_package_name(sym) + +const _pgfplotsx_attrs = Plots.merge_with_base_supported([ + :annotations, + :annotationrotation, + :annotationhalign, + :annotationfontsize, + :annotationfontfamily, + :annotationcolor, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :legend_foreground_color, + :foreground_color_grid, + :foreground_color_axis, + :foreground_color_text, + :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :line, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :bins, + :layout, + :title, + :window_title, + :guide, + :widen, + :lims, + :ticks, + :scale, + :flip, + :titlefontfamily, + :titlefontsize, + :titlefonthalign, + :titlefontvalign, + :titlefontrotation, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_halign, + :legend_font_valign, + :legend_font_rotation, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfonthalign, + :tickfontvalign, + :tickfontrotation, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefonthalign, + :guidefontvalign, + :guidefontrotation, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_titlefontsize, + :colorbar_titlefontcolor, + :colorbar_titlefontrotation, + :colorbar_entry, + :fill, + :fill_z, + :line_z, + :marker_z, + :levels, + :legend_column, + :legend_title, + :legend_title_font_color, + :legend_title_font_pointsize, + :ribbon, + :quiver, + :orientation, + :overwrite_figure, + :polar, + :plot_title, + :plot_titlefontcolor, + :plot_titlefontrotation, + :plot_titlefontsize, + :plot_titlevspan, + :aspect_ratio, + :normalize, + :weights, + :inset_subplots, + :bar_width, + :arrow, + :framestyle, + :tick_direction, + :thickness_scaling, + :camera, + :contour_labels, + :connections, + :thickness_scaling, + :axis, + :draw_arrow, + :minorgrid, + :minorgridalpha, + :minorgridlinewidth, + :minorgridstyle, + :minorticks, + :mirror, + :rotation, + :showaxis, + :tickfontrotation, + :draw_arrow, +]) +const _pgfplotsx_seriestypes = [ + :path, + :scatter, + :straightline, + :path3d, + :scatter3d, + :surface, + :wireframe, + :heatmap, + :mesh3d, + :contour, + :contour3d, + :quiver, + :shape, + :steppre, + :stepmid, + :steppost, + :ysticks, + :xsticks, +] +const _pgfplotsx_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _pgfplotsx_markers = [ + :none, + :auto, + :circle, + :rect, + :diamond, + :utriangle, + :dtriangle, + :ltriangle, + :rtriangle, + :cross, + :xcross, + :x, + :+, + :star5, + :star6, + :pentagon, + :hline, + :vline, +] +const _pgfplotsx_scales = [:identity, :ln, :log2, :log10] +Plots.is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true + +# additional constants +const _pgfplotsx_series_ids = KW() + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + eval(quote + Plots.$f1(::T, $s::Symbol) = $s in $v + Plots.$f2(::T) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- diff --git a/src/backends/pgfplotsx.jl b/ext/PlotsPGFPlotsXExt/pgfplotsx.jl similarity index 99% rename from src/backends/pgfplotsx.jl rename to ext/PlotsPGFPlotsXExt/pgfplotsx.jl index 28479b1c8..8268841af 100644 --- a/src/backends/pgfplotsx.jl +++ b/ext/PlotsPGFPlotsXExt/pgfplotsx.jl @@ -1054,17 +1054,17 @@ function pgfx_fillrange_series!(axis, series, series_func, i, fillrange, rng) else opt[:x][rng], opt[:y][rng] end - return push!(axis, PGFPlotsX.PlotInc(fr_opt, pgfx_fillrange_args(fillrange, args...))) + return push!(axis, PGFPlotsX.PlotInc(fr_opt, pgfx_fillrange_attrs(fillrange, args...))) end -function pgfx_fillrange_args(fillrange, x, y) +function pgfx_fillrange_attrs(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 PGFPlotsX.Coordinates(x_fill, y_fill) end -function pgfx_fillrange_args(fillrange, x, y, z) +function pgfx_fillrange_attrs(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]] diff --git a/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl b/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl new file mode 100644 index 000000000..8abde206f --- /dev/null +++ b/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl @@ -0,0 +1,12 @@ +module PlotsPlotlyJSExt + +using PlotlyJS: PlotlyJS +using Plots.Commons +using Plots.Plotly +using Plots.PlotsPlots +import Plots: _show, _display, closeall, current, isplotnull + +include("initialization.jl") +include("plotlyjs.jl") + +end # module diff --git a/ext/PlotsPlotlyJSExt/initialization.jl b/ext/PlotsPlotlyJSExt/initialization.jl new file mode 100644 index 000000000..34d8077f4 --- /dev/null +++ b/ext/PlotsPlotlyJSExt/initialization.jl @@ -0,0 +1,53 @@ +# unrolling the old # init_backend macro by hand case by case +# this is not a macro for the backend maintainers and explicit control + +const package_str = "PlotlyJS" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct PlotlyJSBackend <: Plots.AbstractBackend end +const T = PlotlyJSBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @info "Initializing $package_str backend in Plots; run `$str()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[T] = sym + + push!(Plots._initialized_backends, sym) + + # Additional setup required by the backend: + +end + +Plots.backend_name(::T) = sym +Plots.backend_package_name(::T) = Plots.backend_package_name(sym) + +const _plotlyjs_attrs = Plots.Plotly._plotly_attrs +const _plotlyjs_seriestypes = Plots.Plotly._plotly_seriestypes +const _plotlyjs_styles = Plots.Plotly._plotly_styles +const _plotlyjs_markers = Plots.Plotly._plotly_markers +const _plotlyjs_scales = Plots.Plotly._plotly_scales + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + eval(quote + Plots.$f1(::T, $s::Symbol) = $s in $v + Plots.$f2(::T) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- diff --git a/src/backends/plotlyjs.jl b/ext/PlotsPlotlyJSExt/plotlyjs.jl similarity index 98% rename from src/backends/plotlyjs.jl rename to ext/PlotsPlotlyJSExt/plotlyjs.jl index b5021087f..0ae9b83df 100644 --- a/src/backends/plotlyjs.jl +++ b/ext/PlotsPlotlyJSExt/plotlyjs.jl @@ -1,8 +1,6 @@ # https://github.com/JuliaPlots/PlotlyJS.jl # ------------------------------------------------------------------------------ -include(_path(:plotly)) - function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) plt[:overwrite_figure] && closeall() plt.o = PlotlyJS.plot() diff --git a/ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl b/ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl new file mode 100644 index 000000000..7e6f022a2 --- /dev/null +++ b/ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl @@ -0,0 +1,31 @@ +module PlotsPlotlyKaleidoExt + +using PlotlyKaleido + +using Plots: Plots, Plot, PlotlyBackend, plotly_show_js +import Plots: _show + +function __init__() + PlotlyKaleido.start() + atexit() do + PlotlyKaleido.kill_kaleido() + end +end + +for (mime, fmt) in ( + "application/pdf" => "pdf", + "image/png" => "png", + "image/svg+xml" => "svg", + "image/eps" => "eps", +) + @eval Plots._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyBackend}) = + PlotlyKaleido.savefig( + io, + sprint(io -> plotly_show_js(io, plt)), + height = plt[:size][2], + width = plt[:size][1], + format = $fmt, + ) +end + +end # module diff --git a/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl b/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl new file mode 100644 index 000000000..13cf65244 --- /dev/null +++ b/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl @@ -0,0 +1,84 @@ +module PlotsPythonPlotExt + +import Plots: + _before_layout_calcs, + _create_backend_figure, + _display, + _show, + _update_min_padding!, + _update_plot_object, + closeall, + is_marker_supported, + labelfunc + +using NaNMath: NaNMath +using Plots.Annotations +using Plots.Arrows +using Plots.Axes +using Plots.Colorbars +using Plots.Colorbars: cbar_fill, cbar_gradient, cbar_lines +using Plots.Colors +using Plots.Commons +using Plots.Commons: _all_markers, _3dTypes, single_color +using Plots.Fonts +using Plots.Fonts: Font, PlotText +using Plots.PlotMeasures +using Plots.PlotMeasures: px2inch +using Plots.PlotUtils: PlotUtils, ColorGradient, plot_color, color_list, cgrad +using Plots.PlotsPlots +using Plots.PlotsSeries +using Plots.Shapes +using Plots.Shapes: Shape +using Plots.Subplots +using Plots.Ticks +using Plots.Ticks: no_minor_intervals +using Plots: + DPI, + Plots, + Surface, + _cycle, + _guess_best_legend_position, + axis_drawing_info, + axis_drawing_info_3d, + bbox, + bottom, + convert_to_polar, + heatmap_edges, + is3d, + is_2tuple, + is_uniformly_spaced, + isautop, + isortho, + labelfunc_tex, + mesh3d_triangles, + left, + merge_with_base_supported, + plotarea, + right, + shape_data, + straightline_data, + top, + isscalar, + isvector, + supported_scales, + ticks_type, + legend_angle, + legend_anchor_index, + legend_pos_from_angle, + width, + ispositive, + height, + bbox_to_pcts +using PythonPlot: PythonPlot + +const PythonCall = PythonPlot.PythonCall +const mpl_toolkits = PythonCall.pynew() # PythonCall.pyimport("mpl_toolkits") +const mpl = PythonPlot.matplotlib +const numpy = PythonCall.pynew() # PythonCall.pyimport("numpy") + +using RecipesPipeline: RecipesPipeline + +include("initialization.jl") +include("pythonplot.jl") + +end # module diff --git a/ext/PlotsPythonPlotExt/initialization.jl b/ext/PlotsPythonPlotExt/initialization.jl new file mode 100644 index 000000000..29af0c990 --- /dev/null +++ b/ext/PlotsPythonPlotExt/initialization.jl @@ -0,0 +1,192 @@ +import Plots: backend_name, backend_package_name, is_marker_supported + +# unrolling the old # init_backend macro by hand case by case +const package_str = "PythonPlot" +const str = "pythonplot" +const sym = :pythonplot + +struct PythonPlotBackend <: Plots.AbstractBackend end +const T = PythonPlotBackend + +get_concrete_backend() = T + +function __init__() + @info "Initializing $package_str backend in Plots; run `$str()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[T] = sym + + push!(Plots._initialized_backends, sym) + + if PythonPlot.version < v"3.4" + @warn """You are using Matplotlib $(PythonPlot.version), which is no longer + officially supported by the Plots community. To ensure smooth Plots.jl + integration update your Matplotlib library to a version ≥ 3.4.0 + """ + end + + # PythonCall.pycopy!(mpl, PythonCall.pyimport("matplotlib")) + PythonCall.pycopy!(mpl_toolkits, PythonCall.pyimport("mpl_toolkits")) + PythonCall.pycopy!(numpy, PythonCall.pyimport("numpy")) + # PythonCall.pyimport("mpl_toolkits.axes_grid1") + numpy.seterr(invalid = "ignore") + PythonPlot.ioff() # we don't want every command to update the figure +end +# Make pythonplot known to Plots +backend_name(::T) = sym +backend_package_name(::T) = backend_package_name(sym) + +const _pythonplot_attrs = merge_with_base_supported([ + :annotations, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :foreground_color_grid, + :legend_foreground_color, + :foreground_color_title, + :foreground_color_axis, + :foreground_color_border, + :foreground_color_guide, + :foreground_color_text, + :label, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :fillstyle, + :bins, + :bar_width, + :bar_edges, + :bar_position, + :title, + :titlelocation, + :titlefont, + :window_title, + :guide, + :guide_position, + :widen, + :lims, + :ticks, + :scale, + :flip, + :rotation, + :titlefontfamily, + :titlefontsize, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_entry, + :colorbar_ticks, + :colorbar_tickfontfamily, + :colorbar_tickfontsize, + :colorbar_tickfonthalign, + :colorbar_tickfontvalign, + :colorbar_tickfontrotation, + :colorbar_tickfontcolor, + :colorbar_titlefontcolor, + :colorbar_titlefontsize, + :colorbar_scale, + :marker_z, + :line, + :line_z, + :fill, + :fill_z, + :fontfamily, + :fontfamily_subplot, + :legend_column, + :legend_font, + :legend_title, + :legend_title_font_color, + :legend_title_font_family, + :legend_title_font_pointsize, + :levels, + :ribbon, + :quiver, + :arrow, + :orientation, + :overwrite_figure, + :polar, + :normalize, + :weights, + :contours, + :aspect_ratio, + :clims, + :inset_subplots, + :dpi, + :stride, + :framestyle, + :tick_direction, + :camera, + :contour_labels, + :connections, +]) + +const _pythonplot_seriestypes = [ + :path, + :steppre, + :stepmid, + :steppost, + :shape, + :straightline, + :scatter, + :hexbin, + :heatmap, + :image, + :contour, + :contour3d, + :path3d, + :scatter3d, + :mesh3d, + :surface, + :wireframe, +] + +const _pythonplot_styles = [:auto, :solid, :dash, :dot, :dashdot] +const _pythonplot_markers = vcat(_all_markers, :pixel) +const _pythonplot_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + eval(quote + Plots.$f1(::T, $s::Symbol) = $s in $v + Plots.$f2(::T) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- diff --git a/src/backends/pythonplot.jl b/ext/PlotsPythonPlotExt/pythonplot.jl similarity index 98% rename from src/backends/pythonplot.jl rename to ext/PlotsPythonPlotExt/pythonplot.jl index 421c11fc9..000dccc97 100644 --- a/src/backends/pythonplot.jl +++ b/ext/PlotsPythonPlotExt/pythonplot.jl @@ -8,18 +8,11 @@ let otherdisplays = splice!(Base.Multimedia.displays, 2:length(Base.Multimedia.d append!(Base.Multimedia.displays, otherdisplays) end -if PythonPlot.version < v"3.4" - @warn """You are using Matplotlib $(PythonPlot.version), which is no longer - officially supported by the Plots community. To ensure smooth Plots.jl - integration update your Matplotlib library to a version ≥ 3.4.0 - """ -end - for k in (:linthresh, :base, :label) # add PythonPlot specific symbols to cache - _attrsymbolcache[k] = Dict{Symbol,Symbol}() + Commons._attrsymbolcache[k] = Dict{Symbol,Symbol}() for letter in (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) - _attrsymbolcache[k][letter] = Symbol(k, letter) + Commons._attrsymbolcache[k][letter] = Symbol(k, letter) end end @@ -424,9 +417,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) if series[:markershape] !== :none && st ∈ _py_marker_series for segment in series_segments(series, :scatter) i, rng = segment.attr_index, segment.range - args = if st === :bar && !isvertical(series) - y[rng], x[rng] - else + args = if st === :bar x[rng], y[rng] end RecipesPipeline.is3d(sp) && (args = (args..., z[rng])) @@ -705,11 +696,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) if (fillrange = series[:fillrange]) !== nothing && st !== :contour 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 - :fill_betweenx, y[rng], x[rng] - end + f, dim1, dim2 = :fill_between, x[rng], y[rng] n = length(dim1) args = if typeof(fillrange) <: Union{Real,AVec} dim1, _cycle(fillrange, rng), dim2 @@ -764,7 +751,7 @@ function _py_set_ticks(sp, ax, ticks, letter) return end - tick_values, tick_labels = if (ttype = ticksType(ticks)) === :ticks + tick_values, tick_labels = if (ttype = ticks_type(ticks)) === :ticks ticks, [] elseif ttype === :ticks_and_labels ticks @@ -796,7 +783,7 @@ function _py_set_scale(ax, sp::Subplot, scale::Symbol, letter::Symbol) else "symlog", KW( - get_attr_symbol(:base, Symbol()) => _logScaleBases[scale], + get_attr_symbol(:base, Symbol()) => _log_scale_bases[scale], get_attr_symbol(:linthresh, Symbol()) => NaNMath.max( 1e-16, _py_compute_axis_minval(sp, sp[get_attr_symbol(letter, :axis)]), @@ -1070,7 +1057,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend}) ticks = framestyle === :none ? nothing : get_ticks(sp, axis) has_major_ticks = ticks !== :none && ticks !== nothing && ticks !== false - has_major_ticks &= if (ttype = ticksType(ticks)) === :ticks + has_major_ticks &= if (ttype = ticks_type(ticks)) === :ticks length(ticks) > 0 elseif ttype === :ticks_and_labels tcs, labs = ticks @@ -1155,7 +1142,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend}) mpl.ticker.AutoMinorLocator(n_minor_intervals) else mpl.ticker.LogLocator( - base = _logScaleBases[scale], + base = _log_scale_bases[scale], subs = 1:n_minor_intervals, ) end |> pyaxis.set_minor_locator diff --git a/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl b/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl new file mode 100644 index 000000000..6d251d60e --- /dev/null +++ b/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl @@ -0,0 +1,41 @@ +module PlotsUnicodePlotsExt + +using UnicodePlots +using Plots: Plots, isijulia, texmath2unicode, straightline_data, shape_data +# TODO: eliminate this list +using Plots: + bbox, + left, + right, + bottom, + top, + plotarea, + axis_drawing_info, + mesh3d_triangles, + _guess_best_legend_position, + prepare_output +using Plots: GridLayout +using RecipesPipeline: RecipesPipeline +using Plots.Arrows +using Plots.Axes +using Plots.Axes: has_ticks +using Plots.Annotations +using Plots.Colorbars +using Plots.Colors +using Plots.Commons +using Plots.Fonts +using Plots.Fonts: Font, PlotText +using Plots.PlotMeasures +using Plots.PlotsPlots +using Plots.PlotsSeries +using Plots.Subplots +using Plots.Shapes +using Plots.Shapes: Shape +using Plots.Ticks + +import Plots: _before_layout_calcs, _display, _show + +include("initialization.jl") +include("unicodeplots.jl") + +end # module diff --git a/ext/PlotsUnicodePlotsExt/initialization.jl b/ext/PlotsUnicodePlotsExt/initialization.jl new file mode 100644 index 000000000..4752da423 --- /dev/null +++ b/ext/PlotsUnicodePlotsExt/initialization.jl @@ -0,0 +1,118 @@ +# unrolling the old # init_backend macro by hand case by case + +const package_str = "UnicodePlots" +const str = "unicodeplots" +const sym = :unicodeplots + +struct UnicodePlotsBackend <: Plots.AbstractBackend end +const T = UnicodePlotsBackend + +get_concrete_backend() = UnicodePlotsBackend # opposite to abstract + +function __init__() + @info "Initializing $package_str backend in Plots; run `$str()` to activate it." + Plots._backendType[sym] = get_concrete_backend() + Plots._backendSymbol[T] = sym + + push!(Plots._initialized_backends, sym) +end +# Make unicodeplots know to Plots +Plots.backend_name(::UnicodePlotsBackend) = sym +Plots.backend_package_name(::UnicodePlotsBackend) = Plots.backend_package_name(sym) + +const _unicodeplots_attrs = Plots.merge_with_base_supported([ + :annotations, + :bins, + :guide, + :widen, + :grid, + :label, + :layout, + :legend, + :legend_title_font_color, + :lims, + :line, + :linealpha, + :linecolor, + :linestyle, + :markershape, + :plot_title, + :quiver, + :arrow, + :seriesalpha, + :seriescolor, + :scale, + :flip, + :title, + # :marker_z, + :line_z, +]) +const _unicodeplots_seriestypes = [ + :path, + :path3d, + :scatter, + :scatter3d, + :straightline, + # :bar, + :shape, + :histogram2d, + :heatmap, + :contour, + # :contour3d, + :image, + :spy, + :surface, + :wireframe, + :mesh3d, +] +const _unicodeplots_styles = [:auto, :solid] +const _unicodeplots_markers = [ + :none, + :auto, + :pixel, + # vvvvvvvvvv shapes + :circle, + :rect, + :star5, + :diamond, + :hexagon, + :cross, + :xcross, + :utriangle, + :dtriangle, + :rtriangle, + :ltriangle, + :pentagon, + # :heptagon, + # :octagon, + :star4, + :star6, + # :star7, + :star8, + :vline, + :hline, + :+, + :x, +] +const _unicodeplots_scales = [:identity, :ln, :log2, :log10] +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + eval(quote + Plots.$f1(::UnicodePlotsBackend, $s::Symbol) = $s in $v + Plots.$f2(::UnicodePlotsBackend) = sort(collect($v)) + end) +end + +## results in: +# Plots.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- diff --git a/src/backends/unicodeplots.jl b/ext/PlotsUnicodePlotsExt/unicodeplots.jl similarity index 96% rename from src/backends/unicodeplots.jl rename to ext/PlotsUnicodePlotsExt/unicodeplots.jl index 057c68d09..469045f49 100644 --- a/src/backends/unicodeplots.jl +++ b/ext/PlotsUnicodePlotsExt/unicodeplots.jl @@ -12,7 +12,7 @@ const _canvas_map = ( should_warn_on_unsupported(::UnicodePlotsBackend) = false -function _before_layout_calcs(plt::Plot{UnicodePlotsBackend}) +function _before_layout_calcs(plt::Plots.Plot{UnicodePlotsBackend}) plt.o = UnicodePlots.Plot[] up_width = UnicodePlots.DEFAULT_WIDTH[] up_height = UnicodePlots.DEFAULT_HEIGHT[] @@ -276,7 +276,7 @@ end # ------------------------------------------------------------------------------------------ -function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) +function _show(io::IO, ::MIME"image/png", plt::Plots.Plot{UnicodePlotsBackend}) applicable(UnicodePlots.save_image, io) || "Plots(UnicodePlots): saving to `.png` requires `import FreeType, FileIO`" |> ArgumentError |> @@ -321,11 +321,11 @@ function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) nothing end -Base.show(plt::Plot{UnicodePlotsBackend}) = show(stdout, plt) -Base.show(io::IO, plt::Plot{UnicodePlotsBackend}) = _show(io, MIME("text/plain"), plt) +Base.show(plt::Plots.Plot{UnicodePlotsBackend}) = show(stdout, plt) +Base.show(io::IO, plt::Plots.Plot{UnicodePlotsBackend}) = _show(io, MIME("text/plain"), plt) # NOTE: _show(...) must be kept for Base.showable (src/output.jl) -function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) +function _show(io::IO, ::MIME"text/plain", plt::Plots.Plot{UnicodePlotsBackend}) prepare_output(plt) nr, nc = size(plt.layout) if nr == 1 && nc == 1 # fast path @@ -386,7 +386,7 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) end # we only support MIME"text/plain", hence display(...) falls back to plain-text on stdout -function _display(plt::Plot{UnicodePlotsBackend}) +function _display(plt::Plots.Plot{UnicodePlotsBackend}) show(stdout, plt) println(stdout) end diff --git a/ext/UnitfulExt.jl b/ext/UnitfulExt.jl index 34b2fc950..3eb13aabc 100644 --- a/ext/UnitfulExt.jl +++ b/ext/UnitfulExt.jl @@ -218,7 +218,7 @@ append_unit_if_needed!(attr, key, u) = append_unit_if_needed!(attr, key, label::ProtectedString, u) = nothing append_unit_if_needed!(attr, key, label::UnitfulString, u) = nothing function append_unit_if_needed!(attr, key, label::Nothing, u) - attr[key] = if attr[:plot_object].backend == Plots.PGFPlotsXBackend() + attr[key] = if attr[:plot_object].backend == Plots._backend_instance(:pgfplotsx) UnitfulString(LaTeXString(latexify(u)), u) else UnitfulString(string(u), u) @@ -226,7 +226,7 @@ function append_unit_if_needed!(attr, key, label::Nothing, u) end function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString} isempty(label) && return attr[key] = UnitfulString(label, u) - if attr[:plot_object].backend == Plots.PGFPlotsXBackend() + if attr[:plot_object].backend == Plots._backend_instance(:pgfplotsx) attr[key] = UnitfulString( LaTeXString( format_unit_label( @@ -323,9 +323,9 @@ end #==================# Plots._transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Quantity} = _ustrip.(getaxisunit(axis), ticks) -Plots.process_limits(lims::AbstractArray{T}, axis) where {T<:Quantity} = +Plots.Axes.process_limits(lims::AbstractArray{T}, axis) where {T<:Quantity} = _ustrip.(getaxisunit(axis), lims) -Plots.process_limits(lims::Tuple{S,T}, axis) where {S<:Quantity,T<:Quantity} = +Plots.Axes.process_limits(lims::Tuple{S,T}, axis) where {S<:Quantity,T<:Quantity} = _ustrip.(getaxisunit(axis), lims) function _ustrip(u, x) diff --git a/src/Annotations.jl b/src/Annotations.jl new file mode 100644 index 000000000..fe1e838ed --- /dev/null +++ b/src/Annotations.jl @@ -0,0 +1,254 @@ +# internal module +module Annotations + +using ..Plots.Commons +using ..Plots.Dates +using ..Plots.Fonts: Font, PlotText, text, font +using ..Plots.Shapes: Shape, _shapes +using ..Plots: Series, Subplot, TimeType, Length +using ..Plots.PlotMeasures: pct +using ..Plots: is_2tuple, is3d, discrete_value! +export EachAnn, + series_annotations, + series_annotations_shapes!, + process_annotation, + locate_annotation, + annotations, + assign_annotation_coord! + +mutable struct SeriesAnnotations + strs::AVec # the labels/names + font::Font + baseshape::Union{Shape,AVec{Shape},Nothing} + scalefactor::Tuple +end + +_text_label(lab::Tuple, font) = text(lab[1], font, lab[2:end]...) +_text_label(lab::PlotText, font) = lab +_text_label(lab, font) = text(lab, font) + +series_annotations(scalar) = series_annotations([scalar]) +series_annotations(anns::SeriesAnnotations) = anns +series_annotations(::Nothing) = nothing + +function series_annotations(anns::AMat{SeriesAnnotations}) + @assert size(anns, 1) == 1 "matrix of SeriesAnnotations must be a row vector" + anns +end + +function series_annotations(anns::AMat, outer_attrs...) + # Types that represent annotations for an entire series + whole_series = Union{AVec,Tuple{AVec,Vararg{Any}}} + + # whole_series types can only be in a row vector + if size(anns, 1) > 1 + for ann in Iterators.filter(ann -> ann isa whole_series, anns) + "Given series annotation must be the only element in its column:\n$ann" |> + ArgumentError |> + throw + end + end + + ann_vec = map(eachcol(anns)) do col + ann = first(col) isa whole_series ? first(col) : col + + # Override arguments from outer tuple with args from inner tuple + strs, inner_attrs = Iterators.peel(wraptuple(ann)) + series_annotations(strs, outer_attrs..., inner_attrs...) + end + + permutedims(ann_vec) +end + +function series_annotations(strs::AVec, args...) + fnt = font() + shp = nothing + scalefactor = 1, 1 + for arg in args + if isa(arg, Shape) || (isa(arg, AVec) && eltype(arg) == Shape) + shp = arg + elseif isa(arg, Font) + fnt = arg + elseif isa(arg, Symbol) && haskey(_shapes, arg) + shp = _shapes[arg] + elseif isa(arg, Number) + scalefactor = arg, arg + elseif is_2tuple(arg) + scalefactor = arg + elseif isa(arg, AVec) + strs = collect(zip(strs, arg)) + else + @warn "Unused SeriesAnnotations arg: $arg ($(typeof(arg)))" + end + end + SeriesAnnotations(map(s -> _text_label(s, fnt), strs), fnt, shp, scalefactor) +end + +function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) + anns = series[:series_annotations] + + if anns !== nothing && anns.baseshape !== nothing + # we use baseshape to overwrite the markershape attribute + # with a list of custom shapes for each + msw, msh = anns.scalefactor + msize = Float64[] + shapes = Vector{Shape}(undef, length(anns.strs)) + for i in eachindex(anns.strs) + str = _cycle(anns.strs, i) + + # get the width and height of the string (in mm) + sw, sh = text_size(str, anns.font.pointsize) + + # how much to scale the base shape? + # note: it's a rough assumption that the shape fills the unit box [-1, -1, 1, 1], + # so we scale the length-2 shape by 1/2 the total length + scalar = backend() == PyPlotBackend() ? 1.7 : 1.0 + xscale = 0.5to_pixels(sw) * scalar + yscale = 0.5to_pixels(sh) * scalar + + # we save the size of the larger direction to the markersize list, + # and then re-scale a copy of baseshape to match the w/h ratio + maxscale = max(xscale, yscale) + push!(msize, maxscale) + baseshape = _cycle(anns.baseshape, i) + shapes[i] = + scale(baseshape, msw * xscale / maxscale, msh * yscale / maxscale, (0, 0)) + end + series[:markershape] = shapes + series[:markersize] = msize + end + nothing +end + +mutable struct EachAnn + anns + x + y +end + +function Base.iterate(ea::EachAnn, i = 1) + (ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y)) && return + + tmp = _cycle(ea.anns.strs, i) + str, fnt = if isa(tmp, PlotText) + tmp.str, tmp.font + else + tmp, ea.anns.font + end + (_cycle(ea.x, i), _cycle(ea.y, i), str, fnt), i + 1 +end + +# ----------------------------------------------------------------------- +annotations(anns::AMat) = map(annotations, anns) +annotations(sa::SeriesAnnotations) = sa +annotations(anns::AVec) = anns +annotations(anns) = Any[anns] +annotations(::Nothing) = [] + +_annotationfont(sp::Subplot) = font(; + family = sp[:annotationfontfamily], + pointsize = sp[:annotationfontsize], + halign = sp[:annotationhalign], + valign = sp[:annotationvalign], + rotation = sp[:annotationrotation], + color = sp[:annotationcolor], +) + +_annotation(sp::Subplot, font, lab, pos...; alphabet = "abcdefghijklmnopqrstuvwxyz") = ( + pos..., + lab === :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : + _text_label(lab, font), +) + +assign_annotation_coord!(axis, x) = discrete_value!(axis, x)[1] +assign_annotation_coord!(axis, x::TimeType) = assign_annotation_coord!(axis, Dates.value(x)) + +_annotation_coords(pos::Symbol) = get(Commons._position_aliases, pos, pos) +_annotation_coords(pos) = pos + +function _process_annotation_2d(sp::Subplot, x, y, lab, font = _annotationfont(sp)) + x = assign_annotation_coord!(sp[:xaxis], x) + y = assign_annotation_coord!(sp[:yaxis], y) + _annotation(sp, font, lab, x, y) +end + +_process_annotation_2d( + sp::Subplot, + pos::Union{Tuple,Symbol}, + lab, + font = _annotationfont(sp), +) = _annotation(sp, font, lab, _annotation_coords(pos)) + +function _process_annotation_3d(sp::Subplot, x, y, z, lab, font = _annotationfont(sp)) + x = assign_annotation_coord!(sp[:xaxis], x) + y = assign_annotation_coord!(sp[:yaxis], y) + z = assign_annotation_coord!(sp[:zaxis], z) + _annotation(sp, font, lab, x, y, z) +end + +_process_annotation_3d( + sp::Subplot, + pos::Union{Tuple,Symbol}, + lab, + font = _annotationfont(sp), +) = _annotation(sp, font, lab, _annotation_coords(pos)) + +function _process_annotation(sp::Subplot, ann, annotation_processor::Function) + ann = makevec.(ann) + [annotation_processor(sp, _cycle.(ann, i)...) for i in 1:maximum(length.(ann))] +end + +# Expand arrays of coordinates, positions and labels into individual annotations +# and make sure labels are of type PlotText +process_annotation(sp::Subplot, ann) = + _process_annotation(sp, ann, is3d(sp) ? _process_annotation_3d : _process_annotation_2d) + +function _relative_position(xmin, xmax, pos::Length{:pct}, scale::Symbol) + # !TODO Add more scales in the future (asinh, sqrt) ? + if scale === :log || scale === :ln + exp(log(xmin) + pos.value * log(xmax / xmin)) + elseif scale === :log10 + exp10(log10(xmin) + pos.value * log10(xmax / xmin)) + elseif scale === :log2 + exp2(log2(xmin) + pos.value * log2(xmax / xmin)) + else # :identity (linear scale) + xmin + pos.value * (xmax - xmin) + end +end + +# annotation coordinates in pct +const position_multiplier = Dict( + :N => (0.5, 0.9), + :NE => (0.9, 0.9), + :E => (0.9, 0.5), + :SE => (0.9, 0.1), + :S => (0.5, 0.1), + :SW => (0.1, 0.1), + :W => (0.1, 0.5), + :NW => (0.1, 0.9), + :topleft => (0.1, 0.9), + :topcenter => (0.5, 0.9), + :topright => (0.9, 0.9), + :bottomleft => (0.1, 0.1), + :bottomcenter => (0.5, 0.1), + :bottomright => (0.9, 0.1), +) + +# Give each annotation coordinates based on specified position +locate_annotation(sp::Subplot, rel::Tuple, label::PlotText) = ( + map(1:length(rel), (:x, :y, :z)) do i, letter + _relative_position( + axis_limits(sp, letter)..., + rel[i] * pct, + sp[get_attr_symbol(letter, :axis)][:scale], + ) + end..., + label, +) + +locate_annotation(sp::Subplot, x, y, label::PlotText) = (x, y, label) +locate_annotation(sp::Subplot, x, y, z, label::PlotText) = (x, y, z, label) +locate_annotation(sp::Subplot, pos::Symbol, label::PlotText) = + locate_annotation(sp, position_multiplier[pos], label) + +end # Annotations diff --git a/src/Arrows.jl b/src/Arrows.jl new file mode 100644 index 000000000..13465aee2 --- /dev/null +++ b/src/Arrows.jl @@ -0,0 +1,62 @@ +module Arrows + +using ..Plots.Commons +export Arrow, arrow, add_arrows + +# style is :open or :closed (for now) +struct Arrow + style::Symbol + side::Symbol # :head (default), :tail, or :both + headlength::Float64 + headwidth::Float64 +end + +""" + arrow(args...) + +Define arrowheads to apply to lines - args are `style` (`:open` or `:closed`), +`side` (`:head`, `:tail` or `:both`), `headlength` and `headwidth` +""" +function arrow(args...) + style, side = :simple, :head + headlength = headwidth = 0.3 + setlength = false + for arg in args + T = typeof(arg) + if T == Symbol + if arg in (:head, :tail, :both) + side = arg + else + style = arg + end + elseif T <: Number + # first we apply to both, but if there's more, then only change width after the first number + headwidth = Float64(arg) + if !setlength + headlength = headwidth + end + setlength = true + elseif T <: Tuple && length(arg) == 2 + headlength, headwidth = Float64(arg[1]), Float64(arg[2]) + else + @warn "Skipped arrow arg $arg" + end + end + Arrow(style, side, headlength, headwidth) +end + +# allow for do-block notation which gets called on every valid start/end pair which +# we need to draw an arrow +function add_arrows(func::Function, x::AVec, y::AVec) + for i in 2:length(x) + xyprev = (x[i - 1], y[i - 1]) + xy = (x[i], y[i]) + if ok(xyprev) && ok(xy) + if i == length(x) || !ok(x[i + 1], y[i + 1]) + # add the arrow from xyprev to xy + func(xyprev, xy) + end + end + end +end +end # Arrows diff --git a/src/Axes.jl b/src/Axes.jl new file mode 100644 index 000000000..d239b52d4 --- /dev/null +++ b/src/Axes.jl @@ -0,0 +1,463 @@ + +module Axes + +export Axis, tickfont, guidefont, widen_factor, scale_inverse_scale_func +export sort_3d_axes, axes_letters, process_axis_arg! +import Plots: get_ticks +using Plots: Plots, RecipesPipeline, Subplot, DefaultsDict, TimeType +using Plots.Commons: _axis_defaults_byletter, _all_axis_attrs, dumpdict +using Plots.Commons +using Plots.Ticks +using Plots.Fonts + +const default_widen_factor = Ref(1.06) +const _widen_seriestypes = ( + :line, + :path, + :steppre, + :stepmid, + :steppost, + :sticks, + :scatter, + :barbins, + :barhist, + :histogram, + :scatterbins, + :scatterhist, + :stepbins, + :stephist, + :bins2d, + :histogram2d, + :bar, + :shape, + :path3d, + :scatter3d, +) + +# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place +mutable struct Axis + sps::Vector{Subplot} + plotattributes::DefaultsDict +end +function Axis(sp::Subplot, letter::Symbol, args...; kw...) + explicit = KW( + :letter => letter, + :extrema => Extrema(), + :discrete_map => Dict(), # map discrete values to discrete indices + :continuous_values => zeros(0), + :discrete_values => [], + :use_minor => false, + :show => true, # show or hide the axis? (useful for linked subplots) + ) + + attr = DefaultsDict(explicit, _axis_defaults_byletter[letter]) + + # update the defaults + attr!(Axis([sp], attr), args...; kw...) +end + +# properly retrieve from axis.attr, passing `:match` to the correct key +Base.getindex(axis::Axis, k::Symbol) = + if (v = axis.plotattributes[k]) === :match + if haskey(Commons.Commons._match_map2, k) + axis.sps[1][Commons.Commons._match_map2[k]] + else + axis[Commons._match_map[k]] + end + else + v + end +Base.setindex!(axis::Axis, v, k::Symbol) = (axis.plotattributes[k] = v) +Base.get(axis::Axis, k::Symbol, v) = get(axis.plotattributes, k, v) + +mutable struct Extrema + emin::Float64 + emax::Float64 +end + +Extrema() = Extrema(Inf, -Inf) +# ------------------------------------------------------------------------- +sort_3d_axes(x, y, z, letter) = + if letter === :x + x, y, z + elseif letter === :y + y, x, z + else + z, y, x + end + +axes_letters(sp, letter) = + if RecipesPipeline.is3d(sp) + sort_3d_axes(:x, :y, :z, letter) + else + letter === :x ? (:x, :y) : (:y, :x) + end + +scale_inverse_scale_func(scale::Symbol) = ( + RecipesPipeline.scale_func(scale), + RecipesPipeline.inverse_scale_func(scale), + scale === :identity, +) +function get_axis(sp::Subplot, letter::Symbol) + axissym = get_attr_symbol(letter, :axis) + if haskey(sp.attr, axissym) + sp.attr[axissym] + else + sp.attr[axissym] = Axis(sp, letter) + end::Axis +end + +function Commons.axis_limits( + sp, + letter, + lims_factor = widen_factor(get_axis(sp, letter)), + consider_aspect = true, +) + axis = get_axis(sp, letter) + ex = axis[:extrema] + amin, amax = ex.emin, ex.emax + lims = process_limits(axis[:lims], axis) + lims === nothing && warn_invalid_limits(axis[:lims], letter) + + if (has_user_lims = lims isa Tuple) + lmin, lmax = lims + if lmin isa Number && isfinite(lmin) + amin = lmin + elseif lmin isa Symbol + lmin === :auto || @warn "Invalid min $(letter)limit" lmin + end + if lmax isa Number && isfinite(lmax) + amax = lmax + elseif lmax isa Symbol + lmax === :auto || @warn "Invalid max $(letter)limit" lmax + end + end + if lims === :symmetric + amax = max(abs(amin), abs(amax)) + amin = -amax + end + if amax ≤ amin && isfinite(amin) + amax = amin + 1.0 + end + if !isfinite(amin) && !isfinite(amax) + amin, amax = zero(amin), one(amax) + end + if ispolar(axis.sps[1]) + if axis[:letter] === :x + amin, amax = 0, 2π + elseif lims === :auto + # widen max radius so ticks dont overlap with theta axis + amin, amax = 0, amax + 0.1abs(amax - amin) + end + elseif lims_factor !== nothing + amin, amax = scale_lims(amin, amax, lims_factor, axis[:scale]) + elseif lims === :round + amin, amax = round_limits(amin, amax, axis[:scale]) + end + + aspect_ratio = get_aspect_ratio(sp) + if ( + !has_user_lims && + consider_aspect && + letter in (:x, :y) && + !(aspect_ratio === :none || RecipesPipeline.is3d(:sp)) + ) + aspect_ratio = aspect_ratio isa Number ? aspect_ratio : 1 + area = Plots.plotarea(sp) + plot_ratio = Plots.height(area) / Plots.width(area) + dist = amax - amin + + factor = if letter === :x + ydist, = axis_limits(sp, :y, widen_factor(sp[:yaxis]), false) |> collect |> diff + axis_ratio = aspect_ratio * ydist / dist + axis_ratio / plot_ratio + else + xdist, = axis_limits(sp, :x, widen_factor(sp[:xaxis]), false) |> collect |> diff + axis_ratio = aspect_ratio * dist / xdist + plot_ratio / axis_ratio + end + + if factor > 1 + center = (amin + amax) / 2 + amin = center + factor * (amin - center) + amax = center + factor * (amax - center) + end + end + + amin, amax +end + +""" +factor to widen axis limits by, or `nothing` if axis widening should be skipped +""" +function widen_factor(axis::Axis; factor = default_widen_factor[]) + if (widen = axis[:widen]) isa Bool + return widen ? factor : nothing + elseif widen isa Number + return widen + else + widen === :auto || @warn "Invalid value specified for `widen`: $widen" + end + + # automatic behavior: widen if limits aren't specified and series type is appropriate + lims = process_limits(axis[:lims], axis) + (lims isa Tuple || lims === :round) && return + for sp in axis.sps, series in series_list(sp) + series.plotattributes[:seriestype] in _widen_seriestypes && return factor + end + nothing +end + +function round_limits(amin, amax, scale) + base = get(_log_scale_bases, scale, 10.0) + factor = base^(1 - round(log(base, amax - amin))) + amin = floor(amin * factor) / factor + amax = ceil(amax * factor) / factor + amin, amax +end + +# NOTE: cannot use `NTuple` here ↓ +process_limits(lims::Tuple{<:Union{Symbol,Real},<:Union{Symbol,Real}}, axis) = lims +process_limits(lims::Symbol, axis) = lims +process_limits(lims::AVec, axis) = + length(lims) == 2 && all(map(x -> x isa Union{Symbol,Real}, lims)) ? Tuple(lims) : + nothing +process_limits(lims, axis) = nothing + +warn_invalid_limits(lims, letter) = @warn """ + Invalid limits for $letter axis. Limits should be a symbol, or a two-element tuple or vector of numbers. + $(letter)lims = $lims + """ +function scale_lims(from, to, factor) + mid, span = (from + to) / 2, (to - from) / 2 + mid .+ (-span, span) .* factor +end + +_scale_lims(::Val{true}, ::Function, ::Function, from, to, factor) = + scale_lims(from, to, factor) +_scale_lims(::Val{false}, f::Function, invf::Function, from, to, factor) = + invf.(scale_lims(f(from), f(to), factor)) + +function scale_lims(from, to, factor, scale) + f, invf, noop = scale_inverse_scale_func(scale) + _scale_lims(Val(noop), f, invf, from, to, factor) +end + +""" + scale_lims!([plt], [letter], factor) + +Scale the limits of the axis specified by `letter` (one of `:x`, `:y`, `:z`) by the +given `factor` around the limits' middle point. +If `letter` is omitted, all axes are affected. +""" +function scale_lims!(sp::Subplot, letter, factor) + axis = get_axis(sp, letter) + from, to = Plots.get_sp_lims(sp, letter) + axis[:lims] = scale_lims(from, to, factor, axis[:scale]) +end +scale_lims!(factor::Number) = scale_lims!(Plots.current(), factor) +scale_lims!(letter::Symbol, factor) = scale_lims!(Plots.current(), letter, factor) +#---------------------------------------------------------------------- +function process_axis_arg!(plotattributes::AKW, arg, letter = "") + T = typeof(arg) + arg = get(_scale_aliases, arg, arg) + if typeof(arg) <: Font + plotattributes[get_attr_symbol(letter, :tickfont)] = arg + plotattributes[get_attr_symbol(letter, :guidefont)] = arg + + elseif arg in _all_scales + plotattributes[get_attr_symbol(letter, :scale)] = arg + + elseif arg in (:flip, :invert, :inverted) + plotattributes[get_attr_symbol(letter, :flip)] = true + + elseif T <: AbstractString + plotattributes[get_attr_symbol(letter, :guide)] = arg + + # xlims/ylims + elseif (T <: Tuple || T <: AVec) && length(arg) == 2 + sym = typeof(arg[1]) <: Number ? :lims : :ticks + plotattributes[get_attr_symbol(letter, sym)] = arg + + # xticks/yticks + elseif T <: AVec + plotattributes[get_attr_symbol(letter, :ticks)] = arg + + elseif arg === nothing + plotattributes[get_attr_symbol(letter, :ticks)] = [] + + elseif T <: Bool || arg in Commons._all_showaxis_attrs + plotattributes[get_attr_symbol(letter, :showaxis)] = Commons.showaxis(arg, letter) + + elseif typeof(arg) <: Number + plotattributes[get_attr_symbol(letter, :rotation)] = arg + + elseif typeof(arg) <: Function + plotattributes[get_attr_symbol(letter, :formatter)] = arg + + elseif !handleColors!( + plotattributes, + arg, + get_attr_symbol(letter, :foreground_color_axis), + ) + @warn "Skipped $(letter)axis arg $arg" + end +end + +has_ticks(axis::Axis) = get(axis, :ticks, nothing) |> Plots.Ticks._has_ticks + +# update an Axis object with magic args and keywords +function attr!(axis::Axis, args...; kw...) + # first process args + plotattributes = axis.plotattributes + foreach(arg -> process_axis_arg!(plotattributes, arg), args) + + # then preprocess keyword arguments + Plots.Commons.preprocess_attributes!(KW(kw)) + + # then override for any keywords... only those keywords that already exists in plotattributes + for (k, v) in kw + haskey(plotattributes, k) || continue + if k === :discrete_values + foreach(x -> discrete_value!(axis, x), v) # add these discrete values to the axis + elseif k === :lims && isa(v, NTuple{2,TimeType}) + plotattributes[k] = (v[1].instant.periods.value, v[2].instant.periods.value) + else + plotattributes[k] = v + end + end + + # replace scale aliases + if haskey(_scale_aliases, plotattributes[:scale]) + plotattributes[:scale] = _scale_aliases[plotattributes[:scale]] + end + + axis +end + +# ------------------------------------------------------------------------- + +Base.show(io::IO, axis::Axis) = dumpdict(io, axis.plotattributes, "Axis") +ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) + +tickfont(ax::Axis) = font(; + family = ax[:tickfontfamily], + pointsize = ax[:tickfontsize], + valign = ax[:tickfontvalign], + halign = ax[:tickfonthalign], + rotation = ax[:tickfontrotation], + color = ax[:tickfontcolor], +) + +guidefont(ax::Axis) = font(; + family = ax[:guidefontfamily], + pointsize = ax[:guidefontsize], + valign = ax[:guidefontvalign], + halign = ax[:guidefonthalign], + rotation = ax[:guidefontrotation], + color = ax[:guidefontcolor], +) + +function _update_axis( + axis::Axis, + plotattributes_in::AKW, + letter::Symbol, + subplot_index::Int, +) + # build the KW of arguments from the letter version (i.e. xticks --> ticks) + kw = KW() + for k in _all_axis_attrs + # first get the args without the letter: `tickfont = font(10)` + # note: we don't pop because we want this to apply to all axes! (delete after all have finished) + if haskey(plotattributes_in, k) + kw[k] = Plots.slice_arg(plotattributes_in[k], subplot_index) + end + + # then get those args that were passed with a leading letter: `xlabel = "X"` + lk = get_attr_symbol(letter, k) + + if haskey(plotattributes_in, lk) + kw[k] = Plots.slice_arg(plotattributes_in[lk], subplot_index) + end + end + + # update the axis + attr!(axis; kw...) + nothing +end + +function _update_axis_colors(axis::Axis) + # # update the axis colors + color_or_nothing!(axis.plotattributes, :foreground_color_axis) + color_or_nothing!(axis.plotattributes, :foreground_color_border) + color_or_nothing!(axis.plotattributes, :foreground_color_guide) + color_or_nothing!(axis.plotattributes, :foreground_color_text) + color_or_nothing!(axis.plotattributes, :foreground_color_grid) + color_or_nothing!(axis.plotattributes, :foreground_color_minor_grid) + nothing +end + +""" +returns (continuous_values, discrete_values) for the ticks on this axis +""" +function get_ticks(sp::Subplot, axis::Axis; update = true, formatter = axis[:formatter]) + if update || !haskey(axis.plotattributes, :optimized_ticks) + dvals = axis[:discrete_values] + ticks = _transform_ticks(axis[:ticks], axis) + axis.plotattributes[:optimized_ticks] = + if ( + axis[:letter] === :x && + ticks isa Symbol && + ticks !== :none && + !isempty(dvals) && + ispolar(sp) + ) + collect(0:(π / 4):(7π / 4)), string.(0:45:315) + else + cvals = axis[:continuous_values] + alims = axis_limits(sp, axis[:letter]) + get_ticks(ticks, cvals, dvals, alims, axis[:scale], formatter) + end + end + axis.plotattributes[:optimized_ticks] +end + +function reset_extrema!(sp::Subplot) + for asym in (:x, :y, :z) + sp[get_attr_symbol(asym, :axis)][:extrema] = Extrema() + end + for series in sp.series_list + expand_extrema!(sp, series.plotattributes) + end +end + +function Plots.expand_extrema!(ex::Extrema, v::Number) + ex.emin = isfinite(v) ? min(v, ex.emin) : ex.emin + ex.emax = isfinite(v) ? max(v, ex.emax) : ex.emax + ex +end + +Plots.expand_extrema!(axis::Axis, v::Number) = expand_extrema!(axis[:extrema], v) + +# these shouldn't impact the extrema +Plots.expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] +Plots.expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] + +function Plots.expand_extrema!( + axis::Axis, + v::Tuple{MIN,MAX}, +) where {MIN<:Number,MAX<:Number} + ex = axis[:extrema]::Extrema + ex.emin = isfinite(v[1]) ? min(v[1], ex.emin) : ex.emin + ex.emax = isfinite(v[2]) ? max(v[2], ex.emax) : ex.emax + ex +end +function Plots.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} + ex = axis[:extrema]::Extrema + foreach(vi -> expand_extrema!(ex, vi), v) + ex +end + +# ------------------------------------------------------------------------- + +end # Axes diff --git a/src/BezierCurves.jl b/src/BezierCurves.jl new file mode 100644 index 000000000..115cc6a15 --- /dev/null +++ b/src/BezierCurves.jl @@ -0,0 +1,22 @@ +module BezierCurves + +import ..Plots + +"create a BezierCurve for plotting" +mutable struct BezierCurve{T<:Tuple} + control_points::Vector{T} +end + +function (bc::BezierCurve)(t::Real) + p = (0.0, 0.0) + n = length(bc.control_points) - 1 + for i in 0:n + p = p .+ bc.control_points[i + 1] .* binomial(n, i) .* (1 - t)^(n - i) .* t^i + end + p +end + +Plots.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = + map(curve, Base.range(first(range), stop = last(range), length = n)) + +end diff --git a/src/colorbars.jl b/src/Colorbars.jl similarity index 88% rename from src/colorbars.jl rename to src/Colorbars.jl index b6c4be499..94bdb5e26 100644 --- a/src/colorbars.jl +++ b/src/Colorbars.jl @@ -1,15 +1,28 @@ +module Colorbars + +export colorbar_style, + get_clims, update_clims, hascolorbar, get_colorbar_ticks, _update_subplot_colorbars +using Plots.Commons: Commons, NaNMath, ignorenan_extrema +using Plots.PlotsSeries +using Plots.Subplots: Subplot, series_list +using Plots.Surfaces: AbstractSurface +using Plots.Ticks +using Plots.Ticks: _transform_ticks +import Plots.Commons.get_clims + # These functions return an operator for use in `get_clims(::Seres, op)` process_clims(lims::Tuple{<:Number,<:Number}) = (zlims -> ifelse.(isfinite.(lims), lims, zlims)) ∘ ignorenan_extrema process_clims(s::Union{Symbol,Nothing,Missing}) = ignorenan_extrema # don't specialize on ::Function otherwise python functions won't work process_clims(f) = f - -get_clims(sp::Subplot)::Tuple{Float64,Float64} = - haskey(sp.attr, :clims_calculated) ? sp[:clims_calculated] : update_clims(sp) get_clims(series::Series)::Tuple{Float64,Float64} = haskey(series.plotattributes, :clims_calculated) ? series[:clims_calculated]::Tuple{Float64,Float64} : update_clims(series) + +get_clims(sp::Subplot)::Tuple{Float64,Float64} = + haskey(sp.attr, :clims_calculated) ? sp[:clims_calculated] : update_clims(sp) + get_clims(sp::Subplot, series::Series)::Tuple{Float64,Float64} = series[:colorbar_entry] ? get_clims(sp) : get_clims(series) @@ -18,7 +31,10 @@ function update_clims(sp::Subplot, op = process_clims(sp[:clims]))::Tuple{Float6 for series in series_list(sp) if series[:colorbar_entry]::Bool # Avoid calling the inner `update_clims` if at all possible; dynamic dispatch hell - if (series[:seriestype] ∈ _z_colored_series && series[:z] !== nothing) || + if ( + series[:seriestype] ∈ Commons._z_colored_series && + series[:z] !== nothing + ) || series[:line_z] !== nothing || series[:marker_z] !== nothing || series[:fill_z] !== nothing @@ -61,7 +77,7 @@ function update_clims(series::Series, op = ignorenan_extrema)::Tuple{Float64,Flo zmin, zmax = Inf, -Inf # keeping this unrolled has higher performance - if series[:seriestype] ∈ _z_colored_series && series[:z] !== nothing + if series[:seriestype] ∈ Commons._z_colored_series && series[:z] !== nothing zmin, zmax = update_clims(zmin, zmax, series[:z], op) end if series[:line_z] !== nothing @@ -127,3 +143,4 @@ end # Dynamic callback from the pipeline if needed _update_subplot_colorbars(sp::Subplot) = update_clims(sp) _update_subplot_colorbars(sp::Subplot, series::Series) = update_clims(sp, series) +end # Colorbars diff --git a/src/Commons/Commons.jl b/src/Commons/Commons.jl new file mode 100644 index 000000000..f98929227 --- /dev/null +++ b/src/Commons/Commons.jl @@ -0,0 +1,294 @@ +"Things that should be common to all backends and frontend modules" +module Commons + +export AVec, AMat, KW, AKW, TicksArgs +export Plots, PLOTS_SEED +export _haligns, _valigns, _cbar_width +# Functions +export get_subplot, + coords, + ispolar, + expand_extrema!, + series_list, + axis_limits, + get_size, + get_thickness_scaling, + get_clims +export fg_color, plot_color, single_color, alpha, isdark, color_or_nothing! +export get_attr_symbol, + _cycle, + _as_gradient, + makevec, + maketuple, + unzip, + get_aspect_ratio, + ok, + handle_surface, + reverse_if, + _debug +export _all_scales, _log_scales, _log_scale_bases, _scale_aliases +export _segmenting_array_attributes, _segmenting_vector_attributes +export anynan, + allnan, + round_base, + floor_base, + ceil_base, + ignorenan_min_max, + ignorenan_extrema, + ignorenan_maximum, + ignorenan_mean, + ignorenan_minimum +#exports from args.jl +export default, wraptuple, merge_with_base_supported + +using Plots: Plots, Printf, NaNMath, cgrad +import Plots: RecipesPipeline +using Plots.Colors: Colorant, @colorant_str +using Plots.ColorTypes: alpha +using Plots.Measures: mm, BoundingBox +using Plots.PlotUtils: PlotUtils, ColorPalette, plot_color, isdark, ColorGradient +using Plots.RecipesBase +using Plots: DEFAULT_LINEWIDTH +using Plots: Statistics + +const AVec = AbstractVector +const AMat = AbstractMatrix +const KW = Dict{Symbol,Any} +const AKW = AbstractDict{Symbol,Any} +const TicksArgs = + Union{AVec{T},Tuple{AVec{T},AVec{S}},Symbol} where {T<:Real,S<:AbstractString} +const PLOTS_SEED = 1234 +const PX_PER_INCH = 100 +const DPI = PX_PER_INCH +const MM_PER_INCH = 25.4 +const MM_PER_PX = MM_PER_INCH / PX_PER_INCH +const _haligns = :hcenter, :left, :right +const _valigns = :vcenter, :top, :bottom +const _cbar_width = 5mm +const _all_scales = [:identity, :ln, :log2, :log10, :asinh, :sqrt] +const _log_scales = [:ln, :log2, :log10] +const _log_scale_bases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) +const _scale_aliases = Dict{Symbol,Symbol}(:none => :identity, :log => :log10) +const _segmenting_vector_attributes = ( + :seriescolor, + :seriesalpha, + :linecolor, + :linealpha, + :linewidth, + :linestyle, + :fillcolor, + :fillalpha, + :fillstyle, + :markercolor, + :markeralpha, + :markersize, + :markerstrokecolor, + :markerstrokealpha, + :markerstrokewidth, + :markershape, +) +const _segmenting_array_attributes = :line_z, :fill_z, :marker_z +const _debug = Ref(false) + +function get_subplot end +function get_clims end +function series_list end +function coords end +function ispolar end +function expand_extrema! end +function axis_limits end +function preprocess_attributes! end +# --------------------------------------------------------------- +wraptuple(x::Tuple) = x +wraptuple(x) = (x,) + +true_or_all_true(f::Function, x::AbstractArray) = all(f, x) +true_or_all_true(f::Function, x) = f(x) + +all_lineLtypes(arg) = + true_or_all_true(a -> get(Commons._typeAliases, a, a) in Commons._all_seriestypes, arg) +all_styles(arg) = + true_or_all_true(a -> get(Commons._styleAliases, a, a) in Commons._all_styles, arg) +all_shapes(arg) = (true_or_all_true( + a -> + get(Commons._marker_aliases, a, a) in Commons._all_markers || a isa Plots.Shape, + arg, +)) +all_alphas(arg) = true_or_all_true( + a -> + (typeof(a) <: Real && a > 0 && a < 1) || ( + typeof(a) <: AbstractFloat && (a == zero(typeof(a)) || a == one(typeof(a))) + ), + arg, +) +all_reals(arg) = true_or_all_true(a -> typeof(a) <: Real, arg) +all_functionss(arg) = true_or_all_true(a -> isa(a, Function), arg) + +# --------------------------------------------------------------- +include("attrs.jl") + +function _override_seriestype_check(plotattributes::AKW, st::Symbol) + # do we want to override the series type? + if !RecipesPipeline.is3d(st) && st ∉ (:contour, :contour3d, :quiver) + if (z = plotattributes[:z]) !== nothing && + size(plotattributes[:x]) == size(plotattributes[:y]) == size(z) + st = st === :scatter ? :scatter3d : :path3d + plotattributes[:seriestype] = st + end + end + st +end + +"These should only be needed in frontend modules" +Plots.@ScopeModule( + Frontend, + Commons, + _subplot_defaults, + _axis_defaults, + _plot_defaults, + _series_defaults, + _match_map, + _match_map2, + @add_attributes, + preprocess_attributes!, + _override_seriestype_check +) + +function fg_color(plotattributes::AKW) + fg = get(plotattributes, :foreground_color, :auto) + if fg === :auto + bg = plot_color(get(plotattributes, :background_color, :white)) + fg = alpha(bg) > 0 && isdark(bg) ? colorant"white" : colorant"black" + else + plot_color(fg) + end +end +function color_or_nothing!(plotattributes, k::Symbol) + plotattributes[k] = (v = plotattributes[k]) === :match ? v : plot_color(v) + nothing +end + +# cache joined symbols so they can be looked up instead of constructed each time +const _attrsymbolcache = Dict{Symbol,Dict{Symbol,Symbol}}() + +get_attr_symbol(letter::Symbol, keyword::String) = get_attr_symbol(letter, Symbol(keyword)) +get_attr_symbol(letter::Symbol, keyword::Symbol) = _attrsymbolcache[letter][keyword] +# ------------------------------------------------------------------------------------ +_cycle(v::AVec, idx::Int) = v[mod(idx, axes(v, 1))] +_cycle(v::AMat, idx::Int) = size(v, 1) == 1 ? v[end, mod(idx, axes(v, 2))] : v[:, mod(idx, axes(v, 2))] +_cycle(v, idx::Int) = v + +_cycle(v::AVec, indices::AVec{Int}) = map(i -> _cycle(v, i), indices) +_cycle(v::AMat, indices::AVec{Int}) = map(i -> _cycle(v, i), indices) +_cycle(v, indices::AVec{Int}) = fill(v, length(indices)) + +_cycle(cl::PlotUtils.AbstractColorList, idx::Int) = cl[mod1(idx, end)] +_cycle(cl::PlotUtils.AbstractColorList, idx::AVec{Int}) = cl[mod1.(idx, end)] + +_as_gradient(grad) = grad +_as_gradient(v::AbstractVector{<:Colorant}) = cgrad(v) +_as_gradient(cp::ColorPalette) = cgrad(cp, categorical = true) +_as_gradient(c::Colorant) = cgrad([c, c]) + +single_color(c, v = 0.5) = c +single_color(grad::ColorGradient, v = 0.5) = grad[v] + +get_gradient(c) = cgrad() +get_gradient(cg::ColorGradient) = cg +get_gradient(cp::ColorPalette) = cgrad(cp, categorical = true) + +makevec(v::AVec) = v +makevec(v::T) where {T} = T[v] + +"duplicate a single value, or pass the 2-tuple through" +maketuple(x::Real) = (x, x) +maketuple(x::Tuple) = x + +RecipesPipeline.unzip(v) = Unzip.unzip(v) # COV_EXCL_LINE + +"collect into columns (convenience for `unzip` from `Unzip.jl`)" +unzip(v) = RecipesPipeline.unzip(v) + +check_aspect_ratio(ar::AbstractVector) = nothing # for PyPlot +check_aspect_ratio(ar::Number) = nothing +check_aspect_ratio(ar::Symbol) = + ar in (:none, :equal, :auto) || throw(ArgumentError("Invalid `aspect_ratio` = $ar")) +check_aspect_ratio(ar::T) where {T} = + throw(ArgumentError("Invalid `aspect_ratio`::$T = $ar ")) + +ok(x::Number, y::Number, z::Number = 0) = isfinite(x) && isfinite(y) && isfinite(z) +ok(tup::Tuple) = ok(tup...) + +"floor number x in base b, note this is different from using Base.round(...; base=b) !" +floor_base(x, b) = round_base(x, b, RoundDown) + +"ceil number x in base b" +ceil_base(x, b) = round_base(x, b, RoundUp) + +round_base(x::T, b, ::RoundingMode{:Down}) where {T} = T(b^floor(log(b, x))) +round_base(x::T, b, ::RoundingMode{:Up}) where {T} = T(b^ceil(log(b, x))) +# define functions that ignores NaNs. To overcome the destructive effects of https://github.com/JuliaLang/julia/pull/12563 +ignorenan_minimum(x::AbstractArray{<:AbstractFloat}) = NaNMath.minimum(x) +ignorenan_minimum(x) = Base.minimum(x) +ignorenan_maximum(x::AbstractArray{<:AbstractFloat}) = NaNMath.maximum(x) +ignorenan_maximum(x) = Base.maximum(x) +ignorenan_mean(x::AbstractArray{<:AbstractFloat}) = NaNMath.mean(x) +ignorenan_mean(x) = Statistics.mean(x) +ignorenan_extrema(x::AbstractArray{<:AbstractFloat}) = NaNMath.extrema(x) +ignorenan_extrema(x) = Base.extrema(x) +ignorenan_min_max(::Any, ex) = ex +function ignorenan_min_max(x::AbstractArray{<:AbstractFloat}, ex::Tuple) + mn, mx = ignorenan_extrema(x) + NaNMath.min(ex[1], mn), NaNMath.max(ex[2], mx) +end + +# helpers to figure out if there are NaN values in a list of array types +anynan(i::Int, args::Tuple) = any(a -> try + isnan(_cycle(a, i)) +catch MethodError + false +end, args) +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) + +handle_surface(z) = z + +reverse_if(x, cond) = cond ? reverse(x) : x + +function get_aspect_ratio(sp) + ar = sp[:aspect_ratio] + check_aspect_ratio(ar) + if ar === :auto + ar = :none + for series in series_list(sp) + if series[:seriestype] === :image + ar = :equal + end + end + end + ar isa Bool && (ar = Int(ar)) # NOTE: Bool <: ... <: Number + ar +end + +get_size(kw) = get(kw, :size, default(:size)) +get_thickness_scaling(kw) = get(kw, :thickness_scaling, default(:thickness_scaling)) + +debug!(on = true) = _debug[] = on +debugshow(io, x) = show(io, x) +debugshow(io, x::AbstractArray) = print(io, summary(x)) + +function dumpdict(io::IO, plotattributes::AKW, prefix = "") + _debug[] || return + println(io) + prefix == "" || println(io, prefix, ":") + for k in sort(collect(keys(plotattributes))) + Printf.@printf(io, "%14s: ", k) + debugshow(io, plotattributes[k]) + println(io) + end + println(io) +end +include("postprocess_attrs.jl") + +end diff --git a/src/Commons/aliases.jl b/src/Commons/aliases.jl new file mode 100644 index 000000000..1a8ed86a7 --- /dev/null +++ b/src/Commons/aliases.jl @@ -0,0 +1,422 @@ +autopick_ignore_none_auto(arr::AVec, idx::Integer) = + _cycle(setdiff(arr, [:none, :auto]), idx) +autopick_ignore_none_auto(notarr, idx::Integer) = notarr + +function aliases_and_autopick( + plotattributes::AKW, + sym::Symbol, + aliases::Dict{Symbol,Symbol}, + options::AVec, + plotIndex::Int, +) + if plotattributes[sym] === :auto + plotattributes[sym] = autopick_ignore_none_auto(options, plotIndex) + elseif haskey(aliases, plotattributes[sym]) + plotattributes[sym] = aliases[plotattributes[sym]] + end +end + +aliases(val) = aliases(_keyAliases, val) +aliases(aliasMap::Dict{Symbol,Symbol}, val) = + filter(x -> x.second == val, aliasMap) |> keys |> collect |> sort + +# ----------------------------------------------------------------------------- +# legend +add_aliases(:legend_position, :legend, :leg, :key, :legends) +add_aliases( + :legend_background_color, + :bg_legend, + :bglegend, + :bgcolor_legend, + :bg_color_legend, + :background_legend, + :background_colour_legend, + :bgcolour_legend, + :bg_colour_legend, + :background_color_legend, +) +add_aliases( + :legend_foreground_color, + :fg_legend, + :fglegend, + :fgcolor_legend, + :fg_color_legend, + :foreground_legend, + :foreground_colour_legend, + :fgcolour_legend, + :fg_colour_legend, + :foreground_color_legend, +) +add_aliases(:legend_font_pointsize, :legendfontsize) +add_aliases( + :legend_title, + :key_title, + :keytitle, + :label_title, + :labeltitle, + :leg_title, + :legtitle, +) +add_aliases(:legend_title_font_pointsize, :legendtitlefontsize) +add_aliases(:plot_title, :suptitle, :subplot_grid_title, :sgtitle, :plot_grid_title) +# margin +add_aliases(:left_margin, :leftmargin) + +add_aliases(:top_margin, :topmargin) +add_aliases(:bottom_margin, :bottommargin) +add_aliases(:right_margin, :rightmargin) + +# colors +add_aliases(:seriescolor, :c, :color, :colour, :colormap, :cmap) +add_aliases(:linecolor, :lc, :lcolor, :lcolour, :linecolour) +add_aliases(:markercolor, :mc, :mcolor, :mcolour, :markercolour) +add_aliases(:markerstrokecolor, :msc, :mscolor, :mscolour, :markerstrokecolour) +add_aliases(:markerstrokewidth, :msw, :mswidth) +add_aliases(:fillcolor, :fc, :fcolor, :fcolour, :fillcolour) + +add_aliases( + :background_color, + :bg, + :bgcolor, + :bg_color, + :background, + :background_colour, + :bgcolour, + :bg_colour, +) +add_aliases( + :background_color_subplot, + :bg_subplot, + :bgsubplot, + :bgcolor_subplot, + :bg_color_subplot, + :background_subplot, + :background_colour_subplot, + :bgcolour_subplot, + :bg_colour_subplot, +) +add_aliases( + :background_color_inside, + :bg_inside, + :bginside, + :bgcolor_inside, + :bg_color_inside, + :background_inside, + :background_colour_inside, + :bgcolour_inside, + :bg_colour_inside, +) +add_aliases( + :background_color_outside, + :bg_outside, + :bgoutside, + :bgcolor_outside, + :bg_color_outside, + :background_outside, + :background_colour_outside, + :bgcolour_outside, + :bg_colour_outside, +) +add_aliases( + :foreground_color, + :fg, + :fgcolor, + :fg_color, + :foreground, + :foreground_colour, + :fgcolour, + :fg_colour, +) + +add_aliases( + :foreground_color_subplot, + :fg_subplot, + :fgsubplot, + :fgcolor_subplot, + :fg_color_subplot, + :foreground_subplot, + :foreground_colour_subplot, + :fgcolour_subplot, + :fg_colour_subplot, +) +add_aliases( + :foreground_color_grid, + :fg_grid, + :fggrid, + :fgcolor_grid, + :fg_color_grid, + :foreground_grid, + :foreground_colour_grid, + :fgcolour_grid, + :fg_colour_grid, + :gridcolor, +) +add_aliases( + :foreground_color_minor_grid, + :fg_minor_grid, + :fgminorgrid, + :fgcolor_minorgrid, + :fg_color_minorgrid, + :foreground_minorgrid, + :foreground_colour_minor_grid, + :fgcolour_minorgrid, + :fg_colour_minor_grid, + :minorgridcolor, +) +add_aliases( + :foreground_color_title, + :fg_title, + :fgtitle, + :fgcolor_title, + :fg_color_title, + :foreground_title, + :foreground_colour_title, + :fgcolour_title, + :fg_colour_title, + :titlecolor, +) +add_aliases( + :foreground_color_axis, + :fg_axis, + :fgaxis, + :fgcolor_axis, + :fg_color_axis, + :foreground_axis, + :foreground_colour_axis, + :fgcolour_axis, + :fg_colour_axis, + :axiscolor, +) +add_aliases( + :foreground_color_border, + :fg_border, + :fgborder, + :fgcolor_border, + :fg_color_border, + :foreground_border, + :foreground_colour_border, + :fgcolour_border, + :fg_colour_border, + :bordercolor, +) +add_aliases( + :foreground_color_text, + :fg_text, + :fgtext, + :fgcolor_text, + :fg_color_text, + :foreground_text, + :foreground_colour_text, + :fgcolour_text, + :fg_colour_text, + :textcolor, +) +add_aliases( + :foreground_color_guide, + :fg_guide, + :fgguide, + :fgcolor_guide, + :fg_color_guide, + :foreground_guide, + :foreground_colour_guide, + :fgcolour_guide, + :fg_colour_guide, + :guidecolor, +) + +# alphas +add_aliases(:seriesalpha, :alpha, :α, :opacity) +add_aliases(:linealpha, :la, :lalpha, :lα, :lineopacity, :lopacity) +add_aliases(:markeralpha, :ma, :malpha, :mα, :markeropacity, :mopacity) +add_aliases(:markerstrokealpha, :msa, :msalpha, :msα, :markerstrokeopacity, :msopacity) +add_aliases(:fillalpha, :fa, :falpha, :fα, :fillopacity, :fopacity) + +# axes attributes +add_axes_aliases(:guide, :label, :lab, :l; generic = false) +add_axes_aliases(:lims, :lim, :limit, :limits, :range) +add_axes_aliases(:ticks, :tick) +add_axes_aliases(:rotation, :rot, :r) +add_axes_aliases(:guidefontsize, :labelfontsize) +add_axes_aliases(:gridalpha, :ga, :galpha, :gα, :gridopacity, :gopacity) +add_axes_aliases( + :gridstyle, + :grid_style, + :gridlinestyle, + :grid_linestyle, + :grid_ls, + :gridls, +) +add_axes_aliases( + :foreground_color_grid, + :fg_grid, + :fggrid, + :fgcolor_grid, + :fg_color_grid, + :foreground_grid, + :foreground_colour_grid, + :fgcolour_grid, + :fg_colour_grid, + :gridcolor, +) +add_axes_aliases( + :foreground_color_minor_grid, + :fg_minor_grid, + :fgminorgrid, + :fgcolor_minorgrid, + :fg_color_minorgrid, + :foreground_minorgrid, + :foreground_colour_minor_grid, + :fgcolour_minorgrid, + :fg_colour_minor_grid, + :minorgridcolor, +) +add_axes_aliases( + :gridlinewidth, + :gridwidth, + :grid_linewidth, + :grid_width, + :gridlw, + :grid_lw, +) +add_axes_aliases( + :minorgridstyle, + :minorgrid_style, + :minorgridlinestyle, + :minorgrid_linestyle, + :minorgrid_ls, + :minorgridls, +) +add_axes_aliases( + :minorgridlinewidth, + :minorgridwidth, + :minorgrid_linewidth, + :minorgrid_width, + :minorgridlw, + :minorgrid_lw, +) +add_axes_aliases( + :tick_direction, + :tickdirection, + :tick_dir, + :tickdir, + :tick_orientation, + :tickorientation, + :tick_or, + :tickor, +) + +# series attributes +add_aliases(:seriestype, :st, :t, :typ, :linetype, :lt) +add_aliases(:label, :lab) +add_aliases(:line, :l) +add_aliases(:linewidth, :w, :width, :lw) +add_aliases(:linestyle, :style, :s, :ls) +add_aliases(:marker, :m, :mark) +add_aliases(:markershape, :shape) +add_aliases(:markersize, :ms, :msize) +add_aliases(:marker_z, :markerz, :zcolor, :mz) +add_aliases(:line_z, :linez, :zline, :lz) +add_aliases(:fill, :f, :area) +add_aliases(:fillrange, :fillrng, :frange, :fillto, :fill_between) +add_aliases(:group, :g, :grouping) +add_aliases(:bins, :bin, :nbin, :nbins, :nb) +add_aliases(:ribbon, :rib) +add_aliases(:annotations, :ann, :anns, :annotate, :annotation) +add_aliases(:xguide, :xlabel, :xlab, :xl) +add_aliases(:xlims, :xlim, :xlimit, :xlimits, :xrange) +add_aliases(:xticks, :xtick) +add_aliases(:xrotation, :xrot, :xr) +add_aliases(:yguide, :ylabel, :ylab, :yl) +add_aliases(:ylims, :ylim, :ylimit, :ylimits, :yrange) +add_aliases(:yticks, :ytick) +add_aliases(:yrotation, :yrot, :yr) +add_aliases(:zguide, :zlabel, :zlab, :zl) +add_aliases(:zlims, :zlim, :zlimit, :zlimits) +add_aliases(:zticks, :ztick) +add_aliases(:zrotation, :zrot, :zr) +add_aliases(:guidefontsize, :labelfontsize) +add_aliases( + :fill_z, + :fillz, + :fz, + :surfacecolor, + :surfacecolour, + :sc, + :surfcolor, + :surfcolour, +) +add_aliases(:colorbar, :cb, :cbar, :colorkey) +add_aliases( + :colorbar_title, + :colorbartitle, + :cb_title, + :cbtitle, + :cbartitle, + :cbar_title, + :colorkeytitle, + :colorkey_title, +) +add_aliases(:clims, :clim, :cbarlims, :cbar_lims, :climits, :color_limits) +add_aliases(:smooth, :regression, :reg) +add_aliases(:levels, :nlevels, :nlev, :levs) +add_aliases(:size, :windowsize, :wsize) +add_aliases(:window_title, :windowtitle, :wtitle) +add_aliases(:show, :gui, :display) +add_aliases(:color_palette, :palette) +add_aliases(:overwrite_figure, :clf, :clearfig, :overwrite, :reuse) +add_aliases(:xerror, :xerr, :xerrorbar) +add_aliases(:yerror, :yerr, :yerrorbar, :err, :errorbar) +add_aliases(:zerror, :zerr, :zerrorbar) +add_aliases(:quiver, :velocity, :quiver2d, :gradient, :vectorfield) +add_aliases(:normalize, :norm, :normed, :normalized) +add_aliases(:show_empty_bins, :showemptybins, :showempty, :show_empty) +add_aliases(:aspect_ratio, :aspectratio, :axis_ratio, :axisratio, :ratio) +add_aliases(:subplot, :sp, :subplt, :splt) +add_aliases(:projection, :proj) +add_aliases(:projection_type, :proj_type) +add_aliases( + :titlelocation, + :title_location, + :title_loc, + :titleloc, + :title_position, + :title_pos, + :titlepos, + :titleposition, + :title_align, + :title_alignment, +) +add_aliases( + :series_annotations, + :series_ann, + :seriesann, + :series_anns, + :seriesanns, + :series_annotation, + :text, + :txt, + :texts, + :txts, +) +add_aliases(:html_output_format, :format, :fmt, :html_format) +add_aliases(:orientation, :direction, :dir) +add_aliases(:inset_subplots, :inset, :floating) +add_aliases(:stride, :wirefame_stride, :surface_stride, :surf_str, :str) + +add_aliases( + :framestyle, + :frame_style, + :frame, + :axesstyle, + :axes_style, + :boxstyle, + :box_style, + :box, + :borderstyle, + :border_style, + :border, +) + +add_aliases(:camera, :cam, :viewangle, :view_angle) +add_aliases(:contour_labels, :contourlabels, :clabels, :clabs) +add_aliases(:warn_on_unsupported, :warn) diff --git a/src/Commons/attrs.jl b/src/Commons/attrs.jl new file mode 100644 index 000000000..551d9d047 --- /dev/null +++ b/src/Commons/attrs.jl @@ -0,0 +1,1276 @@ +makeplural(s::Symbol) = last(string(s)) == 's' ? s : Symbol(string(s, "s")) +make_non_underscore(s::Symbol) = Symbol(replace(string(s), "_" => "")) + +const _keyAliases = Dict{Symbol,Symbol}() + +function add_aliases(sym::Symbol, aliases::Symbol...) + for alias in aliases + (haskey(_keyAliases, alias) || alias === sym) && return + _keyAliases[alias] = sym + end + nothing +end + +function add_axes_aliases(sym::Symbol, aliases::Symbol...; generic::Bool = true) + sym in keys(_axis_defaults) || throw(ArgumentError("Invalid `$sym`")) + generic && add_aliases(sym, aliases...) + for letter in (:x, :y, :z) + add_aliases(Symbol(letter, sym), (Symbol(letter, a) for a in aliases)...) + end +end + +function add_non_underscore_aliases!(aliases::Dict{Symbol,Symbol}) + for (k, v) in aliases + if '_' in string(k) + aliases[make_non_underscore(k)] = v + end + end +end + +replaceAlias!(plotattributes::AKW, k::Symbol, aliases::Dict{Symbol,Symbol}) = + if haskey(aliases, k) + plotattributes[aliases[k]] = RecipesPipeline.pop_kw!(plotattributes, k) + end + +replaceAliases!(plotattributes::AKW, aliases::Dict{Symbol,Symbol}) = + foreach(k -> replaceAlias!(plotattributes, k, aliases), collect(keys(plotattributes))) + +macro attributes(expr::Expr) + RecipesBase.process_recipe_body!(expr) + expr +end + +# ------------------------------------------------------------ + +const _all_axes = [:auto, :left, :right] +const _axes_aliases = Dict{Symbol,Symbol}(:a => :auto, :l => :left, :r => :right) + +const _3dTypes = [:path3d, :scatter3d, :surface, :wireframe, :contour3d, :volume, :mesh3d] +const _all_seriestypes = vcat( + [ + :none, + :line, + :path, + :steppre, + :stepmid, + :steppost, + :sticks, + :scatter, + :heatmap, + :hexbin, + :barbins, + :barhist, + :histogram, + :scatterbins, + :scatterhist, + :stepbins, + :stephist, + :bins2d, + :histogram2d, + :histogram3d, + :density, + :bar, + :hline, + :vline, + :contour, + :pie, + :shape, + :image, + ], + _3dTypes, +) + +const _z_colored_series = [:contour, :contour3d, :heatmap, :histogram2d, :surface, :hexbin] + +const _typeAliases = Dict{Symbol,Symbol}( + :n => :none, + :no => :none, + :l => :line, + :p => :path, + :stepinv => :steppre, + :stepsinv => :steppre, + :stepinverted => :steppre, + :stepsinverted => :steppre, + :step => :steppost, + :steps => :steppost, + :stair => :steppost, + :stairs => :steppost, + :stem => :sticks, + :stems => :sticks, + :dots => :scatter, + :pdf => :density, + :contours => :contour, + :line3d => :path3d, + :surf => :surface, + :wire => :wireframe, + :shapes => :shape, + :poly => :shape, + :polygon => :shape, + :box => :boxplot, + :velocity => :quiver, + :vectorfield => :quiver, + :gradient => :quiver, + :img => :image, + :imshow => :image, + :imagesc => :image, + :hist => :histogram, + :hist2d => :histogram2d, + :bezier => :curves, + :bezier_curves => :curves, +) + +add_non_underscore_aliases!(_typeAliases) + +const _histogram_like = [:histogram, :barhist, :barbins] +const _line_like = [:line, :path, :steppre, :stepmid, :steppost] +const _surface_like = + [:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image] + +like_histogram(seriestype::Symbol) = seriestype in _histogram_like +like_line(seriestype::Symbol) = seriestype in _line_like +like_surface(seriestype::Symbol) = RecipesPipeline.is_surface(seriestype) + +# ------------------------------------------------------------ + +const _all_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _styleAliases = Dict{Symbol,Symbol}( + :a => :auto, + :s => :solid, + :d => :dash, + :dd => :dashdot, + :ddd => :dashdotdot, +) + +const _shape_keys = Symbol[ + :circle, + :rect, + :star5, + :diamond, + :hexagon, + :cross, + :xcross, + :utriangle, + :dtriangle, + :rtriangle, + :ltriangle, + :pentagon, + :heptagon, + :octagon, + :star4, + :star6, + :star7, + :star8, + :vline, + :hline, + :+, + :x, +] + +const _all_markers = vcat(:none, :auto, _shape_keys) #sort(collect(keys(_shapes)))) +const _marker_aliases = Dict{Symbol,Symbol}( + :n => :none, + :no => :none, + :a => :auto, + :ellipse => :circle, + :c => :circle, + :circ => :circle, + :square => :rect, + :sq => :rect, + :r => :rect, + :d => :diamond, + :^ => :utriangle, + :ut => :utriangle, + :utri => :utriangle, + :uptri => :utriangle, + :uptriangle => :utriangle, + :v => :dtriangle, + :V => :dtriangle, + :dt => :dtriangle, + :dtri => :dtriangle, + :downtri => :dtriangle, + :downtriangle => :dtriangle, + :> => :rtriangle, + :rt => :rtriangle, + :rtri => :rtriangle, + :righttri => :rtriangle, + :righttriangle => :rtriangle, + :< => :ltriangle, + :lt => :ltriangle, + :ltri => :ltriangle, + :lighttri => :ltriangle, + :lighttriangle => :ltriangle, + # :+ => :cross, + :plus => :cross, + # :x => :xcross, + :X => :xcross, + :star => :star5, + :s => :star5, + :star1 => :star5, + :s2 => :star8, + :star2 => :star8, + :p => :pentagon, + :pent => :pentagon, + :h => :hexagon, + :hex => :hexagon, + :hep => :heptagon, + :o => :octagon, + :oct => :octagon, + :spike => :vline, +) + +const _position_aliases = Dict{Symbol,Symbol}( + :top_left => :topleft, + :tl => :topleft, + :top_center => :topcenter, + :tc => :topcenter, + :top_right => :topright, + :tr => :topright, + :bottom_left => :bottomleft, + :bl => :bottomleft, + :bottom_center => :bottomcenter, + :bc => :bottomcenter, + :bottom_right => :bottomright, + :br => :bottomright, +) + +const _all_grid_syms = [ + :x, + :y, + :z, + :xy, + :xz, + :yx, + :yz, + :zx, + :zy, + :xyz, + :xzy, + :yxz, + :yzx, + :zxy, + :zyx, + :all, + :both, + :on, + :yes, + :show, + :none, + :off, + :no, + :hide, +] +const _all_grid_attrs = [_all_grid_syms; string.(_all_grid_syms); nothing] +hasgrid(arg::Nothing, letter) = false +hasgrid(arg::Bool, letter) = arg +function hasgrid(arg::Symbol, letter) + if arg in _all_grid_syms + arg in (:all, :both, :on) || occursin(string(letter), string(arg)) + else + @warn "Unknown grid argument $arg; $(get_attr_symbol(letter, :grid)) was set to `true` instead." + true + end +end +hasgrid(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) + +const _all_showaxis_syms = [ + :x, + :y, + :z, + :xy, + :xz, + :yx, + :yz, + :zx, + :zy, + :xyz, + :xzy, + :yxz, + :yzx, + :zxy, + :zyx, + :all, + :both, + :on, + :yes, + :show, + :off, + :no, + :hide, +] +const _all_showaxis_attrs = [_all_grid_syms; string.(_all_grid_syms)] +showaxis(arg::Nothing, letter) = false +showaxis(arg::Bool, letter) = arg +function showaxis(arg::Symbol, letter) + if arg in _all_grid_syms + arg in (:all, :both, :on, :yes) || occursin(string(letter), string(arg)) + else + @warn "Unknown showaxis argument $arg; $(get_attr_symbol(letter, :showaxis)) was set to `true` instead." + true + end +end +showaxis(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) + +const _all_framestyles = [:box, :semi, :axes, :origin, :zerolines, :grid, :none] +const _framestyle_aliases = Dict{Symbol,Symbol}( + :frame => :box, + :border => :box, + :on => :box, + :transparent => :semi, + :semitransparent => :semi, +) + +const _bar_width = 0.8 +# ----------------------------------------------------------------------------- + +const _series_defaults = KW( + :label => :auto, + :colorbar_entry => true, + :seriescolor => :auto, + :seriesalpha => nothing, + :seriestype => :path, + :linestyle => :solid, + :linewidth => :auto, + :linecolor => :auto, + :linealpha => nothing, + :fillrange => nothing, # ribbons, areas, etc + :fillcolor => :match, + :fillalpha => nothing, + :fillstyle => nothing, + :markershape => :none, + :markercolor => :match, + :markeralpha => nothing, + :markersize => 4, + :markerstrokestyle => :solid, + :markerstrokewidth => 1, + :markerstrokecolor => :match, + :markerstrokealpha => nothing, + :bins => :auto, # number of bins for hists + :smooth => false, # regression line? + :group => nothing, # groupby vector + :x => nothing, + :y => nothing, + :z => nothing, # depth for contour, surface, etc + :marker_z => nothing, # value for color scale + :line_z => nothing, + :fill_z => nothing, + :levels => 15, + :bar_position => :overlay, # for bar plots and histograms: could also be stack (stack up) or dodge (side by side) + :bar_width => nothing, + :bar_edges => false, + :xerror => nothing, + :yerror => nothing, + :zerror => nothing, + :ribbon => nothing, + :quiver => nothing, + :arrow => nothing, # allows for adding arrows to line/path... call `arrow(args...)` + :normalize => false, # do we want a normalized histogram? + :weights => nothing, # optional weights for histograms (1D and 2D) + :show_empty_bins => false, # should empty bins in 2D histogram be colored as zero (otherwise they are transparent) + :contours => false, # add contours to 3d surface and wireframe plots + :contour_labels => false, + :subplot => :auto, # which subplot(s) does this series belong to? + :series_annotations => nothing, # a list of annotations which apply to the coordinates of this series + :primary => true, # when true, this "counts" as a series for color selection, etc. the main use is to allow + # one logical series to be broken up (path and markers, for example) + :hover => nothing, # text to display when hovering over the data points + :stride => (1, 1), # array stride for wireframe/surface, the first element is the row stride and the second is the column stride. + :connections => nothing, # tuple of arrays to specify connectivity of a 3d mesh + :z_order => :front, # one of :front, :back or integer in 1:length(sp.series_list) + :permute => :none, # tuple of two symbols to be permuted + :extra_kwargs => Dict(), +) + +const _plot_defaults = KW( + :plot_title => "", + :plot_titleindex => 0, + :plot_titlefontsize => 16, + :plot_titlelocation => :center, # also :left or :right + :plot_titlefontfamily => :match, + :plot_titlefonthalign => :hcenter, + :plot_titlefontvalign => :vcenter, + :plot_titlefontrotation => 0.0, + :plot_titlefontcolor => :match, + :plot_titlevspan => 0.05, # vertical span of the plot title, here 5% + :background_color => colorant"white", # default for all backgrounds, + :background_color_outside => :match, # background outside grid, + :foreground_color => :auto, # default for all foregrounds, and title color, + :fontfamily => "sans-serif", + :size => (600, 400), + :pos => (0, 0), + :window_title => "Plots.jl", + :show => false, + :layout => 1, + :link => :none, + :overwrite_figure => true, + :html_output_format => :auto, + :tex_output_standalone => false, + :inset_subplots => nothing, # optionally pass a vector of (parent,bbox) tuples which are + # the parent layout and the relative bounding box of inset subplots + :dpi => DPI, # dots per inch for images, etc + :thickness_scaling => 1, + :display_type => :auto, + :warn_on_unsupported => true, + :extra_plot_kwargs => Dict(), + :extra_kwargs => :series, # directs collection of extra_kwargs +) + +const _subplot_defaults = KW( + :title => "", + :titlelocation => :center, # also :left or :right + :fontfamily_subplot => :match, + :titlefontfamily => :match, + :titlefontsize => 14, + :titlefonthalign => :hcenter, + :titlefontvalign => :vcenter, + :titlefontrotation => 0.0, + :titlefontcolor => :match, + :background_color_subplot => :match, # default for other bg colors... match takes plot default + :background_color_inside => :match, # background inside grid + :foreground_color_subplot => :match, # default for other fg colors... match takes plot default + :foreground_color_title => :match, # title color + :color_palette => :auto, + :colorbar => :legend, + :clims => :auto, + :colorbar_fontfamily => :match, + :colorbar_ticks => :auto, + :colorbar_tickfontfamily => :match, + :colorbar_tickfontsize => 8, + :colorbar_tickfonthalign => :hcenter, + :colorbar_tickfontvalign => :vcenter, + :colorbar_tickfontrotation => 0.0, + :colorbar_tickfontcolor => :match, + :colorbar_scale => :identity, + :colorbar_formatter => :auto, + :colorbar_discrete_values => [], + :colorbar_continuous_values => zeros(0), + :annotations => [], # annotation tuples... list of (x,y,annotation) + :annotationfontfamily => :match, + :annotationfontsize => 14, + :annotationhalign => :hcenter, + :annotationvalign => :vcenter, + :annotationrotation => 0.0, + :annotationcolor => :match, + :projection => :none, # can also be :polar or :3d + :projection_type => :auto, # can also be :ortho(graphic) or :persp(ective) + :aspect_ratio => :auto, # choose from :none or :equal + :margin => 1mm, + :left_margin => :match, + :top_margin => :match, + :right_margin => :match, + :bottom_margin => :match, + :subplot_index => -1, + :colorbar_title => "", + :colorbar_titlefontsize => 10, + :colorbar_title_location => :center, # also :left or :right + :colorbar_fontfamily => :match, + :colorbar_titlefontfamily => :match, + :colorbar_titlefonthalign => :hcenter, + :colorbar_titlefontvalign => :vcenter, + :colorbar_titlefontrotation => 0.0, + :colorbar_titlefontcolor => :match, + :framestyle => :axes, + :camera => (30, 30), + :extra_kwargs => Dict(), +) + +const _axis_defaults = KW( + :guide => "", + :guide_position => :auto, + :lims => :auto, + :ticks => :auto, + :scale => :identity, + :rotation => 0, + :flip => false, + :link => [], + :tickfontfamily => :match, + :tickfontsize => 8, + :tickfonthalign => :hcenter, + :tickfontvalign => :vcenter, + :tickfontrotation => 0.0, + :tickfontcolor => :match, + :guidefontfamily => :match, + :guidefontsize => 11, + :guidefonthalign => :hcenter, + :guidefontvalign => :vcenter, + :guidefontrotation => 0.0, + :guidefontcolor => :match, + :foreground_color_axis => :match, # axis border/tick colors, + :foreground_color_border => :match, # plot area border/spines, + :foreground_color_text => :match, # tick text color, + :foreground_color_guide => :match, # guide text color, + :discrete_values => [], + :formatter => :auto, + :mirror => false, + :grid => true, + :foreground_color_grid => :match, # grid color + :gridalpha => 0.1, + :gridstyle => :solid, + :gridlinewidth => 0.5, + :foreground_color_minor_grid => :match, # grid color + :minorgridalpha => 0.05, + :minorgridstyle => :solid, + :minorgridlinewidth => 0.5, + :tick_direction => :in, + :minorticks => :auto, + :minorgrid => false, + :showaxis => true, + :widen => :auto, + :draw_arrow => false, + :unitformat => :round, +) + +# add defaults for the letter versions +const _axis_defaults_byletter = KW() + +reset_axis_defaults_byletter!() = + for letter in (:x, :y, :z) + _axis_defaults_byletter[letter] = KW() + for (k, v) in _axis_defaults + _axis_defaults_byletter[letter][k] = v + end + end +reset_axis_defaults_byletter!() + +const _suppress_warnings = Set{Symbol}([ + :x_discrete_indices, + :y_discrete_indices, + :z_discrete_indices, + :subplot, + :subplot_index, + :series_plotindex, + :series_index, + :link, + :plot_object, + :primary, + :smooth, + :relative_bbox, + :force_minpad, + :x_extrema, + :y_extrema, + :z_extrema, +]) + +const _internal_attrs = [ + :plot_object, + :series_plotindex, + :series_index, + :markershape_to_add, + :letter, + :idxfilter, +] + +const _axis_attrs = Set(keys(_axis_defaults)) +const _series_attrs = Set(keys(_series_defaults)) +const _subplot_attrs = Set(keys(_subplot_defaults)) +const _plot_attrs = Set(keys(_plot_defaults)) + +const _magic_axis_attrs = [:axis, :tickfont, :guidefont, :grid, :minorgrid] +const _magic_subplot_attrs = + [:title_font, :legend_font, :legend_title_font, :plot_title_font, :colorbar_titlefont] +const _magic_series_attrs = [:line, :marker, :fill] +const _all_magic_attrs = + Set(union(_magic_axis_attrs, _magic_series_attrs, _magic_subplot_attrs)) + +const _all_axis_attrs = union(_axis_attrs, _magic_axis_attrs) +const _lettered_all_axis_attrs = + Set([Symbol(letter, kw) for letter in (:x, :y, :z) for kw in _all_axis_attrs]) +const _all_subplot_attrs = union(_subplot_attrs, _magic_subplot_attrs) +const _all_series_attrs = union(_series_attrs, _magic_series_attrs) +const _all_plot_attrs = _plot_attrs + +const _all_attrs = + union(_lettered_all_axis_attrs, _all_subplot_attrs, _all_series_attrs, _all_plot_attrs) + +const _deprecated_attributes = Dict{Symbol,Symbol}() +const _all_defaults = KW[_series_defaults, _plot_defaults, _subplot_defaults] + +const _initial_defaults = deepcopy(_all_defaults) +const _initial_axis_defaults = deepcopy(_axis_defaults) + +# to be able to reset font sizes to initial values +const _initial_plt_fontsizes = + Dict(:plot_titlefontsize => _plot_defaults[:plot_titlefontsize]) + +const _initial_sp_fontsizes = Dict( + :titlefontsize => _subplot_defaults[:titlefontsize], + :annotationfontsize => _subplot_defaults[:annotationfontsize], + :colorbar_tickfontsize => _subplot_defaults[:colorbar_tickfontsize], + :colorbar_titlefontsize => _subplot_defaults[:colorbar_titlefontsize], +) + +const _initial_ax_fontsizes = Dict( + :tickfontsize => _axis_defaults[:tickfontsize], + :guidefontsize => _axis_defaults[:guidefontsize], +) + +const _initial_fontsizes = + merge(_initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes) + +const _base_supported_attrs = [ + :color_palette, + :background_color, + :background_color_subplot, + :foreground_color, + :foreground_color_subplot, + :group, + :seriestype, + :seriescolor, + :seriesalpha, + :smooth, + :xerror, + :yerror, + :zerror, + :subplot, + :x, + :y, + :z, + :show, + :size, + :margin, + :left_margin, + :right_margin, + :top_margin, + :bottom_margin, + :html_output_format, + :layout, + :link, + :primary, + :series_annotations, + :subplot_index, + :discrete_values, + :projection, + :show_empty_bins, + :z_order, + :permute, + :unitformat, +] + +function merge_with_base_supported(v::AVec) + v = vcat(v, _base_supported_attrs) + for vi in v + if haskey(_axis_defaults, vi) + for letter in (:x, :y, :z) + push!(v, get_attr_symbol(letter, vi)) + end + end + end + Set(v) +end + +is_subplot_attrs(k) = k in _all_subplot_attrs +is_series_attrs(k) = k in _all_series_attrs +is_axis_attrs(k) = Symbol(chop(string(k); head = 1, tail = 0)) in _all_axis_attrs +is_axis_attr_noletter(k) = k in _all_axis_attrs + +RecipesBase.is_key_supported(k::Symbol) = Plots.is_attr_supported(k) + +# ----------------------------------------------------------------------------- +include("aliases.jl") +# ----------------------------------------------------------------------------- + +function parse_axis_kw(s::Symbol) + s = string(s) + for letter in ('x', 'y', 'z') + startswith(s, letter) && + return (Symbol(letter), Symbol(chop(s, head = 1, tail = 0))) + end + nothing +end + +# update the defaults globally + +""" +`default(key)` returns the current default value for that key. + +`default(key, value)` sets the current default value for that key. + +`default(; kw...)` will set the current default value for each key/value pair. + +`default(plotattributes, key)` returns the key from plotattributes if it exists, otherwise `default(key)`. + +""" +function default(k::Symbol) + k = get(_keyAliases, k, k) + for defaults in _all_defaults + haskey(defaults, k) && return defaults[k] + end + haskey(_axis_defaults, k) && return _axis_defaults[k] + if (axis_k = parse_axis_kw(k)) !== nothing + letter, key = axis_k + return _axis_defaults_byletter[letter][key] + end + k === :letter && return k # for type recipe processing + missing +end + +function default(k::Symbol, v) + k = get(_keyAliases, k, k) + for defaults in _all_defaults + if haskey(defaults, k) + defaults[k] = v + return v + end + end + if haskey(_axis_defaults, k) + _axis_defaults[k] = v + return v + end + if (axis_k = parse_axis_kw(k)) !== nothing + letter, key = axis_k + _axis_defaults_byletter[letter][key] = v + return v + end + k in _suppress_warnings || error("Unknown key: ", k) +end + +function default(; reset = true, kw...) + (reset && isempty(kw)) && reset_defaults() + kw = KW(kw) + preprocess_attributes!(kw) + for (k, v) in kw + default(k, v) + end +end + +default(plotattributes::AKW, k::Symbol) = get(plotattributes, k, default(k)) + +function reset_defaults() + foreach(merge!, _all_defaults, _initial_defaults) + merge!(_axis_defaults, _initial_axis_defaults) + Plots.Fonts.resetfontsizes() + reset_axis_defaults_byletter!() +end + +# ----------------------------------------------------------------------------- + +# if arg is a valid color value, then set plotattributes[csym] and return true +function handle_colors!(plotattributes::AKW, arg, csym::Symbol) + try + plotattributes[csym] = if arg === :auto + :auto + else + plot_color(arg) + end + return true + catch + end + false +end + +function process_line_attr(plotattributes::AKW, arg) + # seriestype + if all_lineLtypes(arg) + plotattributes[:seriestype] = arg + + # linestyle + elseif all_styles(arg) + plotattributes[:linestyle] = arg + + elseif typeof(arg) <: Plots.Stroke + arg.width === nothing || (plotattributes[:linewidth] = arg.width) + arg.color === nothing || ( + plotattributes[:linecolor] = + arg.color === :auto ? :auto : plot_color(arg.color) + ) + arg.alpha === nothing || (plotattributes[:linealpha] = arg.alpha) + arg.style === nothing || (plotattributes[:linestyle] = arg.style) + + elseif typeof(arg) <: Plots.Brush + arg.size === nothing || (plotattributes[:fillrange] = arg.size) + arg.color === nothing || ( + plotattributes[:fillcolor] = + 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) <: Plots.Arrow || arg in (:arrow, :arrows) + plotattributes[:arrow] = arg + + # linealpha + elseif all_alphas(arg) + plotattributes[:linealpha] = arg + + # linewidth + elseif all_reals(arg) + plotattributes[:linewidth] = arg + + # color + elseif !handle_colors!(plotattributes, arg, :linecolor) + @warn "Skipped line arg $arg." + end +end + +function process_marker_attr(plotattributes::AKW, arg) + # markershape + if all_shapes(arg) && !haskey(plotattributes, :markershape) + plotattributes[:markershape] = arg + + # stroke style + elseif all_styles(arg) + plotattributes[:markerstrokestyle] = arg + + elseif typeof(arg) <: Plots.Stroke + arg.width === nothing || (plotattributes[:markerstrokewidth] = arg.width) + arg.color === nothing || ( + plotattributes[:markerstrokecolor] = + arg.color === :auto ? :auto : plot_color(arg.color) + ) + arg.alpha === nothing || (plotattributes[:markerstrokealpha] = arg.alpha) + arg.style === nothing || (plotattributes[:markerstrokestyle] = arg.style) + + elseif typeof(arg) <: Plots.Brush + arg.size === nothing || (plotattributes[:markersize] = arg.size) + arg.color === nothing || ( + plotattributes[:markercolor] = + arg.color === :auto ? :auto : plot_color(arg.color) + ) + arg.alpha === nothing || (plotattributes[:markeralpha] = arg.alpha) + + # linealpha + elseif all_alphas(arg) + plotattributes[:markeralpha] = arg + + # bool + elseif typeof(arg) <: Bool + plotattributes[:markershape] = arg ? :circle : :none + + # markersize + elseif all_reals(arg) + plotattributes[:markersize] = arg + + # markercolor + elseif !handle_colors!(plotattributes, arg, :markercolor) + @warn "Skipped marker arg $arg." + end +end + +function process_fill_attr(plotattributes::AKW, arg) + # fr = get(plotattributes, :fillrange, 0) + if typeof(arg) <: Plots.Brush + arg.size === nothing || (plotattributes[:fillrange] = arg.size) + arg.color === nothing || ( + plotattributes[:fillcolor] = + 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 + + # fillrange function + elseif all_functionss(arg) + plotattributes[:fillrange] = arg + + # fillalpha + elseif all_alphas(arg) + plotattributes[:fillalpha] = arg + + # fillrange provided as vector or number + elseif typeof(arg) <: Union{AbstractArray{<:Real},Real} + plotattributes[:fillrange] = arg + + elseif !handle_colors!(plotattributes, arg, :fillcolor) + plotattributes[:fillrange] = arg + end + # plotattributes[:fillrange] = fr + nothing +end + +function process_grid_attr!(plotattributes::AKW, arg, letter) + if arg in _all_grid_attrs || isa(arg, Bool) + plotattributes[get_attr_symbol(letter, :grid)] = hasgrid(arg, letter) + + elseif all_styles(arg) + plotattributes[get_attr_symbol(letter, :gridstyle)] = arg + + elseif typeof(arg) <: Plots.Stroke + arg.width === nothing || + (plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg.width) + arg.color === nothing || ( + plotattributes[get_attr_symbol(letter, :foreground_color_grid)] = + arg.color in (:auto, :match) ? :match : plot_color(arg.color) + ) + arg.alpha === nothing || + (plotattributes[get_attr_symbol(letter, :gridalpha)] = arg.alpha) + arg.style === nothing || + (plotattributes[get_attr_symbol(letter, :gridstyle)] = arg.style) + + # linealpha + elseif all_alphas(arg) + plotattributes[get_attr_symbol(letter, :gridalpha)] = arg + + # linewidth + elseif all_reals(arg) + plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg + + # color + elseif !handle_colors!( + plotattributes, + arg, + get_attr_symbol(letter, :foreground_color_grid), + ) + @warn "Skipped grid arg $arg." + end +end + +function process_minor_grid_attr!(plotattributes::AKW, arg, letter) + if arg in _all_grid_attrs || isa(arg, Bool) + plotattributes[get_attr_symbol(letter, :minorgrid)] = hasgrid(arg, letter) + + elseif all_styles(arg) + plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg + plotattributes[get_attr_symbol(letter, :minorgrid)] = true + + elseif typeof(arg) <: Plots.Stroke + arg.width === nothing || + (plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg.width) + arg.color === nothing || ( + plotattributes[get_attr_symbol(letter, :foreground_color_minor_grid)] = + arg.color in (:auto, :match) ? :match : plot_color(arg.color) + ) + arg.alpha === nothing || + (plotattributes[get_attr_symbol(letter, :minorgridalpha)] = arg.alpha) + arg.style === nothing || + (plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg.style) + plotattributes[get_attr_symbol(letter, :minorgrid)] = true + + # linealpha + elseif all_alphas(arg) + plotattributes[get_attr_symbol(letter, :minorgridalpha)] = arg + plotattributes[get_attr_symbol(letter, :minorgrid)] = true + + # linewidth + elseif all_reals(arg) + plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg + plotattributes[get_attr_symbol(letter, :minorgrid)] = true + + # color + elseif handle_colors!( + plotattributes, + arg, + get_attr_symbol(letter, :foreground_color_minor_grid), + ) + plotattributes[get_attr_symbol(letter, :minorgrid)] = true + else + @warn "Skipped grid arg $arg." + end +end + +@attributes function process_font_attr!(plotattributes::AKW, fontname::Symbol, arg) + T = typeof(arg) + if fontname in (:legend_font,) + # TODO: this is neccessary while old and new font names coexist and should be standard after the transition + fontname = Symbol(fontname, :_) + end + if T <: Plots.Font + Symbol(fontname, :family) --> arg.family + + # TODO: this is neccessary in the transition from old fontsize to new font_pointsize and should be removed when it is completed + if in(Symbol(fontname, :size), _all_attrs) + Symbol(fontname, :size) --> arg.pointsize + else + Symbol(fontname, :pointsize) --> arg.pointsize + end + Symbol(fontname, :halign) --> arg.halign + Symbol(fontname, :valign) --> arg.valign + Symbol(fontname, :rotation) --> arg.rotation + Symbol(fontname, :color) --> arg.color + elseif arg === :center + Symbol(fontname, :halign) --> :hcenter + Symbol(fontname, :valign) --> :vcenter + elseif arg ∈ _haligns + Symbol(fontname, :halign) --> arg + elseif arg ∈ _valigns + Symbol(fontname, :valign) --> arg + elseif T <: Colorant + Symbol(fontname, :color) --> arg + elseif T <: Symbol || T <: AbstractString + try + Symbol(fontname, :color) --> parse(Colorant, string(arg)) + catch + Symbol(fontname, :family) --> string(arg) + end + elseif typeof(arg) <: Integer + if in(Symbol(fontname, :size), _all_attrs) + Symbol(fontname, :size) --> arg + else + Symbol(fontname, :pointsize) --> arg + end + elseif typeof(arg) <: Real + Symbol(fontname, :rotation) --> convert(Float64, arg) + else + @warn "Skipped font arg: $arg ($(typeof(arg)))" + end +end + +_replace_markershape(shape::Symbol) = get(_marker_aliases, shape, shape) +_replace_markershape(shapes::AVec) = map(_replace_markershape, shapes) +_replace_markershape(shape) = shape + +function _add_markershape(plotattributes::AKW) + # add the markershape if it needs to be added... hack to allow "m=10" to add a shape, + # and still allow overriding in _apply_recipe + ms = pop!(plotattributes, :markershape_to_add, :none) + if !haskey(plotattributes, :markershape) && ms !== :none + plotattributes[:markershape] = ms + end +end + +function convert_legend_value(val::Symbol) + if val in (:both, :all, :yes) + :best + elseif val in (:no, :none) + :none + elseif val in ( + :right, + :left, + :top, + :bottom, + :inside, + :best, + :legend, + :topright, + :topleft, + :bottomleft, + :bottomright, + :outertopright, + :outertopleft, + :outertop, + :outerright, + :outerleft, + :outerbottomright, + :outerbottomleft, + :outerbottom, + :inline, + ) + val + elseif val === :horizontal + -1 + else + error("Invalid symbol for legend: $val") + end +end +convert_legend_value(val::Real) = val +convert_legend_value(val::Bool) = val ? :best : :none +convert_legend_value(val::Nothing) = :none +convert_legend_value(v::Union{Tuple,NamedTuple}) = convert_legend_value.(v) +convert_legend_value(v::Tuple{<:Real,<:Real}) = v +convert_legend_value(v::Tuple{<:Real,Symbol}) = v +convert_legend_value(v::AbstractArray) = map(convert_legend_value, v) + +# ----------------------------------------------------------------------------- + +"""Throw an error if the `levels` keyword argument is not of the correct type +or `levels` is less than 1""" +function check_contour_levels(levels) + if !(levels isa Union{Integer,AVec}) + "the levels keyword argument must be an integer or AbstractVector" |> + ArgumentError |> + throw + elseif levels isa Integer && levels <= 0 + "must pass a positive number of contours to the levels keyword argument" |> + ArgumentError |> + throw + end +end + +# ----------------------------------------------------------------------------- + +# when a value can be `:match`, this is the key that should be used instead for value retrieval +const _match_map = Dict( + :background_color_outside => :background_color, + :legend_background_color => :background_color_subplot, + :background_color_inside => :background_color_subplot, + :legend_foreground_color => :foreground_color_subplot, + :foreground_color_title => :foreground_color_subplot, + :left_margin => :margin, + :top_margin => :margin, + :right_margin => :margin, + :bottom_margin => :margin, + :titlefontfamily => :fontfamily_subplot, + :titlefontcolor => :foreground_color_subplot, + :legend_font_family => :fontfamily_subplot, + :legend_font_color => :foreground_color_subplot, + :legend_title_font_family => :fontfamily_subplot, + :legend_title_font_color => :foreground_color_subplot, + :colorbar_fontfamily => :fontfamily_subplot, + :colorbar_titlefontfamily => :fontfamily_subplot, + :colorbar_titlefontcolor => :foreground_color_subplot, + :colorbar_tickfontfamily => :fontfamily_subplot, + :colorbar_tickfontcolor => :foreground_color_subplot, + :plot_titlefontfamily => :fontfamily, + :plot_titlefontcolor => :foreground_color, + :tickfontcolor => :foreground_color_text, + :guidefontcolor => :foreground_color_guide, + :annotationfontfamily => :fontfamily_subplot, + :annotationcolor => :foreground_color_subplot, +) + +# these can match values from the parent container (axis --> subplot --> plot) +const _match_map2 = Dict( + :background_color_subplot => :background_color, + :foreground_color_subplot => :foreground_color, + :foreground_color_axis => :foreground_color_subplot, + :foreground_color_border => :foreground_color_subplot, + :foreground_color_grid => :foreground_color_subplot, + :foreground_color_minor_grid => :foreground_color_subplot, + :foreground_color_guide => :foreground_color_subplot, + :foreground_color_text => :foreground_color_subplot, + :fontfamily_subplot => :fontfamily, + :tickfontfamily => :fontfamily_subplot, + :guidefontfamily => :fontfamily_subplot, +) + +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- + +has_black_border_for_default(st) = error( + "The seriestype attribute only accepts Symbols, you passed the $(typeof(st)) $st.", +) +has_black_border_for_default(st::Function) = + error("The seriestype attribute only accepts Symbols, you passed the function $st.") +has_black_border_for_default(st::Symbol) = + like_histogram(st) || st in (:hexbin, :bar, :shape) + +ensure_gradient!(plotattributes::AKW, csym::Symbol, asym::Symbol) = + if plotattributes[csym] isa ColorPalette + α = nothing + plotattributes[asym] isa AbstractVector || (α = plotattributes[asym]) + plotattributes[csym] = cgrad(plotattributes[csym], categorical = true, alpha = α) + elseif !(plotattributes[csym] isa ColorGradient) + plotattributes[csym] = + typeof(plotattributes[asym]) <: AbstractVector ? cgrad() : + cgrad(alpha = plotattributes[asym]) + end + +# get a good default linewidth... 0 for surface and heatmaps +_replace_linewidth(plotattributes::AKW) = + if plotattributes[:linewidth] === :auto + plotattributes[:linewidth] = + (get(plotattributes, :seriestype, :path) ∉ (:surface, :heatmap, :image)) * + DEFAULT_LINEWIDTH[] + end + +label_to_string(label::Bool, series_plotindex) = + label ? label_to_string(:auto, series_plotindex) : "" +label_to_string(label::Nothing, series_plotindex) = "" +label_to_string(label::Missing, series_plotindex) = "" +label_to_string(label::Symbol, series_plotindex) = + if label === :auto + string("y", series_plotindex) + elseif label === :none + "" + else + throw(ArgumentError("unsupported symbol $(label) passed to `label`")) + end +label_to_string(label, series_plotindex) = string(label) # Fallback to string promotion + +_series_index(plotattributes, sp) = + if haskey(plotattributes, :series_index) + plotattributes[:series_index]::Int + elseif get(plotattributes, :primary, true) + plotattributes[:series_index] = sp.primary_series_count += 1 + else + plotattributes[:series_index] = sp.primary_series_count + end + +#-------------------------------------------------- +## inspired by Base.@kwdef +""" + add_attributes(level, expr, match_table) + +Takes a `struct` definition and recurses into its fields to create keywords by chaining the field names with the structs' name with underscore. +Also creates pluralized and non-underscore aliases for these keywords. +- `level` indicates which group of `plot`, `subplot`, `series`, etc. the keywords belong to. +- `expr` is the struct definition with default values like `Base.@kwdef` +- `match_table` is an expression of the form `:match = (symbols)`, with symbols whose default value should be `:match` +""" +macro add_attributes(level, expr, match_table) + expr = macroexpand(__module__, expr) # to expand @static + expr isa Expr && expr.head === :struct || error("Invalid usage of @add_attributes") + if (T = expr.args[2]) isa Expr && T.head === :<: + T = T.args[1] + end + + key_dict = KW() + _splitdef!(expr.args[3], key_dict) + + insert_block = Expr(:block) + for (key, value) in key_dict + # e.g. _series_defaults[key] = value + exp_key = Symbol(lowercase(string(T)), "_", key) + pl_key = makeplural(exp_key) + if QuoteNode(exp_key) in match_table.args[2].args + value = QuoteNode(:match) + end + field = QuoteNode(Symbol("_", level, "_defaults")) + push!( + insert_block.args, + Expr( + :(=), + Expr(:ref, Expr(:call, getfield, Plots, field), QuoteNode(exp_key)), + value, + ), + :($add_aliases($(QuoteNode(exp_key)), $(QuoteNode(pl_key)))), + :($add_aliases( + $(QuoteNode(exp_key)), + $(QuoteNode(make_non_underscore(exp_key))), + )), + :($add_aliases( + $(QuoteNode(exp_key)), + $(QuoteNode(make_non_underscore(pl_key))), + )), + ) + end + quote + $expr + $insert_block + end |> esc +end + +function _splitdef!(blk, key_dict) + for i in eachindex(blk.args) + if (ei = blk.args[i]) isa Symbol + # var + continue + elseif ei isa Expr + if ei.head === :(=) + lhs = ei.args[1] + if lhs isa Symbol + # var = defexpr + var = lhs + elseif lhs isa Expr && lhs.head === :(::) && lhs.args[1] isa Symbol + # var::T = defexpr + var = lhs.args[1] + type = lhs.args[2] + if @isdefined type + for field in fieldnames(getproperty(Plots, type)) + key_dict[Symbol(var, "_", field)] = + :(getfield($(ei.args[2]), $(QuoteNode(field)))) + end + end + else + # something else, e.g. inline inner constructor + # F(...) = ... + continue + end + defexpr = ei.args[2] # defexpr + key_dict[var] = defexpr + blk.args[i] = lhs + elseif ei.head === :(::) && ei.args[1] isa Symbol + # var::Typ + var = ei.args[1] + key_dict[var] = defexpr + elseif ei.head === :block + # can arise with use of @static inside type decl + _kwdef!(ei, value_attrs, key_attrs) + end + end + end + blk +end diff --git a/src/Commons/postprocess_attrs.jl b/src/Commons/postprocess_attrs.jl new file mode 100644 index 000000000..f6cfad022 --- /dev/null +++ b/src/Commons/postprocess_attrs.jl @@ -0,0 +1,23 @@ + +# add all pluralized forms to the _keyAliases dict +for arg in _all_attrs + add_aliases(arg, makeplural(arg)) +end + +# fill symbol cache +for letter in (:x, :y, :z) + _attrsymbolcache[letter] = Dict{Symbol,Symbol}() + for k in _axis_attrs + # populate attribute cache + lk = Symbol(letter, k) + _attrsymbolcache[letter][k] = lk + # allow the underscore version too: xguide or x_guide + add_aliases(lk, Symbol(letter, "_", k)) + end + for k in (_magic_axis_attrs..., :(_discrete_indices)) + _attrsymbolcache[letter][k] = Symbol(letter, k) + end +end + +# add all non_underscored forms to the _keyAliases +add_non_underscore_aliases!(_keyAliases) diff --git a/src/Fonts.jl b/src/Fonts.jl new file mode 100644 index 000000000..c2b6edb05 --- /dev/null +++ b/src/Fonts.jl @@ -0,0 +1,177 @@ +module Fonts + +using Plots.Colors +using Plots.Commons +using Plots.Commons: + _initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes, _initial_fontsizes +# keep in mind: these will be reexported and are public API +export font, scalefontsizes, resetfontsizes, text, is_horizontal, Font + +mutable struct Font + family::AbstractString + pointsize::Int + halign::Symbol + valign::Symbol + rotation::Float64 + color::Colorant +end + +""" + font(args...) +Create a Font from a list of features. Values may be specified either as +arguments (which are distinguished by type/value) or as keyword arguments. +# Arguments +- `family`: AbstractString. "serif" or "sans-serif" or "monospace" +- `pointsize`: Integer. Size of font in points +- `halign`: Symbol. Horizontal alignment (:hcenter, :left, or :right) +- `valign`: Symbol. Vertical alignment (:vcenter, :top, or :bottom) +- `rotation`: Real. Angle of rotation for text in degrees (use a non-integer type) +- `color`: Colorant or Symbol +# Examples +```julia-repl +julia> font(8) +julia> font(family="serif", halign=:center, rotation=45.0) +``` +""" +function font(args...; kw...) + # defaults + family = "sans-serif" + pointsize = 14 + halign = :hcenter + valign = :vcenter + rotation = 0 + color = colorant"black" + + for arg in args + T = typeof(arg) + @assert arg !== :match + + if T == Font + family = arg.family + pointsize = arg.pointsize + halign = arg.halign + valign = arg.valign + rotation = arg.rotation + color = arg.color + elseif arg === :center + halign = :hcenter + valign = :vcenter + elseif arg ∈ _haligns + halign = arg + elseif arg ∈ _valigns + valign = arg + elseif T <: Colorant + color = arg + elseif T <: Symbol || T <: AbstractString + try + color = parse(Colorant, string(arg)) + catch + family = string(arg) + end + elseif T <: Integer + pointsize = arg + elseif T <: Real + rotation = convert(Float64, arg) + else + @warn "Unused font arg: $arg ($T)" + end + end + + for sym in keys(kw) + if sym === :family + family = string(kw[sym]) + elseif sym === :pointsize + pointsize = kw[sym] + elseif sym === :halign + halign = kw[sym] + halign === :center && (halign = :hcenter) + @assert halign ∈ _haligns + elseif sym === :valign + valign = kw[sym] + valign === :center && (valign = :vcenter) + @assert valign ∈ _valigns + elseif sym === :rotation + rotation = kw[sym] + elseif sym === :color + col = kw[sym] + color = col isa Colorant ? col : parse(Colorant, col) + else + @warn "Unused font kwarg: $sym" + end + end + + Font(family, pointsize, halign, valign, rotation, color) +end + +function scalefontsize(k::Symbol, factor::Number) + f = default(k) + f = round(Int, factor * f) + default(k, f) +end + +""" + scalefontsizes(factor::Number) + +Scales all **current** font sizes by `factor`. For example `scalefontsizes(1.1)` increases all current font sizes by 10%. To reset to initial sizes, use `scalefontsizes()` +""" +function scalefontsizes(factor::Number) + for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) + scalefontsize(k, factor) + end + + for letter in (:x, :y, :z) + for k in keys(_initial_ax_fontsizes) + scalefontsize(get_attr_symbol(letter, k), factor) + end + end +end + +""" + scalefontsizes() + +Resets font sizes to initial default values. +""" +function scalefontsizes() + for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) + f = default(k) + if k in keys(_initial_fontsizes) + factor = f / _initial_fontsizes[k] + scalefontsize(k, 1.0 / factor) + end + end + + for letter in (:x, :y, :z) + for k in keys(_initial_ax_fontsizes) + if k in keys(_initial_fontsizes) + f = default(get_attr_symbol(letter, k)) + factor = f / _initial_fontsizes[k] + scalefontsize(get_attr_symbol(letter, k), 1.0 / factor) + end + end + end +end + +resetfontsizes() = scalefontsizes() + +"Wrap a string with font info" +struct PlotText + str::AbstractString + font::Font +end +PlotText(str) = PlotText(string(str), font()) + +""" + text(string, args...; kw...) + +Create a PlotText object wrapping a string with font info, for plot annotations. +`args` and `kw` are passed to `font`. +""" +text(t::PlotText) = t +text(t::PlotText, font::Font) = PlotText(t.str, font) +text(str::AbstractString, f::Font) = PlotText(str, f) +text(str, args...; kw...) = PlotText(string(str), font(args...; kw...)) + +Base.length(t::PlotText) = length(t.str) + +is_horizontal(t::PlotText) = abs(sind(t.font.rotation)) ≤ sind(45) +end # Fonts diff --git a/src/plotmeasures.jl b/src/PlotMeasures.jl similarity index 52% rename from src/plotmeasures.jl rename to src/PlotMeasures.jl index 7c9278493..bcbc843ef 100644 --- a/src/plotmeasures.jl +++ b/src/PlotMeasures.jl @@ -1,5 +1,8 @@ module PlotMeasures +export PX_PER_INCH, + DPI, MM_PER_INCH, MM_PER_PX, DEFAULT_BBOX, DEFAULT_MINPAD, DEFAULT_LINEWIDTH + import ..Measures import ..Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height, w, h @@ -11,6 +14,15 @@ export BBox, BoundingBox, mm, cm, inch, px, pct, pt, w, h const px = AbsoluteLength(0.254) const pct = Length{:pct,Float64}(1.0) +const PX_PER_INCH = 100 +const DPI = PX_PER_INCH +const MM_PER_INCH = 25.4 +const MM_PER_PX = MM_PER_INCH / PX_PER_INCH +const _cbar_width = 5mm +const DEFAULT_BBOX = Ref(BoundingBox(0mm, 0mm, 0mm, 0mm)) +const DEFAULT_MINPAD = Ref((20mm, 5mm, 2mm, 10mm)) +const DEFAULT_LINEWIDTH = Ref(1) + Base.convert(::Type{<:Measure}, x::Float64) = x * pct Base.:*(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * m2.value) @@ -18,4 +30,11 @@ Base.:*(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * m1.val Base.:/(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value / m2.value) Base.:/(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value / m1.value) +inch2px(inches::Real) = float(inches * PX_PER_INCH) +px2inch(px::Real) = float(px / PX_PER_INCH) +inch2mm(inches::Real) = float(inches * MM_PER_INCH) +mm2inch(mm::Real) = float(mm / MM_PER_INCH) +px2mm(px::Real) = float(px * MM_PER_PX) +mm2px(mm::Real) = float(mm / MM_PER_PX) + end diff --git a/src/Plots.jl b/src/Plots.jl index 3c8e6b942..fc5ebd9dc 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -16,7 +16,6 @@ using Base.Meta import RecipesBase: plot, plot!, animate, is_explicit, grid import RecipesPipeline -import Requires: @require import RecipesPipeline: inverse_scale_func, datetimeformatter, @@ -32,7 +31,6 @@ import RecipesPipeline: Formatted, reset_kw!, SliceIt, - Surface, pop_kw!, Volume, is3d @@ -51,9 +49,8 @@ export plotarea, KW, - wrap, theme, - + protect, plot, plot!, attr!, @@ -86,20 +83,16 @@ export backends, backend_name, backend_object, - aliases, - Shape, text, font, stroke, brush, - Surface, OHLC, arrow, - Segments, - Formatted, + Shape, + cgrad, - Animation, frame, gif, mov, @@ -109,9 +102,9 @@ export @animate, @gif, @P_str, + Animation, test_examples, - iter_segments, coords, translate, @@ -119,60 +112,94 @@ export rotate, rotate!, center, - BezierCurve, - plotattr, - scalefontsize, scalefontsizes, resetfontsizes #! format: on +using Measures: Measures +include("PlotMeasures.jl") +using .PlotMeasures +using .PlotMeasures: Length, AbsoluteLength, Measure +import .PlotMeasures: width, height # --------------------------------------------------------- - -import NaNMath # define functions that ignores NaNs. To overcome the destructive effects of https://github.com/JuliaLang/julia/pull/12563 -ignorenan_minimum(x::AbstractArray{<:AbstractFloat}) = NaNMath.minimum(x) -ignorenan_minimum(x) = Base.minimum(x) -ignorenan_maximum(x::AbstractArray{<:AbstractFloat}) = NaNMath.maximum(x) -ignorenan_maximum(x) = Base.maximum(x) -ignorenan_mean(x::AbstractArray{<:AbstractFloat}) = NaNMath.mean(x) -ignorenan_mean(x) = Statistics.mean(x) -ignorenan_extrema(x::AbstractArray{<:AbstractFloat}) = NaNMath.extrema(x) -ignorenan_extrema(x) = Base.extrema(x) - +macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) + Expr( + :module, + true, + mod, + Expr( + :block, + Expr( + :import, + Expr( + :(:), + Expr(:., :., :., parent), + (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., + ), + ), + Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...), + ), + ) |> esc +end +using NaNMath: NaNMath +include("Commons/Commons.jl") +using .Commons +using .Commons.Frontend # --------------------------------------------------------- -import Measures -include("plotmeasures.jl") -using .PlotMeasures -import .PlotMeasures: Length, AbsoluteLength, Measure, width, height +include("Fonts.jl") +@reexport using .Fonts +using .Fonts: Font, PlotText +include("Ticks.jl") +using .Ticks +include("Series.jl") +using .PlotsSeries +include("Subplots.jl") +using .Subplots +import .Subplots: plotarea, plotarea!, leftpad, toppad, bottompad, rightpad +include("Axes.jl") +using .Axes +include("Surfaces.jl") +include("Colorbars.jl") +using .Colorbars +include("PlotsPlots.jl") +using .PlotsPlots +include("layouts.jl") # --------------------------------------------------------- - -const PLOTS_SEED = 1234 -const PX_PER_INCH = 100 -const DPI = PX_PER_INCH -const MM_PER_INCH = 25.4 -const MM_PER_PX = MM_PER_INCH / PX_PER_INCH - -include("types.jl") include("utils.jl") -include("colorbars.jl") -include("axes.jl") -include("args.jl") -include("components.jl") +using .Surfaces +include("axes_utils.jl") include("legend.jl") -include("consts.jl") +include("Shapes.jl") +using .Shapes +using .Shapes: Shape, _shapes, rotate! +include("Annotations.jl") +using .Annotations +using .Annotations: SeriesAnnotations, process_annotation +include("Arrows.jl") +using .Arrows +include("Strokes.jl") +using .Strokes +using .Strokes: Stroke, Brush +include("BezierCurves.jl") +using .BezierCurves include("themes.jl") include("plot.jl") include("pipeline.jl") -include("layouts.jl") include("arg_desc.jl") include("recipes.jl") include("animation.jl") include("examples.jl") include("plotattr.jl") -include("backends.jl") -const CURRENT_BACKEND = CurrentBackend(:none) +include("backends/nobackend.jl") +include("abstract_backend.jl") +include("alignment.jl") +const CURRENT_BACKEND = CurrentBackend(:none, NoBackend()) include("output.jl") include("shorthands.jl") include("backends/web.jl") +include("backends/plotly.jl") +using .Plotly include("init.jl") +include("users.jl") end diff --git a/src/PlotsPlots.jl b/src/PlotsPlots.jl new file mode 100644 index 000000000..48f9d983b --- /dev/null +++ b/src/PlotsPlots.jl @@ -0,0 +1,293 @@ +module PlotsPlots + +export Plot, + PlotOrSubplot, + _update_plot_attrs, + plottitlefont, + ignorenan_extrema, + protect, + InputWrapper +import Plots.Axes: _update_axis, scale_lims! +import Plots.Commons: ignorenan_extrema, _cycle +import Plots.Ticks: get_ticks +using Plots: + Plots, + AbstractPlot, + AbstractBackend, + DefaultsDict, + Series, + AbstractLayout, + RecipesPipeline +using Plots.PlotMeasures +using Plots.Colorbars: _update_subplot_colorbars +using Plots.Subplots: Subplot, _update_subplot_colors, _update_margins +using Plots.Axes: Axis, get_axis +using Plots.PlotUtils: get_color_palette +using Plots.Commons +using Plots.Commons.Frontend +using Plots.Fonts: font + +const SubplotMap = Dict{Any,Subplot} +mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} + backend::T # the backend type + n::Int # number of series + attr::DefaultsDict # arguments for the whole plot + series_list::Vector{Series} # arguments for each series + o # the backend's plot object + subplots::Vector{Subplot} + spmap::SubplotMap # provide any label as a map to a subplot + layout::AbstractLayout + inset_subplots::Vector{Subplot} # list of inset subplots + init::Bool + + function Plot() + be = Plots.backend() + new{typeof(be)}( + be, + 0, + DefaultsDict(KW(), Plots._plot_defaults), + Series[], + nothing, + Subplot[], + SubplotMap(), + Plots.EmptyLayout(), + Subplot[], + false, + ) + end + + function Plot(osp::Subplot) + plt = Plot() + plt.layout = Plots.GridLayout(1, 1) + sp = deepcopy(osp) # FIXME: fails `PlotlyJS` ? + plt.layout.grid[1, 1] = sp + # reset some attributes + sp.minpad = PlotMeasures.DEFAULT_MINPAD[] + sp.bbox = PlotMeasures.DEFAULT_BBOX[] + sp.plotarea = PlotMeasures.DEFAULT_BBOX[] + sp.plt = plt # change the enclosing plot + push!(plt.subplots, sp) + plt + end +end # Plot + +const PlotOrSubplot = Union{Plot,Subplot} +# ----------------------------------------------------------- + +struct InputWrapper{T} + obj::T +end +protect(obj::T) where {T} = InputWrapper{T}(obj) +Base.isempty(wrapper::InputWrapper) = false +_cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj +_cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj + +# ----------------------------------------------------------- + +Base.iterate(plt::Plot) = iterate(plt.subplots) +# ------------------------------------------------------- +# push/append for one series + +Base.push!(plt::Plot, args::Real...) = push!(plt, 1, args...) +Base.push!(plt::Plot, i::Integer, args::Real...) = push!(plt.series_list[i], args...) +Base.append!(plt::Plot, args::AbstractVector) = append!(plt, 1, args...) +Base.append!(plt::Plot, i::Integer, args::Real...) = append!(plt.series_list[i], args...) + +# tuples +Base.push!(plt::Plot, t::Tuple) = push!(plt, 1, t...) +Base.push!(plt::Plot, i::Integer, t::Tuple) = push!(plt, i, t...) +Base.append!(plt::Plot, t::Tuple) = append!(plt, 1, t...) +Base.append!(plt::Plot, i::Integer, t::Tuple) = append!(plt, i, t...) + +# ------------------------------------------------------- +# push/append for all series + +# push y[i] to the ith series +function Base.push!(plt::Plot, y::AVec) + ny = length(y) + for i in 1:(plt.n) + push!(plt, i, y[mod1(i, ny)]) + end + plt +end + +# push y[i] to the ith series +# same x for each series +Base.push!(plt::Plot, x::Real, y::AVec) = push!(plt, [x], y) + +# push (x[i], y[i]) to the ith series +function Base.push!(plt::Plot, x::AVec, y::AVec) + nx = length(x) + ny = length(y) + for i in 1:(plt.n) + push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)]) + end + plt +end + +# push (x[i], y[i], z[i]) to the ith series +function Base.push!(plt::Plot, x::AVec, y::AVec, z::AVec) + nx = length(x) + ny = length(y) + nz = length(z) + for i in 1:(plt.n) + push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)], z[mod1(i, nz)]) + end + plt +end + +# --------------------------------------------------------------- + +"Smallest x in plot" +xmin(plt::Plot) = ignorenan_minimum([ + ignorenan_minimum(series.plotattributes[:x]) for series in plt.series_list +]) +"Largest x in plot" +xmax(plt::Plot) = ignorenan_maximum([ + ignorenan_maximum(series.plotattributes[:x]) for series in plt.series_list +]) + +"Extrema of x-values in plot" +ignorenan_extrema(plt::Plot) = (xmin(plt), xmax(plt)) + +# --------------------------------------------------------------- +# indexing notation +# properly retrieve from plt.attr, passing `:match` to the correct key + +Base.getindex(plt::Plot, k::Symbol) = + if (v = plt.attr[k]) === :match + plt[Commons._match_map[k]] + else + v + end +Base.getindex(plt::Plot, i::Union{Vector{<:Integer},Integer}) = plt.subplots[i] +Base.getindex(plt::Plot, r::Integer, c::Integer) = plt.layout[r, c] +Base.setindex!(plt::Plot, xy::NTuple{2}, i::Integer) = (setxy!(plt, xy, i); plt) +Base.setindex!(plt::Plot, xyz::Tuple{3}, i::Integer) = (setxyz!(plt, xyz, i); plt) +Base.setindex!(plt::Plot, v, k::Symbol) = (plt.attr[k] = v) +Base.length(plt::Plot) = length(plt.subplots) +Base.lastindex(plt::Plot) = length(plt) +Base.get(plt::Plot, k::Symbol, v) = get(plt.attr, k, v) + +Base.size(plt::Plot) = size(plt.layout) +Base.size(plt::Plot, i::Integer) = size(plt.layout)[i] +Base.ndims(plt::Plot) = 2 + +# clear out series list, but retain subplots +Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) +Plots.get_subplot(plt::Plot, sp::Subplot) = sp +Plots.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] +Plots.get_subplot(plt::Plot, k) = plt.spmap[k] +Plots.series_list(plt::Plot) = plt.series_list + +get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) + +get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x === sp, plt.subplots) +Plots.RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = + Commons.preprocess_attributes!(plotattributes) + +plottitlefont(p::Plot) = font(; + family = p[:plot_titlefontfamily], + pointsize = p[:plot_titlefontsize], + valign = p[:plot_titlefontvalign], + halign = p[:plot_titlefonthalign], + rotation = p[:plot_titlefontrotation], + color = p[:plot_titlefontcolor], +) + +# update attr from an input dictionary +function _update_plot_attrs(plt::Plot, plotattributes_in::AKW) + for (k, v) in Plots._plot_defaults + Plots.slice_arg!(plotattributes_in, plt.attr, k, 1, true) + end + + # handle colors + plt[:background_color] = plot_color(plt.attr[:background_color]) + plt[:foreground_color] = fg_color(plt.attr) + color_or_nothing!(plt.attr, :background_color_outside) +end + +function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol) + # handle linking here. if we're passed a list of + # other subplots to link to, link them together + (link = axis[:link]) |> isempty && return + for other_sp in link + link_axes!(axis, get_axis(get_subplot(plt, other_sp), letter)) + end + axis.plotattributes[:link] = [] + nothing +end + +function Plots.Axes._update_axis( + plt::Plot, + sp::Subplot, + plotattributes_in::AKW, + letter::Symbol, + subplot_index::Int, +) + # get (maybe initialize) the axis + axis = get_axis(sp, letter) + + _update_axis(axis, plotattributes_in, letter, subplot_index) + + # convert a bool into auto or nothing + if isa(axis[:ticks], Bool) + axis[:ticks] = axis[:ticks] ? :auto : nothing + end + + Plots.Axes._update_axis_colors(axis) + _update_axis_links(plt, axis, letter) + nothing +end + +# update a subplots args and axes +function _update_subplot_attrs( + plt::Plot, + sp::Subplot, + plotattributes_in, + subplot_index::Int, + remove_pair::Bool, +) + anns = RecipesPipeline.pop_kw!(sp.attr, :annotations) + + # grab those args which apply to this subplot + for k in keys(_subplot_defaults) + Plots.slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair) + end + + _update_subplot_colors(sp) + _update_margins(sp) + colorbar_update_keys = + (:clims, :colorbar, :seriestype, :marker_z, :line_z, :fill_z, :colorbar_entry) + if any(haskey.(Ref(plotattributes_in), colorbar_update_keys)) + _update_subplot_colorbars(sp) + end + + lims_warned = false + for letter in (:x, :y, :z) + _update_axis(plt, sp, plotattributes_in, letter, subplot_index) + lk = get_attr_symbol(letter, :lims) + + # warn against using `Range` in x,y,z lims + if !lims_warned && + haskey(plotattributes_in, lk) && + plotattributes_in[lk] isa AbstractRange + @warn "lims should be a Tuple, not $(typeof(plotattributes_in[lk]))." + lims_warned = true + end + end + + Plots.Subplots._update_subplot_periphery(sp, anns) +end + +function scale_lims!(plt::Plot, letter, factor) + foreach(sp -> scale_lims!(sp, letter, factor), plt.subplots) + plt +end +function scale_lims!(plt::Union{Plot,Subplot}, factor) + foreach(letter -> scale_lims!(plt, letter, factor), (:x, :y, :z)) + plt +end +Commons.get_size(plt::Plot) = get_size(plt.attr) +Commons.get_thickness_scaling(plt::Plot) = get_thickness_scaling(plt.attr) +end # PlotsPlots diff --git a/src/Series.jl b/src/Series.jl new file mode 100644 index 000000000..cd66546f1 --- /dev/null +++ b/src/Series.jl @@ -0,0 +1,331 @@ +module PlotsSeries + +export Series, + should_add_to_legend, + get_colorgradient, + iscontour, + isfilledcontour, + contour_levels, + series_segments +export get_linestyle, + get_linewidth, + get_markerstrokealpha, + get_markerstrokealpha, + get_markerstrokecolor, + get_markerstrokewidth, + get_linecolor, + get_linealpha, + get_fillstyle, + get_fillcolor, + get_fillalpha, + get_markercolor, + get_markeralpha +import Plots.Commons: get_subplot, _series_defaults +using Plots.Commons +using Plots.Commons: get_gradient +using Plots.PlotUtils: ColorGradient, plot_color +using Plots: Plots, DefaultsDict, RecipesPipeline, get_attr_symbol, KW + +mutable struct Series + plotattributes::DefaultsDict +end + +Base.getindex(series::Series, k::Symbol) = series.plotattributes[k] +Base.setindex!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) +Base.get(series::Series, k::Symbol, v) = get(series.plotattributes, k, v) +Base.push!(series::Series, args...) = extend_series!(series, args...) +Base.append!(series::Series, args...) = extend_series!(series, args...) + +# TODO: consider removing +attr(series::Series, k::Symbol) = series.plotattributes[k] +attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) +function attr!(series::Series; kw...) + plotattributes = KW(kw) + Plots.Commons.preprocess_attributes!(plotattributes) + for (k, v) in plotattributes + if haskey(_series_defaults, k) + series[k] = v + else + @warn "unused key $k in series attr" + end + end + Plots._series_updated(series[:subplot].plt, series) + series +end + +should_add_to_legend(series::Series) = + series.plotattributes[:primary] && + series.plotattributes[:label] != "" && + series.plotattributes[:seriestype] ∉ ( + :hexbin, + :bins2d, + :histogram2d, + :hline, + :vline, + :contour, + :contourf, + :contour3d, + :surface, + :wireframe, + :heatmap, + :image, + ) + +Plots.get_subplot(series::Series) = series.plotattributes[:subplot] +Plots.RecipesPipeline.is3d(series::Series) = RecipesPipeline.is3d(series.plotattributes) +Plots.ispolar(series::Series) = Plots.ispolar(series.plotattributes[:subplot]) +# ------------------------------------------------------- +# operate on individual series + +function extend_series!(series::Series, yi) + y = extend_series_data!(series, yi, :y) + x = extend_to_length!(series[:x], length(y)) + expand_extrema!(series[:subplot][:xaxis], x) + x, y +end + +extend_series!(series::Series, xi, yi) = + (extend_series_data!(series, xi, :x), extend_series_data!(series, yi, :y)) + +extend_series!(series::Series, xi, yi, zi) = ( + extend_series_data!(series, xi, :x), + extend_series_data!(series, yi, :y), + extend_series_data!(series, zi, :z), +) + +function extend_series_data!(series::Series, v, letter) + copy_series!(series, letter) + d = extend_by_data!(series[letter], v) + expand_extrema!(series[:subplot][get_attr_symbol(letter, :axis)], d) + d +end + +function copy_series!(series, letter) + plt = series[:plot_object] + for s in plt.series_list, l in (:x, :y, :z) + if (s !== series || l !== letter) && s[l] === series[letter] + series[letter] = copy(series[letter]) + end + end +end + +extend_to_length!(v::AbstractRange, n) = range(first(v), step = step(v), length = n) +function extend_to_length!(v::AbstractVector, n) + vmax = isempty(v) ? 0 : ignorenan_maximum(v) + extend_by_data!(v, vmax .+ (1:(n - length(v)))) +end +extend_by_data!(v::AbstractVector, x) = isimmutable(v) ? vcat(v, x) : push!(v, x) +extend_by_data!(v::AbstractVector, x::AbstractVector) = + isimmutable(v) ? vcat(v, x) : append!(v, x) + +for comp in (:line, :fill, :marker) + compcolor = string(comp, :color) + get_compcolor = Symbol(:get_, compcolor) + comp_z = string(comp, :_z) + + compalpha = string(comp, :alpha) + get_compalpha = Symbol(:get_, compalpha) + + @eval begin + # defines `get_linecolor`, `get_fillcolor` and `get_markercolor` <- for grep + function $get_compcolor( + series, + cmin::Real, + cmax::Real, + i::Integer = 1, + s::Symbol = :identity, + ) + c = series[$Symbol($compcolor)] # series[:linecolor], series[:fillcolor], series[:markercolor] + z = series[$Symbol($comp_z)] # series[:line_z], series[:fill_z], series[:marker_z] + if z === nothing + isa(c, ColorGradient) ? c : plot_color(_cycle(c, i)) + else + grad = get_gradient(c) + if s === :identity + get(grad, z[i], (cmin, cmax)) + else + base = _log_scale_bases[s] + get(grad, log(base, z[i]), (log(base, cmin), log(base, cmax))) + end + end + end + + function $get_compcolor(series, i::Integer = 1, s::Symbol = :identity) + if series[$Symbol($comp_z)] === nothing + $get_compcolor(series, 0, 1, i, s) + else + $get_compcolor(series, get_clims(series[:subplot]), i, s) + end + end + + $get_compcolor(series, clims::NTuple{2,<:Number}, args...) = + $get_compcolor(series, clims[1], clims[2], args...) + + $get_compalpha(series, i::Integer = 1) = _cycle(series[$Symbol($compalpha)], i) + end +end + +get_linewidth(series, i::Integer = 1) = _cycle(series[:linewidth], i) +get_linestyle(series, i::Integer = 1) = _cycle(series[:linestyle], i) +get_fillstyle(series, i::Integer = 1) = _cycle(series[:fillstyle], i) + +get_markerstrokecolor(series, i::Integer = 1) = + let msc = series[:markerstrokecolor] + msc isa ColorGradient ? msc : _cycle(msc, i) + end + +get_markerstrokealpha(series, i::Integer = 1) = _cycle(series[:markerstrokealpha], i) +get_markerstrokewidth(series, i::Integer = 1) = _cycle(series[:markerstrokewidth], i) + +function get_colorgradient(series::Series) + if (st = series[:seriestype]) in (:surface, :heatmap) || isfilledcontour(series) + series[:fillcolor] + elseif st in (:contour, :wireframe, :contour3d) + series[:linecolor] + elseif series[:marker_z] !== nothing + series[:markercolor] + elseif series[:line_z] !== nothing + series[:linecolor] + elseif series[:fill_z] !== nothing + series[:fillcolor] + end +end + +iscontour(series::Series) = series[:seriestype] in (:contour, :contour3d) +isfilledcontour(series::Series) = iscontour(series) && series[:fillrange] !== nothing + +function contour_levels(series::Series, clims) + iscontour(series) || error("Not a contour series") + zmin, zmax = clims + levels = series[:levels] + if levels isa Integer + levels = range(zmin, stop = zmax, length = levels + 2) + isfilledcontour(series) || (levels = levels[2:(end - 1)]) + end + levels +end +# ------------------------------------------------------- +Commons.get_size(series::Series) = Commons.get_size(series.plotattributes[:subplot]) +Commons.get_thickness_scaling(series::Series) = + Commons.get_thickness_scaling(series.plotattributes[:subplot]) + +# ------------------------------------------------------- +struct SeriesSegment + # indexes of this segment 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 +struct NaNSegmentsIterator + args::Tuple + n1::Int + n2::Int +end + +function Base.iterate(itr::NaNSegmentsIterator, nextidx::Int = itr.n1) + (i = findfirst(!Plots.Commons.anynan(itr.args), nextidx:(itr.n2))) === nothing && return + nextval = nextidx + i - 1 + + j = findfirst(Plots.Commons.anynan(itr.args), nextval:(itr.n2)) + nextnan = j === nothing ? itr.n2 + 1 : nextval + j - 1 + + nextval:(nextnan - 1), nextnan +end + +Base.IteratorSize(::NaNSegmentsIterator) = Base.SizeUnknown() # COV_EXCL_LINE + +function iter_segments(args...) + tup = Plots.wraptuple(args) + n1 = minimum(map(firstindex, tup)) + n2 = maximum(map(lastindex, tup)) + NaNSegmentsIterator(tup, n1, n2) +end + +# we want to check if a series needs to be split into segments just because +# of its attributes +# check relevant attributes if they have multiple inputs +has_attribute_segments(series::Series) = + any( + series[attr] isa AbstractVector && length(series[attr]) > 1 for + attr in Plots.Commons._segmenting_vector_attributes + ) || any( + series[attr] isa AbstractArray for + attr in Plots.Commons._segmenting_array_attributes + ) + +function series_segments(series::Series, seriestype::Symbol = :path; check = false) + x, y, z = series[:x], series[:y], series[:z] + (x === nothing || isempty(x)) && return UnitRange{Int}[] + + args = RecipesPipeline.is3d(series) ? (x, y, z) : (x, y) + nan_segments = collect(iter_segments(args...)) + + if check + scales = :xscale, :yscale, :zscale + for (n, s) in enumerate(args) + (scale = get(series, scales[n], :identity)) ∈ Plots.Commons._log_scales || + continue + for (i, v) in enumerate(s) + if v <= 0 + @warn "Invalid negative or zero value $v found at series index $i for $scale based $(scales[n])" + @debug "" exception = (DomainError(v), stacktrace()) + break + end + end + end + end + + segments = if has_attribute_segments(series) + map(nan_segments) do r + if seriestype === :shape + warn_on_inconsistent_shape_attrs(series, x, y, z, r) + (SeriesSegment(r, first(r)),) + elseif 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 |> Iterators.flatten + else + (SeriesSegment(r, 1) for r in nan_segments) + end + + warn_on_attr_dim_mismatch(series, x, y, z, segments) + segments +end + +function warn_on_attr_dim_mismatch(series, x, y, z, segments) + isempty(segments) && return + seg_range = UnitRange( + minimum(map(seg -> first(seg.range), segments)), + maximum(map(seg -> last(seg.range), segments)), + ) + for attr in Plots.Commons._segmenting_vector_attributes + if (v = get(series, attr, nothing)) isa Plots.Commons.AVec && + eachindex(v) != seg_range + @warn "Indices $(eachindex(v)) of attribute `$attr` does not match data indices $seg_range." + if any(v -> !isnothing(v) && any(isnan, v), (x, y, z)) + @info """Data contains NaNs or missing values, and indices of `$attr` vector do not match data indices. + If you intend elements of `$attr` to apply to individual NaN-separated segments in the data, + pass each segment in a separate vector instead, and use a row vector for `$attr`. Legend entries + may be suppressed by passing an empty label. + For example, + plot([1:2,1:3], [[4,5],[3,4,5]], label=["y" ""], $attr=[1 2]) + """ + end + end + end +end + +function warn_on_inconsistent_shape_attrs(series, x, y, z, r) + for attr in Plots.Commons._segmenting_vector_attributes + v = get(series, attr, nothing) + if v isa Plots.Commons.AVec && length(unique(v[r])) > 1 + @warn "Different values of `$attr` specified for different shape vertices. Only first one will be used." + break + end + end +end +end # PlotsSeries diff --git a/src/Shapes.jl b/src/Shapes.jl new file mode 100644 index 000000000..bd0544526 --- /dev/null +++ b/src/Shapes.jl @@ -0,0 +1,228 @@ +module Shapes + +using Plots: Plots, RecipesPipeline +using Plots.Commons + +# keep in mind: these will be reexported and are public API +export Shape, + partialcircle, + weave, + makestar, + makeshape, + makecross, + from_polar, + makearrowhead, + center, + scale!, + scale, + translate, + translate!, + rotate, + rotate! + +const P2 = NTuple{2,Float64} +const P3 = NTuple{3,Float64} + +nanpush!(a::AVec{P2}, b) = (push!(a, (NaN, NaN)); push!(a, b); nothing) +nanappend!(a::AVec{P2}, b) = (push!(a, (NaN, NaN)); append!(a, b); nothing) +nanpush!(a::AVec{P3}, b) = (push!(a, (NaN, NaN, NaN)); push!(a, b); nothing) +nanappend!(a::AVec{P3}, b) = (push!(a, (NaN, NaN, NaN)); append!(a, b); nothing) + +compute_angle(v::P2) = (angle = atan(v[2], v[1]); angle < 0 ? 2π - angle : angle) + +# ------------------------------------------------------------- + +struct Shape{X<:Number,Y<:Number} + x::Vector{X} + y::Vector{Y} +end + +""" + shape(x, y) + shape(vertices) + +Construct a polygon to be plotted +""" +Shape(verts::AVec) = Shape(RecipesPipeline.unzip(verts)...) +Shape(s::Shape) = deepcopy(s) +function Shape(x::AVec{X}, y::AVec{Y}) where {X,Y} + return Shape(convert(Vector{X}, x), convert(Vector{Y}, y)) +end + +get_xs(shape::Shape) = shape.x +get_ys(shape::Shape) = shape.y +vertices(shape::Shape) = collect(zip(shape.x, shape.y)) + +"return the vertex points from a Shape or Segments object" +Plots.coords(shape::Shape) = shape.x, shape.y + +Plots.coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shapes)) + +"get an array of tuples of points on a circle with radius `r`" +partialcircle(start_θ, end_θ, n = 20, r = 1) = + [(r * cos(u), r * sin(u)) for u in range(start_θ, stop = end_θ, length = n)] + +"interleave 2 vectors into each other (like a zipper's teeth)" +function weave(x, y; ordering = Vector[x, y]) + ret = eltype(x)[] + done = false + while !done + for o in ordering + try + push!(ret, popfirst!(o)) + catch + end + end + done = isempty(x) && isempty(y) + end + ret +end + +"create a star by weaving together points from an outer and inner circle. `n` is the number of arms" +function makestar(n; offset = -0.5, radius = 1.0) + z1 = offset * π + z2 = z1 + π / (n) + outercircle = partialcircle(z1, z1 + 2π, n + 1, radius) + innercircle = partialcircle(z2, z2 + 2π, n + 1, 0.4radius) + Shape(weave(outercircle, innercircle)) +end + +"create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle" +makeshape(n; offset = -0.5, radius = 1.0) = + Shape(partialcircle(offset * π, offset * π + 2π, n + 1, radius)) + +function makecross(; offset = -0.5, radius = 1.0) + z2 = offset * π + z1 = z2 - π / 8 + outercircle = partialcircle(z1, z1 + 2π, 9, radius) + innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius) + Shape( + weave( + outercircle, + innercircle, + ordering = Vector[outercircle, innercircle, outercircle], + ), + ) +end + +from_polar(angle, dist) = (dist * cos(angle), dist * sin(angle)) + +makearrowhead(angle; h = 2.0, w = 0.4, tip = from_polar(angle, h)) = Shape( + NTuple{2,Float64}[ + (0, 0), + from_polar(angle - 0.5π, w) .- tip, + from_polar(angle + 0.5π, w) .- tip, + (0, 0), + ], +) + +const _shapes = KW( + :circle => makeshape(20), + :rect => makeshape(4, offset = -0.25), + :diamond => makeshape(4), + :utriangle => makeshape(3, offset = 0.5), + :dtriangle => makeshape(3, offset = -0.5), + :rtriangle => makeshape(3, offset = 0.0), + :ltriangle => makeshape(3, offset = 1.0), + :pentagon => makeshape(5), + :hexagon => makeshape(6), + :heptagon => makeshape(7), + :octagon => makeshape(8), + :cross => makecross(offset = -0.25), + :xcross => makecross(), + :vline => Shape([(0, 1), (0, -1)]), + :hline => Shape([(1, 0), (-1, 0)]), + :star4 => makestar(4), + :star5 => makestar(5), + :star6 => makestar(6), + :star7 => makestar(7), + :star8 => makestar(8), +) + +Shape(k::Symbol) = deepcopy(_shapes[k]) + +# ----------------------------------------------------------------------- + +# uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon +"return the centroid of a Shape" +function center(shape::Shape) + x, y = coords(shape) + n = length(x) + A, Cx, Cy = 0, 0, 0 + for i in 1:n + ip1 = i == n ? 1 : i + 1 + A += x[i] * y[ip1] - x[ip1] * y[i] + end + A *= 0.5 + for i in 1:n + ip1 = i == n ? 1 : i + 1 + m = (x[i] * y[ip1] - x[ip1] * y[i]) + Cx += (x[i] + x[ip1]) * m + Cy += (y[i] + y[ip1]) * m + end + Cx / 6A, Cy / 6A +end + +function scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) + sx, sy = coords(shape) + cx, cy = c + for i in eachindex(sx) + sx[i] = (sx[i] - cx) * x + cx + sy[i] = (sy[i] - cy) * y + cy + end + shape +end + +""" + scale(shape, x, y = x, c = center(shape)) + scale!(shape, x, y = x, c = center(shape)) + +Scale shape by a factor. +""" +scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) = + scale!(deepcopy(shape), x, y, c) + +function translate!(shape::Shape, x::Real, y::Real = x) + sx, sy = coords(shape) + for i in eachindex(sx) + sx[i] += x + sy[i] += y + end + shape +end + +""" + translate(shape, x, y = x) + translate!(shape, x, y = x) + +Translate a Shape in space. +""" +translate(shape::Shape, x::Real, y::Real = x) = translate!(deepcopy(shape), x, y) + +rotate_x(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = + ((x - centerx) * cos(θ) - (y - centery) * sin(θ) + centerx) + +rotate_y(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = + ((y - centery) * cos(θ) + (x - centerx) * sin(θ) + centery) + +rotate(x::Real, y::Real, θ::Real, c) = (rotate_x(x, y, θ, c...), rotate_y(x, y, θ, c...)) + +function rotate!(shape::Shape, θ::Real, c = center(shape)) + x, y = coords(shape) + for i in eachindex(x) + xi = rotate_x(x[i], y[i], θ, c...) + yi = rotate_y(x[i], y[i], θ, c...) + x[i], y[i] = xi, yi + end + shape +end + +"rotate an object in space" +function rotate(shape::Shape, θ::Real, c = center(shape)) + x, y = coords(shape) + x_new = rotate_x.(x, y, θ, c...) + y_new = rotate_y.(x, y, θ, c...) + Shape(x_new, y_new) +end + +end # Shapes diff --git a/src/Strokes.jl b/src/Strokes.jl new file mode 100644 index 000000000..8398a7896 --- /dev/null +++ b/src/Strokes.jl @@ -0,0 +1,82 @@ +module Strokes + +export stroke, brush, Stroke, Brush +using Plots.Colors: Colorant +using Plots.Commons: all_alphas, all_reals, all_styles +struct Stroke + width + color + alpha + style +end + +""" + stroke(args...; alpha = nothing) + +Define the properties of the stroke used in plotting lines +""" +function stroke(args...; alpha = nothing) + width = 1 + color = :black + style = :solid + + for arg in args + T = typeof(arg) + + # if arg in _all_styles + if all_styles(arg) + style = arg + elseif T <: Colorant + color = arg + elseif T <: Symbol || T <: AbstractString + try + color = parse(Colorant, string(arg)) + catch + end + elseif all_alphas(arg) + alpha = arg + elseif all_reals(arg) + width = arg + else + @warn "Unused stroke arg: $arg ($(typeof(arg)))" + end + end + + Stroke(width, color, alpha, style) +end + +struct Brush + size # fillrange, markersize, or any other sizey attribute + color + alpha +end + +function brush(args...; alpha = nothing) + size = 1 + color = :black + + for arg in args + T = typeof(arg) + + if T <: Colorant + color = arg + elseif T <: Symbol || T <: AbstractString + try + color = parse(Colorant, string(arg)) + catch + end + elseif all_alphas(arg) + alpha = arg + elseif all_reals(arg) + size = arg + else + @warn "Unused brush arg: $arg ($(typeof(arg)))" + end + end + + Brush(size, color, alpha) +end + +# ----------------------------------------------------------------------- + +end # Strokes diff --git a/src/Subplots.jl b/src/Subplots.jl new file mode 100644 index 000000000..d87f695d8 --- /dev/null +++ b/src/Subplots.jl @@ -0,0 +1,295 @@ +module Subplots + +export Subplot, + colorbartitlefont, + legendfont, + legendtitlefont, + titlefont, + get_series_color, + needs_any_3d_axes, + plotarea, + plotarea!, + toppad, + leftpad, + bottompad, + rightpad +import Plots.Ticks: get_ticks +using Plots: + Plots, + RecipesPipeline, + Series, + AbstractBackend, + AbstractLayout, + BoundingBox, + DefaultsDict +using Plots.RecipesPipeline: RecipesPipeline, Surface, Volume +using Plots.PlotUtils: get_color_palette +using Plots.Commons +using Plots.Commons.Frontend +using Plots.Commons: convert_legend_value, like_surface +using Plots.Fonts +using Plots.PlotMeasures + +# a single subplot +mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout + parent::AbstractLayout + series_list::Vector{Series} # arguments for each series + primary_series_count::Int # Number of primary series in the series list + minpad::Tuple # leftpad, toppad, rightpad, bottompad + bbox::BoundingBox # the canvas area which is available to this subplot + plotarea::BoundingBox # the part where the data goes + attr::DefaultsDict # args specific to this subplot + o # can store backend-specific data... like a pyplot ax + plt # the enclosing Plot object (can't give it a type because of no forward declarations) + + Subplot(::T; parent = Plots.RootLayout()) where {T<:AbstractBackend} = new{T}( + parent, + Series[], + 0, + DEFAULT_MINPAD[], + DEFAULT_BBOX[], + DEFAULT_BBOX[], + DefaultsDict(KW(), _subplot_defaults), + nothing, + nothing, + ) +end + +# properly retrieve from sp.attr, passing `:match` to the correct key +Base.getindex(sp::Subplot, k::Symbol) = + if (v = sp.attr[k]) === :match + if haskey(Commons.Commons._match_map2, k) + sp.plt[Commons.Commons._match_map2[k]] + else + sp[Commons._match_map[k]] + end + else + v + end +Base.getindex(sp::Subplot, i::Union{Vector{<:Integer},Integer}) = series_list(sp)[i] +Base.setindex!(sp::Subplot, v, k::Symbol) = (sp.attr[k] = v) +Base.lastindex(sp::Subplot) = length(series_list(sp)) + +Base.empty!(sp::Subplot) = empty!(sp.series_list) +Base.get(sp::Subplot, k::Symbol, v) = get(sp.attr, k, v) + +# ----------------------------------------------------------------------- + +Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}") + +""" + plotarea(subplot) + +Return the bounding box of a subplot. +""" +plotarea(sp::Subplot) = sp.plotarea +plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox) + +Base.size(sp::Subplot) = (1, 1) +Base.length(sp::Subplot) = 1 +Base.getindex(sp::Subplot, r::Int, c::Int) = sp + +leftpad(sp::Subplot) = sp.minpad[1] +toppad(sp::Subplot) = sp.minpad[2] +rightpad(sp::Subplot) = sp.minpad[3] +bottompad(sp::Subplot) = sp.minpad[4] + +function attr!(sp::Subplot; kw...) + plotattributes = KW(kw) + Plots.Commons.preprocess_attributes!(plotattributes) + for (k, v) in plotattributes + if haskey(_subplot_defaults, k) + sp[k] = v + else + @warn "unused key $k in subplot attr" + end + end + sp +end + +Plots.series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] === sp, sp.plt.series_list) +Plots.RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" +Plots.ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar" + +get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) + +# converts a symbol or string into a Colorant or ColorGradient +# and assigns a color automatically +get_series_color(c, sp::Subplot, n::Int, seriestype) = + if c === :auto + like_surface(seriestype) ? Plots.cgrad() : _cycle(sp[:color_palette], n) + elseif isa(c, Int) + _cycle(sp[:color_palette], c) + else + c + end |> Plots.plot_color + +get_series_color(c::AbstractArray, sp::Subplot, n::Int, seriestype) = + map(x -> get_series_color(x, sp, n, seriestype), c) + +colorbartitlefont(sp::Subplot) = font(; + family = sp[:colorbar_titlefontfamily], + pointsize = sp[:colorbar_titlefontsize], + valign = sp[:colorbar_titlefontvalign], + halign = sp[:colorbar_titlefonthalign], + rotation = sp[:colorbar_titlefontrotation], + color = sp[:colorbar_titlefontcolor], +) + +titlefont(sp::Subplot) = font(; + family = sp[:titlefontfamily], + pointsize = sp[:titlefontsize], + valign = sp[:titlefontvalign], + halign = sp[:titlefonthalign], + rotation = sp[:titlefontrotation], + color = sp[:titlefontcolor], +) + +legendfont(sp::Subplot) = font(; + family = sp[:legend_font_family], + pointsize = sp[:legend_font_pointsize], + valign = sp[:legend_font_valign], + halign = sp[:legend_font_halign], + rotation = sp[:legend_font_rotation], + color = sp[:legend_font_color], +) + +legendtitlefont(sp::Subplot) = font(; + family = sp[:legend_title_font_family], + pointsize = sp[:legend_title_font_pointsize], + valign = sp[:legend_title_font_valign], + halign = sp[:legend_title_font_halign], + rotation = sp[:legend_title_font_rotation], + color = sp[:legend_title_font_color], +) + +function _update_subplot_periphery(sp::Subplot, anns::AVec) + # extend annotations, and ensure we always have a (x,y,PlotText) tuple + newanns = [] + for ann in vcat(anns, sp[:annotations]) + append!(newanns, Plots.process_annotation(sp, ann)) + end + sp.attr[:annotations] = newanns + + # handle legend/colorbar + sp.attr[:legend_position] = convert_legend_value(sp.attr[:legend_position]) + sp.attr[:colorbar] = convert_legend_value(sp.attr[:colorbar]) + if sp.attr[:colorbar] === :legend + sp.attr[:colorbar] = sp.attr[:legend_position] + end + nothing +end + +function _update_subplot_colors(sp::Subplot) + # background colors + color_or_nothing!(sp.attr, :background_color_subplot) + sp.attr[:color_palette] = get_color_palette(sp.attr[:color_palette], 30) + color_or_nothing!(sp.attr, :legend_background_color) + color_or_nothing!(sp.attr, :background_color_inside) + + # foreground colors + color_or_nothing!(sp.attr, :foreground_color_subplot) + color_or_nothing!(sp.attr, :legend_foreground_color) + color_or_nothing!(sp.attr, :foreground_color_title) + nothing +end + +_update_margins(sp::Subplot) = + for sym in (:margin, :left_margin, :top_margin, :right_margin, :bottom_margin) + if (margin = get(sp.attr, sym, nothing)) isa Tuple + # transform e.g. (1, :mm) => 1 * Plots.mm + sp.attr[sym] = margin[1] * getfield(@__MODULE__, margin[2]) + end + end + +needs_any_3d_axes(sp::Subplot) = any( + RecipesPipeline.needs_3d_axes( + _override_seriestype_check(s.plotattributes, s.plotattributes[:seriestype]), + ) for s in series_list(sp) +) + +function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) + + # first expand for the data + for letter in (:x, :y, :z) + data = plotattributes[letter] + if ( + letter !== :z && + plotattributes[:seriestype] === :straightline && + any(series[:seriestype] !== :straightline for series in series_list(sp)) && + length(data) > 1 && + data[1] != data[2] + ) + data = [NaN] + end + axis = sp[get_attr_symbol(letter, :axis)] + + if isa(data, Plots.Volume) + expand_extrema!(sp[:xaxis], data.x_extents) + expand_extrema!(sp[:yaxis], data.y_extents) + expand_extrema!(sp[:zaxis], data.z_extents) + elseif eltype(data) <: Number || + (isa(data, Surface) && all(di -> isa(di, Number), data.surf)) + if !(eltype(data) <: Number) + # huh... must have been a mis-typed surface? lets swap it out + data = plotattributes[letter] = Surface(Matrix{Float64}(data.surf)) + end + expand_extrema!(axis, data) + elseif data !== nothing + # TODO: need more here... gotta track the discrete reference value + # as well as any coord offset (think of boxplot shape coords... they all + # correspond to the same x-value) + plotattributes[letter], + plotattributes[get_attr_symbol(letter, :(_discrete_indices))] = + Plots.discrete_value!(axis, data) + expand_extrema!(axis, plotattributes[letter]) + end + end + + # expand for fillrange + fr = plotattributes[:fillrange] + if fr === nothing && plotattributes[:seriestype] === :bar + fr = 0.0 + end + if fr !== nothing && !RecipesPipeline.is3d(plotattributes) + axis = sp.attr[:yaxis] + if typeof(fr) <: Tuple + foreach(x -> expand_extrema!(axis, x), fr) + else + expand_extrema!(axis, fr) + end + end + + # expand for bar_width + if plotattributes[:seriestype] === :bar + dsym = :x + data = plotattributes[dsym] + + if (bw = plotattributes[:bar_width]) === nothing + pos = filter(>(0), diff(sort(data))) + plotattributes[:bar_width] = bw = _bar_width * ignorenan_minimum(pos) + end + axis = sp.attr[get_attr_symbol(dsym, :axis)] + expand_extrema!(axis, ignorenan_maximum(data) + 0.5maximum(bw)) + expand_extrema!(axis, ignorenan_minimum(data) - 0.5minimum(bw)) + end + + # expand for heatmaps + if plotattributes[:seriestype] === :heatmap + for letter in (:x, :y) + data = plotattributes[letter] + axis = sp[get_attr_symbol(letter, :axis)] + scale = get(plotattributes, get_attr_symbol(letter, :scale), :identity) + expand_extrema!(axis, Plots.heatmap_edges(data, scale)) + end + end +end + +function Plots.expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) + expand_extrema!(sp[:xaxis], (xmin, xmax)) + expand_extrema!(sp[:yaxis], (ymin, ymax)) +end + +Commons.get_size(sp::Subplot) = Commons.get_size(sp.plt) +Commons.get_thickness_scaling(sp::Subplot) = Commons.get_thickness_scaling(sp.plt) +end # Subplots diff --git a/src/Surfaces.jl b/src/Surfaces.jl new file mode 100644 index 000000000..87c915d47 --- /dev/null +++ b/src/Surfaces.jl @@ -0,0 +1,22 @@ +module Surfaces + +export SurfaceFunction, Surface + +import Plots: Plots, expand_extrema!, Commons +using Plots.Axes: Axis +using RecipesPipeline: AbstractSurface, Surface +using Plots.Commons + +function Plots.expand_extrema!(a::Axis, surf::Surface) + ex = a[:extrema] + foreach(x -> expand_extrema!(ex, x), surf.surf) + ex +end + +"For the case of representing a surface as a function of x/y... can possibly avoid allocations." +struct SurfaceFunction <: AbstractSurface + f::Function +end + +Commons.handle_surface(z::Surface) = permutedims(z.surf) +end diff --git a/src/Ticks.jl b/src/Ticks.jl new file mode 100644 index 000000000..01999f041 --- /dev/null +++ b/src/Ticks.jl @@ -0,0 +1,100 @@ +module Ticks + +export get_ticks, _has_ticks, _transform_ticks, get_minor_ticks +using Plots.Commons +using Plots.Dates + +const DEFAULT_MINOR_INTERVALS = Ref(5) # 5 intervals -> 4 ticks + +# get_ticks from axis symbol :x, :y, or :z + +get_ticks(ticks::NTuple{2,Any}, args...) = ticks +get_ticks(::Nothing, cvals::T, args...) where {T} = T[], String[] +get_ticks(ticks::Bool, args...) = + ticks ? get_ticks(:auto, args...) : get_ticks(nothing, args...) +get_ticks(::T, args...) where {T} = + throw(ArgumentError("Unknown ticks type in get_ticks: $T")) + +# do not specify array item type to also catch e.g. "xlabel=[]" and "xlabel=([],[])" +_has_ticks(v::AVec) = !isempty(v) +_has_ticks(t::Tuple{AVec,AVec}) = !isempty(t[1]) +_has_ticks(s::Symbol) = s !== :none +_has_ticks(b::Bool) = b +_has_ticks(::Nothing) = false +_has_ticks(::Any) = true + +_transform_ticks(ticks, axis) = ticks +_transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Dates.TimeType} = + Dates.value.(ticks) +_transform_ticks(ticks::NTuple{2,Any}, axis) = (_transform_ticks(ticks[1], axis), ticks[2]) + +function num_minor_intervals(axis) + # FIXME: `minorticks` should be fixed in `2.0` to be the number of ticks, not intervals + # see github.com/JuliaPlots/Plots.jl/pull/4528 + n_intervals = axis[:minorticks] + if !(n_intervals isa Bool) && n_intervals isa Integer && n_intervals ≥ 0 + max(1, n_intervals) # 0 intervals makes no sense + else # `:auto` or `true` + if (base = get(_log_scale_bases, axis[:scale], nothing)) == 10 + Int(base - 1) + else + DEFAULT_MINOR_INTERVALS[] + end + end::Int +end + +no_minor_intervals(axis) = + if (n_intervals = axis[:minorticks]) === false + true # must be tested with `===` since Bool <: Integer + elseif n_intervals ∈ (:none, nothing) + true + elseif (n_intervals === :auto && !axis[:minorgrid]) + true + else + false + end + +function get_minor_ticks(sp, axis, ticks_and_labels) + no_minor_intervals(axis) && return + ticks = first(ticks_and_labels) + length(ticks) < 2 && return + + amin, amax = axis_limits(sp, axis[:letter]) + scale = axis[:scale] + base = get(_log_scale_bases, scale, nothing) + + # add one phantom tick either side of the ticks to ensure minor ticks extend to the axis limits + if (log_scaled = scale ∈ _log_scales) + sub = round(Int, log(base, ticks[2] / ticks[1])) + ticks = [ticks[1] / base; ticks; ticks[end] * base] + else + sub = 1 # unused + ratio = length(ticks) > 2 ? (ticks[3] - ticks[2]) / (ticks[2] - ticks[1]) : 1 + first_step = ticks[2] - ticks[1] + last_step = ticks[end] - ticks[end - 1] + ticks = [ticks[1] - first_step / ratio; ticks; ticks[end] + last_step * ratio] + end + + n_minor_intervals = num_minor_intervals(axis) + minorticks = sizehint!(eltype(ticks)[], n_minor_intervals * sub * length(ticks)) + for i in 2:length(ticks) + lo = ticks[i - 1] + hi = ticks[i] + (isfinite(lo) && isfinite(hi) && hi > lo) || continue + if log_scaled + for e in 1:sub + lo_ = lo * base^(e - 1) + hi_ = lo_ * base + step = (hi_ - lo_) / n_minor_intervals + rng = (lo_ + (e > 1 ? 0 : step)):step:(hi_ - (e < sub ? 0 : step / 2)) + append!(minorticks, collect(rng)) + end + else + step = (hi - lo) / n_minor_intervals + append!(minorticks, collect((lo + step):step:(hi - step / 2))) + end + end + minorticks[amin .≤ minorticks .≤ amax] +end + +end # Ticks diff --git a/src/abstract_backend.jl b/src/abstract_backend.jl new file mode 100644 index 000000000..10cfafdbf --- /dev/null +++ b/src/abstract_backend.jl @@ -0,0 +1,181 @@ +const _plots_project = Pkg.Types.read_package(normpath(@__DIR__, "..", "Project.toml")) +const _current_plots_version = _plots_project.version +const _plots_compats = _plots_project.compat + +const _backendSymbol = Dict{DataType,Symbol}(NoBackend => :none) +const _backendType = Dict{Symbol,DataType}(:none => NoBackend) +const _backend_packages = (gr = :GR, unicodeplots = :UnicodePlots, pgfplotsx = :PGFPlotsX, pythonplot = :PythonPlot, plotly = nothing, plotlyjs = :PlotlyJS, inspectdr = :InspectDR, gaston = :Gaston, hdf5 = :HDF5) +const _initialized_backends = Set{Symbol}() +const _backends = keys(_backend_packages) + +const _plots_deps = let toml = Pkg.TOML.parsefile(normpath(@__DIR__, "..", "Project.toml")) + merge(toml["deps"], toml["extras"]) +end +_create_backend_figure(plt::Plot) = nothing +_initialize_subplot(plt::Plot, sp::Subplot) = nothing + +_series_added(plt::Plot, series::Series) = nothing +_series_updated(plt::Plot, series::Series) = nothing + +_before_layout_calcs(plt::Plot) = nothing + +title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt +guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt + +closeall(::AbstractBackend) = nothing + +mutable struct CurrentBackend + sym::Symbol + pkg::AbstractBackend +end + +""" +Returns the current plotting package name. Initializes package on first call. +""" +backend() = CURRENT_BACKEND.pkg + +"Returns a list of supported backends" +backends() = _backends + +backend_name() = CURRENT_BACKEND.sym +_backend_instance(sym::Symbol)::AbstractBackend = _backendType[sym]() + +backend_package_name(sym::Symbol = backend_name()) = _backend_packages[sym] + +# Traits to be implemented by the extensions +backend_name(::AbstractBackend) = @info "`backend_name(::Backend) not implemented." +backend_package_name(::AbstractBackend) = + @info "`backend_package_name(::Backend) not implemented." + +initialized(sym::Symbol) = sym ∈ _initialized_backends + +""" +Set the plot backend. +""" +function backend(pkg::AbstractBackend) + sym = backend_name(pkg) + if !initialized(sym) + _initialize_backend(pkg) + push!(_initialized_backends, sym) + end + CURRENT_BACKEND.sym = sym + CURRENT_BACKEND.pkg = pkg + pkg +end + +backend(sym::Symbol) = + if sym in _backends + if initialized(sym) + backend(_backend_instance(sym)) + else + name = backend_package_name(sym) + @warn "`:$sym` is not initialized, import it first to trigger the extension --- e.g. $(name === nothing ? '`' : string("`import ", name, ";")) $sym()`." + backend() + end + else + error("Unsupported backend $sym") + end + +function get_backend_module(name::Symbol) + ext_name = Symbol("Plots", name, "Ext") + ext = Base.get_extension(@__MODULE__, ext_name) + if !isnothing(ext) + module_name = ext + # Concrete as opposed to abstract + ConcreteBackend = ext.get_concrete_backend() + return (module_name, ConcreteBackend) + else + @error "Extension $name is not loaded yet, run `import $name` to load it" + return nothing + end +end + +# -- Create backend init functions by hand as the corresponding structs do not +# exist yet + +for be in _backends + @eval begin + function $be(; kw...) + default(; reset = false, kw...) + backend(Symbol($be)) + end + export $be + end +end + +# --------------------------------------------------------- +# create the various `is_xxx_supported` and `supported_xxxs` methods +# these methods should be overloaded (dispatched) by each backend in its init_code +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + @eval begin + $f1(::AbstractBackend, $s) = false + $f1(be::AbstractBackend, $s::AbstractVector) = all(v -> $f1(be, v), $s) + $f1($s) = $f1(backend(), $s) + $f2() = $f2(backend()) + end +end +# ----------------------------------------------------------------------------- + +should_warn_on_unsupported(::AbstractBackend) = _plot_defaults[:warn_on_unsupported] + +const _already_warned = Dict{Symbol,Set{Symbol}}() +function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) + _to_warn = Set{Symbol}() + bend = backend_name(pkg) + already_warned = get!(_already_warned, bend) do + Set{Symbol}() + end + extra_kwargs = Dict{Symbol,Any}() + for k in Plots.explicitkeys(plotattributes) + (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue + k in Commons._suppress_warnings && continue + if ismissing(default(k)) + extra_kwargs[k] = pop_kw!(plotattributes, k) + elseif plotattributes[k] != default(k) + k in already_warned || push!(_to_warn, k) + end + end + + if !isempty(_to_warn) && + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) + for k in sort(collect(_to_warn)) + push!(already_warned, k) + if k in keys(Commons._deprecated_attributes) + @warn """ + Keyword argument `$k` is deprecated. + Please use `$(Commons._deprecated_attributes[k])` instead. + """ + else + @warn "Keyword argument $k not supported with $pkg. Choose from: $(join(supported_attrs(pkg), ", "))" + end + end + end + extra_kwargs +end + +function warn_on_unsupported(pkg::AbstractBackend, plotattributes) + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return + is_seriestype_supported(pkg, plotattributes[:seriestype]) || + @warn "seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))" + is_style_supported(pkg, plotattributes[:linestyle]) || + @warn "linestyle $(plotattributes[:linestyle]) is unsupported with $pkg. Choose from: $(supported_styles(pkg))" + is_marker_supported(pkg, plotattributes[:markershape]) || + @warn "markershape $(plotattributes[:markershape]) is unsupported with $pkg. Choose from: $(supported_markers(pkg))" +end + +function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW) + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return + for k in (:xscale, :yscale, :zscale, :scale) + if haskey(plotattributes, k) + v = plotattributes[k] + if !all(is_scale_supported.(Ref(pkg), v)) + @warn """ + scale $v is unsupported with $pkg. + Choose from: $(supported_scales(pkg)) + """ + end + end + end +end diff --git a/src/alignment.jl b/src/alignment.jl new file mode 100644 index 000000000..1588a6dcd --- /dev/null +++ b/src/alignment.jl @@ -0,0 +1,65 @@ +"Returns the (width,height) of a text label." +function text_size(lablen::Int, sz::Number, rot::Number = 0) + # we need to compute the size of the ticks generically + # this means computing the bounding box and then getting the width/height + # note: + ptsz = sz * pt + width = 0.8lablen * ptsz + + # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles + height = abs(sind(rot)) * width + abs(cosd(rot)) * ptsz + width = abs(sind(rot + 90)) * width + abs(cosd(rot + 90)) * ptsz + width, height +end +text_size(lab::AbstractString, sz::Number, rot::Number = 0) = + text_size(length(lab), sz, rot) +text_size(lab::PlotText, sz::Number, rot::Number = 0) = text_size(length(lab.str), sz, rot) + +# account for the size/length/rotation of tick labels +function tick_padding(sp::Subplot, axis::Axis) + if (ticks = get_ticks(sp, axis)) === nothing + 0mm + else + vals, labs = ticks + isempty(labs) && return 0mm + # ptsz = axis[:tickfont].pointsize * pt + longest_label = maximum(length(lab) for lab in labs) + + # generalize by "rotating" y labels + rot = axis[:rotation] + (axis[:letter] === :y ? 90 : 0) + + # # we need to compute the size of the ticks generically + # # this means computing the bounding box and then getting the width/height + # labelwidth = 0.8longest_label * ptsz + # + # + # # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles + # hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm + + # get the height of the rotated label + text_size(longest_label, axis[:tickfontsize], rot)[2] + end +end + +# Set the (left, top, right, bottom) minimum padding around the plot area +# to fit ticks, tick labels, guides, colorbars, etc. +function _update_min_padding!(sp::Subplot) + # TODO: something different when `RecipesPipeline.is3d(sp) == true` + leftpad = tick_padding(sp, sp[:yaxis]) + sp[:left_margin] + guide_padding(sp[:yaxis]) + toppad = sp[:top_margin] + title_padding(sp) + rightpad = sp[:right_margin] + bottompad = tick_padding(sp, sp[:xaxis]) + sp[:bottom_margin] + guide_padding(sp[:xaxis]) + + # switch them? + if sp[:xaxis][:mirror] + bottompad, toppad = toppad, bottompad + end + if sp[:yaxis][:mirror] + leftpad, rightpad = rightpad, leftpad + end + + # @show (leftpad, toppad, rightpad, bottompad) + sp.minpad = (leftpad, toppad, rightpad, bottompad) +end + +_update_plot_object(plt::Plot) = nothing diff --git a/src/arg_desc.jl b/src/arg_desc.jl index 33eb6cd7c..f448804cb 100644 --- a/src/arg_desc.jl +++ b/src/arg_desc.jl @@ -8,20 +8,20 @@ const _arg_desc = KW( :label => (AStr, "The label for a series, which appears in a legend. If empty, no legend entry is added."), :seriescolor => (ColorType, "The base color for this series. `:auto` (the default) will select a color from the subplot's `color_palette`, based on the order it was added to the subplot. Also describes the colormap for surfaces."), :seriesalpha => (Real, "The alpha/opacity override for the series. `nothing` (the default) means it will take the alpha value of the color."), - :seriestype => (Symbol, "This is the identifier of the type of visualization for this series. Choose from $(_allTypes) or any series recipes which are defined."), - :linestyle => (Symbol, "Style of the line (for path and bar stroke). Choose from $(_allStyles)"), + :seriestype => (Symbol, "This is the identifier of the type of visualization for this series. Choose from $(Commons._all_seriestypes) or any series recipes which are defined."), + :linestyle => (Symbol, "Style of the line (for path and bar stroke). Choose from $(Commons._all_styles)"), :linewidth => (Real, "Width of the line (in pixels)."), :linecolor => (ColorType, "Color of the line (for path and bar stroke). `:match` will take the value from `:seriescolor`, (though histogram/bar types use `:black` as a default)."), :linealpha => (Real, "The alpha/opacity override for the line. `nothing` (the default) means it will take the alpha value of linecolor."), :fillrange => (Union{Real,AVec}, "Fills area between fillrange and `y` for line-types, sets the base for `bar`, `sticks` types, and similar for other types."), :fillcolor => (ColorType, "Color of the filled area of path or bar types. `:match` will take the value from `:seriescolor`."), :fillalpha => (Real, "The alpha/opacity override for the fill area. `nothing` (the default) means it will take the alpha value of fillcolor."), - :markershape => (Union{Symbol,Shape,AVec}, "Choose from $(_allMarkers)."), + :markershape => (Union{Symbol,Shape,AVec}, "Choose from $(Commons._all_markers)."), :fillstyle => (Symbol, "Style of the fill area. `nothing` (the default) means solid fill. Choose from :/, :\\, :|, :-, :+, :x."), :markercolor => (ColorType, "Color of the interior of the marker or shape. `:match` will take the value from `:seriescolor`."), :markeralpha => (Real, "The alpha/opacity override for the marker interior. `nothing` (the default) means it will take the alpha value of markercolor."), :markersize => (Union{Real,AVec}, "Size (radius pixels) of the markers."), - :markerstrokestyle => (Symbol, "Style of the marker stroke (border). Choose from $(_allStyles)."), + :markerstrokestyle => (Symbol, "Style of the marker stroke (border). Choose from $(Commons._all_styles)."), :markerstrokewidth => (Real, "Width of the marker stroke (border) in pixels."), :markerstrokecolor => (ColorType, "Color of the marker stroke (border). `:match` will take the value from `:foreground_color_subplot`."), :markerstrokealpha => (Real, "The alpha/opacity override for the marker stroke (border). `nothing` (the default) means it will take the alpha value of markerstrokecolor."), @@ -38,9 +38,8 @@ const _arg_desc = KW( :fill_z => (AMat, "Matrix of the same size as z matrix, which specifies the color of the 3D surface."), :levels => (Union{AVec,Integer}, "Singleton for number of contours or iterable for contour values. Determines contour levels for a contour type."), :permute => (NTuple{2,Symbol}, "Permutes data and axis properties of the axes given in the tuple, e.g. (:x, :y)."), - :orientation => (Symbol, "(deprecated in favor of `:permute`) Horizontal or vertical orientation for bar types. Values `:h`, `:hor`, `:horizontal` correspond to horizontal (sideways, anchored to y-axis), and `:v`, `:vert`, and `:vertical` correspond to vertical (the default)."), :bar_position => (Symbol, "Choose from `:overlay` (default), `:stack`. (warning: may only be partially implemented)."), - :bar_width => (Real, " Width of bars in data coordinates. When `nothing`, chooses based on `x` (or `y` when `orientation = :h`)."), + :bar_width => (Real, " Width of bars in data coordinates. When `nothing`, chooses based on `x`."), :bar_edges => (Bool, "Align bars to edges (true), or centers (the default) ?"), :xerror => (Union{AVec,NTuple{2,AVec}}, "`x` (horizontal) error relative to x-value. If 2-tuple of vectors, the first vector corresponds to the left error (and the second to the right)."), :yerror => (Union{AVec,NTuple{2,AVec}}, "`y` (vertical) error relative to y-value. If 2-tuple of vectors, the first vector corresponds to the bottom error (and the second to the top)."), @@ -126,7 +125,7 @@ const _arg_desc = KW( :colorbar_tickfontfamily => (Union{AStr,Symbol}, "String or Symbol. Font family of colorbar tick labels."), :colorbar_tickfontsize => (Integer, "Font pointsize of colorbar tick entries."), :colorbar_tickfontcolor => (ColorType, "Font color of colorbar tick entries."), - :colorbar_scale => (Symbol, "Scale of the colorbar axis. Choose from $(_allScales)."), + :colorbar_scale => (Symbol, "Scale of the colorbar axis. Choose from $(Commons._all_scales)."), :colorbar_formatter => (Union{Function,Symbol}, "Choose from (:scientific, :plain, :none, :auto), or a method which converts a number to a string for tick labeling."), :legend_font => (Font, "Font of legend items."), :legend_titlefont => (Font, "Font of the legend title."), @@ -147,7 +146,7 @@ const _arg_desc = KW( :bottom_margin => (Union{Tuple,Real,Symbol}, "Specifies the extra padding on the bottom of the subplot (`:match` matches `:margin`)."), :subplot_index => (Integer, "Internal (not set by user). Specifies the index of this subplot in the Plot's `plt.subplot` list."), :colorbar_title => (AStr, "Title of colorbar."), - :framestyle => (Symbol, "Style of the axes frame. Choose from $(_allFramestyles)."), + :framestyle => (Symbol, "Style of the axes frame. Choose from $(Commons._all_framestyles)."), :camera => (NTuple{2,Real}, "Sets the view angle (azimuthal, elevation) for 3D plots."), # axis args @@ -159,7 +158,7 @@ const _arg_desc = KW( `:symmetric` sets the limits to be symmetric around zero. Set `widen=true` to widen the specified limits (as occurs when lims are not specified)."""), :ticks => (TicksType, "Tick values, (tickvalues, ticklabels), `:auto`/`true`, `:none`/`false`/`nothing` (ticks disabled), or `:native` (tells backend to calculate ticks by itself; good idea for interactive backends with mouse zooming)."), - :scale => (Symbol, "Scale of the axis. Choose from $(_allScales)."), + :scale => (Symbol, "Scale of the axis. Choose from $(Commons._all_scales)."), :rotation => (Real, "Degrees rotation of tick labels."), :flip => (Bool, "Should we flip (reverse) the axis ?"), :formatter => (Union{Symbol,Function}, "Choose from (:scientific, :plain or :auto), or a method which converts a number to a string for tick labeling."), @@ -183,19 +182,19 @@ const _arg_desc = KW( :grid => (Union{Bool,Symbol,AStr}, "Show the grid lines ? `true`, `false`, `:show`, `:hide`, `:yes`, `:no`, `:x`, `:y`, `:z`, `:xy`, ..., `:all`, `:none`, `:off`."), :foreground_color_grid => (ColorType, "Color of grid lines (`:match` matches `:foreground_color_subplot`)."), :gridalpha => (Real, "The alpha/opacity override for the grid lines."), - :gridstyle => (Symbol, "Style of the grid lines. Choose from $(_allStyles)."), + :gridstyle => (Symbol, "Style of the grid lines. Choose from $(Commons._all_styles)."), :gridlinewidth => (Real, "Width of the grid lines (in pixels)."), :foreground_color_minor_grid => (ColorType, "Color of minor grid lines (`:match` matches `:foreground_color_subplot`)."), :minorgrid => (Bool, "Adds minor grid lines and ticks to the plot. Set minorticks to change number of gridlines."), :minorticks => (Integer, "Number of minor intervals between major ticks."), :minorgridalpha => (Real, "The alpha/opacity override for the minorgrid lines."), - :minorgridstyle => (Symbol, "Style of the minor grid lines. Choose from $(_allStyles)."), + :minorgridstyle => (Symbol, "Style of the minor grid lines. Choose from $(Commons._all_styles)."), :minorgridlinewidth => (Real, "Width of the minor grid lines (in pixels)."), :tick_direction => (Symbol, "Direction of the ticks. Choose from (`:in`, `:out`, `:none`)."), :showaxis => (Union{Bool,Symbol,AStr}, "Show the axis. `true`, `false`, `:show`, `:hide`, `:yes`, `:no`, `:x`, `:y`, `:z`, `:xy`, ..., `:all`, `:off`."), :widen => (Union{Bool,Real,Symbol}, """ Widen the axis limits by a small factor to avoid cut-off markers and lines at the borders. - If set to `true`, scale the axis limits by the default factor of $(default_widen_factor). + If set to `true`, scale the axis limits by the default factor of $(Axes.default_widen_factor). A different factor may be specified by setting `widen` to a number. Defaults to `:auto`, which widens by the default factor unless limits were manually set. See also the `scale_limits!` function for scaling axis limits in an existing plot."""), diff --git a/src/args.jl b/src/args.jl deleted file mode 100644 index 94c44f10b..000000000 --- a/src/args.jl +++ /dev/null @@ -1,2220 +0,0 @@ -makeplural(s::Symbol) = last(string(s)) == 's' ? s : Symbol(string(s, "s")) -make_non_underscore(s::Symbol) = Symbol(replace(string(s), "_" => "")) - -const _keyAliases = Dict{Symbol,Symbol}() - -function add_aliases(sym::Symbol, aliases::Symbol...) - for alias in aliases - (haskey(_keyAliases, alias) || alias === sym) && return - _keyAliases[alias] = sym - end - nothing -end - -function add_axes_aliases(sym::Symbol, aliases::Symbol...; generic::Bool = true) - sym in keys(_axis_defaults) || throw(ArgumentError("Invalid `$sym`")) - generic && add_aliases(sym, aliases...) - for letter in (:x, :y, :z) - add_aliases(Symbol(letter, sym), (Symbol(letter, a) for a in aliases)...) - end -end - -function add_non_underscore_aliases!(aliases::Dict{Symbol,Symbol}) - for (k, v) in aliases - if '_' in string(k) - aliases[make_non_underscore(k)] = v - end - end -end - -macro attributes(expr::Expr) - RecipesBase.process_recipe_body!(expr) - expr -end - -# ------------------------------------------------------------ - -const _allAxes = [:auto, :left, :right] -const _axesAliases = Dict{Symbol,Symbol}(:a => :auto, :l => :left, :r => :right) - -const _3dTypes = [:path3d, :scatter3d, :surface, :wireframe, :contour3d, :volume, :mesh3d] -const _allTypes = vcat( - [ - :none, - :line, - :path, - :steppre, - :stepmid, - :steppost, - :sticks, - :scatter, - :heatmap, - :hexbin, - :barbins, - :barhist, - :histogram, - :scatterbins, - :scatterhist, - :stepbins, - :stephist, - :bins2d, - :histogram2d, - :histogram3d, - :density, - :bar, - :hline, - :vline, - :contour, - :pie, - :shape, - :image, - ], - _3dTypes, -) - -const _z_colored_series = [:contour, :contour3d, :heatmap, :histogram2d, :surface, :hexbin] - -const _typeAliases = Dict{Symbol,Symbol}( - :n => :none, - :no => :none, - :l => :line, - :p => :path, - :stepinv => :steppre, - :stepsinv => :steppre, - :stepinverted => :steppre, - :stepsinverted => :steppre, - :step => :steppost, - :steps => :steppost, - :stair => :steppost, - :stairs => :steppost, - :stem => :sticks, - :stems => :sticks, - :dots => :scatter, - :pdf => :density, - :contours => :contour, - :line3d => :path3d, - :surf => :surface, - :wire => :wireframe, - :shapes => :shape, - :poly => :shape, - :polygon => :shape, - :box => :boxplot, - :velocity => :quiver, - :vectorfield => :quiver, - :gradient => :quiver, - :img => :image, - :imshow => :image, - :imagesc => :image, - :hist => :histogram, - :hist2d => :histogram2d, - :bezier => :curves, - :bezier_curves => :curves, -) - -add_non_underscore_aliases!(_typeAliases) - -const _histogram_like = [:histogram, :barhist, :barbins] -const _line_like = [:line, :path, :steppre, :stepmid, :steppost] -const _surface_like = - [:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image] - -like_histogram(seriestype::Symbol) = seriestype in _histogram_like -like_line(seriestype::Symbol) = seriestype in _line_like -like_surface(seriestype::Symbol) = RecipesPipeline.is_surface(seriestype) - -RecipesPipeline.is3d(series::Series) = RecipesPipeline.is3d(series.plotattributes) -RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" -ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar" -ispolar(series::Series) = ispolar(series.plotattributes[:subplot]) - -# ------------------------------------------------------------ - -const _allStyles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _styleAliases = Dict{Symbol,Symbol}( - :a => :auto, - :s => :solid, - :d => :dash, - :dd => :dashdot, - :ddd => :dashdotdot, -) - -const _shape_keys = Symbol[ - :circle, - :rect, - :star5, - :diamond, - :hexagon, - :cross, - :xcross, - :utriangle, - :dtriangle, - :rtriangle, - :ltriangle, - :pentagon, - :heptagon, - :octagon, - :star4, - :star6, - :star7, - :star8, - :vline, - :hline, - :+, - :x, -] - -const _allMarkers = vcat(:none, :auto, _shape_keys) #sort(collect(keys(_shapes)))) -const _markerAliases = Dict{Symbol,Symbol}( - :n => :none, - :no => :none, - :a => :auto, - :ellipse => :circle, - :c => :circle, - :circ => :circle, - :square => :rect, - :sq => :rect, - :r => :rect, - :d => :diamond, - :^ => :utriangle, - :ut => :utriangle, - :utri => :utriangle, - :uptri => :utriangle, - :uptriangle => :utriangle, - :v => :dtriangle, - :V => :dtriangle, - :dt => :dtriangle, - :dtri => :dtriangle, - :downtri => :dtriangle, - :downtriangle => :dtriangle, - :> => :rtriangle, - :rt => :rtriangle, - :rtri => :rtriangle, - :righttri => :rtriangle, - :righttriangle => :rtriangle, - :< => :ltriangle, - :lt => :ltriangle, - :ltri => :ltriangle, - :lighttri => :ltriangle, - :lighttriangle => :ltriangle, - # :+ => :cross, - :plus => :cross, - # :x => :xcross, - :X => :xcross, - :star => :star5, - :s => :star5, - :star1 => :star5, - :s2 => :star8, - :star2 => :star8, - :p => :pentagon, - :pent => :pentagon, - :h => :hexagon, - :hex => :hexagon, - :hep => :heptagon, - :o => :octagon, - :oct => :octagon, - :spike => :vline, -) - -const _positionAliases = Dict{Symbol,Symbol}( - :top_left => :topleft, - :tl => :topleft, - :top_center => :topcenter, - :tc => :topcenter, - :top_right => :topright, - :tr => :topright, - :bottom_left => :bottomleft, - :bl => :bottomleft, - :bottom_center => :bottomcenter, - :bc => :bottomcenter, - :bottom_right => :bottomright, - :br => :bottomright, -) - -const _allScales = [:identity, :ln, :log2, :log10, :asinh, :sqrt] -const _logScales = [:ln, :log2, :log10] -const _logScaleBases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) -const _scaleAliases = Dict{Symbol,Symbol}(:none => :identity, :log => :log10) - -const _allGridSyms = [ - :x, - :y, - :z, - :xy, - :xz, - :yx, - :yz, - :zx, - :zy, - :xyz, - :xzy, - :yxz, - :yzx, - :zxy, - :zyx, - :all, - :both, - :on, - :yes, - :show, - :none, - :off, - :no, - :hide, -] -const _allGridArgs = [_allGridSyms; string.(_allGridSyms); nothing] -hasgrid(arg::Nothing, letter) = false -hasgrid(arg::Bool, letter) = arg -function hasgrid(arg::Symbol, letter) - if arg in _allGridSyms - arg in (:all, :both, :on) || occursin(string(letter), string(arg)) - else - @warn "Unknown grid argument $arg; $(get_attr_symbol(letter, :grid)) was set to `true` instead." - true - end -end -hasgrid(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) - -const _allShowaxisSyms = [ - :x, - :y, - :z, - :xy, - :xz, - :yx, - :yz, - :zx, - :zy, - :xyz, - :xzy, - :yxz, - :yzx, - :zxy, - :zyx, - :all, - :both, - :on, - :yes, - :show, - :off, - :no, - :hide, -] -const _allShowaxisArgs = [_allGridSyms; string.(_allGridSyms)] -showaxis(arg::Nothing, letter) = false -showaxis(arg::Bool, letter) = arg -function showaxis(arg::Symbol, letter) - if arg in _allGridSyms - arg in (:all, :both, :on, :yes) || occursin(string(letter), string(arg)) - else - @warn "Unknown showaxis argument $arg; $(get_attr_symbol(letter, :showaxis)) was set to `true` instead." - true - end -end -showaxis(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) - -const _allFramestyles = [:box, :semi, :axes, :origin, :zerolines, :grid, :none] -const _framestyleAliases = Dict{Symbol,Symbol}( - :frame => :box, - :border => :box, - :on => :box, - :transparent => :semi, - :semitransparent => :semi, -) - -const _bar_width = 0.8 -# ----------------------------------------------------------------------------- - -const _series_defaults = KW( - :label => :auto, - :colorbar_entry => true, - :seriescolor => :auto, - :seriesalpha => nothing, - :seriestype => :path, - :linestyle => :solid, - :linewidth => :auto, - :linecolor => :auto, - :linealpha => nothing, - :fillrange => nothing, # ribbons, areas, etc - :fillcolor => :match, - :fillalpha => nothing, - :fillstyle => nothing, - :markershape => :none, - :markercolor => :match, - :markeralpha => nothing, - :markersize => 4, - :markerstrokestyle => :solid, - :markerstrokewidth => 1, - :markerstrokecolor => :match, - :markerstrokealpha => nothing, - :bins => :auto, # number of bins for hists - :smooth => false, # regression line? - :group => nothing, # groupby vector - :x => nothing, - :y => nothing, - :z => nothing, # depth for contour, surface, etc - :marker_z => nothing, # value for color scale - :line_z => nothing, - :fill_z => nothing, - :levels => 15, - :orientation => :vertical, - :bar_position => :overlay, # for bar plots and histograms: could also be stack (stack up) or dodge (side by side) - :bar_width => nothing, - :bar_edges => false, - :xerror => nothing, - :yerror => nothing, - :zerror => nothing, - :ribbon => nothing, - :quiver => nothing, - :arrow => nothing, # allows for adding arrows to line/path... call `arrow(args...)` - :normalize => false, # do we want a normalized histogram? - :weights => nothing, # optional weights for histograms (1D and 2D) - :show_empty_bins => false, # should empty bins in 2D histogram be colored as zero (otherwise they are transparent) - :contours => false, # add contours to 3d surface and wireframe plots - :contour_labels => false, - :subplot => :auto, # which subplot(s) does this series belong to? - :series_annotations => nothing, # a list of annotations which apply to the coordinates of this series - :primary => true, # when true, this "counts" as a series for color selection, etc. the main use is to allow - # one logical series to be broken up (path and markers, for example) - :hover => nothing, # text to display when hovering over the data points - :stride => (1, 1), # array stride for wireframe/surface, the first element is the row stride and the second is the column stride. - :connections => nothing, # tuple of arrays to specify connectivity of a 3d mesh - :z_order => :front, # one of :front, :back or integer in 1:length(sp.series_list) - :permute => :none, # tuple of two symbols to be permuted - :extra_kwargs => Dict(), -) - -const _plot_defaults = KW( - :plot_title => "", - :plot_titleindex => 0, - :plot_titlefontsize => 16, - :plot_titlelocation => :center, # also :left or :right - :plot_titlefontfamily => :match, - :plot_titlefonthalign => :hcenter, - :plot_titlefontvalign => :vcenter, - :plot_titlefontrotation => 0.0, - :plot_titlefontcolor => :match, - :plot_titlevspan => 0.05, # vertical span of the plot title, here 5% - :background_color => colorant"white", # default for all backgrounds, - :background_color_outside => :match, # background outside grid, - :foreground_color => :auto, # default for all foregrounds, and title color, - :fontfamily => "sans-serif", - :size => (600, 400), - :pos => (0, 0), - :window_title => "Plots.jl", - :show => false, - :layout => 1, - :link => :none, - :overwrite_figure => true, - :html_output_format => :auto, - :tex_output_standalone => false, - :inset_subplots => nothing, # optionally pass a vector of (parent,bbox) tuples which are - # the parent layout and the relative bounding box of inset subplots - :dpi => DPI, # dots per inch for images, etc - :thickness_scaling => 1, - :display_type => :auto, - :warn_on_unsupported => true, - :extra_plot_kwargs => Dict(), - :extra_kwargs => :series, # directs collection of extra_kwargs -) - -const _subplot_defaults = KW( - :title => "", - :titlelocation => :center, # also :left or :right - :fontfamily_subplot => :match, - :titlefontfamily => :match, - :titlefontsize => 14, - :titlefonthalign => :hcenter, - :titlefontvalign => :vcenter, - :titlefontrotation => 0.0, - :titlefontcolor => :match, - :background_color_subplot => :match, # default for other bg colors... match takes plot default - :background_color_inside => :match, # background inside grid - :foreground_color_subplot => :match, # default for other fg colors... match takes plot default - :foreground_color_title => :match, # title color - :color_palette => :auto, - :colorbar => :legend, - :clims => :auto, - :colorbar_fontfamily => :match, - :colorbar_ticks => :auto, - :colorbar_tickfontfamily => :match, - :colorbar_tickfontsize => 8, - :colorbar_tickfonthalign => :hcenter, - :colorbar_tickfontvalign => :vcenter, - :colorbar_tickfontrotation => 0.0, - :colorbar_tickfontcolor => :match, - :colorbar_scale => :identity, - :colorbar_formatter => :auto, - :colorbar_discrete_values => [], - :colorbar_continuous_values => zeros(0), - :annotations => [], # annotation tuples... list of (x,y,annotation) - :annotationfontfamily => :match, - :annotationfontsize => 14, - :annotationhalign => :hcenter, - :annotationvalign => :vcenter, - :annotationrotation => 0.0, - :annotationcolor => :match, - :projection => :none, # can also be :polar or :3d - :projection_type => :auto, # can also be :ortho(graphic) or :persp(ective) - :aspect_ratio => :auto, # choose from :none or :equal - :margin => 1mm, - :left_margin => :match, - :top_margin => :match, - :right_margin => :match, - :bottom_margin => :match, - :subplot_index => -1, - :colorbar_title => "", - :colorbar_titlefontsize => 10, - :colorbar_title_location => :center, # also :left or :right - :colorbar_fontfamily => :match, - :colorbar_titlefontfamily => :match, - :colorbar_titlefonthalign => :hcenter, - :colorbar_titlefontvalign => :vcenter, - :colorbar_titlefontrotation => 0.0, - :colorbar_titlefontcolor => :match, - :framestyle => :axes, - :camera => (30, 30), - :extra_kwargs => Dict(), -) - -const _axis_defaults = KW( - :guide => "", - :guide_position => :auto, - :lims => :auto, - :ticks => :auto, - :scale => :identity, - :rotation => 0, - :flip => false, - :link => [], - :tickfontfamily => :match, - :tickfontsize => 8, - :tickfonthalign => :hcenter, - :tickfontvalign => :vcenter, - :tickfontrotation => 0.0, - :tickfontcolor => :match, - :guidefontfamily => :match, - :guidefontsize => 11, - :guidefonthalign => :hcenter, - :guidefontvalign => :vcenter, - :guidefontrotation => 0.0, - :guidefontcolor => :match, - :foreground_color_axis => :match, # axis border/tick colors, - :foreground_color_border => :match, # plot area border/spines, - :foreground_color_text => :match, # tick text color, - :foreground_color_guide => :match, # guide text color, - :discrete_values => [], - :formatter => :auto, - :mirror => false, - :grid => true, - :foreground_color_grid => :match, # grid color - :gridalpha => 0.1, - :gridstyle => :solid, - :gridlinewidth => 0.5, - :foreground_color_minor_grid => :match, # grid color - :minorgridalpha => 0.05, - :minorgridstyle => :solid, - :minorgridlinewidth => 0.5, - :tick_direction => :in, - :minorticks => :auto, - :minorgrid => false, - :showaxis => true, - :widen => :auto, - :draw_arrow => false, - :unitformat => :round, -) - -const _suppress_warnings = Set{Symbol}([ - :x_discrete_indices, - :y_discrete_indices, - :z_discrete_indices, - :subplot, - :subplot_index, - :series_plotindex, - :series_index, - :link, - :plot_object, - :primary, - :smooth, - :relative_bbox, - :force_minpad, - :x_extrema, - :y_extrema, - :z_extrema, -]) - -is_subplot_attr(k) = k in _all_subplot_args -is_series_attr(k) = k in _all_series_args -is_axis_attr(k) = Symbol(chop(string(k); head = 1, tail = 0)) in _all_axis_args -is_axis_attr_noletter(k) = k in _all_axis_args - -RecipesBase.is_key_supported(k::Symbol) = is_attr_supported(k) - -# ----------------------------------------------------------------------------- -autopick_ignore_none_auto(arr::AVec, idx::Integer) = - _cycle(setdiff(arr, [:none, :auto]), idx) -autopick_ignore_none_auto(notarr, idx::Integer) = notarr - -function aliasesAndAutopick( - plotattributes::AKW, - sym::Symbol, - aliases::Dict{Symbol,Symbol}, - options::AVec, - plotIndex::Int, -) - if plotattributes[sym] === :auto - plotattributes[sym] = autopick_ignore_none_auto(options, plotIndex) - elseif haskey(aliases, plotattributes[sym]) - plotattributes[sym] = aliases[plotattributes[sym]] - end -end - -aliases(val) = aliases(_keyAliases, val) -aliases(aliasMap::Dict{Symbol,Symbol}, val) = - filter(x -> x.second == val, aliasMap) |> keys |> collect |> sort - -# ----------------------------------------------------------------------------- -# legend -add_aliases(:legend_position, :legend, :leg, :key, :legends) -add_aliases( - :legend_background_color, - :bg_legend, - :bglegend, - :bgcolor_legend, - :bg_color_legend, - :background_legend, - :background_colour_legend, - :bgcolour_legend, - :bg_colour_legend, - :background_color_legend, -) -add_aliases( - :legend_foreground_color, - :fg_legend, - :fglegend, - :fgcolor_legend, - :fg_color_legend, - :foreground_legend, - :foreground_colour_legend, - :fgcolour_legend, - :fg_colour_legend, - :foreground_color_legend, -) -add_aliases(:legend_font_pointsize, :legendfontsize) -add_aliases( - :legend_title, - :key_title, - :keytitle, - :label_title, - :labeltitle, - :leg_title, - :legtitle, -) -add_aliases(:legend_title_font_pointsize, :legendtitlefontsize) -add_aliases(:plot_title, :suptitle, :subplot_grid_title, :sgtitle, :plot_grid_title) -# margin -add_aliases(:left_margin, :leftmargin) - -add_aliases(:top_margin, :topmargin) -add_aliases(:bottom_margin, :bottommargin) -add_aliases(:right_margin, :rightmargin) - -# colors -add_aliases(:seriescolor, :c, :color, :colour, :colormap, :cmap) -add_aliases(:linecolor, :lc, :lcolor, :lcolour, :linecolour) -add_aliases(:markercolor, :mc, :mcolor, :mcolour, :markercolour) -add_aliases(:markerstrokecolor, :msc, :mscolor, :mscolour, :markerstrokecolour) -add_aliases(:markerstrokewidth, :msw, :mswidth) -add_aliases(:fillcolor, :fc, :fcolor, :fcolour, :fillcolour) - -add_aliases( - :background_color, - :bg, - :bgcolor, - :bg_color, - :background, - :background_colour, - :bgcolour, - :bg_colour, -) -add_aliases( - :background_color_subplot, - :bg_subplot, - :bgsubplot, - :bgcolor_subplot, - :bg_color_subplot, - :background_subplot, - :background_colour_subplot, - :bgcolour_subplot, - :bg_colour_subplot, -) -add_aliases( - :background_color_inside, - :bg_inside, - :bginside, - :bgcolor_inside, - :bg_color_inside, - :background_inside, - :background_colour_inside, - :bgcolour_inside, - :bg_colour_inside, -) -add_aliases( - :background_color_outside, - :bg_outside, - :bgoutside, - :bgcolor_outside, - :bg_color_outside, - :background_outside, - :background_colour_outside, - :bgcolour_outside, - :bg_colour_outside, -) -add_aliases( - :foreground_color, - :fg, - :fgcolor, - :fg_color, - :foreground, - :foreground_colour, - :fgcolour, - :fg_colour, -) - -add_aliases( - :foreground_color_subplot, - :fg_subplot, - :fgsubplot, - :fgcolor_subplot, - :fg_color_subplot, - :foreground_subplot, - :foreground_colour_subplot, - :fgcolour_subplot, - :fg_colour_subplot, -) -add_aliases( - :foreground_color_grid, - :fg_grid, - :fggrid, - :fgcolor_grid, - :fg_color_grid, - :foreground_grid, - :foreground_colour_grid, - :fgcolour_grid, - :fg_colour_grid, - :gridcolor, -) -add_aliases( - :foreground_color_minor_grid, - :fg_minor_grid, - :fgminorgrid, - :fgcolor_minorgrid, - :fg_color_minorgrid, - :foreground_minorgrid, - :foreground_colour_minor_grid, - :fgcolour_minorgrid, - :fg_colour_minor_grid, - :minorgridcolor, -) -add_aliases( - :foreground_color_title, - :fg_title, - :fgtitle, - :fgcolor_title, - :fg_color_title, - :foreground_title, - :foreground_colour_title, - :fgcolour_title, - :fg_colour_title, - :titlecolor, -) -add_aliases( - :foreground_color_axis, - :fg_axis, - :fgaxis, - :fgcolor_axis, - :fg_color_axis, - :foreground_axis, - :foreground_colour_axis, - :fgcolour_axis, - :fg_colour_axis, - :axiscolor, -) -add_aliases( - :foreground_color_border, - :fg_border, - :fgborder, - :fgcolor_border, - :fg_color_border, - :foreground_border, - :foreground_colour_border, - :fgcolour_border, - :fg_colour_border, - :bordercolor, -) -add_aliases( - :foreground_color_text, - :fg_text, - :fgtext, - :fgcolor_text, - :fg_color_text, - :foreground_text, - :foreground_colour_text, - :fgcolour_text, - :fg_colour_text, - :textcolor, -) -add_aliases( - :foreground_color_guide, - :fg_guide, - :fgguide, - :fgcolor_guide, - :fg_color_guide, - :foreground_guide, - :foreground_colour_guide, - :fgcolour_guide, - :fg_colour_guide, - :guidecolor, -) - -# alphas -add_aliases(:seriesalpha, :alpha, :α, :opacity) -add_aliases(:linealpha, :la, :lalpha, :lα, :lineopacity, :lopacity) -add_aliases(:markeralpha, :ma, :malpha, :mα, :markeropacity, :mopacity) -add_aliases(:markerstrokealpha, :msa, :msalpha, :msα, :markerstrokeopacity, :msopacity) -add_aliases(:fillalpha, :fa, :falpha, :fα, :fillopacity, :fopacity) - -# axes attributes -add_axes_aliases(:guide, :label, :lab, :l; generic = false) -add_axes_aliases(:lims, :lim, :limit, :limits, :range) -add_axes_aliases(:ticks, :tick) -add_axes_aliases(:rotation, :rot, :r) -add_axes_aliases(:guidefontsize, :labelfontsize) -add_axes_aliases(:gridalpha, :ga, :galpha, :gα, :gridopacity, :gopacity) -add_axes_aliases( - :gridstyle, - :grid_style, - :gridlinestyle, - :grid_linestyle, - :grid_ls, - :gridls, -) -add_axes_aliases( - :foreground_color_grid, - :fg_grid, - :fggrid, - :fgcolor_grid, - :fg_color_grid, - :foreground_grid, - :foreground_colour_grid, - :fgcolour_grid, - :fg_colour_grid, - :gridcolor, -) -add_axes_aliases( - :foreground_color_minor_grid, - :fg_minor_grid, - :fgminorgrid, - :fgcolor_minorgrid, - :fg_color_minorgrid, - :foreground_minorgrid, - :foreground_colour_minor_grid, - :fgcolour_minorgrid, - :fg_colour_minor_grid, - :minorgridcolor, -) -add_axes_aliases( - :gridlinewidth, - :gridwidth, - :grid_linewidth, - :grid_width, - :gridlw, - :grid_lw, -) -add_axes_aliases( - :minorgridstyle, - :minorgrid_style, - :minorgridlinestyle, - :minorgrid_linestyle, - :minorgrid_ls, - :minorgridls, -) -add_axes_aliases( - :minorgridlinewidth, - :minorgridwidth, - :minorgrid_linewidth, - :minorgrid_width, - :minorgridlw, - :minorgrid_lw, -) -add_axes_aliases( - :tick_direction, - :tickdirection, - :tick_dir, - :tickdir, - :tick_orientation, - :tickorientation, - :tick_or, - :tickor, -) - -# series attributes -add_aliases(:seriestype, :st, :t, :typ, :linetype, :lt) -add_aliases(:label, :lab) -add_aliases(:line, :l) -add_aliases(:linewidth, :w, :width, :lw) -add_aliases(:linestyle, :style, :s, :ls) -add_aliases(:marker, :m, :mark) -add_aliases(:markershape, :shape) -add_aliases(:markersize, :ms, :msize) -add_aliases(:marker_z, :markerz, :zcolor, :mz) -add_aliases(:line_z, :linez, :zline, :lz) -add_aliases(:fill, :f, :area) -add_aliases(:fillrange, :fillrng, :frange, :fillto, :fill_between) -add_aliases(:group, :g, :grouping) -add_aliases(:bins, :bin, :nbin, :nbins, :nb) -add_aliases(:ribbon, :rib) -add_aliases(:annotations, :ann, :anns, :annotate, :annotation) -add_aliases(:xguide, :xlabel, :xlab, :xl) -add_aliases(:xlims, :xlim, :xlimit, :xlimits, :xrange) -add_aliases(:xticks, :xtick) -add_aliases(:xrotation, :xrot, :xr) -add_aliases(:yguide, :ylabel, :ylab, :yl) -add_aliases(:ylims, :ylim, :ylimit, :ylimits, :yrange) -add_aliases(:yticks, :ytick) -add_aliases(:yrotation, :yrot, :yr) -add_aliases(:zguide, :zlabel, :zlab, :zl) -add_aliases(:zlims, :zlim, :zlimit, :zlimits) -add_aliases(:zticks, :ztick) -add_aliases(:zrotation, :zrot, :zr) -add_aliases(:guidefontsize, :labelfontsize) -add_aliases( - :fill_z, - :fillz, - :fz, - :surfacecolor, - :surfacecolour, - :sc, - :surfcolor, - :surfcolour, -) -add_aliases(:colorbar, :cb, :cbar, :colorkey) -add_aliases( - :colorbar_title, - :colorbartitle, - :cb_title, - :cbtitle, - :cbartitle, - :cbar_title, - :colorkeytitle, - :colorkey_title, -) -add_aliases(:clims, :clim, :cbarlims, :cbar_lims, :climits, :color_limits) -add_aliases(:smooth, :regression, :reg) -add_aliases(:levels, :nlevels, :nlev, :levs) -add_aliases(:size, :windowsize, :wsize) -add_aliases(:window_title, :windowtitle, :wtitle) -add_aliases(:show, :gui, :display) -add_aliases(:color_palette, :palette) -add_aliases(:overwrite_figure, :clf, :clearfig, :overwrite, :reuse) -add_aliases(:xerror, :xerr, :xerrorbar) -add_aliases(:yerror, :yerr, :yerrorbar, :err, :errorbar) -add_aliases(:zerror, :zerr, :zerrorbar) -add_aliases(:quiver, :velocity, :quiver2d, :gradient, :vectorfield) -add_aliases(:normalize, :norm, :normed, :normalized) -add_aliases(:show_empty_bins, :showemptybins, :showempty, :show_empty) -add_aliases(:aspect_ratio, :aspectratio, :axis_ratio, :axisratio, :ratio) -add_aliases(:subplot, :sp, :subplt, :splt) -add_aliases(:projection, :proj) -add_aliases(:projection_type, :proj_type) -add_aliases( - :titlelocation, - :title_location, - :title_loc, - :titleloc, - :title_position, - :title_pos, - :titlepos, - :titleposition, - :title_align, - :title_alignment, -) -add_aliases( - :series_annotations, - :series_ann, - :seriesann, - :series_anns, - :seriesanns, - :series_annotation, - :text, - :txt, - :texts, - :txts, -) -add_aliases(:html_output_format, :format, :fmt, :html_format) -add_aliases(:orientation, :direction, :dir) -add_aliases(:inset_subplots, :inset, :floating) -add_aliases(:stride, :wirefame_stride, :surface_stride, :surf_str, :str) - -add_aliases( - :framestyle, - :frame_style, - :frame, - :axesstyle, - :axes_style, - :boxstyle, - :box_style, - :box, - :borderstyle, - :border_style, - :border, -) - -add_aliases(:camera, :cam, :viewangle, :view_angle) -add_aliases(:contour_labels, :contourlabels, :clabels, :clabs) -add_aliases(:warn_on_unsupported, :warn) - -# ----------------------------------------------------------------------------- - -function parse_axis_kw(s::Symbol) - s = string(s) - for letter in ('x', 'y', 'z') - startswith(s, letter) && - return (Symbol(letter), Symbol(chop(s, head = 1, tail = 0))) - end - nothing -end - -# update the defaults globally - -""" -`default(key)` returns the current default value for that key. - -`default(key, value)` sets the current default value for that key. - -`default(; kw...)` will set the current default value for each key/value pair. - -`default(plotattributes, key)` returns the key from plotattributes if it exists, otherwise `default(key)`. - -""" -function default(k::Symbol) - k = get(_keyAliases, k, k) - for defaults in _all_defaults - haskey(defaults, k) && return defaults[k] - end - haskey(_axis_defaults, k) && return _axis_defaults[k] - if (axis_k = parse_axis_kw(k)) !== nothing - letter, key = axis_k - return _axis_defaults_byletter[letter][key] - end - k === :letter && return k # for type recipe processing - missing -end - -function default(k::Symbol, v) - k = get(_keyAliases, k, k) - for defaults in _all_defaults - if haskey(defaults, k) - defaults[k] = v - return v - end - end - if haskey(_axis_defaults, k) - _axis_defaults[k] = v - return v - end - if (axis_k = parse_axis_kw(k)) !== nothing - letter, key = axis_k - _axis_defaults_byletter[letter][key] = v - return v - end - k in _suppress_warnings || error("Unknown key: ", k) -end - -function default(; reset = true, kw...) - (reset && isempty(kw)) && reset_defaults() - kw = KW(kw) - Plots.preprocess_attributes!(kw) - for (k, v) in kw - default(k, v) - end -end - -default(plotattributes::AKW, k::Symbol) = get(plotattributes, k, default(k)) - -function reset_defaults() - foreach(merge!, _all_defaults, _initial_defaults) - merge!(_axis_defaults, _initial_axis_defaults) - reset_axis_defaults_byletter!() -end - -# ----------------------------------------------------------------------------- - -# if arg is a valid color value, then set plotattributes[csym] and return true -function handleColors!(plotattributes::AKW, arg, csym::Symbol) - try - plotattributes[csym] = if arg === :auto - :auto - else - plot_color(arg) - end - return true - catch - end - false -end - -function processLineArg(plotattributes::AKW, arg) - # seriestype - if allLineTypes(arg) - plotattributes[:seriestype] = arg - - # linestyle - elseif allStyles(arg) - plotattributes[:linestyle] = arg - - elseif typeof(arg) <: Stroke - arg.width === nothing || (plotattributes[:linewidth] = arg.width) - arg.color === nothing || ( - plotattributes[:linecolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:linealpha] = arg.alpha) - arg.style === nothing || (plotattributes[:linestyle] = arg.style) - - elseif typeof(arg) <: Brush - arg.size === nothing || (plotattributes[:fillrange] = arg.size) - arg.color === nothing || ( - plotattributes[:fillcolor] = - 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 - - # linealpha - elseif allAlphas(arg) - plotattributes[:linealpha] = arg - - # linewidth - elseif allReals(arg) - plotattributes[:linewidth] = arg - - # color - elseif !handleColors!(plotattributes, arg, :linecolor) - @warn "Skipped line arg $arg." - end -end - -function processMarkerArg(plotattributes::AKW, arg) - # markershape - if allShapes(arg) && !haskey(plotattributes, :markershape) - plotattributes[:markershape] = arg - - # stroke style - elseif allStyles(arg) - plotattributes[:markerstrokestyle] = arg - - elseif typeof(arg) <: Stroke - arg.width === nothing || (plotattributes[:markerstrokewidth] = arg.width) - arg.color === nothing || ( - plotattributes[:markerstrokecolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:markerstrokealpha] = arg.alpha) - arg.style === nothing || (plotattributes[:markerstrokestyle] = arg.style) - - elseif typeof(arg) <: Brush - arg.size === nothing || (plotattributes[:markersize] = arg.size) - arg.color === nothing || ( - plotattributes[:markercolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:markeralpha] = arg.alpha) - - # linealpha - elseif allAlphas(arg) - plotattributes[:markeralpha] = arg - - # bool - elseif typeof(arg) <: Bool - plotattributes[:markershape] = arg ? :circle : :none - - # markersize - elseif allReals(arg) - plotattributes[:markersize] = arg - - # markercolor - elseif !handleColors!(plotattributes, arg, :markercolor) - @warn "Skipped marker arg $arg." - end -end - -function processFillArg(plotattributes::AKW, arg) - # fr = get(plotattributes, :fillrange, 0) - if typeof(arg) <: Brush - arg.size === nothing || (plotattributes[:fillrange] = arg.size) - arg.color === nothing || ( - plotattributes[:fillcolor] = - 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 - - # fillrange function - elseif allFunctions(arg) - plotattributes[:fillrange] = arg - - # fillalpha - elseif allAlphas(arg) - plotattributes[:fillalpha] = arg - - # fillrange provided as vector or number - elseif typeof(arg) <: Union{AbstractArray{<:Real},Real} - plotattributes[:fillrange] = arg - - elseif !handleColors!(plotattributes, arg, :fillcolor) - plotattributes[:fillrange] = arg - end - # plotattributes[:fillrange] = fr - nothing -end - -function processGridArg!(plotattributes::AKW, arg, letter) - if arg in _allGridArgs || isa(arg, Bool) - plotattributes[get_attr_symbol(letter, :grid)] = hasgrid(arg, letter) - - elseif allStyles(arg) - plotattributes[get_attr_symbol(letter, :gridstyle)] = arg - - elseif typeof(arg) <: Stroke - arg.width === nothing || - (plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg.width) - arg.color === nothing || ( - plotattributes[get_attr_symbol(letter, :foreground_color_grid)] = - arg.color in (:auto, :match) ? :match : plot_color(arg.color) - ) - arg.alpha === nothing || - (plotattributes[get_attr_symbol(letter, :gridalpha)] = arg.alpha) - arg.style === nothing || - (plotattributes[get_attr_symbol(letter, :gridstyle)] = arg.style) - - # linealpha - elseif allAlphas(arg) - plotattributes[get_attr_symbol(letter, :gridalpha)] = arg - - # linewidth - elseif allReals(arg) - plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg - - # color - elseif !handleColors!( - plotattributes, - arg, - get_attr_symbol(letter, :foreground_color_grid), - ) - @warn "Skipped grid arg $arg." - end -end - -function processMinorGridArg!(plotattributes::AKW, arg, letter) - if arg in _allGridArgs || isa(arg, Bool) - plotattributes[get_attr_symbol(letter, :minorgrid)] = hasgrid(arg, letter) - - elseif allStyles(arg) - plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg - plotattributes[get_attr_symbol(letter, :minorgrid)] = true - - elseif typeof(arg) <: Stroke - arg.width === nothing || - (plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg.width) - arg.color === nothing || ( - plotattributes[get_attr_symbol(letter, :foreground_color_minor_grid)] = - arg.color in (:auto, :match) ? :match : plot_color(arg.color) - ) - arg.alpha === nothing || - (plotattributes[get_attr_symbol(letter, :minorgridalpha)] = arg.alpha) - arg.style === nothing || - (plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg.style) - plotattributes[get_attr_symbol(letter, :minorgrid)] = true - - # linealpha - elseif allAlphas(arg) - plotattributes[get_attr_symbol(letter, :minorgridalpha)] = arg - plotattributes[get_attr_symbol(letter, :minorgrid)] = true - - # linewidth - elseif allReals(arg) - plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg - plotattributes[get_attr_symbol(letter, :minorgrid)] = true - - # color - elseif handleColors!( - plotattributes, - arg, - get_attr_symbol(letter, :foreground_color_minor_grid), - ) - plotattributes[get_attr_symbol(letter, :minorgrid)] = true - else - @warn "Skipped grid arg $arg." - end -end - -@attributes function processFontArg!(plotattributes::AKW, fontname::Symbol, arg) - T = typeof(arg) - if fontname in (:legend_font,) - # TODO: this is neccessary while old and new font names coexist and should be standard after the transition - fontname = Symbol(fontname, :_) - end - if T <: Font - Symbol(fontname, :family) --> arg.family - - # TODO: this is neccessary in the transition from old fontsize to new font_pointsize and should be removed when it is completed - if in(Symbol(fontname, :size), _all_args) - Symbol(fontname, :size) --> arg.pointsize - else - Symbol(fontname, :pointsize) --> arg.pointsize - end - Symbol(fontname, :halign) --> arg.halign - Symbol(fontname, :valign) --> arg.valign - Symbol(fontname, :rotation) --> arg.rotation - Symbol(fontname, :color) --> arg.color - elseif arg === :center - Symbol(fontname, :halign) --> :hcenter - Symbol(fontname, :valign) --> :vcenter - elseif arg ∈ _haligns - Symbol(fontname, :halign) --> arg - elseif arg ∈ _valigns - Symbol(fontname, :valign) --> arg - elseif T <: Colorant - Symbol(fontname, :color) --> arg - elseif T <: Symbol || T <: AbstractString - try - Symbol(fontname, :color) --> parse(Colorant, string(arg)) - catch - Symbol(fontname, :family) --> string(arg) - end - elseif typeof(arg) <: Integer - if in(Symbol(fontname, :size), _all_args) - Symbol(fontname, :size) --> arg - else - Symbol(fontname, :pointsize) --> arg - end - elseif typeof(arg) <: Real - Symbol(fontname, :rotation) --> convert(Float64, arg) - else - @warn "Skipped font arg: $arg ($(typeof(arg)))" - end -end - -_replace_markershape(shape::Symbol) = get(_markerAliases, shape, shape) -_replace_markershape(shapes::AVec) = map(_replace_markershape, shapes) -_replace_markershape(shape) = shape - -function _add_markershape(plotattributes::AKW) - # add the markershape if it needs to be added... hack to allow "m=10" to add a shape, - # and still allow overriding in _apply_recipe - ms = pop!(plotattributes, :markershape_to_add, :none) - if !haskey(plotattributes, :markershape) && ms !== :none - plotattributes[:markershape] = ms - end -end - -"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases." -function preprocess_attributes!(plotattributes::AKW) - replaceAliases!(plotattributes, _keyAliases) - - # handle axis args common to all axis - args = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :axis, ())) - showarg = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :showaxis, ())) - for arg in wraptuple((args..., showarg...)) - for letter in (:x, :y, :z) - process_axis_arg!(plotattributes, arg, letter) - end - end - # handle axis args - for letter in (:x, :y, :z) - asym = get_attr_symbol(letter, :axis) - args = RecipesPipeline.pop_kw!(plotattributes, asym, ()) - if !(typeof(args) <: Axis) - for arg in wraptuple(args) - process_axis_arg!(plotattributes, arg, letter) - end - end - end - - # vline and others accesses the y argument but actually maps it to the x axis. - # Hence, we have to take care of formatters - if treats_y_as_x(get(plotattributes, :seriestype, :path)) - xformatter = get(plotattributes, :xformatter, :auto) - yformatter = get(plotattributes, :yformatter, :auto) - yformatter !== :auto && (plotattributes[:xformatter] = yformatter) - xformatter === :auto && - haskey(plotattributes, :yformatter) && - pop!(plotattributes, :yformatter) - end - - # handle grid args common to all axes - args = RecipesPipeline.pop_kw!(plotattributes, :grid, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) - processGridArg!(plotattributes, arg, letter) - end - end - # handle individual axes grid args - for letter in (:x, :y, :z) - gridsym = get_attr_symbol(letter, :grid) - args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) - for arg in wraptuple(args) - processGridArg!(plotattributes, arg, letter) - end - end - # handle minor grid args common to all axes - args = RecipesPipeline.pop_kw!(plotattributes, :minorgrid, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) - processMinorGridArg!(plotattributes, arg, letter) - end - end - # handle individual axes grid args - for letter in (:x, :y, :z) - gridsym = get_attr_symbol(letter, :minorgrid) - args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) - for arg in wraptuple(args) - processMinorGridArg!(plotattributes, arg, letter) - end - end - # handle font args common to all axes - for fontname in (:tickfont, :guidefont) - args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) - processFontArg!(plotattributes, get_attr_symbol(letter, fontname), arg) - end - end - end - # handle individual axes font args - for letter in (:x, :y, :z) - for fontname in (:tickfont, :guidefont) - args = RecipesPipeline.pop_kw!( - plotattributes, - get_attr_symbol(letter, fontname), - (), - ) - for arg in wraptuple(args) - processFontArg!(plotattributes, get_attr_symbol(letter, fontname), arg) - end - end - end - # handle axes args - for k in _axis_args - if haskey(plotattributes, k) && k !== :link - v = plotattributes[k] - for letter in (:x, :y, :z) - lk = get_attr_symbol(letter, k) - if !is_explicit(plotattributes, lk) - plotattributes[lk] = v - end - end - end - end - - # fonts - for fontname in - (:titlefont, :legend_title_font, :plot_titlefont, :colorbar_titlefont, :legend_font) - args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) - for arg in wraptuple(args) - processFontArg!(plotattributes, fontname, arg) - end - end - - # handle line args - for arg in wraptuple(RecipesPipeline.pop_kw!(plotattributes, :line, ())) - processLineArg(plotattributes, arg) - end - - if haskey(plotattributes, :seriestype) && - haskey(_typeAliases, plotattributes[:seriestype]) - plotattributes[:seriestype] = _typeAliases[plotattributes[:seriestype]] - end - - # handle marker args... default to ellipse if shape not set - anymarker = false - for arg in wraptuple(get(plotattributes, :marker, ())) - processMarkerArg(plotattributes, arg) - anymarker = true - end - RecipesPipeline.reset_kw!(plotattributes, :marker) - if haskey(plotattributes, :markershape) - plotattributes[:markershape] = _replace_markershape(plotattributes[:markershape]) - if plotattributes[:markershape] === :none && - get(plotattributes, :seriestype, :path) in - (:scatter, :scatterbins, :scatterhist, :scatter3d) #the default should be :auto, not :none, so that :none can be set explicitly and would be respected - plotattributes[:markershape] = :circle - end - elseif anymarker - plotattributes[:markershape_to_add] = :circle # add it after _apply_recipe - end - - # handle fill - for arg in wraptuple(get(plotattributes, :fill, ())) - processFillArg(plotattributes, arg) - end - RecipesPipeline.reset_kw!(plotattributes, :fill) - - # handle series annotations - if haskey(plotattributes, :series_annotations) - plotattributes[:series_annotations] = - series_annotations(wraptuple(plotattributes[:series_annotations])...) - end - - # convert into strokes and brushes - - if haskey(plotattributes, :arrow) - a = plotattributes[:arrow] - plotattributes[:arrow] = if a == true - arrow() - elseif a in (false, nothing, :none) - nothing - elseif !(typeof(a) <: Arrow || typeof(a) <: AbstractArray{Arrow}) - arrow(wraptuple(a)...) - else - a - end - end - - # legends - defaults are set in `src/components.jl` (see `@add_attributes`) - if haskey(plotattributes, :legend_position) - plotattributes[:legend_position] = - convertLegendValue(plotattributes[:legend_position]) - end - if haskey(plotattributes, :colorbar) - plotattributes[:colorbar] = convertLegendValue(plotattributes[:colorbar]) - end - - # framestyle - if haskey(plotattributes, :framestyle) && - haskey(_framestyleAliases, plotattributes[:framestyle]) - plotattributes[:framestyle] = _framestyleAliases[plotattributes[:framestyle]] - end - - # contours - if haskey(plotattributes, :levels) - check_contour_levels(plotattributes[:levels]) - end - - # warnings for moved recipes - st = get(plotattributes, :seriestype, :path) - if st in (:boxplot, :violin, :density) && - !haskey( - Base.loaded_modules, - Base.PkgId(Base.UUID("f3b207a7-027a-5e70-b257-86293d7955fd"), "StatsPlots"), - ) - @warn "seriestype $st has been moved to StatsPlots. To use: \`Pkg.add(\"StatsPlots\"); using StatsPlots\`" - end - nothing -end -RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = - Plots.preprocess_attributes!(plotattributes) - -# ----------------------------------------------------------------------------- - -const _already_warned = Dict{Symbol,Set{Symbol}}() -const _to_warn = Set{Symbol}() - -should_warn_on_unsupported(::AbstractBackend) = _plot_defaults[:warn_on_unsupported] - -function warn_on_unsupported_args(pkg::AbstractBackend, plotattributes) - empty!(_to_warn) - bend = backend_name(pkg) - already_warned = get!(_already_warned, bend) do - Set{Symbol}() - end - extra_kwargs = Dict{Symbol,Any}() - for k in explicitkeys(plotattributes) - (is_attr_supported(pkg, k) && k ∉ keys(_deprecated_attributes)) && continue - k in _suppress_warnings && continue - if ismissing(default(k)) - extra_kwargs[k] = pop_kw!(plotattributes, k) - elseif plotattributes[k] != default(k) - k in already_warned || push!(_to_warn, k) - end - end - - if !isempty(_to_warn) && - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) - for k in sort(collect(_to_warn)) - push!(already_warned, k) - if k in keys(_deprecated_attributes) - @warn """ - Keyword argument `$k` is deprecated. - Please use `$(_deprecated_attributes[k])` instead. - """ - else - @warn "Keyword argument $k not supported with $pkg. Choose from: $(join(supported_attrs(pkg), ", "))" - end - end - end - extra_kwargs -end - -# _markershape_supported(pkg::AbstractBackend, shape::Symbol) = shape in supported_markers(pkg) -# _markershape_supported(pkg::AbstractBackend, shape::Shape) = Shape in supported_markers(pkg) -# _markershape_supported(pkg::AbstractBackend, shapes::AVec) = all([_markershape_supported(pkg, shape) for shape in shapes]) - -function warn_on_unsupported(pkg::AbstractBackend, plotattributes) - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return - is_seriestype_supported(pkg, plotattributes[:seriestype]) || - @warn "seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))" - is_style_supported(pkg, plotattributes[:linestyle]) || - @warn "linestyle $(plotattributes[:linestyle]) is unsupported with $pkg. Choose from: $(supported_styles(pkg))" - is_marker_supported(pkg, plotattributes[:markershape]) || - @warn "markershape $(plotattributes[:markershape]) is unsupported with $pkg. Choose from: $(supported_markers(pkg))" -end - -function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW) - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return - for k in (:xscale, :yscale, :zscale, :scale) - if haskey(plotattributes, k) - v = plotattributes[k] - if !all(is_scale_supported.(Ref(pkg), v)) - @warn """ - scale $v is unsupported with $pkg. - Choose from: $(supported_scales(pkg)) - """ - end - end - end -end - -# ----------------------------------------------------------------------------- - -function convertLegendValue(val::Symbol) - if val in (:both, :all, :yes) - :best - elseif val in (:no, :none) - :none - elseif val in ( - :right, - :left, - :top, - :bottom, - :inside, - :best, - :legend, - :topright, - :topleft, - :bottomleft, - :bottomright, - :outertopright, - :outertopleft, - :outertop, - :outerright, - :outerleft, - :outerbottomright, - :outerbottomleft, - :outerbottom, - :inline, - ) - val - elseif val === :horizontal - -1 - else - error("Invalid symbol for legend: $val") - end -end -convertLegendValue(val::Real) = val -convertLegendValue(val::Bool) = val ? :best : :none -convertLegendValue(val::Nothing) = :none -convertLegendValue(v::Union{Tuple,NamedTuple}) = convertLegendValue.(v) -convertLegendValue(v::Tuple{<:Real,<:Real}) = v -convertLegendValue(v::Tuple{<:Real,Symbol}) = v -convertLegendValue(v::AbstractArray) = map(convertLegendValue, v) - -# ----------------------------------------------------------------------------- - -"""Throw an error if the `levels` keyword argument is not of the correct type -or `levels` is less than 1""" -function check_contour_levels(levels) - if !(levels isa Union{Integer,AVec}) - "the levels keyword argument must be an integer or AbstractVector" |> - ArgumentError |> - throw - elseif levels isa Integer && levels <= 0 - "must pass a positive number of contours to the levels keyword argument" |> - ArgumentError |> - throw - end -end - -# ----------------------------------------------------------------------------- - -# 1-row matrices will give an element -# multi-row matrices will give a column -# InputWrapper just gives the contents -# anything else is returned as-is -function slice_arg(v::AMat, idx::Int) - isempty(v) && return v - c = mod1(idx, size(v, 2)) - m, n = axes(v) - size(v, 1) == 1 ? v[first(m), n[c]] : v[:, n[c]] -end -slice_arg(wrapper::InputWrapper, idx) = wrapper.obj -slice_arg(v::NTuple{2,AMat}, idx::Int) = slice_arg(v[1], idx), slice_arg(v[2], idx) -slice_arg(v, idx) = v - -# given an argument key `k`, extract the argument value for this index, -# and set into plotattributes[k]. Matrices are sliced by column. -# if nothing is set (or container is empty), return the existing value. -function slice_arg!( - plotattributes_in, - plotattributes_out, - k::Symbol, - idx::Int, - remove_pair::Bool, -) - v = get(plotattributes_in, k, plotattributes_out[k]) - plotattributes_out[k] = if haskey(plotattributes_in, k) && k ∉ _plot_args - slice_arg(v, idx) - else - v - end - remove_pair && RecipesPipeline.reset_kw!(plotattributes_in, k) - nothing -end - -# ----------------------------------------------------------------------------- - -function color_or_nothing!(plotattributes, k::Symbol) - plotattributes[k] = (v = plotattributes[k]) === :match ? v : plot_color(v) - nothing -end - -# ----------------------------------------------------------------------------- - -# when a value can be `:match`, this is the key that should be used instead for value retrieval -const _match_map = Dict( - :background_color_outside => :background_color, - :legend_background_color => :background_color_subplot, - :background_color_inside => :background_color_subplot, - :legend_foreground_color => :foreground_color_subplot, - :foreground_color_title => :foreground_color_subplot, - :left_margin => :margin, - :top_margin => :margin, - :right_margin => :margin, - :bottom_margin => :margin, - :titlefontfamily => :fontfamily_subplot, - :titlefontcolor => :foreground_color_subplot, - :legend_font_family => :fontfamily_subplot, - :legend_font_color => :foreground_color_subplot, - :legend_title_font_family => :fontfamily_subplot, - :legend_title_font_color => :foreground_color_subplot, - :colorbar_fontfamily => :fontfamily_subplot, - :colorbar_titlefontfamily => :fontfamily_subplot, - :colorbar_titlefontcolor => :foreground_color_subplot, - :colorbar_tickfontfamily => :fontfamily_subplot, - :colorbar_tickfontcolor => :foreground_color_subplot, - :plot_titlefontfamily => :fontfamily, - :plot_titlefontcolor => :foreground_color, - :tickfontcolor => :foreground_color_text, - :guidefontcolor => :foreground_color_guide, - :annotationfontfamily => :fontfamily_subplot, - :annotationcolor => :foreground_color_subplot, -) - -# these can match values from the parent container (axis --> subplot --> plot) -const _match_map2 = Dict( - :background_color_subplot => :background_color, - :foreground_color_subplot => :foreground_color, - :foreground_color_axis => :foreground_color_subplot, - :foreground_color_border => :foreground_color_subplot, - :foreground_color_grid => :foreground_color_subplot, - :foreground_color_minor_grid => :foreground_color_subplot, - :foreground_color_guide => :foreground_color_subplot, - :foreground_color_text => :foreground_color_subplot, - :fontfamily_subplot => :fontfamily, - :tickfontfamily => :fontfamily_subplot, - :guidefontfamily => :fontfamily_subplot, -) - -# properly retrieve from plt.attr, passing `:match` to the correct key -Base.getindex(plt::Plot, k::Symbol) = - if (v = plt.attr[k]) === :match - plt[_match_map[k]] - else - v - end - -# properly retrieve from sp.attr, passing `:match` to the correct key -Base.getindex(sp::Subplot, k::Symbol) = - if (v = sp.attr[k]) === :match - if haskey(_match_map2, k) - sp.plt[_match_map2[k]] - else - sp[_match_map[k]] - end - else - v - end - -# properly retrieve from axis.attr, passing `:match` to the correct key -Base.getindex(axis::Axis, k::Symbol) = - if (v = axis.plotattributes[k]) === :match - if haskey(_match_map2, k) - axis.sps[1][_match_map2[k]] - else - axis[_match_map[k]] - end - else - v - end - -Base.getindex(series::Series, k::Symbol) = series.plotattributes[k] - -Base.setindex!(plt::Plot, v, k::Symbol) = (plt.attr[k] = v) -Base.setindex!(sp::Subplot, v, k::Symbol) = (sp.attr[k] = v) -Base.setindex!(axis::Axis, v, k::Symbol) = (axis.plotattributes[k] = v) -Base.setindex!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) - -Base.get(plt::Plot, k::Symbol, v) = get(plt.attr, k, v) -Base.get(sp::Subplot, k::Symbol, v) = get(sp.attr, k, v) -Base.get(axis::Axis, k::Symbol, v) = get(axis.plotattributes, k, v) -Base.get(series::Series, k::Symbol, v) = get(series.plotattributes, k, v) - -# ----------------------------------------------------------------------------- - -function fg_color(plotattributes::AKW) - fg = get(plotattributes, :foreground_color, :auto) - if fg === :auto - bg = plot_color(get(plotattributes, :background_color, :white)) - fg = alpha(bg) > 0 && isdark(bg) ? colorant"white" : colorant"black" - else - plot_color(fg) - end -end - -# update attr from an input dictionary -function _update_plot_args(plt::Plot, plotattributes_in::AKW) - for (k, v) in _plot_defaults - slice_arg!(plotattributes_in, plt.attr, k, 1, true) - end - - # handle colors - plt[:background_color] = plot_color(plt.attr[:background_color]) - plt[:foreground_color] = fg_color(plt.attr) - color_or_nothing!(plt.attr, :background_color_outside) -end - -# ----------------------------------------------------------------------------- - -function _update_subplot_periphery(sp::Subplot, anns::AVec) - # extend annotations, and ensure we always have a (x,y,PlotText) tuple - newanns = [] - for ann in vcat(anns, sp[:annotations]) - append!(newanns, process_annotation(sp, ann)) - end - sp.attr[:annotations] = newanns - - # handle legend/colorbar - sp.attr[:legend_position] = convertLegendValue(sp.attr[:legend_position]) - sp.attr[:colorbar] = convertLegendValue(sp.attr[:colorbar]) - if sp.attr[:colorbar] === :legend - sp.attr[:colorbar] = sp.attr[:legend_position] - end - nothing -end - -function _update_subplot_colors(sp::Subplot) - # background colors - color_or_nothing!(sp.attr, :background_color_subplot) - sp.attr[:color_palette] = get_color_palette(sp.attr[:color_palette], 30) - color_or_nothing!(sp.attr, :legend_background_color) - color_or_nothing!(sp.attr, :background_color_inside) - - # foreground colors - color_or_nothing!(sp.attr, :foreground_color_subplot) - color_or_nothing!(sp.attr, :legend_foreground_color) - color_or_nothing!(sp.attr, :foreground_color_title) - nothing -end - -_update_margins(sp::Subplot) = - for sym in (:margin, :left_margin, :top_margin, :right_margin, :bottom_margin) - if (margin = get(sp.attr, sym, nothing)) isa Tuple - # transform e.g. (1, :mm) => 1 * Plots.mm - sp.attr[sym] = margin[1] * getfield(@__MODULE__, margin[2]) - end - end - -function _update_axis( - plt::Plot, - sp::Subplot, - plotattributes_in::AKW, - letter::Symbol, - subplot_index::Int, -) - # get (maybe initialize) the axis - axis = get_axis(sp, letter) - - _update_axis(axis, plotattributes_in, letter, subplot_index) - - # convert a bool into auto or nothing - if isa(axis[:ticks], Bool) - axis[:ticks] = axis[:ticks] ? :auto : nothing - end - - _update_axis_colors(axis) - _update_axis_links(plt, axis, letter) - nothing -end - -function _update_axis( - axis::Axis, - plotattributes_in::AKW, - letter::Symbol, - subplot_index::Int, -) - # build the KW of arguments from the letter version (i.e. xticks --> ticks) - kw = KW() - for k in _all_axis_args - # first get the args without the letter: `tickfont = font(10)` - # note: we don't pop because we want this to apply to all axes! (delete after all have finished) - if haskey(plotattributes_in, k) - kw[k] = slice_arg(plotattributes_in[k], subplot_index) - end - - # then get those args that were passed with a leading letter: `xlabel = "X"` - lk = get_attr_symbol(letter, k) - - if haskey(plotattributes_in, lk) - kw[k] = slice_arg(plotattributes_in[lk], subplot_index) - end - end - - # update the axis - attr!(axis; kw...) - nothing -end - -function _update_axis_colors(axis::Axis) - # # update the axis colors - color_or_nothing!(axis.plotattributes, :foreground_color_axis) - color_or_nothing!(axis.plotattributes, :foreground_color_border) - color_or_nothing!(axis.plotattributes, :foreground_color_guide) - color_or_nothing!(axis.plotattributes, :foreground_color_text) - color_or_nothing!(axis.plotattributes, :foreground_color_grid) - color_or_nothing!(axis.plotattributes, :foreground_color_minor_grid) - nothing -end - -function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol) - # handle linking here. if we're passed a list of - # other subplots to link to, link them together - (link = axis[:link]) |> isempty && return - for other_sp in link - link_axes!(axis, get_axis(get_subplot(plt, other_sp), letter)) - end - axis.plotattributes[:link] = [] - nothing -end - -# update a subplots args and axes -function _update_subplot_args( - plt::Plot, - sp::Subplot, - plotattributes_in, - subplot_index::Int, - remove_pair::Bool, -) - anns = RecipesPipeline.pop_kw!(sp.attr, :annotations) - - # grab those args which apply to this subplot - for k in keys(_subplot_defaults) - slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair) - end - - _update_subplot_colors(sp) - _update_margins(sp) - colorbar_update_keys = - (:clims, :colorbar, :seriestype, :marker_z, :line_z, :fill_z, :colorbar_entry) - if any(haskey.(Ref(plotattributes_in), colorbar_update_keys)) - _update_subplot_colorbars(sp) - end - - lims_warned = false - for letter in (:x, :y, :z) - _update_axis(plt, sp, plotattributes_in, letter, subplot_index) - lk = get_attr_symbol(letter, :lims) - - # warn against using `Range` in x,y,z lims - if !lims_warned && - haskey(plotattributes_in, lk) && - plotattributes_in[lk] isa AbstractRange - @warn "lims should be a Tuple, not $(typeof(plotattributes_in[lk]))." - lims_warned = true - end - end - - _update_subplot_periphery(sp, anns) -end - -# ----------------------------------------------------------------------------- - -has_black_border_for_default(st) = error( - "The seriestype attribute only accepts Symbols, you passed the $(typeof(st)) $st.", -) -has_black_border_for_default(st::Function) = - error("The seriestype attribute only accepts Symbols, you passed the function $st.") -has_black_border_for_default(st::Symbol) = - like_histogram(st) || st in (:hexbin, :bar, :shape) - -# converts a symbol or string into a Colorant or ColorGradient -# and assigns a color automatically -get_series_color(c, sp::Subplot, n::Int, seriestype) = - if c === :auto - like_surface(seriestype) ? cgrad() : _cycle(sp[:color_palette], n) - elseif isa(c, Int) - _cycle(sp[:color_palette], c) - else - c - end |> plot_color - -get_series_color(c::AbstractArray, sp::Subplot, n::Int, seriestype) = - map(x -> get_series_color(x, sp, n, seriestype), c) - -ensure_gradient!(plotattributes::AKW, csym::Symbol, asym::Symbol) = - if plotattributes[csym] isa ColorPalette - α = nothing - plotattributes[asym] isa AbstractVector || (α = plotattributes[asym]) - plotattributes[csym] = cgrad(plotattributes[csym], categorical = true, alpha = α) - elseif !(plotattributes[csym] isa ColorGradient) - plotattributes[csym] = - typeof(plotattributes[asym]) <: AbstractVector ? cgrad() : - cgrad(alpha = plotattributes[asym]) - end - -const DEFAULT_LINEWIDTH = Ref(1) - -# get a good default linewidth... 0 for surface and heatmaps -_replace_linewidth(plotattributes::AKW) = - if plotattributes[:linewidth] === :auto - plotattributes[:linewidth] = - (get(plotattributes, :seriestype, :path) ∉ (:surface, :heatmap, :image)) * - DEFAULT_LINEWIDTH[] - end - -function _slice_series_args!(plotattributes::AKW, plt::Plot, sp::Subplot, commandIndex::Int) - for k in keys(_series_defaults) - haskey(plotattributes, k) && - slice_arg!(plotattributes, plotattributes, k, commandIndex, false) - end - plotattributes -end - -label_to_string(label::Bool, series_plotindex) = - label ? label_to_string(:auto, series_plotindex) : "" -label_to_string(label::Nothing, series_plotindex) = "" -label_to_string(label::Missing, series_plotindex) = "" -label_to_string(label::Symbol, series_plotindex) = - if label === :auto - string("y", series_plotindex) - elseif label === :none - "" - else - throw(ArgumentError("unsupported symbol $(label) passed to `label`")) - end -label_to_string(label, series_plotindex) = string(label) # Fallback to string promotion - -function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) - pkg = plt.backend - globalIndex = plotattributes[:series_plotindex] - plotIndex = _series_index(plotattributes, sp) - - aliasesAndAutopick( - plotattributes, - :linestyle, - _styleAliases, - supported_styles(pkg), - plotIndex, - ) - aliasesAndAutopick( - plotattributes, - :markershape, - _markerAliases, - supported_markers(pkg), - plotIndex, - ) - - # update alphas - for asym in (:linealpha, :markeralpha, :fillalpha) - if plotattributes[asym] === nothing - plotattributes[asym] = plotattributes[:seriesalpha] - end - end - if plotattributes[:markerstrokealpha] === nothing - plotattributes[:markerstrokealpha] = plotattributes[:markeralpha] - end - - # update series color - scolor = plotattributes[:seriescolor] - stype = plotattributes[:seriestype] - plotattributes[:seriescolor] = scolor = get_series_color(scolor, sp, plotIndex, stype) - - # update other colors (`linecolor`, `markercolor`, `fillcolor`) <- for grep - for s in (:line, :marker, :fill) - csym, asym = Symbol(s, :color), Symbol(s, :alpha) - plotattributes[csym] = if plotattributes[csym] === :auto - plot_color(if has_black_border_for_default(stype) && s === :line - sp[:foreground_color_subplot] - else - scolor - end) - elseif plotattributes[csym] === :match - plot_color(scolor) - else - get_series_color(plotattributes[csym], sp, plotIndex, stype) - end - end - - # update markerstrokecolor - plotattributes[:markerstrokecolor] = if plotattributes[:markerstrokecolor] === :match - plot_color(sp[:foreground_color_subplot]) - elseif plotattributes[:markerstrokecolor] === :auto - get_series_color(plotattributes[:markercolor], sp, plotIndex, stype) - else - get_series_color(plotattributes[:markerstrokecolor], sp, plotIndex, stype) - end - - # if marker_z, fill_z or line_z are set, ensure we have a gradient - if plotattributes[:marker_z] !== nothing - ensure_gradient!(plotattributes, :markercolor, :markeralpha) - end - if plotattributes[:line_z] !== nothing - ensure_gradient!(plotattributes, :linecolor, :linealpha) - end - if plotattributes[:fill_z] !== nothing - ensure_gradient!(plotattributes, :fillcolor, :fillalpha) - end - - # scatter plots don't have a line, but must have a shape - if plotattributes[:seriestype] in (:scatter, :scatterbins, :scatterhist, :scatter3d) - plotattributes[:linewidth] = 0 - if plotattributes[:markershape] === :none - plotattributes[:markershape] = :circle - end - end - - # set label - plotattributes[:label] = label_to_string.(plotattributes[:label], globalIndex) - - _replace_linewidth(plotattributes) - plotattributes -end - -_series_index(plotattributes, sp) = - if haskey(plotattributes, :series_index) - plotattributes[:series_index]::Int - elseif get(plotattributes, :primary, true) - plotattributes[:series_index] = sp.primary_series_count += 1 - else - plotattributes[:series_index] = sp.primary_series_count - end - -#-------------------------------------------------- -## inspired by Base.@kwdef -""" - add_attributes(level, expr, match_table) - -Takes a `struct` definition and recurses into its fields to create keywords by chaining the field names with the structs' name with underscore. -Also creates pluralized and non-underscore aliases for these keywords. -- `level` indicates which group of `plot`, `subplot`, `series`, etc. the keywords belong to. -- `expr` is the struct definition with default values like `Base.@kwdef` -- `match_table` is an expression of the form `:match = (symbols)`, with symbols whose default value should be `:match` -""" -macro add_attributes(level, expr, match_table) - expr = macroexpand(__module__, expr) # to expand @static - expr isa Expr && expr.head === :struct || error("Invalid usage of @add_attributes") - if (T = expr.args[2]) isa Expr && T.head === :<: - T = T.args[1] - end - - key_dict = KW() - _splitdef!(expr.args[3], key_dict) - - insert_block = Expr(:block) - for (key, value) in key_dict - # e.g. _series_defualts[key] = value - exp_key = Symbol(lowercase(string(T)), "_", key) - pl_key = makeplural(exp_key) - if QuoteNode(exp_key) in match_table.args[2].args - value = QuoteNode(:match) - end - field = QuoteNode(Symbol("_", level, "_defaults")) - push!( - insert_block.args, - Expr( - :(=), - Expr(:ref, Expr(:call, getfield, Plots, field), QuoteNode(exp_key)), - value, - ), - :(Plots.add_aliases($(QuoteNode(exp_key)), $(QuoteNode(pl_key)))), - :(Plots.add_aliases( - $(QuoteNode(exp_key)), - $(QuoteNode(Plots.make_non_underscore(exp_key))), - )), - :(Plots.add_aliases( - $(QuoteNode(exp_key)), - $(QuoteNode(Plots.make_non_underscore(pl_key))), - )), - ) - end - quote - $expr - $insert_block - end |> esc -end - -function _splitdef!(blk, key_dict) - for i in eachindex(blk.args) - if (ei = blk.args[i]) isa Symbol - # var - continue - elseif ei isa Expr - if ei.head === :(=) - lhs = ei.args[1] - if lhs isa Symbol - # var = defexpr - var = lhs - elseif lhs isa Expr && lhs.head === :(::) && lhs.args[1] isa Symbol - # var::T = defexpr - var = lhs.args[1] - type = lhs.args[2] - if @isdefined type - for field in fieldnames(getproperty(Plots, type)) - key_dict[Symbol(var, "_", field)] = - :(getfield($(ei.args[2]), $(QuoteNode(field)))) - end - end - else - # something else, e.g. inline inner constructor - # F(...) = ... - continue - end - defexpr = ei.args[2] # defexpr - key_dict[var] = defexpr - blk.args[i] = lhs - elseif ei.head === :(::) && ei.args[1] isa Symbol - # var::Typ - var = ei.args[1] - key_dict[var] = defexpr - elseif ei.head === :block - # can arise with use of @static inside type decl - _kwdef!(ei, value_args, key_args) - end - end - end - blk -end diff --git a/src/axes.jl b/src/axes.jl deleted file mode 100644 index f9b733cd3..000000000 --- a/src/axes.jl +++ /dev/null @@ -1,1096 +0,0 @@ - -# xaxis(args...; kw...) = Axis(:x, args...; kw...) -# yaxis(args...; kw...) = Axis(:y, args...; kw...) -# zaxis(args...; kw...) = Axis(:z, args...; kw...) - -# ------------------------------------------------------------------------- - -function Axis(sp::Subplot, letter::Symbol, args...; kw...) - explicit = KW( - :letter => letter, - :extrema => Extrema(), - :discrete_map => Dict(), # map discrete values to discrete indices - :continuous_values => zeros(0), - :discrete_values => [], - :use_minor => false, - :show => true, # show or hide the axis? (useful for linked subplots) - ) - - attr = DefaultsDict(explicit, _axis_defaults_byletter[letter]) - - # update the defaults - attr!(Axis([sp], attr), args...; kw...) -end - -function get_axis(sp::Subplot, letter::Symbol) - axissym = get_attr_symbol(letter, :axis) - if haskey(sp.attr, axissym) - sp.attr[axissym] - else - sp.attr[axissym] = Axis(sp, letter) - end::Axis -end - -function process_axis_arg!(plotattributes::AKW, arg, letter = "") - T = typeof(arg) - arg = get(_scaleAliases, arg, arg) - if typeof(arg) <: Font - plotattributes[get_attr_symbol(letter, :tickfont)] = arg - plotattributes[get_attr_symbol(letter, :guidefont)] = arg - - elseif arg in _allScales - plotattributes[get_attr_symbol(letter, :scale)] = arg - - elseif arg in (:flip, :invert, :inverted) - plotattributes[get_attr_symbol(letter, :flip)] = true - - elseif T <: AbstractString - plotattributes[get_attr_symbol(letter, :guide)] = arg - - # xlims/ylims - elseif (T <: Tuple || T <: AVec) && length(arg) == 2 - sym = typeof(arg[1]) <: Number ? :lims : :ticks - plotattributes[get_attr_symbol(letter, sym)] = arg - - # xticks/yticks - elseif T <: AVec - plotattributes[get_attr_symbol(letter, :ticks)] = arg - - elseif arg === nothing - plotattributes[get_attr_symbol(letter, :ticks)] = [] - - elseif T <: Bool || arg in _allShowaxisArgs - plotattributes[get_attr_symbol(letter, :showaxis)] = showaxis(arg, letter) - - elseif typeof(arg) <: Number - plotattributes[get_attr_symbol(letter, :rotation)] = arg - - elseif typeof(arg) <: Function - plotattributes[get_attr_symbol(letter, :formatter)] = arg - - elseif !handleColors!( - plotattributes, - arg, - get_attr_symbol(letter, :foreground_color_axis), - ) - @warn "Skipped $(letter)axis arg $arg" - end -end - -# update an Axis object with magic args and keywords -function attr!(axis::Axis, args...; kw...) - # first process args - plotattributes = axis.plotattributes - foreach(arg -> process_axis_arg!(plotattributes, arg), args) - - # then preprocess keyword arguments - Plots.preprocess_attributes!(KW(kw)) - - # then override for any keywords... only those keywords that already exists in plotattributes - for (k, v) in kw - haskey(plotattributes, k) || continue - if k === :discrete_values - foreach(x -> discrete_value!(axis, x), v) # add these discrete values to the axis - elseif k === :lims && isa(v, NTuple{2,TimeType}) - plotattributes[k] = (v[1].instant.periods.value, v[2].instant.periods.value) - else - plotattributes[k] = v - end - end - - # replace scale aliases - if haskey(_scaleAliases, plotattributes[:scale]) - plotattributes[:scale] = _scaleAliases[plotattributes[:scale]] - end - - axis -end - -# ------------------------------------------------------------------------- - -Base.show(io::IO, axis::Axis) = dumpdict(io, axis.plotattributes, "Axis") -ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) - -const _label_func = - Dict{Symbol,Function}(:log10 => x -> "10^$x", :log2 => x -> "2^$x", :ln => x -> "e^$x") -labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string) - -const _label_func_tex = Dict{Symbol,Function}( - :log10 => x -> "10^{$x}", - :log2 => x -> "2^{$x}", - :ln => x -> "e^{$x}", -) -labelfunc_tex(scale::Symbol) = get(_label_func_tex, scale, convert_sci_unicode) - -function optimal_ticks_and_labels(ticks, alims, scale, formatter) - amin, amax = alims - - # scale the limits - sf, invsf, noop = scale_inverse_scale_func(scale) - - # If the axis input was a Date or DateTime use a special logic to find - # "round" Date(Time)s as ticks - # This bypasses the rest of optimal_ticks_and_labels, because - # optimize_datetime_ticks returns ticks AND labels: the label format (Date - # or DateTime) is chosen based on the time span between amin and amax - # rather than on the input format - # TODO: maybe: non-trivial scale (:ln, :log2, :log10) for date/datetime - - if ticks === nothing && noop - if formatter == RecipesPipeline.dateformatter - # optimize_datetime_ticks returns ticks and labels(!) based on - # integers/floats corresponding to the DateTime type. Thus, the axes - # limits, which resulted from converting the Date type to integers, - # are converted to 'DateTime integers' (actually floats) before - # being passed to optimize_datetime_ticks. - # (convert(Int, convert(DateTime, convert(Date, i))) == 87600000*i) - ticks, labels = - optimize_datetime_ticks(864e5 * amin, 864e5 * amax; k_min = 2, k_max = 4) - # Now the ticks are converted back to floats corresponding to Dates. - return ticks / 864e5, labels - elseif formatter == RecipesPipeline.datetimeformatter - return optimize_datetime_ticks(amin, amax; k_min = 2, k_max = 4) - end - end - - # get a list of well-laid-out ticks - scaled_ticks = if ticks === nothing - optimize_ticks( - sf(amin), - sf(amax); - k_min = scale ∈ _logScales ? 2 : 4, # minimum number of ticks - k_max = 8, # maximum number of ticks - scale, - ) |> first - elseif typeof(ticks) <: Int - optimize_ticks( - sf(amin), - sf(amax); - k_min = ticks, # minimum number of ticks - k_max = ticks, # maximum number of ticks - k_ideal = ticks, - # `strict_span = false` rewards cases where the span of the - # chosen ticks is not too much bigger than amin - amax: - strict_span = false, - scale, - ) |> first - else - map(sf, filter(t -> amin ≤ t ≤ amax, ticks)) - end - unscaled_ticks = noop ? scaled_ticks : map(invsf, scaled_ticks) - - labels::Vector{String} = if any(isfinite, unscaled_ticks) - get_labels(formatter, scaled_ticks, scale) - else - String[] # no finite ticks to show... - end - - unscaled_ticks, labels -end - -function get_labels(formatter::Symbol, scaled_ticks, scale) - if formatter in (:auto, :plain, :scientific, :engineering) - return map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, formatter)) - elseif formatter === :latex - return map( - l -> string("\$", replace(convert_sci_unicode(l), '×' => "\\times"), "\$"), - get_labels(:auto, scaled_ticks, scale), - ) - elseif formatter === :none - return String[] - end -end -function get_labels(formatter::Function, scaled_ticks, scale) - sf, invsf, _ = scale_inverse_scale_func(scale) - fticks = map(formatter ∘ invsf, scaled_ticks) - # extrema can extend outside the region where Categorical tick values are defined - # CategoricalArrays's recipe gives "missing" label to those - filter!(!ismissing, fticks) - eltype(fticks) <: Number && return get_labels(:auto, map(sf, fticks), scale) - return fticks -end - -# returns (continuous_values, discrete_values) for the ticks on this axis -function get_ticks(sp::Subplot, axis::Axis; update = true, formatter = axis[:formatter]) - if update || !haskey(axis.plotattributes, :optimized_ticks) - dvals = axis[:discrete_values] - ticks = _transform_ticks(axis[:ticks], axis) - axis.plotattributes[:optimized_ticks] = - if ( - axis[:letter] === :x && - ticks isa Symbol && - ticks !== :none && - !isempty(dvals) && - ispolar(sp) - ) - collect(0:(π / 4):(7π / 4)), string.(0:45:315) - else - cvals = axis[:continuous_values] - alims = axis_limits(sp, axis[:letter]) - get_ticks(ticks, cvals, dvals, alims, axis[:scale], formatter) - end - end - axis.plotattributes[:optimized_ticks] -end - -# Ticks getter functions -for l in (:x, :y, :z) - axis = string(l, "-axis") # "x-axis" - ticks = string(l, "ticks") # "xticks" - f = Symbol(ticks) # :xticks - @eval begin - """ - $($f)(p::Plot) - - returns a vector of the $($axis) ticks of the subplots of `p`. - - Example use: - - ```jldoctest - julia> p = plot(1:5, $($ticks)=[1,2]) - - julia> $($f)(p) - 1-element Vector{Tuple{Vector{Float64}, Vector{String}}}: - ([1.0, 2.0], ["1", "2"]) - ``` - - If `p` consists of a single subplot, you might want to grab - only the first element, via - - ```jldoctest - julia> $($f)(p)[1] - ([1.0, 2.0], ["1", "2"]) - ``` - - or you can call $($f) on the first (only) subplot of `p` via - - ```jldoctest - julia> $($f)(p[1]) - ([1.0, 2.0], ["1", "2"]) - ``` - """ - $f(p::Plot) = get_ticks(p, $(Meta.quot(l))) - """ - $($f)(sp::Subplot) - - returns the $($axis) ticks of the subplot `sp`. - - Note that the ticks are returned as tuples of values and labels: - - ```jldoctest - julia> sp = plot(1:5, $($ticks)=[1,2]).subplots[1] - Subplot{1} - - julia> $($f)(sp) - ([1.0, 2.0], ["1", "2"]) - ``` - """ - $f(sp::Subplot) = get_ticks(sp, $(Meta.quot(l))) - export $f - end -end -# get_ticks from axis symbol :x, :y, or :z -get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) -get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) - -get_ticks(ticks::Symbol, cvals::T, dvals, args...) where {T} = - if ticks === :none - T[], String[] - elseif !isempty(dvals) - n = length(dvals) - if ticks === :all || n < 16 - cvals, string.(dvals) - else - Δ = ceil(Int, n / 10) - rng = Δ:Δ:n - cvals[rng], string.(dvals[rng]) - end - else - optimal_ticks_and_labels(nothing, args...) - end - -get_ticks(ticks::AVec, cvals, dvals, args...) = optimal_ticks_and_labels(ticks, args...) -get_ticks(ticks::Int, dvals, cvals, args...) = - if isempty(dvals) - optimal_ticks_and_labels(ticks, args...) - else - rng = round.(Int, range(1, stop = length(dvals), length = ticks)) - cvals[rng], string.(dvals[rng]) - end -get_ticks(ticks::NTuple{2,Any}, args...) = ticks -get_ticks(::Nothing, cvals::T, args...) where {T} = T[], String[] -get_ticks(ticks::Bool, args...) = - ticks ? get_ticks(:auto, args...) : get_ticks(nothing, args...) -get_ticks(::T, args...) where {T} = - throw(ArgumentError("Unknown ticks type in get_ticks: $T")) - -# do not specify array item type to also catch e.g. "xlabel=[]" and "xlabel=([],[])" -_has_ticks(v::AVec) = !isempty(v) -_has_ticks(t::Tuple{AVec,AVec}) = !isempty(t[1]) -_has_ticks(s::Symbol) = s !== :none -_has_ticks(b::Bool) = b -_has_ticks(::Nothing) = false -_has_ticks(::Any) = true - -has_ticks(axis::Axis) = get(axis, :ticks, nothing) |> _has_ticks - -_transform_ticks(ticks, axis) = ticks -_transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Dates.TimeType} = - Dates.value.(ticks) -_transform_ticks(ticks::NTuple{2,Any}, axis) = (_transform_ticks(ticks[1], axis), ticks[2]) - -const DEFAULT_MINOR_INTERVALS = Ref(5) # 5 intervals -> 4 ticks - -function num_minor_intervals(axis) - # FIXME: `minorticks` should be fixed in `2.0` to be the number of ticks, not intervals - # see github.com/JuliaPlots/Plots.jl/pull/4528 - n_intervals = axis[:minorticks] - if !(n_intervals isa Bool) && n_intervals isa Integer && n_intervals ≥ 0 - max(1, n_intervals) # 0 intervals makes no sense - else # `:auto` or `true` - if (base = get(_logScaleBases, axis[:scale], nothing)) == 10 - Int(base - 1) - else - DEFAULT_MINOR_INTERVALS[] - end - end::Int -end - -no_minor_intervals(axis) = - if (n_intervals = axis[:minorticks]) === false - true # must be tested with `===` since Bool <: Integer - elseif n_intervals ∈ (:none, nothing) - true - elseif (n_intervals === :auto && !axis[:minorgrid]) - true - else - false - end - -function get_minor_ticks(sp, axis, ticks_and_labels) - no_minor_intervals(axis) && return - ticks = first(ticks_and_labels) - length(ticks) < 2 && return - - amin, amax = axis_limits(sp, axis[:letter]) - scale = axis[:scale] - base = get(_logScaleBases, scale, nothing) - - # add one phantom tick either side of the ticks to ensure minor ticks extend to the axis limits - if (log_scaled = scale ∈ _logScales) - sub = round(Int, log(base, ticks[2] / ticks[1])) - ticks = [ticks[1] / base; ticks; ticks[end] * base] - else - sub = 1 # unused - ratio = length(ticks) > 2 ? (ticks[3] - ticks[2]) / (ticks[2] - ticks[1]) : 1 - first_step = ticks[2] - ticks[1] - last_step = ticks[end] - ticks[end - 1] - ticks = [ticks[1] - first_step / ratio; ticks; ticks[end] + last_step * ratio] - end - - n_minor_intervals = num_minor_intervals(axis) - minorticks = sizehint!(eltype(ticks)[], n_minor_intervals * sub * length(ticks)) - for i in 2:length(ticks) - lo = ticks[i - 1] - hi = ticks[i] - (isfinite(lo) && isfinite(hi) && hi > lo) || continue - if log_scaled - for e in 1:sub - lo_ = lo * base^(e - 1) - hi_ = lo_ * base - step = (hi_ - lo_) / n_minor_intervals - rng = (lo_ + (e > 1 ? 0 : step)):step:(hi_ - (e < sub ? 0 : step / 2)) - append!(minorticks, collect(rng)) - end - else - step = (hi - lo) / n_minor_intervals - append!(minorticks, collect((lo + step):step:(hi - step / 2))) - end - end - minorticks[amin .≤ minorticks .≤ amax] -end - -# ------------------------------------------------------------------------- - -function reset_extrema!(sp::Subplot) - for asym in (:x, :y, :z) - sp[get_attr_symbol(asym, :axis)][:extrema] = Extrema() - end - for series in sp.series_list - expand_extrema!(sp, series.plotattributes) - end -end - -function expand_extrema!(ex::Extrema, v::Number) - ex.emin = isfinite(v) ? min(v, ex.emin) : ex.emin - ex.emax = isfinite(v) ? max(v, ex.emax) : ex.emax - ex -end - -expand_extrema!(axis::Axis, v::Number) = expand_extrema!(axis[:extrema], v) - -# these shouldn't impact the extrema -expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] -expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] - -function expand_extrema!(axis::Axis, v::Tuple{MIN,MAX}) where {MIN<:Number,MAX<:Number} - ex = axis[:extrema]::Extrema - ex.emin = isfinite(v[1]) ? min(v[1], ex.emin) : ex.emin - ex.emax = isfinite(v[2]) ? max(v[2], ex.emax) : ex.emax - ex -end -function expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} - ex = axis[:extrema]::Extrema - foreach(vi -> expand_extrema!(ex, vi), v) - ex -end - -function expand_extrema!(sp::Subplot, plotattributes::AKW) - vert = isvertical(plotattributes) - - # first expand for the data - for letter in (:x, :y, :z) - data = plotattributes[if vert - letter - else - letter === :x ? :y : letter === :y ? :x : :z - end] - if ( - letter !== :z && - plotattributes[:seriestype] === :straightline && - any(series[:seriestype] !== :straightline for series in series_list(sp)) && - length(data) > 1 && - data[1] != data[2] - ) - data = [NaN] - end - axis = sp[get_attr_symbol(letter, :axis)] - - if isa(data, Volume) - expand_extrema!(sp[:xaxis], data.x_extents) - expand_extrema!(sp[:yaxis], data.y_extents) - expand_extrema!(sp[:zaxis], data.z_extents) - elseif eltype(data) <: Number || - (isa(data, Surface) && all(di -> isa(di, Number), data.surf)) - if !(eltype(data) <: Number) - # huh... must have been a mis-typed surface? lets swap it out - data = plotattributes[letter] = Surface(Matrix{Float64}(data.surf)) - end - expand_extrema!(axis, data) - elseif data !== nothing - # TODO: need more here... gotta track the discrete reference value - # as well as any coord offset (think of boxplot shape coords... they all - # correspond to the same x-value) - plotattributes[letter], - plotattributes[get_attr_symbol(letter, :(_discrete_indices))] = - discrete_value!(axis, data) - expand_extrema!(axis, plotattributes[letter]) - end - end - - # # expand for fillrange/bar_width - # fillaxis, baraxis = sp.attr[:yaxis], sp.attr[:xaxis] - # if isvertical(plotattributes) - # fillaxis, baraxis = baraxis, fillaxis - # end - - # expand for fillrange - fr = plotattributes[:fillrange] - if fr === nothing && plotattributes[:seriestype] === :bar - fr = 0.0 - end - if fr !== nothing && !RecipesPipeline.is3d(plotattributes) - axis = sp.attr[vert ? :yaxis : :xaxis] - if typeof(fr) <: Tuple - foreach(x -> expand_extrema!(axis, x), fr) - else - expand_extrema!(axis, fr) - end - end - - # expand for bar_width - if plotattributes[:seriestype] === :bar - dsym = vert ? :x : :y - data = plotattributes[dsym] - - if (bw = plotattributes[:bar_width]) === nothing - pos = filter(>(0), diff(sort(data))) - plotattributes[:bar_width] = bw = _bar_width * ignorenan_minimum(pos) - end - axis = sp.attr[get_attr_symbol(dsym, :axis)] - expand_extrema!(axis, ignorenan_maximum(data) + 0.5maximum(bw)) - expand_extrema!(axis, ignorenan_minimum(data) - 0.5minimum(bw)) - end - - # expand for heatmaps - if plotattributes[:seriestype] === :heatmap - for letter in (:x, :y) - data = plotattributes[letter] - axis = sp[get_attr_symbol(letter, :axis)] - scale = get(plotattributes, get_attr_symbol(letter, :scale), :identity) - expand_extrema!(axis, heatmap_edges(data, scale)) - end - end -end - -function expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) - expand_extrema!(sp[:xaxis], (xmin, xmax)) - expand_extrema!(sp[:yaxis], (ymin, ymax)) -end - -# ------------------------------------------------------------------------- - -function scale_lims(from, to, factor) - mid, span = (from + to) / 2, (to - from) / 2 - mid .+ (-span, span) .* factor -end - -_scale_lims(::Val{true}, ::Function, ::Function, from, to, factor) = - scale_lims(from, to, factor) -_scale_lims(::Val{false}, f::Function, invf::Function, from, to, factor) = - invf.(scale_lims(f(from), f(to), factor)) - -function scale_lims(from, to, factor, scale) - f, invf, noop = scale_inverse_scale_func(scale) - _scale_lims(Val(noop), f, invf, from, to, factor) -end - -""" - scale_lims!([plt], [letter], factor) - -Scale the limits of the axis specified by `letter` (one of `:x`, `:y`, `:z`) by the -given `factor` around the limits' middle point. -If `letter` is omitted, all axes are affected. -""" -function scale_lims!(sp::Subplot, letter, factor) - axis = Plots.get_axis(sp, letter) - from, to = Plots.get_sp_lims(sp, letter) - axis[:lims] = scale_lims(from, to, factor, axis[:scale]) -end -function scale_lims!(plt::Plot, letter, factor) - foreach(sp -> scale_lims!(sp, letter, factor), plt.subplots) - plt -end -scale_lims!(letter::Symbol, factor) = scale_lims!(current(), letter, factor) -function scale_lims!(plt::Union{Plot,Subplot}, factor) - foreach(letter -> scale_lims!(plt, letter, factor), (:x, :y, :z)) - plt -end -scale_lims!(factor::Number) = scale_lims!(current(), factor) - -# figure out if widening is a good idea. -const _widen_seriestypes = ( - :line, - :path, - :steppre, - :stepmid, - :steppost, - :sticks, - :scatter, - :barbins, - :barhist, - :histogram, - :scatterbins, - :scatterhist, - :stepbins, - :stephist, - :bins2d, - :histogram2d, - :bar, - :shape, - :path3d, - :scatter3d, -) - -const default_widen_factor = Ref(1.06) - -# factor to widen axis limits by, or `nothing` if axis widening should be skipped -function widen_factor(axis::Axis; factor = default_widen_factor[]) - if (widen = axis[:widen]) isa Bool - return widen ? factor : nothing - elseif widen isa Number - return widen - else - widen === :auto || @warn "Invalid value specified for `widen`: $widen" - end - - # automatic behavior: widen if limits aren't specified and series type is appropriate - lims = process_limits(axis[:lims], axis) - (lims isa Tuple || lims === :round) && return - for sp in axis.sps, series in series_list(sp) - series.plotattributes[:seriestype] in _widen_seriestypes && return factor - end - nothing -end - -function round_limits(amin, amax, scale) - base = get(_logScaleBases, scale, 10.0) - factor = base^(1 - round(log(base, amax - amin))) - amin = floor(amin * factor) / factor - amax = ceil(amax * factor) / factor - amin, amax -end - -# NOTE: cannot use `NTuple` here ↓ -process_limits(lims::Tuple{<:Union{Symbol,Real},<:Union{Symbol,Real}}, axis) = lims -process_limits(lims::Symbol, axis) = lims -process_limits(lims::AVec, axis) = - length(lims) == 2 && all(map(x -> x isa Union{Symbol,Real}, lims)) ? Tuple(lims) : - nothing -process_limits(lims, axis) = nothing - -warn_invalid_limits(lims, letter) = @warn """ - Invalid limits for $letter axis. Limits should be a symbol, or a two-element tuple or vector of numbers. - $(letter)lims = $lims - """ - -# using the axis extrema and limit overrides, return the min/max value for this axis -function axis_limits( - sp, - letter, - lims_factor = widen_factor(get_axis(sp, letter)), - consider_aspect = true, -) - axis = get_axis(sp, letter) - ex = axis[:extrema] - amin, amax = ex.emin, ex.emax - lims = process_limits(axis[:lims], axis) - lims === nothing && warn_invalid_limits(axis[:lims], letter) - - if (has_user_lims = lims isa Tuple) - lmin, lmax = lims - if lmin isa Number && isfinite(lmin) - amin = lmin - elseif lmin isa Symbol - lmin === :auto || @warn "Invalid min $(letter)limit" lmin - end - if lmax isa Number && isfinite(lmax) - amax = lmax - elseif lmax isa Symbol - lmax === :auto || @warn "Invalid max $(letter)limit" lmax - end - end - if lims === :symmetric - amax = max(abs(amin), abs(amax)) - amin = -amax - end - if amax ≤ amin && isfinite(amin) - amax = amin + 1.0 - end - if !isfinite(amin) && !isfinite(amax) - amin, amax = zero(amin), one(amax) - end - if ispolar(axis.sps[1]) - if axis[:letter] === :x - amin, amax = 0, 2π - elseif lims === :auto - # widen max radius so ticks dont overlap with theta axis - amin, amax = 0, amax + 0.1abs(amax - amin) - end - elseif lims_factor !== nothing - amin, amax = scale_lims(amin, amax, lims_factor, axis[:scale]) - elseif lims === :round - amin, amax = round_limits(amin, amax, axis[:scale]) - end - - aspect_ratio = get_aspect_ratio(sp) - if ( - !has_user_lims && - consider_aspect && - letter in (:x, :y) && - !(aspect_ratio === :none || RecipesPipeline.is3d(:sp)) - ) - aspect_ratio = aspect_ratio isa Number ? aspect_ratio : 1 - area = plotarea(sp) - plot_ratio = height(area) / width(area) - dist = amax - amin - - factor = if letter === :x - ydist, = axis_limits(sp, :y, widen_factor(sp[:yaxis]), false) |> collect |> diff - axis_ratio = aspect_ratio * ydist / dist - axis_ratio / plot_ratio - else - xdist, = axis_limits(sp, :x, widen_factor(sp[:xaxis]), false) |> collect |> diff - axis_ratio = aspect_ratio * dist / xdist - plot_ratio / axis_ratio - end - - if factor > 1 - center = (amin + amax) / 2 - amin = center + factor * (amin - center) - amax = center + factor * (amax - center) - end - end - - amin, amax -end - -# ------------------------------------------------------------------------- - -# these methods track the discrete (categorical) values which correspond to axis continuous values (cv) -# whenever we have discrete values, we automatically set the ticks to match. -# we return (continuous_value, discrete_index) -discrete_value!(plotattributes, letter::Symbol, dv) = - let l = if plotattributes[:permute] !== :none - filter(!=(letter), plotattributes[:permute]) |> only - else - letter - end - discrete_value!(plotattributes[:subplot][get_attr_symbol(l, :axis)], dv) - end - -discrete_value!(axis::Axis, dv) = - if (cv_idx = get(axis[:discrete_map], dv, -1)) == -1 - ex = axis[:extrema] - cv = NaNMath.max(0.5, ex.emax + 1) - expand_extrema!(axis, cv) - push!(axis[:discrete_values], dv) - push!(axis[:continuous_values], cv) - cv_idx = length(axis[:discrete_values]) - axis[:discrete_map][dv] = cv_idx - cv, cv_idx - else - cv = axis[:continuous_values][cv_idx] - cv, cv_idx - end - -# continuous value... just pass back with axis negative index -discrete_value!(axis::Axis, cv::Number) = (cv, -1) - -# add the discrete value for each item. return the continuous values and the indices -function discrete_value!(axis::Axis, v::AVec) - cvec = zeros(axes(v)) - discrete_indices = similar(Array{Int}, axes(v)) - for i in eachindex(v) - cvec[i], discrete_indices[i] = discrete_value!(axis, v[i]) - end - cvec, discrete_indices -end - -# add the discrete value for each item. return the continuous values and the indices -function discrete_value!(axis::Axis, v::AMat) - cmat = zeros(axes(v)) - discrete_indices = similar(Array{Int}, axes(v)) - for I in eachindex(v) - cmat[I], discrete_indices[I] = discrete_value!(axis, v[I]) - end - cmat, discrete_indices -end - -discrete_value!(axis::Axis, v::Surface) = map(Surface, discrete_value!(axis, v.surf)) - -# ------------------------------------------------------------------------- - -const grid_factor_2d = Ref(1.2) -const grid_factor_3d = Ref(grid_factor_2d[] / 100) - -function add_major_or_minor_segments_2d( - sp, - ax, - oax, - oas, - oamM, - ticks, - grid, - tick_segments, - segments, - factor, - cond, -) - ticks === nothing && return - if cond - f, invf = scale_inverse_scale_func(oax[:scale]) - tick_start, tick_stop = if sp[:framestyle] === :origin - oamin, oamax = oamM - t = invf(f(0) + factor * (f(oamax) - f(oamin))) - (-t, t) - else - ticks_in = ax[:tick_direction] === :out ? -1 : 1 - oa1, oa2 = oas - t = invf(f(oa1) + factor * (f(oa2) - f(oa1)) * ticks_in) - (oa1, t) - end - end - isy = ax[:letter] === :y - for tick in ticks - (ax[:showaxis] && cond) && push!( - tick_segments, - reverse_if((tick, tick_start), isy), - reverse_if((tick, tick_stop), isy), - ) - grid && push!( - segments, - reverse_if((tick, first(oamM)), isy), - reverse_if((tick, last(oamM)), isy), - ) - end -end - -# compute the line segments which should be drawn for this axis -function axis_drawing_info(sp, letter) - # get axis objects, ticks and minor ticks - letters = axes_letters(sp, letter) - ax, oax = map(l -> sp[get_attr_symbol(l, :axis)], letters) - (amin, amax), oamM = map(l -> axis_limits(sp, l), letters) - - ticks = get_ticks(sp, ax, update = false) - minor_ticks = get_minor_ticks(sp, ax, ticks) - - # initialize the segments - segments, tick_segments, grid_segments, minorgrid_segments, border_segments = - map(_ -> Segments(2), 1:5) - - if sp[:framestyle] !== :none - isy = letter === :y - oa1, oa2 = oas = if sp[:framestyle] in (:origin, :zerolines) - 0, 0 - else - xor(ax[:mirror], oax[:flip]) ? reverse(oamM) : oamM - end - if ax[:showaxis] - if sp[:framestyle] !== :grid - push!(segments, reverse_if((amin, oa1), isy), reverse_if((amax, oa1), isy)) - # don't show the 0 tick label for the origin framestyle - if ( - sp[:framestyle] === :origin && - ticks ∉ (:none, nothing, false) && - length(ticks) > 1 - ) - if (i = findfirst(==(0), ticks[1])) !== nothing - deleteat!(ticks[1], i) - deleteat!(ticks[2], i) - end - end - end - # top spine - sp[:framestyle] in (:semi, :box) && push!( - border_segments, - reverse_if((amin, oa2), isy), - reverse_if((amax, oa2), isy), - ) - end - if ax[:ticks] ∉ (:none, nothing, false) - ax_length = letter === :x ? height(sp.plotarea).value : width(sp.plotarea).value - - # add major grid segments - add_major_or_minor_segments_2d( - sp, - ax, - oax, - oas, - oamM, - first(ticks), - ax[:grid], - tick_segments, - grid_segments, - grid_factor_2d[] / ax_length, - ax[:tick_direction] !== :none, - ) - if sp[:framestyle] === :box - add_major_or_minor_segments_2d( - sp, - ax, - oax, - reverse(oas), - oamM, - first(ticks), - ax[:grid], - tick_segments, - grid_segments, - grid_factor_2d[] / ax_length, - ax[:tick_direction] !== :none, - ) - end - - # add minor grid segments - if ax[:minorticks] ∉ (:none, nothing, false) || ax[:minorgrid] - add_major_or_minor_segments_2d( - sp, - ax, - oax, - oas, - oamM, - minor_ticks, - ax[:minorgrid], - tick_segments, - minorgrid_segments, - grid_factor_2d[] / 2ax_length, - true, - ) - if sp[:framestyle] === :box - add_major_or_minor_segments_2d( - sp, - ax, - oax, - reverse(oas), - oamM, - minor_ticks, - ax[:minorgrid], - tick_segments, - minorgrid_segments, - grid_factor_2d[] / 2ax_length, - true, - ) - end - end - end - end - - ( - ticks = ticks, - segments = segments, - tick_segments = tick_segments, - grid_segments = grid_segments, - minorgrid_segments = minorgrid_segments, - border_segments = border_segments, - ) -end - -function add_major_or_minor_segments_3d( - sp, - ax, - nax, - nas, - fas, - namM, - ticks, - grid, - tick_segments, - segments, - factor, - cond, -) - ticks === nothing && return - if cond - f, invf = scale_inverse_scale_func(nax[:scale]) - tick_start, tick_stop = if sp[:framestyle] === :origin - namin, namax = namM - t = invf(f(0) + factor * (f(namax) - f(namin))) - (-t, t) - else - na0, na1 = nas - ticks_in = ax[:tick_direction] === :out ? -1 : 1 - t = invf(f(na0) + factor * (f(na1) - f(na0)) * ticks_in) - (na0, t) - end - end - if grid - gas = sp[:framestyle] in (:origin, :zerolines) ? namM : nas - fa0_, fa1_ = reverse_if(fas, ax[:mirror]) - ga0_, ga1_ = reverse_if(gas, ax[:mirror]) - end - letter = ax[:letter] - for tick in ticks - (ax[:showaxis] && cond) && push!( - tick_segments, - sort_3d_axes(tick, tick_start, first(fas), letter), - sort_3d_axes(tick, tick_stop, first(fas), letter), - ) - grid && push!( - segments, - sort_3d_axes(tick, ga0_, fa0_, letter), - sort_3d_axes(tick, ga1_, fa0_, letter), - sort_3d_axes(tick, ga1_, fa0_, letter), - sort_3d_axes(tick, ga1_, fa1_, letter), - ) - end -end - -function axis_drawing_info_3d(sp, letter) - letters = axes_letters(sp, letter) - ax, nax, fax = map(l -> sp[get_attr_symbol(l, :axis)], letters) - (amin, amax), namM, famM = map(l -> axis_limits(sp, l), letters) - - ticks = get_ticks(sp, ax, update = false) - minor_ticks = get_minor_ticks(sp, ax, ticks) - - # initialize the segments - segments, tick_segments, grid_segments, minorgrid_segments, border_segments = - map(_ -> Segments(3), 1:5) - - if sp[:framestyle] !== :none # && letter === :x - na0, na1 = - nas = if sp[:framestyle] in (:origin, :zerolines) - 0, 0 - else - reverse_if(reverse_if(namM, letter === :y), xor(ax[:mirror], nax[:flip])) - end - fa0, fa1 = fas = if sp[:framestyle] in (:origin, :zerolines) - 0, 0 - else - reverse_if(famM, xor(ax[:mirror], fax[:flip])) - end - if ax[:showaxis] - if sp[:framestyle] !== :grid - push!( - segments, - sort_3d_axes(amin, na0, fa0, letter), - sort_3d_axes(amax, na0, fa0, letter), - ) - # don't show the 0 tick label for the origin framestyle - if ( - sp[:framestyle] === :origin && - ticks ∉ (:none, nothing, false) && - length(ticks) > 1 - ) - if (i = findfirst(==(0), ticks[1])) !== nothing - deleteat!(ticks[1], i) - deleteat!(ticks[2], i) - end - end - end - sp[:framestyle] in (:semi, :box) && push!( - border_segments, - sort_3d_axes(amin, na1, fa1, letter), - sort_3d_axes(amax, na1, fa1, letter), - ) - end - - if ax[:ticks] ∉ (:none, nothing, false) - # add major grid segments - add_major_or_minor_segments_3d( - sp, - ax, - nax, - nas, - fas, - namM, - first(ticks), - ax[:grid], - tick_segments, - grid_segments, - grid_factor_3d[], - ax[:tick_direction] !== :none, - ) - - # add minor grid segments - if ax[:minorticks] ∉ (:none, nothing, false) || ax[:minorgrid] - add_major_or_minor_segments_3d( - sp, - ax, - nax, - nas, - fas, - namM, - minor_ticks, - ax[:minorgrid], - tick_segments, - minorgrid_segments, - grid_factor_3d[] / 2, - true, - ) - end - end - end - - ( - ticks = ticks, - segments = segments, - tick_segments = tick_segments, - grid_segments = grid_segments, - minorgrid_segments = minorgrid_segments, - border_segments = border_segments, - ) -end - -reverse_if(x, cond) = cond ? reverse(x) : x diff --git a/src/axes_utils.jl b/src/axes_utils.jl new file mode 100644 index 000000000..46e0a461a --- /dev/null +++ b/src/axes_utils.jl @@ -0,0 +1,553 @@ +const _label_func = + Dict{Symbol,Function}(:log10 => x -> "10^$x", :log2 => x -> "2^$x", :ln => x -> "e^$x") +labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string) + +const _label_func_tex = Dict{Symbol,Function}( + :log10 => x -> "10^{$x}", + :log2 => x -> "2^{$x}", + :ln => x -> "e^{$x}", +) +labelfunc_tex(scale::Symbol) = get(_label_func_tex, scale, convert_sci_unicode) + +function optimal_ticks_and_labels(ticks, alims, scale, formatter) + amin, amax = alims + + # scale the limits + sf, invsf, noop = scale_inverse_scale_func(scale) + + # If the axis input was a Date or DateTime use a special logic to find + # "round" Date(Time)s as ticks + # This bypasses the rest of optimal_ticks_and_labels, because + # optimize_datetime_ticks returns ticks AND labels: the label format (Date + # or DateTime) is chosen based on the time span between amin and amax + # rather than on the input format + # TODO: maybe: non-trivial scale (:ln, :log2, :log10) for date/datetime + + if ticks === nothing && noop + if formatter == RecipesPipeline.dateformatter + # optimize_datetime_ticks returns ticks and labels(!) based on + # integers/floats corresponding to the DateTime type. Thus, the axes + # limits, which resulted from converting the Date type to integers, + # are converted to 'DateTime integers' (actually floats) before + # being passed to optimize_datetime_ticks. + # (convert(Int, convert(DateTime, convert(Date, i))) == 87600000*i) + ticks, labels = + optimize_datetime_ticks(864e5 * amin, 864e5 * amax; k_min = 2, k_max = 4) + # Now the ticks are converted back to floats corresponding to Dates. + return ticks / 864e5, labels + elseif formatter == RecipesPipeline.datetimeformatter + return optimize_datetime_ticks(amin, amax; k_min = 2, k_max = 4) + end + end + + # get a list of well-laid-out ticks + scaled_ticks = if ticks === nothing + optimize_ticks( + sf(amin), + sf(amax); + k_min = scale ∈ _log_scales ? 2 : 4, # minimum number of ticks + k_max = 8, # maximum number of ticks + scale, + ) |> first + elseif typeof(ticks) <: Int + optimize_ticks( + sf(amin), + sf(amax); + k_min = ticks, # minimum number of ticks + k_max = ticks, # maximum number of ticks + k_ideal = ticks, + # `strict_span = false` rewards cases where the span of the + # chosen ticks is not too much bigger than amin - amax: + strict_span = false, + scale, + ) |> first + else + map(sf, filter(t -> amin ≤ t ≤ amax, ticks)) + end + unscaled_ticks = noop ? scaled_ticks : map(invsf, scaled_ticks) + + labels::Vector{String} = if any(isfinite, unscaled_ticks) + get_labels(formatter, scaled_ticks, scale) + else + String[] # no finite ticks to show... + end + + unscaled_ticks, labels +end + +Ticks.get_ticks(ticks::Symbol, cvals::T, dvals, args...) where {T} = + if ticks === :none + T[], String[] + elseif !isempty(dvals) + n = length(dvals) + if ticks === :all || n < 16 + cvals, string.(dvals) + else + Δ = ceil(Int, n / 10) + rng = Δ:Δ:n + cvals[rng], string.(dvals[rng]) + end + else + optimal_ticks_and_labels(nothing, args...) + end + +Ticks.get_ticks(ticks::AVec, cvals, dvals, args...) = + optimal_ticks_and_labels(ticks, args...) +Ticks.get_ticks(ticks::Int, dvals, cvals, args...) = + if isempty(dvals) + optimal_ticks_and_labels(ticks, args...) + else + rng = round.(Int, range(1, stop = length(dvals), length = ticks)) + cvals[rng], string.(dvals[rng]) + end + +function get_labels(formatter::Symbol, scaled_ticks, scale) + if formatter in (:auto, :plain, :scientific, :engineering) + return map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, formatter)) + elseif formatter === :latex + return map( + l -> string("\$", replace(convert_sci_unicode(l), '×' => "\\times"), "\$"), + get_labels(:auto, scaled_ticks, scale), + ) + elseif formatter === :none + return String[] + end +end +function get_labels(formatter::Function, scaled_ticks, scale) + sf, invsf, _ = scale_inverse_scale_func(scale) + fticks = map(formatter ∘ invsf, scaled_ticks) + # extrema can extend outside the region where Categorical tick values are defined + # CategoricalArrays's recipe gives "missing" label to those + filter!(!ismissing, fticks) + eltype(fticks) <: Number && return get_labels(:auto, map(sf, fticks), scale) + return fticks +end + +# Ticks getter functions +for l in (:x, :y, :z) + axis = string(l, "-axis") # "x-axis" + ticks = string(l, "ticks") # "xticks" + f = Symbol(ticks) # :xticks + @eval begin + """ + $($f)(p::Plot) + + returns a vector of the $($axis) ticks of the subplots of `p`. + + Example use: + + ```jldoctest + julia> p = plot(1:5, $($ticks)=[1,2]) + + julia> $($f)(p) + 1-element Vector{Tuple{Vector{Float64}, Vector{String}}}: + ([1.0, 2.0], ["1", "2"]) + ``` + + If `p` consists of a single subplot, you might want to grab + only the first element, via + + ```jldoctest + julia> $($f)(p)[1] + ([1.0, 2.0], ["1", "2"]) + ``` + + or you can call $($f) on the first (only) subplot of `p` via + + ```jldoctest + julia> $($f)(p[1]) + ([1.0, 2.0], ["1", "2"]) + ``` + """ + $f(p::Plot) = get_ticks(p, $(Meta.quot(l))) + """ + $($f)(sp::Subplot) + + returns the $($axis) ticks of the subplot `sp`. + + Note that the ticks are returned as tuples of values and labels: + + ```jldoctest + julia> sp = plot(1:5, $($ticks)=[1,2]).subplots[1] + Subplot{1} + + julia> $($f)(sp) + ([1.0, 2.0], ["1", "2"]) + ``` + """ + $f(sp::Subplot) = get_ticks(sp, $(Meta.quot(l))) + export $f + end +end + +# ------------------------------------------------------------------------- + +# using the axis extrema and limit overrides, return the min/max value for this axis + +# ------------------------------------------------------------------------- + +# these methods track the discrete (categorical) values which correspond to axis continuous values (cv) +# whenever we have discrete values, we automatically set the ticks to match. +# we return (continuous_value, discrete_index) +discrete_value!(plotattributes, letter::Symbol, dv) = + let l = if plotattributes[:permute] !== :none + filter(!=(letter), plotattributes[:permute]) |> only + else + letter + end + discrete_value!(plotattributes[:subplot][get_attr_symbol(l, :axis)], dv) + end + +discrete_value!(axis::Axis, dv) = + if (cv_idx = get(axis[:discrete_map], dv, -1)) == -1 + ex = axis[:extrema] + cv = NaNMath.max(0.5, ex.emax + 1) + expand_extrema!(axis, cv) + push!(axis[:discrete_values], dv) + push!(axis[:continuous_values], cv) + cv_idx = length(axis[:discrete_values]) + axis[:discrete_map][dv] = cv_idx + cv, cv_idx + else + cv = axis[:continuous_values][cv_idx] + cv, cv_idx + end + +# continuous value... just pass back with axis negative index +discrete_value!(axis::Axis, cv::Number) = (cv, -1) + +# add the discrete value for each item. return the continuous values and the indices +function discrete_value!(axis::Axis, v::AVec) + cvec = zeros(axes(v)) + discrete_indices = similar(Array{Int}, axes(v)) + for i in eachindex(v) + cvec[i], discrete_indices[i] = discrete_value!(axis, v[i]) + end + cvec, discrete_indices +end + +# add the discrete value for each item. return the continuous values and the indices +function discrete_value!(axis::Axis, v::AMat) + cmat = zeros(axes(v)) + discrete_indices = similar(Array{Int}, axes(v)) + for I in eachindex(v) + cmat[I], discrete_indices[I] = discrete_value!(axis, v[I]) + end + cmat, discrete_indices +end + +discrete_value!(axis::Axis, v::Surface) = map(Surface, discrete_value!(axis, v.surf)) + +# ------------------------------------------------------------------------- + +const grid_factor_2d = Ref(1.2) +const grid_factor_3d = Ref(grid_factor_2d[] / 100) + +function add_major_or_minor_segments_2d( + sp, + ax, + oax, + oas, + oamM, + ticks, + grid, + tick_segments, + segments, + factor, + cond, +) + ticks === nothing && return + if cond + f, invf = scale_inverse_scale_func(oax[:scale]) + tick_start, tick_stop = if sp[:framestyle] === :origin + oamin, oamax = oamM + t = invf(f(0) + factor * (f(oamax) - f(oamin))) + (-t, t) + else + ticks_in = ax[:tick_direction] === :out ? -1 : 1 + oa1, oa2 = oas + t = invf(f(oa1) + factor * (f(oa2) - f(oa1)) * ticks_in) + (oa1, t) + end + end + isy = ax[:letter] === :y + for tick in ticks + (ax[:showaxis] && cond) && push!( + tick_segments, + reverse_if((tick, tick_start), isy), + reverse_if((tick, tick_stop), isy), + ) + grid && push!( + segments, + reverse_if((tick, first(oamM)), isy), + reverse_if((tick, last(oamM)), isy), + ) + end +end + +# compute the line segments which should be drawn for this axis +function axis_drawing_info(sp, letter) + # get axis objects, ticks and minor ticks + letters = axes_letters(sp, letter) + ax, oax = map(l -> sp[get_attr_symbol(l, :axis)], letters) + (amin, amax), oamM = map(l -> axis_limits(sp, l), letters) + + ticks = get_ticks(sp, ax, update = false) + minor_ticks = get_minor_ticks(sp, ax, ticks) + + # initialize the segments + segments, tick_segments, grid_segments, minorgrid_segments, border_segments = + map(_ -> Segments(2), 1:5) + + if sp[:framestyle] !== :none + isy = letter === :y + oa1, oa2 = oas = if sp[:framestyle] in (:origin, :zerolines) + 0, 0 + else + xor(ax[:mirror], oax[:flip]) ? reverse(oamM) : oamM + end + if ax[:showaxis] + if sp[:framestyle] !== :grid + push!(segments, reverse_if((amin, oa1), isy), reverse_if((amax, oa1), isy)) + # don't show the 0 tick label for the origin framestyle + if ( + sp[:framestyle] === :origin && + ticks ∉ (:none, nothing, false) && + length(ticks) > 1 + ) + if (i = findfirst(==(0), ticks[1])) !== nothing + deleteat!(ticks[1], i) + deleteat!(ticks[2], i) + end + end + end + # top spine + sp[:framestyle] in (:semi, :box) && push!( + border_segments, + reverse_if((amin, oa2), isy), + reverse_if((amax, oa2), isy), + ) + end + if ax[:ticks] ∉ (:none, nothing, false) + ax_length = letter === :x ? height(sp.plotarea).value : width(sp.plotarea).value + + # add major grid segments + add_major_or_minor_segments_2d( + sp, + ax, + oax, + oas, + oamM, + first(ticks), + ax[:grid], + tick_segments, + grid_segments, + grid_factor_2d[] / ax_length, + ax[:tick_direction] !== :none, + ) + if sp[:framestyle] === :box + add_major_or_minor_segments_2d( + sp, + ax, + oax, + reverse(oas), + oamM, + first(ticks), + ax[:grid], + tick_segments, + grid_segments, + grid_factor_2d[] / ax_length, + ax[:tick_direction] !== :none, + ) + end + + # add minor grid segments + if ax[:minorticks] ∉ (:none, nothing, false) || ax[:minorgrid] + add_major_or_minor_segments_2d( + sp, + ax, + oax, + oas, + oamM, + minor_ticks, + ax[:minorgrid], + tick_segments, + minorgrid_segments, + grid_factor_2d[] / 2ax_length, + true, + ) + if sp[:framestyle] === :box + add_major_or_minor_segments_2d( + sp, + ax, + oax, + reverse(oas), + oamM, + minor_ticks, + ax[:minorgrid], + tick_segments, + minorgrid_segments, + grid_factor_2d[] / 2ax_length, + true, + ) + end + end + end + end + + ( + ticks = ticks, + segments = segments, + tick_segments = tick_segments, + grid_segments = grid_segments, + minorgrid_segments = minorgrid_segments, + border_segments = border_segments, + ) +end + +function add_major_or_minor_segments_3d( + sp, + ax, + nax, + nas, + fas, + namM, + ticks, + grid, + tick_segments, + segments, + factor, + cond, +) + ticks === nothing && return + if cond + f, invf = scale_inverse_scale_func(nax[:scale]) + tick_start, tick_stop = if sp[:framestyle] === :origin + namin, namax = namM + t = invf(f(0) + factor * (f(namax) - f(namin))) + (-t, t) + else + na0, na1 = nas + ticks_in = ax[:tick_direction] === :out ? -1 : 1 + t = invf(f(na0) + factor * (f(na1) - f(na0)) * ticks_in) + (na0, t) + end + end + if grid + gas = sp[:framestyle] in (:origin, :zerolines) ? namM : nas + fa0_, fa1_ = reverse_if(fas, ax[:mirror]) + ga0_, ga1_ = reverse_if(gas, ax[:mirror]) + end + letter = ax[:letter] + for tick in ticks + (ax[:showaxis] && cond) && push!( + tick_segments, + sort_3d_axes(tick, tick_start, first(fas), letter), + sort_3d_axes(tick, tick_stop, first(fas), letter), + ) + grid && push!( + segments, + sort_3d_axes(tick, ga0_, fa0_, letter), + sort_3d_axes(tick, ga1_, fa0_, letter), + sort_3d_axes(tick, ga1_, fa0_, letter), + sort_3d_axes(tick, ga1_, fa1_, letter), + ) + end +end + +function axis_drawing_info_3d(sp, letter) + letters = axes_letters(sp, letter) + ax, nax, fax = map(l -> sp[get_attr_symbol(l, :axis)], letters) + (amin, amax), namM, famM = map(l -> axis_limits(sp, l), letters) + + ticks = get_ticks(sp, ax, update = false) + minor_ticks = get_minor_ticks(sp, ax, ticks) + + # initialize the segments + segments, tick_segments, grid_segments, minorgrid_segments, border_segments = + map(_ -> Segments(3), 1:5) + + if sp[:framestyle] !== :none # && letter === :x + na0, na1 = + nas = if sp[:framestyle] in (:origin, :zerolines) + 0, 0 + else + reverse_if(reverse_if(namM, letter === :y), xor(ax[:mirror], nax[:flip])) + end + fa0, fa1 = fas = if sp[:framestyle] in (:origin, :zerolines) + 0, 0 + else + reverse_if(famM, xor(ax[:mirror], fax[:flip])) + end + if ax[:showaxis] + if sp[:framestyle] !== :grid + push!( + segments, + sort_3d_axes(amin, na0, fa0, letter), + sort_3d_axes(amax, na0, fa0, letter), + ) + # don't show the 0 tick label for the origin framestyle + if ( + sp[:framestyle] === :origin && + ticks ∉ (:none, nothing, false) && + length(ticks) > 1 + ) + if (i = findfirst(==(0), ticks[1])) !== nothing + deleteat!(ticks[1], i) + deleteat!(ticks[2], i) + end + end + end + sp[:framestyle] in (:semi, :box) && push!( + border_segments, + sort_3d_axes(amin, na1, fa1, letter), + sort_3d_axes(amax, na1, fa1, letter), + ) + end + + if ax[:ticks] ∉ (:none, nothing, false) + # add major grid segments + add_major_or_minor_segments_3d( + sp, + ax, + nax, + nas, + fas, + namM, + first(ticks), + ax[:grid], + tick_segments, + grid_segments, + grid_factor_3d[], + ax[:tick_direction] !== :none, + ) + + # add minor grid segments + if ax[:minorticks] ∉ (:none, nothing, false) || ax[:minorgrid] + add_major_or_minor_segments_3d( + sp, + ax, + nax, + nas, + fas, + namM, + minor_ticks, + ax[:minorgrid], + tick_segments, + minorgrid_segments, + grid_factor_3d[] / 2, + true, + ) + end + end + end + + ( + ticks = ticks, + segments = segments, + tick_segments = tick_segments, + grid_segments = grid_segments, + minorgrid_segments = minorgrid_segments, + border_segments = border_segments, + ) +end diff --git a/src/backends.jl b/src/backends.jl deleted file mode 100644 index 6b803858b..000000000 --- a/src/backends.jl +++ /dev/null @@ -1,1788 +0,0 @@ -struct NoBackend <: AbstractBackend end - -const _plots_project = Pkg.Types.read_package(normpath(@__DIR__, "..", "Project.toml")) -const _current_plots_version = _plots_project.version -const _plots_compats = _plots_project.compat - -const _backendSymbol = Dict{DataType,Symbol}(NoBackend => :none) -const _backendType = Dict{Symbol,DataType}(:none => NoBackend) -const _backend_packages = Dict{Symbol,Symbol}() -const _initialized_backends = Set{Symbol}() -const _backends = Symbol[] - -const _plots_deps = let toml = Pkg.TOML.parsefile(normpath(@__DIR__, "..", "Project.toml")) - merge(toml["deps"], toml["extras"]) -end - -function _check_installed(backend::Union{Module,AbstractString,Symbol}; warn = true) - sym = Symbol(lowercase(string(backend))) - if warn && !haskey(_backend_packages, sym) - @warn "backend `$sym` is not compatible with `Plots`." - return - end - # lowercase -> CamelCase, falling back to the given input for `PlotlyBase` ... - str = string(get(_backend_packages, sym, backend)) - str == "Plotly" && (str *= "Base") # FIXME: `Plots` inconsistency, `plotly` should be named `plotlybase` - # check supported - if warn && !haskey(_plots_compats, str) - @warn "backend `$str` is not compatible with `Plots`." - return - end - # check installed - pkg_id = if str == "GR" - # FIXME: remove in `Plots2.0` (`GR` won't be a hard Plots dependency anymore). - Base.identify_package(Plots, str) # GR can be in the Manifest or in the Project - else - Base.identify_package(str) # a Project dependency - end - version = if pkg_id === nothing - nothing - else - get(Pkg.dependencies(), pkg_id.uuid, (; version = nothing)).version - end - version === nothing && @warn "backend `$str` is not installed." - version -end - -function _check_compat(m::Module; warn = true) - (be_v = _check_installed(m; warn)) === nothing && return - if (be_c = _plots_compats[string(m)]) isa String # julia 1.6 - if be_v ∉ Pkg.Types.semver_spec(be_c) - @warn "`$m` $be_v is not compatible with this version of `Plots`. The declared compatibility is $(be_c)." - end - else - if intersect(be_v, be_c.val) |> isempty - @warn "`$m` $be_v is not compatible with this version of `Plots`. The declared compatibility is $(be_c.str)." - end - end - nothing -end - -_path(sym::Symbol) = - if sym ∈ (:pgfplots, :pyplot) - @path joinpath(@__DIR__, "backends", "deprecated", "$sym.jl") - else - @path joinpath(@__DIR__, "backends", "$sym.jl") - end - -"Returns a list of supported backends" -backends() = _backends - -"Returns the name of the current backend" -backend_name() = CURRENT_BACKEND.sym - -_backend_instance(sym::Symbol)::AbstractBackend = - haskey(_backendType, sym) ? _backendType[sym]() : error("Unsupported backend $sym") - -backend_package_name(sym::Symbol = backend_name()) = _backend_packages[sym] - -macro init_backend(s) - package_str = string(s) - str = lowercase(package_str) - sym = Symbol(str) - T = Symbol(string(s) * "Backend") - quote - struct $T <: AbstractBackend end - export $sym - $sym(; kw...) = (default(; reset = false, kw...); backend($T())) - backend_name(::$T) = Symbol($str) - backend_package_name(::$T) = backend_package_name(Symbol($str)) - push!(_backends, Symbol($str)) - _backendType[Symbol($str)] = $T - _backendSymbol[$T] = Symbol($str) - _backend_packages[Symbol($str)] = Symbol($package_str) - end |> esc -end - -macro require_backend(pkg) - be = QuoteNode(Symbol(lowercase("$pkg"))) - quote - backend_name() === $be || @require $pkg = $(_plots_deps["$pkg"]) begin - include(_path($be)) - end - end |> esc -end - -# --------------------------------------------------------- - -# don't do anything as a default -_create_backend_figure(plt::Plot) = nothing -_initialize_subplot(plt::Plot, sp::Subplot) = nothing - -_series_added(plt::Plot, series::Series) = nothing -_series_updated(plt::Plot, series::Series) = nothing - -_before_layout_calcs(plt::Plot) = nothing - -title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt -guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt - -closeall(::AbstractBackend) = nothing - -"Returns the (width,height) of a text label." -function text_size(lablen::Int, sz::Number, rot::Number = 0) - # we need to compute the size of the ticks generically - # this means computing the bounding box and then getting the width/height - # note: - ptsz = sz * pt - width = 0.8lablen * ptsz - - # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles - height = abs(sind(rot)) * width + abs(cosd(rot)) * ptsz - width = abs(sind(rot + 90)) * width + abs(cosd(rot + 90)) * ptsz - width, height -end -text_size(lab::AbstractString, sz::Number, rot::Number = 0) = - text_size(length(lab), sz, rot) -text_size(lab::PlotText, sz::Number, rot::Number = 0) = text_size(length(lab.str), sz, rot) - -# account for the size/length/rotation of tick labels -function tick_padding(sp::Subplot, axis::Axis) - if (ticks = get_ticks(sp, axis)) === nothing - 0mm - else - vals, labs = ticks - isempty(labs) && return 0mm - # ptsz = axis[:tickfont].pointsize * pt - longest_label = maximum(length(lab) for lab in labs) - - # generalize by "rotating" y labels - rot = axis[:rotation] + (axis[:letter] === :y ? 90 : 0) - - # # we need to compute the size of the ticks generically - # # this means computing the bounding box and then getting the width/height - # labelwidth = 0.8longest_label * ptsz - # - # - # # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles - # hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm - - # get the height of the rotated label - text_size(longest_label, axis[:tickfontsize], rot)[2] - end -end - -# Set the (left, top, right, bottom) minimum padding around the plot area -# to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot) - # TODO: something different when `RecipesPipeline.is3d(sp) == true` - leftpad = tick_padding(sp, sp[:yaxis]) + sp[:left_margin] + guide_padding(sp[:yaxis]) - toppad = sp[:top_margin] + title_padding(sp) - rightpad = sp[:right_margin] - bottompad = tick_padding(sp, sp[:xaxis]) + sp[:bottom_margin] + guide_padding(sp[:xaxis]) - - # switch them? - if sp[:xaxis][:mirror] - bottompad, toppad = toppad, bottompad - end - if sp[:yaxis][:mirror] - leftpad, rightpad = rightpad, leftpad - end - - # @show (leftpad, toppad, rightpad, bottompad) - sp.minpad = (leftpad, toppad, rightpad, bottompad) -end - -_update_plot_object(plt::Plot) = nothing - -# --------------------------------------------------------- - -mutable struct CurrentBackend - sym::Symbol - pkg::AbstractBackend -end -CurrentBackend(sym::Symbol) = CurrentBackend(sym, _backend_instance(sym)) - -# --------------------------------------------------------- -# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: -# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" -# ==> this must always be done during precompilation, otherwise -# the cache will not invalidate when preferences change -const PLOTS_DEFAULT_BACKEND = lowercase(load_preference(Plots, "default_backend", "gr")) - -function load_default_backend() - # environment variable preempts the `Preferences` based mechanism - CURRENT_BACKEND.sym = - get(ENV, "PLOTS_DEFAULT_BACKEND", PLOTS_DEFAULT_BACKEND) |> lowercase |> Symbol - backend(CURRENT_BACKEND.sym) -end - -function set_default_backend!( - backend::Union{Nothing,AbstractString,Symbol} = nothing; - force = true, - kw..., -) - if backend === nothing - delete_preferences!(Plots, "default_backend"; force, kw...) - else - # NOTE: `_check_installed` already throws a warning - if (value = lowercase(string(backend))) |> _check_installed !== nothing - set_preferences!(Plots, "default_backend" => value; force, kw...) - end - end - nothing -end - -function diagnostics(io::IO = stdout) - origin = if has_preference(Plots, "default_backend") - "`Preferences`" - elseif haskey(ENV, "PLOTS_DEFAULT_BACKEND") - "environment variable" - else - "fallback" - end - if (be = backend_name()) === :none - @info "no `Plots` backends currently initialized" - else - be_name = string(backend_package_name(be)) - @info "selected `Plots` backend: $be_name, from $origin" - Pkg.status( - ["Plots", "RecipesBase", "RecipesPipeline", be_name]; - mode = Pkg.PKGMODE_MANIFEST, - io, - ) - end - nothing -end - -# --------------------------------------------------------- - -""" -Returns the current plotting package name. Initializes package on first call. -""" -function backend() - CURRENT_BACKEND.sym === :none && load_default_backend() - CURRENT_BACKEND.pkg -end - -initialized(sym::Symbol) = sym ∈ _initialized_backends - -""" -Set the plot backend. -""" -function backend(pkg::AbstractBackend) - sym = backend_name(pkg) - if !initialized(sym) - _initialize_backend(pkg) - push!(_initialized_backends, sym) - end - CURRENT_BACKEND.sym = sym - CURRENT_BACKEND.pkg = pkg - pkg -end - -backend(sym::Symbol) = - if sym in _backends - backend(_backend_instance(sym)) - else - @warn "`:$sym` is not a supported backend." - backend() - end - -const _deprecated_backends = - [:qwt, :winston, :bokeh, :gadfly, :immerse, :glvisualize, :pgfplots] - -# --------------------------------------------------------- - -# these are args which every backend supports because they're not used in the backend code -const _base_supported_args = [ - :color_palette, - :background_color, - :background_color_subplot, - :foreground_color, - :foreground_color_subplot, - :group, - :seriestype, - :seriescolor, - :seriesalpha, - :smooth, - :xerror, - :yerror, - :zerror, - :subplot, - :x, - :y, - :z, - :show, - :size, - :margin, - :left_margin, - :right_margin, - :top_margin, - :bottom_margin, - :html_output_format, - :layout, - :link, - :primary, - :series_annotations, - :subplot_index, - :discrete_values, - :projection, - :show_empty_bins, - :z_order, - :permute, - :unitformat, -] - -function merge_with_base_supported(v::AVec) - v = vcat(v, _base_supported_args) - for vi in v - if haskey(_axis_defaults, vi) - for letter in (:x, :y, :z) - push!(v, get_attr_symbol(letter, vi)) - end - end - end - Set(v) -end - -@init_backend PyPlot -@init_backend PythonPlot -@init_backend UnicodePlots -@init_backend Plotly -@init_backend PlotlyJS -@init_backend GR -@init_backend PGFPlots -@init_backend PGFPlotsX -@init_backend InspectDR -@init_backend HDF5 -@init_backend Gaston - -# --------------------------------------------------------- - -# create the various `is_xxx_supported` and `supported_xxxs` methods -# by default they pass through to checking membership in `_gr_xxx` -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - @eval begin - $f1(::AbstractBackend, $s) = false - $f1(be::AbstractBackend, $s::AbstractVector) = all(v -> $f1(be, v), $s) - $f1($s) = $f1(backend(), $s) - $f2() = $f2(backend()) - end - - for be in backends() - be_type = typeof(_backend_instance(be)) - v = Symbol("_", be, "_", s) - @eval begin - $f1(::$be_type, $s::Symbol) = $s in $v - $f2(::$be_type) = sort(collect($v)) - end - end -end - -################################################################################ -# custom hooks - -# @require and imports -function _pre_imports(pkg::AbstractBackend) - @eval @require_backend $(backend_package_name(pkg)) - nothing -end - -# global definitions `const` and `include` -function _post_imports(pkg::AbstractBackend) - name = backend_package_name(pkg) - @eval const $name = Main.$name # so that the module is available in `Plots` - nothing -end - -# function calls, pointer initializations, ... -_runtime_init(::AbstractBackend) = nothing - -################################################################################ -# initialize the backends -function _initialize_backend(pkg::AbstractBackend) - _pre_imports(pkg) - name = backend_package_name(pkg) - # NOTE: this is a hack importing in `Main` (expecting the package to be in `Project.toml`, remove in `Plots@2.0`) - # FIXME: remove hard `GR` dependency in `Plots@2.0` - @eval name === :GR ? Plots : Main begin - import $name - export $name - $(_check_compat)($name) - end - _post_imports(pkg) - _runtime_init(pkg) - nothing -end - -# ------------------------------------------------------------------------------ -# gr -_post_imports(::GRBackend) = nothing - -const _gr_attr = merge_with_base_supported([ - :annotations, - :annotationrotation, - :annotationhalign, - :annotationfontsize, - :annotationfontfamily, - :annotationcolor, - :annotationvalign, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :legend_foreground_color, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :fillstyle, - :bins, - :layout, - :title, - :window_title, - :guide, - :widen, - :lims, - :ticks, - :scale, - :flip, - :titlefontfamily, - :titlefontsize, - :titlefonthalign, - :titlefontvalign, - :titlefontrotation, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_halign, - :legend_font_valign, - :legend_font_rotation, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfonthalign, - :tickfontvalign, - :tickfontrotation, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefonthalign, - :guidefontvalign, - :guidefontrotation, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_titlefont, - :colorbar_titlefontsize, - :colorbar_titlefontrotation, - :colorbar_titlefontcolor, - :colorbar_entry, - :colorbar_scale, - :clims, - :fill, - :fill_z, - :fontfamily, - :fontfamily_subplot, - :line_z, - :marker_z, - :legend_column, - :legend_font, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_rotation, - :legend_title_font_pointsize, - :legend_title_font_valigm, - :levels, - :line, - :ribbon, - :quiver, - :orientation, - :overwrite_figure, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontfamily, - :plot_titlefontrotation, - :plot_titlefontsize, - :plot_titlelocation, - :plot_titlevspan, - :polar, - :aspect_ratio, - :normalize, - :weights, - :inset_subplots, - :bar_width, - :arrow, - :framestyle, - :tick_direction, - :camera, - :contour_labels, - :connections, - :axis, - :thickness_scaling, - :minorgrid, - :minorgridalpha, - :minorgridlinewidth, - :minorgridstyle, - :minorticks, - :mirror, - :rotation, - :showaxis, - :tickfonthalign, - :formatter, - :mirror, - :guidefont, -]) -const _gr_seriestype = [ - :path, - :scatter, - :straightline, - :heatmap, - :image, - :contour, - :path3d, - :scatter3d, - :surface, - :wireframe, - :mesh3d, - :volume, - :shape, -] -const _gr_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _gr_marker = vcat(_allMarkers, :pixel) -const _gr_scale = [:identity, :ln, :log2, :log10] -is_marker_supported(::GRBackend, shape::Shape) = true - -# ------------------------------------------------------------------------------ -# plotly -_pre_imports(::PlotlyBackend) = nothing -_post_imports(::PlotlyBackend) = @eval begin - const PlotlyBase = Main.PlotlyBase - const PlotlyKaleido = Main.PlotlyKaleido - # FIXME: in Plots `2.0`, `plotly` backend should be re-named to `plotlybase` - # so that we can trigger include on `@require` instead of this - PLOTS_DEFAULT_BACKEND == "plotly" || include(_path(:plotly)) - include(_path(:plotlybase)) -end -function _initialize_backend(pkg::PlotlyBackend) - try - _pre_imports(pkg) - @eval Main begin - import PlotlyBase - import PlotlyKaleido - $(_check_compat)(PlotlyBase; warn = false) # NOTE: don't warn, since those are not backends, but deps - $(_check_compat)(PlotlyKaleido, warn = false) - end - _post_imports(pkg) - _runtime_init(pkg) - catch err - if err isa ArgumentError - @warn "Failed to load integration with PlotlyBase & PlotlyKaleido." exception = - (err, catch_backtrace()) - else - rethrow(err) - end - # NOTE: `plotly` is special in the way that it does not require dependencies for displaying a plot - # as a result, we cannot rely on the `@require` mechanism for loading glue code - # this is why it must be done here. - PLOTS_DEFAULT_BACKEND == "plotly" || @eval include(_path(:plotly)) - end - @static if isdefined(Base.Experimental, :register_error_hint) - Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs - if exc.f === _show && - length(argtypes) == 3 && - argtypes[2] <: MIME"image/png" && - argtypes[3] <: Plot{PlotlyBackend} - println( - io, - "\n\nTip: For saving/rendering as png with the `Plotly` backend `PlotlyBase` and `PlotlyKaleido` need to be installed.", - ) - end - end - end -end - -const _plotly_attr = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :legend_foreground_color, - :foreground_color_guide, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :foreground_color_title, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :markerstrokestyle, - :fill, - :fillrange, - :fillcolor, - :fillalpha, - :fontfamily, - :fontfamily_subplot, - :bins, - :title, - :titlelocation, - :titlefontfamily, - :titlefontsize, - :titlefonthalign, - :titlefontvalign, - :titlefontcolor, - :legend_column, - :legend_font, - :legend_font_family, - :legend_font_pointsize, - :legend_font_color, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_pointsize, - :tickfontfamily, - :tickfontsize, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefontcolor, - :window_title, - :arrow, - :guide, - :widen, - :lims, - :line, - :ticks, - :scale, - :flip, - :rotation, - :tickfont, - :guidefont, - :legendfont, - :grid, - :gridalpha, - :gridlinewidth, - :legend, - :colorbar, - :colorbar_title, - :colorbar_entry, - :marker_z, - :fill_z, - :line_z, - :levels, - :ribbon, - :quiver, - :orientation, - # :overwrite_figure, - :polar, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontfamily, - :plot_titlefontsize, - :plot_titlelocation, - :plot_titlevspan, - :normalize, - :weights, - # :contours, - :aspect_ratio, - :hover, - :inset_subplots, - :bar_width, - :clims, - :framestyle, - :tick_direction, - :camera, - :contour_labels, - :connections, - :xformatter, - :xshowaxis, - :xguidefont, - :yformatter, - :yshowaxis, - :yguidefont, - :zformatter, - :zguidefont, -]) - -const _plotly_seriestype = [ - :path, - :scatter, - :heatmap, - :contour, - :surface, - :wireframe, - :path3d, - :scatter3d, - :shape, - :scattergl, - :straightline, - :mesh3d, -] -const _plotly_style = [:auto, :solid, :dash, :dot, :dashdot] -const _plotly_marker = [ - :none, - :auto, - :circle, - :rect, - :diamond, - :utriangle, - :dtriangle, - :cross, - :xcross, - :pentagon, - :hexagon, - :octagon, - :vline, - :hline, - :x, -] -const _plotly_scale = [:identity, :log10] - -defaultOutputFormat(plt::Plot{Plots.PlotlyBackend}) = "html" - -# ------------------------------------------------------------------------------ -# pgfplots - -const _pgfplots_attr = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - # :background_color_outside, - # :legend_foreground_color, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :markerstrokestyle, - :fillrange, - :fillcolor, - :fillalpha, - :bins, - # :bar_width, :bar_edges, - :title, - # :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :tickfont, - :guidefont, - :legendfont, - :grid, - :legend, - :colorbar, - :colorbar_title, - :fill_z, - :line_z, - :marker_z, - :levels, - # :ribbon, :quiver, :arrow, - # :orientation, - # :overwrite_figure, - :polar, - # :normalize, :weights, :contours, - :aspect_ratio, - :tick_direction, - :framestyle, - :camera, - :contour_labels, -]) -const _pgfplots_seriestype = [ - :path, - :path3d, - :scatter, - :steppre, - :stepmid, - :steppost, - :histogram2d, - :ysticks, - :xsticks, - :contour, - :shape, - :straightline, -] -const _pgfplots_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _pgfplots_marker = [ - :none, - :auto, - :circle, - :rect, - :diamond, - :utriangle, - :dtriangle, - :cross, - :xcross, - :star5, - :pentagon, - :hline, - :vline, -] #vcat(_allMarkers, Shape) -const _pgfplots_scale = [:identity, :ln, :log2, :log10] - -# ------------------------------------------------------------------------------ -# plotlyjs - -const _plotlyjs_attr = _plotly_attr -const _plotlyjs_seriestype = _plotly_seriestype -const _plotlyjs_style = _plotly_style -const _plotlyjs_marker = _plotly_marker -const _plotlyjs_scale = _plotly_scale - -# ------------------------------------------------------------------------------ -# pyplot - -_post_imports(::PyPlotBackend) = @eval begin - const PyPlot = Main.PyPlot - const PyCall = Main.PyPlot.PyCall -end -_runtime_init(::PyPlotBackend) = @eval begin - pycolors = PyCall.pyimport("matplotlib.colors") - pypath = PyCall.pyimport("matplotlib.path") - mplot3d = PyCall.pyimport("mpl_toolkits.mplot3d") - axes_grid1 = PyCall.pyimport("mpl_toolkits.axes_grid1") - pypatches = PyCall.pyimport("matplotlib.patches") - pyticker = PyCall.pyimport("matplotlib.ticker") - pycmap = PyCall.pyimport("matplotlib.cm") - pynp = PyCall.pyimport("numpy") - - pynp."seterr"(invalid = "ignore") - - PyPlot.ioff() # we don't want every command to update the figure -end - -function _initialize_backend(pkg::PyPlotBackend) - _pre_imports(pkg) - @eval Main begin - import PyPlot - export PyPlot - $(_check_compat)(PyPlot) - end - _post_imports(pkg) - _runtime_init(pkg) -end - -const _pyplot_attr = merge_with_base_supported([ - :annotations, - :annotationrotation, - :annotationhalign, - :annotationfontsize, - :annotationfontfamily, - :annotationcolor, - :annotationvalign, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :foreground_color_grid, - :legend_foreground_color, - :foreground_color_title, - :foreground_color_axis, - :foreground_color_border, - :foreground_color_guide, - :foreground_color_text, - :label, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :fillstyle, - :bins, - :bar_width, - :bar_edges, - :bar_position, - :title, - :titlelocation, - :titlefont, - :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :titlefontfamily, - :titlefontsize, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_entry, - :colorbar_ticks, - :colorbar_tickfontfamily, - :colorbar_tickfontsize, - :colorbar_tickfonthalign, - :colorbar_tickfontvalign, - :colorbar_tickfontrotation, - :colorbar_tickfontcolor, - :colorbar_titlefontcolor, - :colorbar_titlefontsize, - :colorbar_scale, - :marker_z, - :line, - :line_z, - :fill, - :fill_z, - :fontfamily, - :fontfamily_subplot, - :legend_column, - :legend_font, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_pointsize, - :levels, - :ribbon, - :quiver, - :arrow, - :orientation, - :overwrite_figure, - :polar, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontfamily, - :plot_titlefontsize, - :plot_titlelocation, - :plot_titlevspan, - :normalize, - :weights, - :contours, - :aspect_ratio, - :clims, - :inset_subplots, - :dpi, - :stride, - :framestyle, - :tick_direction, - :thickness_scaling, - :camera, - :contour_labels, - :connections, - :thickness_scaling, - :axis, - :minorgrid, - :minorgridalpha, - :minorgridlinewidth, - :minorgridstyle, - :minorticks, - :mirror, - :showaxis, - :tickfontrotation, - :formatter, - :guidefont, -]) -const _pyplot_seriestype = [ - :path, - :steppre, - :stepmid, - :steppost, - :shape, - :straightline, - :scatter, - :hexbin, - :heatmap, - :image, - :contour, - :contour3d, - :path3d, - :scatter3d, - :mesh3d, - :surface, - :wireframe, -] -const _pyplot_style = [:auto, :solid, :dash, :dot, :dashdot] -const _pyplot_marker = vcat(_allMarkers, :pixel) -const _pyplot_scale = [:identity, :ln, :log2, :log10] - -# ------------------------------------------------------------------------------ -# pythonplot - -_post_imports(::PythonPlotBackend) = @eval begin - const PythonPlot = Main.PythonPlot - const PythonCall = Main.PythonPlot.PythonCall - mpl_toolkits = PythonCall.pyimport("mpl_toolkits") - mpl = PythonCall.pyimport("matplotlib") - numpy = PythonCall.pyimport("numpy") - - PythonCall.pyimport("mpl_toolkits.axes_grid1") - numpy.seterr(invalid = "ignore") - - PythonPlot.ioff() # we don't want every command to update the figure -end -_runtime_init(::PythonPlotBackend) = nothing - -function _initialize_backend(pkg::PythonPlotBackend) - _pre_imports(pkg) - @eval Main begin - import PythonPlot - $(_check_compat)(PythonPlot) - end - _post_imports(pkg) - _runtime_init(pkg) -end - -const _pythonplot_seriestype = _pyplot_seriestype -const _pythonplot_marker = _pyplot_marker -const _pythonplot_style = _pyplot_style -const _pythonplot_scale = _pyplot_scale - -const _pythonplot_attr = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :foreground_color_grid, - :legend_foreground_color, - :foreground_color_title, - :foreground_color_axis, - :foreground_color_border, - :foreground_color_guide, - :foreground_color_text, - :label, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :fillstyle, - :bins, - :bar_width, - :bar_edges, - :bar_position, - :title, - :titlelocation, - :titlefont, - :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :titlefontfamily, - :titlefontsize, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_entry, - :colorbar_ticks, - :colorbar_tickfontfamily, - :colorbar_tickfontsize, - :colorbar_tickfonthalign, - :colorbar_tickfontvalign, - :colorbar_tickfontrotation, - :colorbar_tickfontcolor, - :colorbar_titlefontcolor, - :colorbar_titlefontsize, - :colorbar_scale, - :marker_z, - :line, - :line_z, - :fill, - :fill_z, - :fontfamily, - :fontfamily_subplot, - :legend_column, - :legend_font, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_pointsize, - :levels, - :ribbon, - :quiver, - :arrow, - :orientation, - :overwrite_figure, - :polar, - :normalize, - :weights, - :contours, - :aspect_ratio, - :clims, - :inset_subplots, - :dpi, - :stride, - :framestyle, - :tick_direction, - :camera, - :contour_labels, - :connections, -]) - -# ------------------------------------------------------------------------------ -# gaston - -const _gaston_attr = merge_with_base_supported([ - :annotations, - # :background_color_legend, - # :background_color_inside, - # :background_color_outside, - # :foreground_color_legend, - # :foreground_color_grid, :foreground_color_axis, - # :foreground_color_text, :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - # :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :markerstrokestyle, - # :fillrange, :fillcolor, :fillalpha, - # :bins, - # :bar_width, :bar_edges, - :title, - :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :tickfont, - :guidefont, - :legendfont, - :grid, - :legend, - # :colorbar, :colorbar_title, - # :fill_z, :line_z, :marker_z, :levels, - # :ribbon, - :quiver, - :arrow, - # :orientation, :overwrite_figure, - :polar, - # :normalize, :weights, :contours, - :aspect_ratio, - :tick_direction, - # :framestyle, - # :camera, - # :contour_labels, - :connections, -]) - -const _gaston_seriestype = [ - :path, - :path3d, - :scatter, - :steppre, - :stepmid, - :steppost, - :ysticks, - :xsticks, - :contour, - :shape, - :straightline, - :scatter3d, - :contour3d, - :wireframe, - :heatmap, - :surface, - :mesh3d, - :image, -] - -const _gaston_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] - -const _gaston_marker = [ - :none, - :auto, - :pixel, - :cross, - :xcross, - :+, - :x, - :star5, - :rect, - :circle, - :utriangle, - :dtriangle, - :diamond, - :pentagon, - # :hline, - # :vline, -] - -const _gaston_scale = [:identity, :ln, :log2, :log10] - -# ------------------------------------------------------------------------------ -# unicodeplots - -const _unicodeplots_attr = merge_with_base_supported([ - :annotations, - :bins, - :guide, - :widen, - :grid, - :label, - :layout, - :legend, - :legend_title_font_color, - :lims, - :line, - :linealpha, - :linecolor, - :linestyle, - :markershape, - :plot_title, - :quiver, - :arrow, - :seriesalpha, - :seriescolor, - :scale, - :flip, - :title, - # :marker_z, - :line_z, -]) -const _unicodeplots_seriestype = [ - :path, - :path3d, - :scatter, - :scatter3d, - :straightline, - # :bar, - :shape, - :histogram2d, - :heatmap, - :contour, - # :contour3d, - :image, - :spy, - :surface, - :wireframe, - :mesh3d, -] -const _unicodeplots_style = [:auto, :solid] -const _unicodeplots_marker = [ - :none, - :auto, - :pixel, - # vvvvvvvvvv shapes - :circle, - :rect, - :star5, - :diamond, - :hexagon, - :cross, - :xcross, - :utriangle, - :dtriangle, - :rtriangle, - :ltriangle, - :pentagon, - # :heptagon, - # :octagon, - :star4, - :star6, - # :star7, - :star8, - :vline, - :hline, - :+, - :x, -] -const _unicodeplots_scale = [:identity, :ln, :log2, :log10] - -# ------------------------------------------------------------------------------ -# hdf5 - -const _hdf5_attr = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :foreground_color_grid, - :legend_foreground_color, - :foreground_color_title, - :foreground_color_axis, - :foreground_color_border, - :foreground_color_guide, - :foreground_color_text, - :label, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :bins, - :bar_width, - :bar_edges, - :bar_position, - :title, - :titlelocation, - :titlefont, - :window_title, - :guide, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :tickfont, - :guidefont, - :legendfont, - :grid, - :legend, - :colorbar, - :marker_z, - :line_z, - :fill_z, - :levels, - :ribbon, - :quiver, - :arrow, - :orientation, - :overwrite_figure, - :polar, - :normalize, - :weights, - :contours, - :aspect_ratio, - :clims, - :inset_subplots, - :dpi, - :colorbar_title, -]) -const _hdf5_seriestype = [ - :path, - :steppre, - :stepmid, - :steppost, - :shape, - :straightline, - :scatter, - :hexbin, - :heatmap, - :image, - :contour, - :contour3d, - :path3d, - :scatter3d, - :surface, - :wireframe, -] -const _hdf5_style = [:auto, :solid, :dash, :dot, :dashdot] -const _hdf5_marker = vcat(_allMarkers, :pixel) -const _hdf5_scale = [:identity, :ln, :log2, :log10] - -# Additional constants -# Dict has problems using "Types" as keys. Initialize in "_initialize_backend": -const HDF5PLOT_MAP_STR2TELEM = Dict{String,Type}() -const HDF5PLOT_MAP_TELEM2STR = Dict{Type,String}() - -# Don't really like this global variable... Very hacky -mutable struct HDF5Plot_PlotRef - ref::Union{Plot,Nothing} -end -const HDF5PLOT_PLOTREF = HDF5Plot_PlotRef(nothing) - -# ------------------------------------------------------------------------------ -# inspectdr - -const _inspectdr_attr = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - :background_color_outside, - # :foreground_color_grid, - :legend_foreground_color, - :foreground_color_title, - :foreground_color_axis, - :foreground_color_border, - :foreground_color_guide, - :foreground_color_text, - :label, - :seriescolor, - :seriesalpha, - :line, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :markerstrokestyle, #Causes warning not to have it... what is this? - :fillcolor, - :fillalpha, #:fillrange, - # :bins, :bar_width, :bar_edges, :bar_position, - :title, - :titlelocation, - :window_title, - :guide, - :widen, - :lims, - :scale, #:ticks, :flip, :rotation, - :titlefontfamily, - :titlefontsize, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefontcolor, - :grid, - :legend_position, #:colorbar, - # :marker_z, - # :line_z, - # :levels, - # :ribbon, :quiver, :arrow, - # :orientation, - :overwrite_figure, - :polar, - # :normalize, :weights, - # :contours, :aspect_ratio, - # :clims, - # :inset_subplots, - :dpi, - # :colorbar_title, -]) -const _inspectdr_style = [:auto, :solid, :dash, :dot, :dashdot] -const _inspectdr_seriestype = [ - :path, - :scatter, - :shape, - :straightline, #, :steppre, :stepmid, :steppost -] -#see: _allMarkers, _shape_keys -const _inspectdr_marker = Symbol[ - :none, - :auto, - :circle, - :rect, - :diamond, - :cross, - :xcross, - :utriangle, - :dtriangle, - :rtriangle, - :ltriangle, - :pentagon, - :hexagon, - :heptagon, - :octagon, - :star4, - :star5, - :star6, - :star7, - :star8, - :vline, - :hline, - :+, - :x, -] - -const _inspectdr_scale = [:identity, :ln, :log2, :log10] -# ------------------------------------------------------------------------------ -# pgfplotsx - -_pre_imports(::PGFPlotsXBackend) = @eval Plots begin - import LaTeXStrings: LaTeXString - import UUIDs: uuid4 - import Latexify - import Contour - @require_backend PGFPlotsX -end - -function _initialize_backend(pkg::PGFPlotsXBackend) - _pre_imports(pkg) - @eval Main begin - import PGFPlotsX - export PGFPlotsX - $(_check_compat)(PGFPlotsX) - end - _post_imports(pkg) - _runtime_init(pkg) -end - -const _pgfplotsx_attr = merge_with_base_supported([ - :annotations, - :annotationrotation, - :annotationhalign, - :annotationfontsize, - :annotationfontfamily, - :annotationcolor, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :legend_foreground_color, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :line, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :bins, - :layout, - :title, - :window_title, - :guide, - :widen, - :lims, - :ticks, - :scale, - :flip, - :titlefontfamily, - :titlefontsize, - :titlefonthalign, - :titlefontvalign, - :titlefontrotation, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_halign, - :legend_font_valign, - :legend_font_rotation, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfonthalign, - :tickfontvalign, - :tickfontrotation, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefonthalign, - :guidefontvalign, - :guidefontrotation, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_titlefontsize, - :colorbar_titlefontcolor, - :colorbar_titlefontrotation, - :colorbar_entry, - :fill, - :fill_z, - :line_z, - :marker_z, - :levels, - :legend_column, - :legend_title, - :legend_title_font_color, - :legend_title_font_pointsize, - :ribbon, - :quiver, - :orientation, - :overwrite_figure, - :polar, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontrotation, - :plot_titlefontsize, - :plot_titlevspan, - :aspect_ratio, - :normalize, - :weights, - :inset_subplots, - :bar_width, - :arrow, - :framestyle, - :tick_direction, - :thickness_scaling, - :camera, - :contour_labels, - :connections, - :thickness_scaling, - :axis, - :draw_arrow, - :minorgrid, - :minorgridalpha, - :minorgridlinewidth, - :minorgridstyle, - :minorticks, - :mirror, - :rotation, - :showaxis, - :tickfontrotation, - :draw_arrow, -]) -const _pgfplotsx_seriestype = [ - :path, - :scatter, - :straightline, - :path3d, - :scatter3d, - :surface, - :wireframe, - :heatmap, - :mesh3d, - :contour, - :contour3d, - :quiver, - :shape, - :steppre, - :stepmid, - :steppost, - :ysticks, - :xsticks, -] -const _pgfplotsx_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _pgfplotsx_marker = [ - :none, - :auto, - :circle, - :rect, - :diamond, - :utriangle, - :dtriangle, - :ltriangle, - :rtriangle, - :cross, - :xcross, - :x, - :+, - :star5, - :star6, - :pentagon, - :hline, - :vline, -] -const _pgfplotsx_scale = [:identity, :ln, :log2, :log10] -is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true - -# additional constants -const _pgfplotsx_series_ids = KW() diff --git a/src/backends/deprecated/pgfplots.jl b/src/backends/deprecated/pgfplots.jl deleted file mode 100644 index 3e992dba8..000000000 --- a/src/backends/deprecated/pgfplots.jl +++ /dev/null @@ -1,739 +0,0 @@ -# https://github.com/sisl/PGFPlots.jl - -# significant contributions by: @pkofod - -# -------------------------------------------------------------------------------------- -# COV_EXCL_START -const _pgfplots_linestyles = KW( - :solid => "solid", - :dash => "dashed", - :dot => "dotted", - :dashdot => "dashdotted", - :dashdotdot => "dashdotdotted", -) - -const _pgfplots_markers = KW( - :none => "none", - :cross => "+", - :xcross => "x", - :+ => "+", - :x => "x", - :utriangle => "triangle*", - :dtriangle => "triangle*", - :circle => "*", - :rect => "square*", - :star5 => "star", - :star6 => "asterisk", - :diamond => "diamond*", - :pentagon => "pentagon*", - :hline => "-", - :vline => "|", -) - -const _pgfplots_legend_pos = KW( - :bottomleft => "south west", - :bottomright => "south east", - :topright => "north east", - :topleft => "north west", - :outertopright => "outer north east", -) - -const _pgf_series_extrastyle = KW( - :steppre => "const plot mark right", - :stepmid => "const plot mark mid", - :steppost => "const plot", - :sticks => "ycomb", - :ysticks => "ycomb", - :xsticks => "xcomb", -) - -# PGFPlots uses the anchors to define orientations for example to align left -# one needs to use the right edge as anchor -const _pgf_annotation_halign = KW(:center => "", :left => "right", :right => "left") - -const _pgf_framestyles = [:box, :axes, :origin, :zerolines, :grid, :none] -const _pgf_framestyle_defaults = Dict(:semi => :box) -function pgf_framestyle(style::Symbol) - if style in _pgf_framestyles - return style - else - default_style = get(_pgf_framestyle_defaults, style, :axes) - @warn "Framestyle :$style is not (yet) supported by the PGFPlots backend. :$default_style was cosen instead." - default_style - end -end - -# -------------------------------------------------------------------------------------- - -# takes in color,alpha, and returns color and alpha appropriate for pgf style -function pgf_color(c::Colorant) - cstr = @sprintf "{rgb,1:red,%.8f;green,%.8f;blue,%.8f}" red(c) green(c) blue(c) - cstr, alpha(c) -end - -function pgf_color(grad::ColorGradient) - # Can't handle ColorGradient here, fallback to defaults. - cstr = @sprintf "{rgb,1:red,%.8f;green,%.8f;blue,%.8f}" 0.0 0.60560316 0.97868012 - cstr, 1 -end - -# Generates a colormap for pgfplots based on a ColorGradient -pgf_colormap(grad::ColorGradient) = join( - map(c -> @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)), grad.colors), - ", ", -) - -pgf_thickness_scaling(plt::Plot) = plt[:thickness_scaling] -pgf_thickness_scaling(sp::Subplot) = pgf_thickness_scaling(sp.plt) -pgf_thickness_scaling(series) = pgf_thickness_scaling(series[:subplot]) - -function pgf_fillstyle(plotattributes, i = 1) - cstr, a = pgf_color(get_fillcolor(plotattributes, i)) - fa = get_fillalpha(plotattributes, i) - if fa !== nothing - a = fa - end - "fill = $cstr, fill opacity=$a" -end - -function pgf_linestyle(linewidth::Real, color, α = 1, linestyle = "solid") - cstr, a = pgf_color(plot_color(color, α)) - """ - color = $cstr, - draw opacity = $a, - line width = $linewidth, - $(get(_pgfplots_linestyles, linestyle, "solid"))""" -end - -function pgf_linestyle(plotattributes, i = 1) - lw = pgf_thickness_scaling(plotattributes) * get_linewidth(plotattributes, i) - lc = get_linecolor(plotattributes, i) - la = get_linealpha(plotattributes, i) - ls = get_linestyle(plotattributes, i) - return pgf_linestyle(lw, lc, la, ls) -end - -function pgf_font(fontsize, thickness_scaling = 1, font = "\\selectfont") - fs = fontsize * thickness_scaling - return string("{\\fontsize{", fs, " pt}{", 1.3fs, " pt}", font, "}") -end - -function pgf_marker(plotattributes, i = 1) - shape = _cycle(plotattributes[:markershape], i) - cstr, a = pgf_color( - plot_color(get_markercolor(plotattributes, i), get_markeralpha(plotattributes, i)), - ) - cstr_stroke, a_stroke = pgf_color( - plot_color( - get_markerstrokecolor(plotattributes, i), - get_markerstrokealpha(plotattributes, i), - ), - ) - return string( - "mark = $(get(_pgfplots_markers, shape, "*")),\n", - "mark size = $(pgf_thickness_scaling(plotattributes) * 0.5 * _cycle(plotattributes[:markersize], i)),\n", - plotattributes[:seriestype] === :scatter ? "only marks,\n" : "", - "mark options = { - color = $cstr_stroke, draw opacity = $a_stroke, - fill = $cstr, fill opacity = $a, - line width = $(pgf_thickness_scaling(plotattributes) * _cycle(plotattributes[:markerstrokewidth], i)), - rotate = $(shape === :dtriangle ? 180 : 0), - $(get(_pgfplots_linestyles, _cycle(plotattributes[:markerstrokestyle], i), "solid")) - }", - ) -end - -function pgf_add_annotation!(o, x, y, val, thickness_scaling = 1) - # Construct the style string. - # Currently supports color and orientation - cstr, a = pgf_color(val.font.color) - push!( - o, - PGFPlots.Plots.Node( - val.str, # Annotation Text - x, - y, - style = """ - $(get(_pgf_annotation_halign,val.font.halign,"")), - color=$cstr, draw opacity=$(convert(Float16,a)), - rotate=$(val.font.rotation), - font=$(pgf_font(val.font.pointsize, thickness_scaling)) - """, - ), - ) -end - -# -------------------------------------------------------------------------------------- - -function pgf_series(sp::Subplot, series::Series) - plotattributes = series.plotattributes - st = plotattributes[:seriestype] - series_collection = PGFPlots.Plot[] - - # function args - args = if st === :contour - plotattributes[:z].surf, plotattributes[:x], plotattributes[:y] - elseif RecipesPipeline.is3d(st) - plotattributes[:x], plotattributes[:y], plotattributes[:z] - elseif st === :straightline - straightline_data(series) - elseif st === :shape - shape_data(series) - elseif ispolar(sp) - theta, r = plotattributes[:x], plotattributes[:y] - rad2deg.(theta), r - else - plotattributes[:x], plotattributes[:y] - end - - # PGFPlots can't handle non-Vector? - # args = map(a -> if typeof(a) <: AbstractVector && typeof(a) != Vector - # collect(a) - # else - # a - # end, args) - - if st in (:contour, :histogram2d) - style = [] - kw = KW() - push!(style, pgf_linestyle(plotattributes)) - push!(style, pgf_marker(plotattributes)) - push!(style, "forget plot") - - kw[:style] = join(style, ',') - func = if st === :histogram2d - PGFPlots.Histogram2 - else - kw[:labels] = series[:contour_labels] - kw[:levels] = series[:levels] - 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(plotattributes, i)) - push!(style, pgf_marker(plotattributes, i)) - - if st === :shape - push!(style, pgf_fillstyle(plotattributes, i)) - end - - # add to legend? - if i == 1 && sp[:legend_position] !== :none && should_add_to_legend(series) - if plotattributes[:fillrange] !== nothing - push!(style, "forget plot") - push!(series_collection, pgf_fill_legend_hack(plotattributes, args)) - else - kw[:legendentry] = plotattributes[:label] - if st === :shape # || plotattributes[:fillrange] !== nothing - push!(style, "area legend") - end - 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 = RecipesPipeline.is3d(series) ? PGFPlots.Linear3 : PGFPlots.Linear - return func(pgf_fillrange_args(fillrange, args...)...; kw...) -end - -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 - -function pgf_fill_legend_hack(plotattributes, args) - style = [] - kw = KW() - push!(style, pgf_linestyle(plotattributes, 1)) - push!(style, pgf_marker(plotattributes, 1)) - push!(style, pgf_fillstyle(plotattributes, 1)) - push!(style, "area legend") - kw[:legendentry] = plotattributes[:label] - kw[:style] = join(style, ',') - st = plotattributes[:seriestype] - func = if st === :path3d - PGFPlots.Linear3 - elseif st === :scatter - PGFPlots.Scatter - else - PGFPlots.Linear - end - return func(([arg[1]] for arg in args)...; kw...) -end - -# ---------------------------------------------------------------- - -function pgf_axis(sp::Subplot, letter) - axis = sp[get_attr_symbol(letter, :axis)] - style = [] - kw = KW() - - # turn off scaled ticks - push!(style, "scaled $(letter) ticks = false") - - # set to supported framestyle - framestyle = pgf_framestyle(sp[:framestyle]) - - # axis guide - kw[get_attr_symbol(letter, :label)] = axis[:guide] - - # axis label position - labelpos = "" - if letter === :x && axis[:guide_position] === :top - labelpos = "at={(0.5,1)},above," - elseif letter === :y && axis[:guide_position] === :right - labelpos = "at={(1,0.5)},below," - end - - # Add label font - cstr, α = pgf_color(plot_color(axis[:guidefontcolor])) - push!( - style, - string( - letter, - "label style = {", - labelpos, - "font = ", - pgf_font(axis[:guidefontsize], pgf_thickness_scaling(sp)), - ", color = ", - cstr, - ", draw opacity = ", - α, - ", rotate = ", - axis[:guidefontrotation], - "}", - ), - ) - - # flip/reverse? - axis[:flip] && push!(style, "$letter dir=reverse") - - # scale - scale = axis[:scale] - if scale in (:log2, :ln, :log10) - kw[get_attr_symbol(letter, :mode)] = "log" - scale === :ln || push!(style, "log basis $letter=$(scale === :log2 ? 2 : 10)") - end - - # ticks on or off - if axis[:ticks] in (nothing, false, :none) || framestyle === :none - push!(style, "$(letter)majorticks=false") - end - - # grid on or off - if axis[:grid] && framestyle !== :none - push!(style, "$(letter)majorgrids = true") - else - push!(style, "$(letter)majorgrids = false") - end - - # limits - # TODO: support zlims - if letter !== :z - lims = - ispolar(sp) && letter === :x ? rad2deg.(axis_limits(sp, :x)) : - axis_limits(sp, letter) - kw[get_attr_symbol(letter, :min)] = lims[1] - kw[get_attr_symbol(letter, :max)] = lims[2] - end - - if !(axis[:ticks] in (nothing, false, :none, :native)) && framestyle !== :none - ticks = get_ticks(sp, axis) - #pgf plot ignores ticks with angle below 90 when xmin = 90 so shift values - tick_values = - ispolar(sp) && letter === :x ? [rad2deg.(ticks[1])[3:end]..., 360, 405] : - ticks[1] - push!(style, string(letter, "tick = {", join(tick_values, ","), "}")) - if axis[:showaxis] && axis[:scale] in (:ln, :log2, :log10) && axis[:ticks] === :auto - # wrap the power part of label with } - tick_labels = Vector{String}(undef, length(ticks[2])) - for (i, label) in enumerate(ticks[2]) - base, power = split(label, "^") - power = string("{", power, "}") - tick_labels[i] = string(base, "^", power) - end - push!( - style, - string(letter, "ticklabels = {\$", join(tick_labels, "\$,\$"), "\$}"), - ) - elseif axis[:showaxis] - tick_labels = - ispolar(sp) && letter === :x ? [ticks[2][3:end]..., "0", "45"] : ticks[2] - if axis[:formatter] in (:scientific, :auto) - tick_labels = string.("\$", convert_sci_unicode.(tick_labels), "\$") - tick_labels = replace.(tick_labels, Ref("×" => "\\times")) - end - push!(style, string(letter, "ticklabels = {", join(tick_labels, ","), "}")) - else - push!(style, string(letter, "ticklabels = {}")) - end - push!( - style, - string( - letter, - "tick align = ", - (axis[:tick_direction] === :out ? "outside" : "inside"), - ), - ) - cstr, α = pgf_color(plot_color(axis[:tickfontcolor])) - push!( - style, - string( - letter, - "ticklabel style = {font = ", - pgf_font(axis[:tickfontsize], pgf_thickness_scaling(sp)), - ", color = ", - cstr, - ", draw opacity = ", - α, - ", rotate = ", - axis[:tickfontrotation], - "}", - ), - ) - push!( - style, - string( - letter, - " grid style = {", - pgf_linestyle( - pgf_thickness_scaling(sp) * axis[:gridlinewidth], - axis[:foreground_color_grid], - axis[:gridalpha], - axis[:gridstyle], - ), - "}", - ), - ) - end - - # framestyle - if framestyle in (:axes, :origin) - axispos = framestyle === :axes ? "left" : "middle" - if axis[:draw_arrow] - push!(style, string("axis ", letter, " line = ", axispos)) - else - # the * after line disables the arrow at the axis - push!(style, string("axis ", letter, " line* = ", axispos)) - end - end - - if framestyle === :zerolines - push!(style, string("extra ", letter, " ticks = 0")) - push!(style, string("extra ", letter, " tick labels = ")) - push!( - style, - string( - "extra ", - letter, - " tick style = {grid = major, major grid style = {", - pgf_linestyle( - pgf_thickness_scaling(sp), - axis[:foreground_color_border], - 1.0, - ), - "}}", - ), - ) - end - - if !axis[:showaxis] - push!(style, "separate axis lines") - end - if !axis[:showaxis] || framestyle in (:zerolines, :grid, :none) - push!(style, string(letter, " axis line style = {draw opacity = 0}")) - else - push!( - style, - string( - letter, - " axis line style = {", - pgf_linestyle( - pgf_thickness_scaling(sp), - axis[:foreground_color_border], - 1.0, - ), - "}", - ), - ) - end - - # return the style list and KW args - style, kw -end - -# ---------------------------------------------------------------- - -function _update_plot_object(plt::Plot{PGFPlotsBackend}) - plt.o = PGFPlots.Axis[] - # Obtain the total height of the plot by extracting the maximal bottom - # coordinate from the bounding box. - total_height = bottom(bbox(plt.layout)) - - for sp in plt.subplots - # first build the PGFPlots.Axis object - style = ["unbounded coords=jump"] - kw = KW() - - # add to style/kw for each axis - for letter in (:x, :y, :z) - if letter !== :z || RecipesPipeline.is3d(sp) - axisstyle, axiskw = pgf_axis(sp, letter) - append!(style, axisstyle) - merge!(kw, axiskw) - end - end - - # bounding box values are in mm - # note: bb origin is top-left, pgf is bottom-left - # A round on 2 decimal places should be enough precision for 300 dpi - # plots. - bb = bbox(sp) - push!( - style, - """ - xshift = $(left(bb).value)mm, - yshift = $(round((total_height - (bottom(bb))).value, digits=2))mm, - axis background/.style={fill=$(pgf_color(sp[:background_color_inside])[1])} -""", - ) - kw[:width] = "$(width(bb).value)mm" - kw[:height] = "$(height(bb).value)mm" - - if sp[:title] != "" - kw[:title] = "$(sp[:title])" - cstr, α = pgf_color(plot_color(sp[:titlefontcolor])) - push!( - style, - string( - "title style = {font = ", - pgf_font(sp[:titlefontsize], pgf_thickness_scaling(sp)), - ", color = ", - cstr, - ", draw opacity = ", - α, - ", rotate = ", - sp[:titlefontrotation], - "}", - ), - ) - end - - if get_aspect_ratio(sp) in (1, :equal) - kw[:axisEqual] = "true" - end - - legpos = sp[:legend_position] - if haskey(_pgfplots_legend_pos, legpos) - kw[:legendPos] = _pgfplots_legend_pos[legpos] - end - cstr, bg_alpha = pgf_color(plot_color(sp[:legend_background_color])) - fg_alpha = alpha(plot_color(sp[:legend_foreground_color])) - - push!( - style, - string( - "legend style = {", - pgf_linestyle( - pgf_thickness_scaling(sp), - sp[:legend_foreground_color], - fg_alpha, - "solid", - ), - ",", - "fill = $cstr,", - "fill opacity = $bg_alpha,", - "text opacity = $(alpha(plot_color(sp[:legend_font_color]))),", - "font = ", - pgf_font(sp[:legend_font_pointsize], pgf_thickness_scaling(sp)), - "}", - ), - ) - - if any(s[:seriestype] === :contour for s in series_list(sp)) - kw[:view] = "{0}{90}" - kw[:colorbar] = !(sp[:colorbar] in (:none, :off, :hide, false)) - elseif RecipesPipeline.is3d(sp) - azim, elev = sp[:camera] - kw[:view] = "{$(azim)}{$(elev)}" - end - - axisf = PGFPlots.Axis - if sp[:projection] === :polar - axisf = PGFPlots.PolarAxis - #make radial axis vertical - kw[:xmin] = 90 - kw[:xmax] = 450 - end - - # Search series for any gradient. In case one series uses a gradient set - # the colorbar and colomap. - # The reasoning behind doing this on the axis level is that pgfplots - # colorbar seems to only works on axis level and needs the proper colormap for - # correctly displaying it. - # It's also possible to assign the colormap to the series itself but - # then the colormap needs to be added twice, once for the axis and once for the - # series. - # 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, :linecolor) - if typeof(series.plotattributes[col]) == ColorGradient - push!( - style, - "colormap={plots}{$(pgf_colormap(series.plotattributes[col]))}", - ) - - if sp[:colorbar] === :none - kw[:colorbar] = "false" - else - kw[:colorbar] = "true" - end - # goto is needed to break out of col and series for - @goto colorbar_end - end - end - end - @label colorbar_end - - push!(style, "colorbar style={title=$(sp[:colorbar_title])}") - o = axisf(; style = join(style, ","), kw...) - - # add the series object to the PGFPlots.Axis - for series in series_list(sp) - push!.(Ref(o), pgf_series(sp, series)) - - # add series annotations - anns = series[:series_annotations] - for (xi, yi, str, fnt) in EachAnn(anns, series[:x], series[:y]) - pgf_add_annotation!( - o, - xi, - yi, - PlotText(str, fnt), - pgf_thickness_scaling(series), - ) - end - end - - # add the annotations - for ann in sp[:annotations] - pgf_add_annotation!( - o, - locate_annotation(sp, ann...)..., - pgf_thickness_scaling(sp), - ) - end - - # add the PGFPlots.Axis to the list - push!(plt.o, o) - end -end - -_show(io::IO, mime::MIME"image/svg+xml", plt::Plot{PGFPlotsBackend}) = show(io, mime, plt.o) - -function _show(io::IO, mime::MIME"application/pdf", plt::Plot{PGFPlotsBackend}) - # prepare the object - pgfplt = PGFPlots.plot(plt.o) - - # save a pdf - fn = tempname() * ".pdf" - PGFPlots.save(PGFPlots.PDF(fn), pgfplt) - - # read it into io - write(io, read(open(fn), String)) - - # cleanup - PGFPlots.cleanup(plt.o) -end - -function _show(io::IO, mime::MIME"application/x-tex", plt::Plot{PGFPlotsBackend}) - fn = tempname() * ".tex" - PGFPlots.save( - fn, - backend_object(plt), - include_preamble = plt.attr[:tex_output_standalone], - ) - write(io, read(open(fn), String)) -end - -function _display(plt::Plot{PGFPlotsBackend}) - # prepare the object - pgfplt = PGFPlots.plot(plt.o) - - # save an svg - fn = string(tempname(), ".svg") - PGFPlots.save(PGFPlots.SVG(fn), pgfplt) - - # show it - open_browser_window(fn) - - # cleanup - PGFPlots.cleanup(plt.o) -end - -# COV_EXCL_STOP diff --git a/src/backends/deprecated/pyplot.jl b/src/backends/deprecated/pyplot.jl deleted file mode 100644 index 4fc33cfd2..000000000 --- a/src/backends/deprecated/pyplot.jl +++ /dev/null @@ -1,1640 +0,0 @@ -# https://github.com/JuliaPy/PyPlot.jl -# COV_EXCL_START - -is_marker_supported(::PyPlotBackend, shape::Shape) = true - -# -------------------------------------------------------------------------------------- - -# problem: https://github.com/tbreloff/Plots.jl/issues/308 -# solution: hack from @stevengj: https://github.com/JuliaPy/PyPlot.jl/pull/223#issuecomment-229747768 -let otherdisplays = splice!(Base.Multimedia.displays, 2:length(Base.Multimedia.displays)) - append!(Base.Multimedia.displays, otherdisplays) -end - -# "support" matplotlib v3.4 -if PyPlot.version < v"3.4" - @warn """You are using Matplotlib $(PyPlot.version), which is no longer - officially supported by the Plots community. To ensure smooth Plots.jl - integration update your Matplotlib library to a version >= 3.4.0 - - If you have used Conda.jl to install PyPlot (default installation), - upgrade your matplotlib via Conda.jl and rebuild the PyPlot. - - If you are not sure, here are the default instructions: - - In Julia REPL: - ``` - import Pkg; - Pkg.add("Conda") - import Conda - Conda.update() - Pkg.build("PyPlot") - ``` - """ -end - -# PyCall API changes in v1.90.0 -isdefined(PyPlot.PyCall, :_setproperty!) || - @warn "Plots no longer supports PyCall < 1.90.0 and PyPlot < 2.8.0. Either update PyCall and PyPlot or pin Plots to a version <= 0.23.2." - -# # convert colorant to 4-tuple RGBA -# py_color(c::Colorant, α=nothing) = map(f->float(f(convertColor(c,α))), (red, green, blue, alpha)) -# py_color(cvec::ColorVector, α=nothing) = map(py_color, convertColor(cvec, α).v) -# py_color(grad::ColorGradient, α=nothing) = map(c -> py_color(c, α), grad.colors) -# py_color(scheme::ColorScheme, α=nothing) = py_color(convertColor(getColor(scheme), α)) -# py_color(vec::AVec, α=nothing) = map(c->py_color(c,α), vec) -# py_color(c, α=nothing) = py_color(convertColor(c, α)) - -# function py_colormap(c::ColorGradient, α=nothing) -# pyvals = [(v, py_color(getColorZ(c, v), α)) for v in c.values] -# pycolors["LinearSegmentedColormap"][:from_list]("tmp", pyvals) -# end - -# # convert vectors and ColorVectors to standard ColorGradients -# # TODO: move this logic to colors.jl and keep a barebones wrapper for pyplot -# py_colormap(cv::ColorVector, α=nothing) = py_colormap(ColorGradient(cv.v), α) -# py_colormap(v::AVec, α=nothing) = py_colormap(ColorGradient(v), α) - -# # anything else just gets a bluesred gradient -# py_colormap(c, α=nothing) = py_colormap(default_gradient(), α) - -for k in (:linthresh, :base, :label) - # add PyPlot specific symbols to cache - _attrsymbolcache[k] = Dict{Symbol,Symbol}() - for letter in (:x, :y, :z, Symbol(""), :top, :bottom, :left, :right) - _attrsymbolcache[k][letter] = Symbol(k, letter) - end -end - -py_handle_surface(v) = v -py_handle_surface(z::Surface) = z.surf - -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::PlotUtils.AbstractColorList) = py_color(color_list(grad)) -py_color(c::Colorant, α) = py_color(plot_color(c, α)) - -function py_colormap(cg::ColorGradient) - pyvals = collect(zip(cg.values, py_color(PlotUtils.color_list(cg)))) - cm = pycolors."LinearSegmentedColormap"."from_list"("tmp", pyvals) - cm."set_bad"(color = (0, 0, 0, 0.0), alpha = 0.0) - cm -end -function py_colormap(cg::PlotUtils.CategoricalColorGradient) - r = range(0, stop = 1, length = 256) - pyvals = collect(zip(r, py_color(cg[r]))) - cm = pycolors."LinearSegmentedColormap"."from_list"("tmp", pyvals) - cm."set_bad"(color = (0, 0, 0, 0.0), alpha = 0.0) - cm -end -py_colormap(c) = py_colormap(_as_gradient(c)) - -function py_shading(c, z) - cmap = py_colormap(c) - ls = pycolors."LightSource"(270, 45) - ls."shade"(z, cmap, vert_exag = 0.1, blend_mode = "soft") -end - -# get the style (solid, dashed, etc) -function py_linestyle(seriestype::Symbol, linestyle::Symbol) - seriestype === :none && return " " - linestyle === :solid && return "-" - linestyle === :dash && return "--" - linestyle === :dot && return ":" - linestyle === :dashdot && return "-." - @warn "Unknown linestyle $linestyle" - return "-" -end - -function py_marker(marker::Shape) - x, y = coords(marker) - n = length(x) - mat = zeros(n + 1, 2) - for i in eachindex(x) - mat[i, 1] = x[i] - mat[i, 2] = y[i] - end - mat[n + 1, :] = @view mat[1, :] - pypath."Path"(mat) -end - -# get the marker shape -function py_marker(marker::Symbol) - marker === :none && return " " - marker === :circle && return "o" - marker === :rect && return "s" - marker === :diamond && return "D" - marker === :utriangle && return "^" - marker === :dtriangle && return "v" - marker === :+ && return "+" - marker === :x && return "x" - marker === :star5 && return "*" - marker === :pentagon && return "p" - marker === :hexagon && return "h" - marker === :octagon && return "8" - marker === :pixel && return "," - marker === :hline && return "_" - marker === :vline && return "|" - haskey(_shapes, marker) && return py_marker(_shapes[marker]) - - @warn "Unknown marker $marker" - return "o" -end - -# py_marker(markers::AVec) = map(py_marker, markers) -function py_marker(markers::AVec) - @warn "Vectors of markers are currently unsupported in PyPlot: $markers" - py_marker(markers[1]) -end - -# pass through -function py_marker(marker::AbstractString) - @assert length(marker) == 1 - marker -end - -function py_stepstyle(seriestype::Symbol) - seriestype === :steppost && return "steps-post" - seriestype === :stepmid && return "steps-mid" - seriestype === :steppre && return "steps-pre" - return "default" -end - -function py_fillstepstyle(seriestype::Symbol) - seriestype === :steppost && return "post" - seriestype === :stepmid && return "mid" - seriestype === :steppre && return "pre" - return nothing -end - -py_fillstyle(::Nothing) = nothing -py_fillstyle(fillstyle::Symbol) = string(fillstyle) - -function py_get_matching_math_font(parent_fontfamily) - # matplotlib supported math fonts according to - # https://matplotlib.org/stable/tutorials/text/mathtext.html - py_math_supported_fonts = Dict{String,String}( - "sans-serif" => "dejavusans", - "serif" => "dejavuserif", - "cm" => "cm", - "stix" => "stix", - "stixsans" => "stixsans", - ) - # Fallback to "dejavusans" or "dejavuserif" in case the parentfont is different - # from supported by matplotlib fonts - matching_font(font) = occursin("serif", lowercase(font)) ? "dejavuserif" : "dejavusans" - get(py_math_supported_fonts, parent_fontfamily, matching_font(parent_fontfamily)) -end - -get_locator_and_formatter(vals::AVec) = - pyticker."FixedLocator"(eachindex(vals)), pyticker."FixedFormatter"(vals) - -function add_pyfixedformatter(cbar, vals::AVec) - cbar[:locator], cbar[:formatter] = get_locator_and_formatter(vals) - cbar[:update_ticks]() -end - -labelfunc(scale::Symbol, backend::PyPlotBackend) = - PyPlot.LaTeXStrings.latexstring ∘ labelfunc_tex(scale) - -function py_mask_nans(z) - # pynp["ma"][:masked_invalid](z))) - PyPlot.PyCall.pycall(pynp."ma"."masked_invalid", Any, z) - # pynp["ma"][:masked_where](pynp["isnan"](z),z) -end - -# --------------------------------------------------------------------------- - -function fix_xy_lengths!(plt::Plot{PyPlotBackend}, series::Series) - if series[:x] !== nothing - x, y = series[:x], series[:y] - nx, ny = length(x), length(y) - if !isa(get(series.plotattributes, :z, nothing), Surface) && nx != ny - if nx < ny - series[:x] = map(i -> Float64(x[mod1(i, nx)]), 1:ny) - else - series[:y] = map(i -> Float64(y[mod1(i, ny)]), 1:nx) - end - end - end -end - -py_linecolormap(series::Series) = - py_colormap(cgrad(series[:linecolor], alpha = get_linealpha(series))) -py_markercolormap(series::Series) = - py_colormap(cgrad(series[:markercolor], alpha = get_markeralpha(series))) -py_fillcolormap(series::Series) = - py_colormap(cgrad(series[:fillcolor], alpha = get_fillalpha(series))) - -# --------------------------------------------------------------------------- - -# TODO: these can probably be removed eventually... right now they're just keeping things working before cleanup - -# getAxis(sp::Subplot) = sp.o - -# function getAxis(plt::Plot{PyPlotBackend}, series::Series) -# sp = get_subplot(plt, get(series.plotattributes, :subplot, 1)) -# getAxis(sp) -# end - -# getfig(o) = o - -# --------------------------------------------------------------------------- -# Figure utils -- F*** matplotlib for making me work so hard to figure this crap out - -# the drawing surface -py_canvas(fig) = fig."canvas" - -# the object controlling draw commands -py_renderer(fig) = py_canvas(fig)."get_renderer"() - -# draw commands... paint the screen (probably updating internals too) -py_drawfig(fig) = fig."draw"(py_renderer(fig)) -# py_drawax(ax) = ax[:draw](py_renderer(ax[:get_figure]())) - -# get a vector [left, right, bottom, top] in PyPlot coords (origin is bottom-left (0, 0)!) -py_extents(obj) = obj."get_window_extent"()."get_points"() - -# compute a bounding box (with origin top-left), however pyplot gives coords with origin bottom-left -function py_bbox(obj) - fl, fr, fb, ft = bb = py_extents(obj."get_figure"()) - l, r, b, t = ex = py_extents(obj) - # @show obj bb ex - # BoundingBox(x0, y0, width, height) - BoundingBox(l * px, (ft - t) * px, (r - l) * px, (t - b) * px) -end - -py_bbox(::Nothing) = BoundingBox(0mm, 0mm) - -# get the bounding box of the union of the objects -function py_bbox(v::AVec) - bbox_union = DEFAULT_BBOX[] - for obj in v - bbox_union += py_bbox(obj) - end - bbox_union -end - -# bounding box: union of axis tick labels -py_bbox_ticks(ax, letter) = - if ax.name == "3d" - py_bbox(nothing) # FIXME: broken in `3d` - else - py_bbox(getproperty(ax, Symbol("get_" * letter * "ticklabels"))()) - end - -# bounding box: axis guide -py_bbox_axislabel(ax, letter) = - py_bbox(getproperty(ax, Symbol("get_" * letter * "axis"))().label) - -# bounding box: union of axis ticks and guide -function py_bbox_axis(ax, letter) - ticks = py_bbox_ticks(ax, letter) - labels = py_bbox_axislabel(ax, letter) - ticks + labels -end - -# bounding box: axis title -function py_bbox_title(ax) - bb = DEFAULT_BBOX[] - for s in (:title, :_left_title, :_right_title) - bb += py_bbox(getproperty(ax, s)) - end - bb -end - -# bounding box: legend -py_bbox_legend(ax) = py_bbox(ax."get_legend"()) -py_thickness_scale(plt::Plot{PyPlotBackend}, ptsz) = ptsz * plt[:thickness_scaling] - -# --------------------------------------------------------------------------- - -# Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{PyPlotBackend}) - w, h = map(px2inch, Tuple(s * plt[:dpi] / Plots.DPI for s in plt[:size])) - - # # reuse the current figure? - fig = if plt[:overwrite_figure] - PyPlot.gcf() - else - fig = PyPlot.figure() - # finalizer(fig, close) - fig - end - - # clear the figure - # PyPlot.clf() - fig -end - -# Set up the subplot within the backend object. -# function _initialize_subplot(plt::Plot{PyPlotBackend}, sp::Subplot{PyPlotBackend}) - -function py_init_subplot(plt::Plot{PyPlotBackend}, sp::Subplot{PyPlotBackend}) - fig = plt.o - projection = (proj = sp[:projection]) in (nothing, :none) ? nothing : string(proj) - kw = if projection == "3d" - # PyPlot defaults to "persp" projection by default, we choose to unify backends - # by using a default "ortho" proj when `:auto` - (; - proj_type = ( - auto = "ortho", - ortho = "ortho", - orthographic = "ortho", - persp = "persp", - perspective = "persp", - )[sp[:projection_type]] - ) - else - (;) - end - # add a new axis, and force it to create a new one by setting a distinct label - ax = fig."add_subplot"(; label = string(gensym()), projection = projection, kw...) - sp.o = ax -end - -# --------------------------------------------------------------------------- - -function py_add_series(plt::Plot{PyPlotBackend}, series::Series) - # plotattributes = series.plotattributes - st = series[:seriestype] - sp = series[:subplot] - ax = sp.o - - # PyPlot doesn't handle mismatched x/y - fix_xy_lengths!(plt, series) - - # ax = getAxis(plt, series) - x, y, z = (py_handle_surface(series[letter]) for letter in (:x, :y, :z)) - if st === :straightline - x, y = straightline_data(series) - elseif st === :shape - x, y = shape_data(series) - end - - if ispolar(series) - # make negative radii positive and flip the angle - # (PyPlot ignores negative radii) - for i in eachindex(y) - if y[i] < 0 - y[i] = -y[i] - x[i] -= π - end - end - end - - xyargs = st in _3dTypes ? (x, y, z) : (x, y) - - # handle zcolor and get c/cmap - needs_colorbar = hascolorbar(sp) - vmin, vmax = clims = get_clims(sp, series) - - # Dict to store extra kwargs - extrakw = if st === :wireframe || st === :hexbin - # vmin, vmax cause an error for wireframe plot - # We are not supporting clims for hexbin as calculation of bins is not trivial - KW() - else - KW(:vmin => vmin, :vmax => vmax) - end - - # holds references to any python object representing the matplotlib series - handles = [] - discrete_colorbar_values = nothing - - # pass in an integer value as an arg, but a levels list as a keyword arg - levels = series[:levels] - levelargs = if isscalar(levels) - levels - elseif isvector(levels) - extrakw[:levels] = levels - () - end - - # add custom frame shapes to markershape? - series_annotations_shapes!(series, :xy) - - # for each plotting command, optionally build and add a series handle to the list - - # line plot - if st in (:path, :path3d, :steppre, :stepmid, :steppost, :straightline) - if maximum(series[:linewidth]) > 0 - for (k, segment) in enumerate(series_segments(series, st; check = true)) - i, rng = segment.attr_index, segment.range - handle = ax."plot"( - (arg[rng] for arg in xyargs)...; - 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)), - linestyle = py_linestyle(st, get_linestyle(series, i)), - solid_capstyle = "butt", - dash_capstyle = "butt", - drawstyle = py_stepstyle(st), - )[1] - push!(handles, handle) - end - - a = series[:arrow] - if a !== nothing && !RecipesPipeline.is3d(st) # TODO: handle 3d later - if typeof(a) != Arrow - @warn "Unexpected type for arrow: $(typeof(a))" - else - arrowprops = KW( - :arrowstyle => "simple,head_length=$(a.headlength),head_width=$(a.headwidth)", - :shrinkA => 0, - :shrinkB => 0, - :edgecolor => py_color(get_linecolor(series)), - :facecolor => py_color(get_linecolor(series)), - :linewidth => py_thickness_scale(plt, get_linewidth(series)), - :linestyle => py_linestyle(st, get_linestyle(series)), - ) - add_arrows(x, y) do xyprev, xy - ax."annotate"( - "", - xytext = ( - 0.001xyprev[1] + 0.999xy[1], - 0.001xyprev[2] + 0.999xy[2], - ), - xy = xy, - arrowprops = arrowprops, - zorder = 999, - ) - end - end - end - end - end - - # add markers? - if series[:markershape] !== :none && - st in (:path, :scatter, :path3d, :scatter3d, :steppre, :stepmid, :steppost, :bar) - for segment in series_segments(series, :scatter) - i, rng = segment.attr_index, segment.range - args = if st === :bar && !isvertical(series) - y[rng], x[rng] - else - x[rng], y[rng] - end - if RecipesPipeline.is3d(sp) - args = (args..., z[rng]) - end - - handle = ax."scatter"( - args...; - label = series[:label], - zorder = series[:series_plotindex] + 0.5, - marker = py_marker(_cycle(series[:markershape], i)), - s = py_thickness_scale(plt, _cycle(series[:markersize], i)) .^ 2, - facecolors = py_color( - get_markercolor(series, i), - get_markeralpha(series, i), - ), - edgecolors = py_color( - get_markerstrokecolor(series, i), - get_markerstrokealpha(series, i), - ), - linewidths = py_thickness_scale(plt, get_markerstrokewidth(series, i)), - extrakw..., - ) - push!(handles, handle) - end - end - - if st === :hexbin - sekw = series[:extra_kwargs] - extrakw[:mincnt] = get(sekw, :mincnt, nothing) - extrakw[:edgecolors] = get(sekw, :edgecolors, py_color(get_linecolor(series))) - handle = ax."hexbin"( - x, - y; - label = series[:label], - C = series[:weights], - gridsize = series[:bins] === :auto ? 100 : series[:bins], # 100 is the default value - linewidths = py_thickness_scale(plt, series[:linewidth]), - alpha = series[:fillalpha], - cmap = py_fillcolormap(series), # applies to the pcolorfast object - zorder = series[:series_plotindex], - extrakw..., - ) - push!(handles, handle) - end - - if st in (:contour, :contour3d) - if st === :contour3d - extrakw[:extend3d] = true - if !ismatrix(x) || !ismatrix(y) - x, y = repeat(x', length(y), 1), repeat(y, 1, length(x)) - end - end - - if typeof(series[:linecolor]) <: AbstractArray - extrakw[:colors] = py_color.(series[:linecolor]) - else - extrakw[:cmap] = py_linecolormap(series) - end - - # contour lines - handle = ax."contour"( - x, - y, - z, - levelargs...; - label = series[:label], - zorder = series[:series_plotindex], - linewidths = py_thickness_scale(plt, series[:linewidth]), - linestyles = py_linestyle(st, series[:linestyle]), - extrakw..., - ) - if series[:contour_labels] == true - ax."clabel"(handle, handle.levels) - end - push!(handles, handle) - - # contour fills - if series[:fillrange] !== nothing - handle = ax."contourf"( - x, - y, - z, - levelargs...; - label = series[:label], - zorder = series[:series_plotindex] + 0.5, - alpha = series[:fillalpha], - extrakw..., - ) - push!(handles, handle) - end - end - - if st in (:surface, :wireframe) - if z isa AbstractMatrix - if !ismatrix(x) || !ismatrix(y) - x, y = repeat(x', length(y), 1), repeat(y, 1, length(x)) - end - if st === :surface - if series[:fill_z] !== nothing - # the surface colors are different than z-value - extrakw[:facecolors] = - py_shading(series[:fillcolor], py_handle_surface(series[:fill_z])) - extrakw[:shade] = false - else - extrakw[:cmap] = py_fillcolormap(series) - end - end - handle = getproperty(ax, st === :surface ? :plot_surface : :plot_wireframe)( - x, - y, - z; - label = series[:label], - zorder = series[:series_plotindex], - rstride = series[:stride][1], - cstride = series[:stride][2], - linewidth = py_thickness_scale(plt, series[:linewidth]), - edgecolor = py_color(get_linecolor(series)), - extrakw..., - ) - push!(handles, handle) - - # contours on the axis planes - if series[:contours] - for (zdir, mat) in (("x", x), ("y", y), ("z", z)) - offset = (zdir == "y" ? ignorenan_maximum : ignorenan_minimum)(mat) - handle = ax."contourf"( - x, - y, - z, - levelargs...; - zdir = zdir, - cmap = py_fillcolormap(series), - offset = (zdir == "y" ? ignorenan_maximum : ignorenan_minimum)(mat), # where to draw the contour plane - ) - push!(handles, handle) - end - end - - elseif typeof(z) <: AbstractVector - # tri-surface plot (https://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html#tri-surface-plots) - handle = ax."plot_trisurf"( - x, - y, - z; - label = series[:label], - zorder = series[:series_plotindex], - cmap = py_fillcolormap(series), - linewidth = py_thickness_scale(plt, series[:linewidth]), - edgecolor = py_color(get_linecolor(series)), - extrakw..., - ) - push!(handles, handle) - else - error("Unsupported z type $(typeof(z)) for seriestype=$st") - end - end - - if st === :mesh3d - polygons = if series[:connections] isa AbstractVector{<:AbstractVector{Int}} - # Combination of any polygon types - broadcast(inds -> broadcast(i -> [x[i], y[i], z[i]], inds), series[:connections]) - elseif series[:connections] isa AbstractVector{NTuple{N,Int}} where {N} - # Only N-gons - connections have to be 1-based (indexing) - broadcast(inds -> broadcast(i -> [x[i], y[i], z[i]], inds), series[:connections]) - elseif series[:connections] isa NTuple{3,<:AbstractVector{Int}} - # Only triangles - connections have to be 0-based (indexing) - ci, cj, ck = series[:connections] - if !(length(ci) == length(cj) == length(ck)) - "Argument connections must consist of equally sized arrays." |> - ArgumentError |> - throw - end - broadcast( - j -> broadcast(i -> [x[i], y[i], z[i]], [ci[j] + 1, cj[j] + 1, ck[j] + 1]), - eachindex(ci), - ) - else - "Unsupported `:connections` type $(typeof(series[:connections])) for seriestype=$st" |> - ArgumentError |> - throw - end - col = mplot3d.art3d.Poly3DCollection( - polygons, - linewidths = py_thickness_scale(plt, series[:linewidth]), - edgecolor = py_color(get_linecolor(series)), - facecolor = py_color(series[:fillcolor]), - alpha = get_fillalpha(series), - zorder = series[:series_plotindex], - ) - handle = ax."add_collection3d"(col) - # Fix for handle: https://stackoverflow.com/questions/54994600/pyplot-legend-poly3dcollection-object-has-no-attribute-edgecolors2d - # It seems there aren't two different alpha values for edge and face - handle._facecolors2d = py_color(series[:fillcolor]) - handle._edgecolors2d = py_color(get_linecolor(series)) - push!(handles, handle) - end - - if st === :image - xmin, xmax = ignorenan_extrema(series[:x]) - ymin, ymax = ignorenan_extrema(series[:y]) - dx = (xmax - xmin) / (length(series[:x]) - 1) / 2 - dy = (ymax - ymin) / (length(series[:y]) - 1) / 2 - z = if eltype(z) <: Colors.AbstractGray - float(z) - elseif eltype(z) <: Colorant - map(c -> Float64[red(c), green(c), blue(c), alpha(c)], z) - else - z # hopefully it's in a data format that will "just work" with imshow - end - handle = ax."imshow"( - z; - zorder = series[:series_plotindex], - cmap = py_colormap(cgrad(plot_color([:black, :white]))), - vmin = 0.0, - vmax = 1.0, - extent = (xmin - dx, xmax + dx, ymax + dy, ymin - dy), - ) - push!(handles, handle) - - # expand extrema... handle is AxesImage object - xmin, xmax, ymax, ymin = handle."get_extent"() - expand_extrema!(sp, xmin, xmax, ymin, ymax) - # sp[:yaxis].series[:flip] = true - end - - if st === :heatmap - x, y = heatmap_edges(x, sp[:xaxis][:scale], y, sp[:yaxis][:scale], size(z)) - - expand_extrema!(sp[:xaxis], x) - expand_extrema!(sp[:yaxis], y) - dvals = sp[:zaxis][:discrete_values] - isempty(dvals) || (discrete_colorbar_values = dvals) - - handle = ax."pcolormesh"( - x, - y, - py_mask_nans(z); - label = series[:label], - zorder = series[:series_plotindex], - cmap = py_fillcolormap(series), - alpha = series[:fillalpha], - # edgecolors = (series[:linewidth] > 0 ? py_linecolor(series) : "face"), - extrakw..., - ) - push!(handles, handle) - end - - if st === :shape - handle = [] - for segment in series_segments(series) - i, rng = segment.attr_index, segment.range - if length(rng) > 1 - lc = get_linecolor(series, clims, i) - fc = get_fillcolor(series, clims, i) - la = get_linealpha(series, i) - fa = get_fillalpha(series, i) - ls = get_linestyle(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(lc, la), - facecolor = py_color(fc, has_fs ? 0 : fa), - linewidth = py_thickness_scale(plt, get_linewidth(series, i)), - 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) - end - - series[:serieshandle] = handles - - # # smoothing - # handleSmooth(plt, ax, series, series[:smooth]) - - # handle area filling - fillrange = series[:fillrange] - if fillrange !== nothing && st !== :contour - 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 - :fill_betweenx, y[rng], x[rng] - end - n = length(dim1) - args = if typeof(fillrange) <: Union{Real,AVec} - dim1, _cycle(fillrange, rng), dim2 - elseif is_2tuple(fillrange) - 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], - # 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 - end - - # this is all we need to add the series_annotations text - anns = series[:series_annotations] - for (xi, yi, str, fnt) in EachAnn(anns, x, y) - py_add_annotations(sp, xi, yi, PlotText(str, fnt)) - end -end - -# -------------------------------------------------------------------------- - -function py_set_lims(ax, sp::Subplot, axis::Axis) - letter = axis[:letter] - lfrom, lto = axis_limits(sp, letter) - getproperty(ax, Symbol("set_", letter, "lim"))(lfrom, lto) -end - -function py_set_ticks(sp, ax, ticks, letter) - ticks === :auto && return - axis = getproperty(ax, get_attr_symbol(letter, :axis)) - if ticks === :none || ticks === nothing || ticks == false - kw = KW() - for dir in (:top, :bottom, :left, :right) - kw[dir] = kw[get_attr_symbol(:label, dir)] = false - end - axis."set_tick_params"(; which = "both", kw...) - return - end - - if (ttype = ticksType(ticks)) === :ticks - axis."set_ticks"(ticks) - elseif ttype === :ticks_and_labels - axis."set_ticks"(ticks[1]) - axis."set_ticklabels"(ticks[2]) - else - error("Invalid input for $(letter)ticks: $ticks") - end -end - -function py_compute_axis_minval(sp::Subplot, axis::Axis) - # compute the smallest absolute value for the log scale's linear threshold - minval = 1.0 - sps = axis.sps - for sp in sps, series in series_list(sp) - (v = series.plotattributes[axis[:letter]]) |> isempty && continue - minval = NaNMath.min(minval, ignorenan_minimum(abs.(v))) - end - - # now if the axis limits go to a smaller abs value, use that instead - vmin, vmax = axis_limits(sp, axis[:letter]) - NaNMath.min(minval, abs(vmin), abs(vmax)) -end - -function py_set_scale(ax, sp::Subplot, scale::Symbol, letter::Symbol) - scale in supported_scales() || return @warn "Unhandled scale value in pyplot: $scale" - func = getproperty(ax, Symbol("set_", letter, "scale")) - pyletter = PyPlot.version ≥ v"3.3" ? Symbol("") : letter # https://matplotlib.org/3.3.0/api/api_changes.html - kw = KW() - arg = if scale === :identity - "linear" - else - kw[get_attr_symbol(:base, pyletter)] = if scale === :ln - ℯ - elseif scale === :log2 - 2 - elseif scale === :log10 - 10 - end - axis = sp[get_attr_symbol(letter, :axis)] - kw[get_attr_symbol(:linthresh, pyletter)] = - NaNMath.max(1e-16, py_compute_axis_minval(sp, axis)) - "symlog" - end - func(arg; kw...) -end - -py_set_scale(ax, sp::Subplot, axis::Axis) = - py_set_scale(ax, sp, axis[:scale], axis[:letter]) - -py_set_spine_color(spines, color) = - foreach(loc -> getproperty(spines, loc)."set_color"(color), spines) - -py_set_spine_color(spines::Dict, color) = - for (_, spine) in spines - spine."set_color"(color) - end - -function py_set_axis_colors(sp, ax, a::Axis) - py_set_spine_color(ax.spines, py_color(a[:foreground_color_border])) - axissym = get_attr_symbol(a[:letter], :axis) - if PyPlot.PyCall.hasproperty(ax, axissym) - tickcolor = - sp[:framestyle] in (:zerolines, :grid) ? - py_color(plot_color(a[:foreground_color_grid], a[:gridalpha])) : - py_color(a[:foreground_color_axis]) - ax."tick_params"( - axis = string(a[:letter]), - which = "both", - colors = tickcolor, - labelcolor = py_color(a[:tickfontcolor]), - ) - getproperty(ax, axissym).label.set_color(py_color(a[:guidefontcolor])) - end -end - -# -------------------------------------------------------------------------- -py_hide_spines(ax) = - foreach(spine -> getproperty(ax.spines, string(spine))."set_visible"(false), ax.spines) - -function _before_layout_calcs(plt::Plot{PyPlotBackend}) - # update the fig - w, h = plt[:size] - fig = plt.o - fig."clear"() - fig."set_size_inches"(w / DPI, h / DPI, forward = true) - fig."set_facecolor"(py_color(plt[:background_color_outside])) - fig."set_dpi"(plt[:dpi]) - - # resize the window - PyPlot.plt."get_current_fig_manager"().resize(w, h) - - # initialize subplots - foreach(sp -> py_init_subplot(plt, sp), plt.subplots) - - # add the series - foreach(series -> py_add_series(plt, series), plt.series_list) - - # update subplots - for sp in plt.subplots - (ax = sp.o) === nothing && continue - - # add the annotations - for ann in sp[:annotations] - py_add_annotations(sp, locate_annotation(sp, ann...)...) - end - - # title - if !isempty(sp[:title]) - loc = lowercase(string(sp[:titlelocation])) - func = getproperty(ax, if loc == "left" - :_left_title - elseif loc == "right" - :_right_title - else - :title - end) - func."set_text"(sp[:title]) - func."set_fontsize"(py_thickness_scale(plt, sp[:titlefontsize])) - func."set_family"(sp[:titlefontfamily]) - func."set_math_fontfamily"(py_get_matching_math_font(sp[:titlefontfamily])) - func."set_color"(py_color(sp[:titlefontcolor])) - # ax[:set_title](sp[:title], loc = loc) - end - - # add the colorbar legend - if hascolorbar(sp) - # add keyword args for a discrete colorbar - slist = series_list(sp) - colorbar_series = slist[findfirst(hascolorbar.(slist))] - handle = colorbar_series[:serieshandle][end] - kw = KW() - if !isempty(sp[:zaxis][:discrete_values]) && - colorbar_series[:seriestype] === :heatmap - locator, formatter = get_locator_and_formatter(sp[:zaxis][:discrete_values]) - # kw[:values] = eachindex(sp[:zaxis][:discrete_values]) - kw[:values] = sp[:zaxis][:continuous_values] - 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, :marker_z) - ) - cmin, cmax = get_clims(sp) - norm = pycolors."Normalize"(vmin = cmin, vmax = cmax) - f = if colorbar_series[:line_z] !== nothing - py_linecolormap - elseif colorbar_series[:fill_z] !== nothing - py_fillcolormap - else - py_markercolormap - end - cmap = pycmap."ScalarMappable"(norm = norm, cmap = f(colorbar_series)) - cmap."set_array"([]) - handle = cmap - end - kw[:spacing] = "proportional" - - if RecipesPipeline.is3d(sp) || ispolar(sp) - cbax = fig."add_axes"( - [0.9, 0.1, 0.03, 0.8], - label = string("cbar", sp[:subplot_index]), - ) - cb = fig."colorbar"(handle; cax = cbax, kw...) - else - # divider approach works only with 2d plots - divider = axes_grid1.make_axes_locatable(ax) - # width = axes_grid1.axes_size.AxesY(ax, aspect=1.0 / 3.5) - # pad = axes_grid1.axes_size.Fraction(0.5, width) # Colorbar is spaced 0.5 of its size away from the ax - # cbax = divider.append_axes("right", size=width, pad=pad) # This approach does not work well in subplots - colorbar_position, colorbar_pad, colorbar_orientation = - if sp[:colorbar] === :left - string(sp[:colorbar]), "5%", "vertical" - elseif sp[:colorbar] === :top - string(sp[:colorbar]), "2.5%", "horizontal" - elseif sp[:colorbar] === :bottom - string(sp[:colorbar]), "5%", "horizontal" - else - "right", "2.5%", "vertical" - end - - cbax = divider.append_axes( - colorbar_position, - size = "5%", - pad = colorbar_pad, - label = string("cbar", sp[:subplot_index]), - ) # Reasonable value works most of the usecases - cb = fig."colorbar"( - handle; - cax = cbax, - orientation = colorbar_orientation, - kw..., - ) - - if sp[:colorbar] === :left - cbax.yaxis.set_ticks_position("left") - elseif sp[:colorbar] === :top - cbax.xaxis.set_ticks_position("top") - elseif sp[:colorbar] === :bottom - cbax.xaxis.set_ticks_position("bottom") - end - end - - cb."set_label"( - sp[:colorbar_title], - size = py_thickness_scale(plt, sp[:colorbar_titlefontsize]), - family = sp[:colorbar_titlefontfamily], - math_fontfamily = py_get_matching_math_font(sp[:colorbar_titlefontfamily]), - color = py_color(sp[:colorbar_titlefontcolor]), - ) - - # cb."formatter".set_useOffset(false) # This for some reason does not work, must be a pyplot bug, instead this is a workaround: - cb."formatter".set_powerlimits((-Inf, Inf)) - cb."update_ticks"() - - ticks = get_colorbar_ticks(sp) - axis, cbar_axis, ticks_letter = if sp[:colorbar] in (:top, :bottom) - sp[:xaxis], cb."ax"."xaxis", :x # colorbar inherits from x axis - else - sp[:yaxis], cb."ax"."yaxis", :y # colorbar inherits from y axis - end - py_set_scale(cb.ax, sp, sp[:colorbar_scale], ticks_letter) - sp[:colorbar_ticks] === :native || py_set_ticks(sp, cb.ax, ticks, ticks_letter) - - for lab in cbar_axis."get_ticklabels"() - lab."set_fontsize"(py_thickness_scale(plt, sp[:colorbar_tickfontsize])) - lab."set_family"(sp[:colorbar_tickfontfamily]) - lab."set_math_fontfamily"( - py_get_matching_math_font(sp[:colorbar_tickfontfamily]), - ) - lab."set_color"(py_color(sp[:colorbar_tickfontcolor])) - end - - # Adjust thickness of the cbar ticks - intensity = 0.5 - cbar_axis."set_tick_params"( - direction = axis[:tick_direction] === :out ? "out" : "in", - width = py_thickness_scale(plt, intensity), - length = axis[:tick_direction] === :none ? 0 : - 5py_thickness_scale(plt, intensity), - ) - - cb.outline."set_linewidth"(py_thickness_scale(plt, 1)) - - sp.attr[:cbar_handle] = cb - sp.attr[:cbar_ax] = cbax - end - - # framestyle - if !ispolar(sp) && !RecipesPipeline.is3d(sp) - for pos in ("left", "right", "top", "bottom") - # Scale all axes by default first - getproperty(ax.spines, pos)."set_linewidth"(py_thickness_scale(plt, 1)) - end - - # Then set visible some of them - if sp[:framestyle] === :semi - intensity = 0.5 - - pyspine = getproperty(ax.spines, sp[:yaxis][:mirror] ? "left" : "right") - pyspine."set_alpha"(intensity) - pyspine."set_linewidth"(py_thickness_scale(plt, intensity)) - - pyspine = getproperty(ax.spines, sp[:xaxis][:mirror] ? "bottom" : "top") - pyspine."set_linewidth"(py_thickness_scale(plt, intensity)) - pyspine."set_alpha"(intensity) - elseif sp[:framestyle] === :box - ax.tick_params(top = true) # Add ticks too - ax.tick_params(right = true) # Add ticks too - elseif sp[:framestyle] in (:axes, :origin) - getproperty(ax.spines, sp[:xaxis][:mirror] ? "bottom" : "top")."set_visible"( - false, - ) - getproperty(ax.spines, sp[:yaxis][:mirror] ? "left" : "right")."set_visible"( - false, - ) - if sp[:framestyle] === :origin - ax.spines."bottom"."set_position"("zero") - ax.spines."left"."set_position"("zero") - end - elseif sp[:framestyle] in (:grid, :none, :zerolines) - py_hide_spines(ax) - if sp[:framestyle] === :zerolines - ax."axhline"( - y = 0, - color = py_color(sp[:xaxis][:foreground_color_axis]), - lw = py_thickness_scale(plt, 0.75), - ) - ax."axvline"( - x = 0, - color = py_color(sp[:yaxis][:foreground_color_axis]), - lw = py_thickness_scale(plt, 0.75), - ) - end - end - - if sp[:xaxis][:mirror] - ax.xaxis."set_label_position"("top") # the guides - sp[:framestyle] === :box || ax.xaxis."tick_top"() - end - - if sp[:yaxis][:mirror] - ax.yaxis."set_label_position"("right") # the guides - sp[:framestyle] === :box || ax.yaxis."tick_right"() - end - end - - # axis attributes - for letter in (:x, :y, :z) - axissym = get_attr_symbol(letter, :axis) - PyPlot.PyCall.hasproperty(ax, axissym) || continue - axis = sp[axissym] - pyaxis = getproperty(ax, axissym) - - if axis[:guide_position] !== :auto && letter !== :z - pyaxis."set_label_position"(axis[:guide_position]) - end - - py_set_scale(ax, sp, axis) - py_set_lims(ax, sp, axis) - (ispolar(sp) && letter === :y) && ax."set_rlabel_position"(90) - ticks = sp[:framestyle] === :none ? nothing : get_ticks(sp, axis) - - # don't show the 0 tick label for the origin framestyle - if sp[:framestyle] === :origin && length(ticks) > 1 - ticks[2][ticks[1] .== 0] .= "" - end - - # Set ticks - fontProperties = Dict( - "family" => axis[:tickfontfamily], - "math_fontfamily" => py_get_matching_math_font(axis[:tickfontfamily]), - "size" => py_thickness_scale(plt, axis[:tickfontsize]), - "rotation" => axis[:tickfontrotation], - ) - - positions = getproperty(ax, Symbol("get_", letter, "ticks"))() - pyaxis.set_major_locator(pyticker.FixedLocator(positions)) - - kw = if RecipesPipeline.is3d(sp) - NamedTuple(Symbol(k) => v for (k, v) in fontProperties) - else - (; fontdict = PyPlot.PyCall.PyDict(fontProperties)) - end - - getproperty(ax, Symbol("set_", letter, "ticklabels"))(positions; kw...) - - py_set_ticks(sp, ax, ticks, letter) - - if axis[:ticks] === :native # it is easier to reset than to account for this - py_set_lims(ax, sp, axis) - pyaxis.set_major_locator(pyticker.AutoLocator()) - pyaxis.set_major_formatter(pyticker.ScalarFormatter()) - end - - # Tick marks - intensity = 0.5 # this value corresponds to scaling of other grid elements - pyaxis."set_tick_params"( - direction = axis[:tick_direction] === :out ? "out" : "in", - width = py_thickness_scale(plt, intensity), - length = axis[:tick_direction] === :none ? 0 : - 5py_thickness_scale(plt, intensity), - ) - - getproperty(ax, Symbol("set_", letter, "label"))(axis[:guide]) - if get(axis.plotattributes, :flip, false) - getproperty(ax, Symbol("invert_", letter, "axis"))() - end - pyaxis."label"."set_fontsize"(py_thickness_scale(plt, axis[:guidefontsize])) - pyaxis."label"."set_family"(axis[:guidefontfamily]) - pyaxis."label"."set_math_fontfamily"( - py_get_matching_math_font(axis[:guidefontfamily]), - ) - - RecipesPipeline.is3d(sp) && pyaxis."set_rotate_label"(false) - - if letter === :y && !RecipesPipeline.is3d(sp) - axis[:guidefontrotation] + 90 - else - axis[:guidefontrotation] - end |> pyaxis."label"."set_rotation" - - if axis[:grid] && ticks ∉ (:none, nothing, false) - pyaxis."grid"( - true, - color = py_color(axis[:foreground_color_grid]), - linestyle = py_linestyle(:line, axis[:gridstyle]), - linewidth = py_thickness_scale(plt, axis[:gridlinewidth]), - alpha = axis[:gridalpha], - ) - ax."set_axisbelow"(true) - else - pyaxis."grid"(false) - end - - n_minor_intervals = axis[:minorticks] - if !no_minor_intervals(axis) && n_minor_intervals isa Integer - n_minor_intervals isa Bool || pyaxis."set_minor_locator"( - # NOTE: AutoMinorLocator expects a number of intervals - PyPlot.matplotlib.ticker.AutoMinorLocator(n_minor_intervals), - ) - pyaxis."set_tick_params"( - which = "minor", - direction = axis[:tick_direction] === :out ? "out" : "in", - length = axis[:tick_direction] === :none ? 0 : - py_thickness_scale(plt, intensity), - ) - end - - if axis[:minorgrid] - no_minor_intervals(axis) || ax."minorticks_on"() # Check if ticks were already configured - pyaxis."set_tick_params"( - which = "minor", - direction = axis[:tick_direction] === :out ? "out" : "in", - length = axis[:tick_direction] === :none ? 0 : - py_thickness_scale(plt, intensity), - ) - - pyaxis."grid"( - true, - which = "minor", - color = py_color(axis[:foreground_color_grid]), - linestyle = py_linestyle(:line, axis[:minorgridstyle]), - linewidth = py_thickness_scale(plt, axis[:minorgridlinewidth]), - alpha = axis[:minorgridalpha], - ) - end - - py_set_axis_colors(sp, ax, axis) - end - - # showaxis - if !sp[:xaxis][:showaxis] - kw = KW() - ispolar(sp) && ax.spines."polar".set_visible(false) - for dir in (:top, :bottom) - ispolar(sp) || getproperty(ax.spines, string(dir)).set_visible(false) - kw[dir] = kw[get_attr_symbol(:label, dir)] = false - end - ax."xaxis"."set_tick_params"(; which = "both", kw...) - end - if !sp[:yaxis][:showaxis] - kw = KW() - for dir in (:left, :right) - ispolar(sp) || getproperty(ax.spines, string(dir)).set_visible(false) - kw[dir] = kw[get_attr_symbol(:label, dir)] = false - end - ax."yaxis"."set_tick_params"(; which = "both", kw...) - end - - # aspect ratio - if (ratio = get_aspect_ratio(sp)) !== :none - if RecipesPipeline.is3d(sp) - if ratio === :auto - nothing - elseif ratio === :equal - ax."set_box_aspect"((1, 1, 1)) - else - ax."set_box_aspect"(ratio) - end - else - ax."set_aspect"(isa(ratio, Symbol) ? string(ratio) : ratio, anchor = "C") - end - end - - # camera/view angle - if RecipesPipeline.is3d(sp) - # convert azimuth to match GR behaviour - azimuth, elevation = sp[:camera] .- (90, 0) - ax."view_init"(elevation, azimuth) - end - - # legend - py_add_legend(plt, sp, ax) - - # this sets the bg color inside the grid - ax."set_facecolor"(py_color(sp[:background_color_inside])) - - # link axes - x_ax_link, y_ax_link = sp[:xaxis].sps[1].o, sp[:yaxis].sps[1].o - if (twinx = ax != x_ax_link) - ax."get_shared_x_axes"()."join"(ax, x_ax_link) - end - if (twiny = ax != y_ax_link) - ax."get_shared_y_axes"()."join"(ax, y_ax_link) - end - end - py_drawfig(fig) -end - -expand_padding!(padding, bb, plotbb) = - if ispositive(width(bb)) && ispositive(height(bb)) - padding[1] = max(padding[1], left(plotbb) - left(bb)) - padding[2] = max(padding[2], top(plotbb) - top(bb)) - padding[3] = max(padding[3], right(bb) - right(plotbb)) - padding[4] = max(padding[4], bottom(bb) - bottom(plotbb)) - end - -# Set the (left, top, right, bottom) minimum padding around the plot area -# to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{PyPlotBackend}) - (ax = sp.o) === nothing && return sp.minpad - plotbb = py_bbox(ax) - - # TODO: this should initialize to the margin from sp.attr - # figure out how much the axis components and title "stick out" from the plot area - padding = [0mm, 0mm, 0mm, 0mm] # leftpad, toppad, rightpad, bottompad - - for bb in ( - py_bbox_axis(ax, "x"), - py_bbox_axis(ax, "y"), - py_bbox_title(ax), - py_bbox_legend(ax), - ) - expand_padding!(padding, bb, plotbb) - end - - if haskey(sp.attr, :cbar_ax) # Treat colorbar the same way - cbar_ax = sp.attr[:cbar_handle]."ax" - for bb in - (py_bbox_axis(cbar_ax, "x"), py_bbox_axis(cbar_ax, "y"), py_bbox_title(cbar_ax)) - expand_padding!(padding, bb, plotbb) - end - end - - # optionally add the width of colorbar labels and colorbar to rightpad - if RecipesPipeline.is3d(sp) - expand_padding!(padding, py_bbox_axis(ax, "z"), plotbb) - if haskey(sp.attr, :cbar_ax) - sp.attr[:cbar_bbox] = py_bbox(sp.attr[:cbar_handle]."ax") - end - end - - # add in the user-specified margin - padding .+= [sp[:left_margin], sp[:top_margin], sp[:right_margin], sp[:bottom_margin]] - - dpi_factor = Plots.DPI / sp.plt[:dpi] - - sp.minpad = Tuple(dpi_factor .* padding) -end - -# ----------------------------------------------------------------- - -function py_add_annotations(sp::Subplot{PyPlotBackend}, x, y, val) - ax = sp.o - ax."annotate"(val, xy = (x, y), zorder = 999, annotation_clip = false) -end - -function py_add_annotations(sp::Subplot{PyPlotBackend}, x, y, val::PlotText) - ax = sp.o - ax."annotate"( - val.str, - xy = (x, y), - family = val.font.family, - color = py_color(val.font.color), - horizontalalignment = val.font.halign === :hcenter ? "center" : - string(val.font.halign), - verticalalignment = val.font.valign === :vcenter ? "center" : - string(val.font.valign), - rotation = val.font.rotation, - size = py_thickness_scale(sp.plt, val.font.pointsize), - zorder = 999, - annotation_clip = false, - ) -end - -# ----------------------------------------------------------------- - -py_legend_pos(pos::Tuple{S,T}) where {S<:Real,T<:Real} = "lower left" - -function py_legend_pos(pos::Tuple{<:Real,Symbol}) - (s, c) = sincosd(pos[1]) - if pos[2] === :outer - s = -s - c = -c - end - yanchors = "lower", "center", "upper" - xanchors = "left", "center", "right" - join([yanchors[legend_anchor_index(s)], xanchors[legend_anchor_index(c)]], ' ') -end - -function py_legend_bbox(pos::Tuple{T,Symbol}) where {T<:Real} - pos[2] === :outer && - return legend_pos_from_angle(pos[1], -0.15, 0.5, 1.0, -0.15, 0.5, 1.0) - legend_pos_from_angle(pos[1], 0.0, 0.5, 1.0, 0.0, 0.5, 1.0) -end - -py_legend_bbox(pos) = pos - -function py_add_legend(plt::Plot, sp::Subplot, ax) - (leg = sp[:legend_position]) === :none && return - - # gotta do this to ensure both axes are included - labels, handles = [], [] - nseries = 0 - for series in series_list(sp) - should_add_to_legend(series) || continue - nseries += 1 - clims = get_clims(sp, series) - # add a line/marker and a label - if series[:seriestype] === :shape || series[:fillrange] !== nothing - lc = get_linecolor(series, clims) - fc = get_fillcolor(series, clims) - la = get_linealpha(series) - fa = get_fillalpha(series) - ls = get_linestyle(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", - ) - push!(handles, line_handle) - - # 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", - ) - - # plot two handles on top of each other by passing in a tuple - # https://matplotlib.org/stable/tutorials/intermediate/legend_guide.html - push!(handles, fill_handle) - end - elseif series[:seriestype] in - (:path, :straightline, :scatter, :steppre, :stepmid, :steppost) - has_line = 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, - has_line * sp[:legend_font_pointsize] / 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.8sp[:legend_font_pointsize]), - 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.8get_markerstrokewidth(series) * sp[:legend_font_pointsize] / - 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 - - # if anything was added, call ax.legend and set the colors - if !isempty(handles) - leg = legend_angle(leg) - ncol = if (lc = sp[:legend_column]) < 0 - nseries - elseif lc > 1 - lc == nseries || - @warn "n° of legend_column=$lc is not compatible with n° of series=$nseries" - nseries - else - 1 - end - leg = ax."legend"( - handles, - labels; - loc = py_legend_pos(leg), - bbox_to_anchor = py_legend_bbox(leg), - scatterpoints = 1, - fontsize = py_thickness_scale(plt, sp[:legend_font_pointsize]), - facecolor = py_color(sp[:legend_background_color]), - edgecolor = py_color(sp[:legend_foreground_color]), - framealpha = alpha(plot_color(sp[:legend_background_color])), - fancybox = false, # makes the legend box square - borderpad = 0.8, # to match GR legendbox - ncol, - ) - leg."get_frame"()."set_linewidth"(py_thickness_scale(plt, 1)) - leg."set_zorder"(1_000) - if sp[:legend_title] !== nothing - leg."set_title"(sp[:legend_title]) - PyPlot.plt."setp"( - leg."get_title"(), - color = py_color(sp[:legend_title_font_color]), - family = sp[:legend_title_font_family], - fontsize = py_thickness_scale(plt, sp[:legend_title_font_pointsize]), - ) - end - - for txt in leg."get_texts"() - PyPlot.plt."setp"( - txt, - color = py_color(sp[:legend_font_color]), - family = sp[:legend_font_family], - fontsize = py_thickness_scale(plt, sp[:legend_font_pointsize]), - ) - end - end -end - -# ----------------------------------------------------------------- - -# Use the bounding boxes (and methods left/top/right/bottom/width/height) `sp.bbox` and `sp.plotarea` to -# position the subplot in the backend. -function _update_plot_object(plt::Plot{PyPlotBackend}) - for sp in plt.subplots - (ax = sp.o) === nothing && return - figw, figh = sp.plt[:size] - figw, figh = figw * px, figh * px - pcts = bbox_to_pcts(sp.plotarea, figw, figh) - ax."set_position"(pcts) - - if haskey(sp.attr, :cbar_ax) && RecipesPipeline.is3d(sp) # 2D plots are completely handled by axis dividers - bb = sp.attr[:cbar_bbox] - # this is the bounding box of just the colors of the colorbar (not labels) - pad = 2mm - cb_bbox = BoundingBox( - right(sp.bbox) - 2width(bb) - 2pad, # x0 - top(sp.bbox) + pad, # y0 - width(bb), # width - height(sp.bbox) - 2pad, # height - ) - pcts = get( - sp[:extra_kwargs], - "3d_colorbar_axis", - bbox_to_pcts(cb_bbox, figw, figh), - ) - - sp.attr[:cbar_ax]."set_position"(pcts) - end - end - PyPlot.draw() -end - -# ----------------------------------------------------------------- -# display/output - -_display(plt::Plot{PyPlotBackend}) = plt.o."show"() - -for (mime, fmt) in ( - "application/eps" => "eps", - "image/eps" => "eps", - "application/pdf" => "pdf", - "image/png" => "png", - "application/postscript" => "ps", - "image/svg+xml" => "svg", - "application/x-tex" => "pgf", -) - @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PyPlotBackend}) - fig = plt.o - fig."canvas"."print_figure"( - io, - format = $fmt, - # bbox_inches = "tight", - # figsize = map(px2inch, plt[:size]), - facecolor = fig."get_facecolor"(), - edgecolor = "none", - dpi = plt[:dpi], - ) - end -end - -closeall(::PyPlotBackend) = PyPlot.plt."close"("all") - -# COV_EXCL_STOP diff --git a/src/backends/nobackend.jl b/src/backends/nobackend.jl new file mode 100644 index 000000000..0135b9c1b --- /dev/null +++ b/src/backends/nobackend.jl @@ -0,0 +1,15 @@ +struct NoBackend <: AbstractBackend end + +backend_name(::NoBackend) = :none + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + @eval begin + $f1(::NoBackend, $s::Symbol) = true + $f2(::NoBackend) = $(getproperty(Commons, Symbol("_all_", s, 's'))) + end +end + +_display(::Plot{NoBackend}) = + @info "No backend activated yet. Load the backend library and call the activation function to do so.\nE.g. `import GR; gr()` activates the GR backend." diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 1c251c639..d4b7a4b7d 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -1,18 +1,196 @@ # https://plot.ly/javascript/getting-started +module Plotly -_plotly_framestyle(style::Symbol) = - if style in (:box, :axes, :zerolines, :grid, :none) - style - else - default_style = get((semi = :box, origin = :zerolines), style, :axes) - @warn "Framestyle :$style is not supported by Plotly and PlotlyJS. :$default_style was chosen instead." - default_style - end - -# -------------------------------------------------------------------------------------- +export PlotlyBackend, plotly_show_js, plotly_series, plotly_layout, embeddable_html using UUIDs - +using Statistics: mean +using Plots: bbox_to_pcts, labelfunc_tex, is_2tuple, ticks_type, recursive_merge +using Plots.Annotations +using Plots.Axes +using Plots.Colorbars +using Plots.Colors: Colorant +using Plots.Commons +using Plots.Fonts +using Plots.Fonts: PlotText +using Plots.PlotMeasures +using Plots.PlotsPlots +using Plots.PlotsSeries +using Plots.PlotUtils: PlotUtils, ColorGradient, rgba_string, rgb_string +using Plots.RecipesPipeline: RecipesPipeline +using Plots.Subplots +using Plots.Surfaces +using Plots.Ticks +import Plots: labelfunc, _show, _display, default_output_format +import Plots: backend_name, backend_package_name + +struct PlotlyBackend <: Plots.AbstractBackend end +Plots._backendType[:plotly] = PlotlyBackend +Plots._backendSymbol[PlotlyBackend] = :plotly + +push!(Plots._initialized_backends, :plotly) +backend_name(::PlotlyBackend) = :plotly +backend_package_name(::PlotlyBackend) = backend_package_name(:plotly) + +const _plotly_attrs = merge_with_base_supported([ + :annotations, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :legend_foreground_color, + :foreground_color_guide, + :foreground_color_grid, + :foreground_color_axis, + :foreground_color_text, + :foreground_color_border, + :foreground_color_title, + :label, + :seriescolor, + :seriesalpha, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :markerstrokestyle, + :fill, + :fillrange, + :fillcolor, + :fillalpha, + :fontfamily, + :fontfamily_subplot, + :bins, + :title, + :titlelocation, + :titlefontfamily, + :titlefontsize, + :titlefonthalign, + :titlefontvalign, + :titlefontcolor, + :legend_column, + :legend_font, + :legend_font_family, + :legend_font_pointsize, + :legend_font_color, + :legend_title, + :legend_title_font_color, + :legend_title_font_family, + :legend_title_font_pointsize, + :tickfontfamily, + :tickfontsize, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefontcolor, + :window_title, + :arrow, + :guide, + :widen, + :lims, + :line, + :ticks, + :scale, + :flip, + :rotation, + :tickfont, + :guidefont, + :legendfont, + :grid, + :gridalpha, + :gridlinewidth, + :legend, + :colorbar, + :colorbar_title, + :colorbar_entry, + :marker_z, + :fill_z, + :line_z, + :levels, + :ribbon, + :quiver, + :orientation, + # :overwrite_figure, + :polar, + :plot_title, + :plot_titlefontcolor, + :plot_titlefontfamily, + :plot_titlefontsize, + :plot_titlelocation, + :plot_titlevspan, + :normalize, + :weights, + # :contours, + :aspect_ratio, + :hover, + :inset_subplots, + :bar_width, + :clims, + :framestyle, + :tick_direction, + :camera, + :contour_labels, + :connections, + :xformatter, + :xshowaxis, + :xguidefont, + :yformatter, + :yshowaxis, + :yguidefont, + :zformatter, + :zguidefont, +]) + +const _plotly_seriestypes = [ + :path, + :scatter, + :heatmap, + :contour, + :surface, + :wireframe, + :path3d, + :scatter3d, + :shape, + :scattergl, + :straightline, + :mesh3d, +] +const _plotly_styles = [:auto, :solid, :dash, :dot, :dashdot] +const _plotly_markers = [ + :none, + :auto, + :circle, + :rect, + :diamond, + :utriangle, + :dtriangle, + :cross, + :xcross, + :pentagon, + :hexagon, + :octagon, + :vline, + :hline, + :x, +] +const _plotly_scales = [:identity, :log10] + +default_output_format(plt::Plot{PlotlyBackend}) = "html" + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_plotly_", s, "s") + eval(quote + Plots.$f1(::PlotlyBackend, $s::Symbol) = $s in $v + Plots.$f2(::PlotlyBackend) = sort(collect($v)) + end) +end # ---------------------------------------------------------------- function labelfunc(scale::Symbol, backend::PlotlyBackend) @@ -25,6 +203,15 @@ function labelfunc(scale::Symbol, backend::PlotlyBackend) end end +_plotly_framestyle(style::Symbol) = + if style in (:box, :axes, :zerolines, :grid, :none) + style + else + default_style = get((semi = :box, origin = :zerolines), style, :axes) + @warn "Framestyle :$style is not supported by Plotly and PlotlyJS. :$default_style was chosen instead." + default_style + end + plotly_font(font::Font, color = font.color) = KW( :family => font.family, :size => round(Int, 1.4font.pointsize), @@ -159,7 +346,7 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing) # ticks if axis[:ticks] !== :native ticks = get_ticks(sp, axis) - ttype = ticksType(ticks) + ttype = ticks_type(ticks) if ttype === :ticks ax[:tickmode] = "array" ax[:tickvals] = ticks @@ -1134,3 +1321,4 @@ _show(io::IO, ::MIME"application/vnd.plotly.v1+json", plot::Plot{PlotlyBackend}) _show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = write(io, embeddable_html(plt)) _display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) +end # module diff --git a/src/backends/plotlybase.jl b/src/backends/plotlybase.jl deleted file mode 100644 index 8e614a48e..000000000 --- a/src/backends/plotlybase.jl +++ /dev/null @@ -1,27 +0,0 @@ -function plotly_traces(plt::Plot) - traces = PlotlyBase.GenericTrace[] - for series_dict in plotly_series(plt) - plotly_type = pop!(series_dict, :type) - push!(traces, PlotlyBase.GenericTrace(plotly_type; series_dict...)) - end - return traces -end - -function plotlybase_syncplot(plt::Plot) - plt.o = PlotlyBase.Plot() - PlotlyBase.addtraces!(plt.o, plotly_traces(plt)...) - layout = plotly_layout(plt) - w, h = plt[:size] - PlotlyBase.relayout!(plt.o, layout, width = w, height = h) - return plt.o -end - -for (mime, fmt) in ( - "application/pdf" => "pdf", - "image/png" => "png", - "image/svg+xml" => "svg", - "image/eps" => "eps", -) - @eval _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyBackend}) = - PlotlyKaleido.savefig(io, plotlybase_syncplot(plt), format = $fmt) -end diff --git a/src/components.jl b/src/components.jl deleted file mode 100644 index 665b518f3..000000000 --- a/src/components.jl +++ /dev/null @@ -1,814 +0,0 @@ -const P2 = NTuple{2,Float64} -const P3 = NTuple{3,Float64} - -const _haligns = :hcenter, :left, :right -const _valigns = :vcenter, :top, :bottom - -nanpush!(a::AVec{P2}, b) = (push!(a, (NaN, NaN)); push!(a, b); nothing) -nanappend!(a::AVec{P2}, b) = (push!(a, (NaN, NaN)); append!(a, b); nothing) -nanpush!(a::AVec{P3}, b) = (push!(a, (NaN, NaN, NaN)); push!(a, b); nothing) -nanappend!(a::AVec{P3}, b) = (push!(a, (NaN, NaN, NaN)); append!(a, b); nothing) - -compute_angle(v::P2) = (angle = atan(v[2], v[1]); angle < 0 ? 2π - angle : angle) - -# ------------------------------------------------------------- - -struct Shape{X<:Number,Y<:Number} - x::Vector{X} - y::Vector{Y} -end - -""" - Shape(x, y) - Shape(vertices) - -Construct a polygon to be plotted -""" -Shape(verts::AVec) = Shape(RecipesPipeline.unzip(verts)...) -Shape(s::Shape) = deepcopy(s) -function Shape(x::AVec{X}, y::AVec{Y}) where {X,Y} - return Shape(convert(Vector{X}, x), convert(Vector{Y}, y)) -end - -get_xs(shape::Shape) = shape.x -get_ys(shape::Shape) = shape.y -vertices(shape::Shape) = collect(zip(shape.x, shape.y)) - -#deprecated -@deprecate shape_coords coords - -"return the vertex points from a Shape or Segments object" -coords(shape::Shape) = shape.x, shape.y - -coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shapes)) - -"get an array of tuples of points on a circle with radius `r`" -partialcircle(start_θ, end_θ, n = 20, r = 1) = - [(r * cos(u), r * sin(u)) for u in range(start_θ, stop = end_θ, length = n)] - -"interleave 2 vectors into each other (like a zipper's teeth)" -function weave(x, y; ordering = Vector[x, y]) - ret = eltype(x)[] - done = false - while !done - for o in ordering - try - push!(ret, popfirst!(o)) - catch - end - end - done = isempty(x) && isempty(y) - end - ret -end - -"create a star by weaving together points from an outer and inner circle. `n` is the number of arms" -function makestar(n; offset = -0.5, radius = 1.0) - z1 = offset * π - z2 = z1 + π / (n) - outercircle = partialcircle(z1, z1 + 2π, n + 1, radius) - innercircle = partialcircle(z2, z2 + 2π, n + 1, 0.4radius) - Shape(weave(outercircle, innercircle)) -end - -"create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle" -makeshape(n; offset = -0.5, radius = 1.0) = - Shape(partialcircle(offset * π, offset * π + 2π, n + 1, radius)) - -function makecross(; offset = -0.5, radius = 1.0) - z2 = offset * π - z1 = z2 - π / 8 - outercircle = partialcircle(z1, z1 + 2π, 9, radius) - innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius) - Shape( - weave( - outercircle, - innercircle, - ordering = Vector[outercircle, innercircle, outercircle], - ), - ) -end - -from_polar(angle, dist) = (dist * cos(angle), dist * sin(angle)) - -makearrowhead(angle; h = 2.0, w = 0.4, tip = from_polar(angle, h)) = Shape( - NTuple{2,Float64}[ - (0, 0), - from_polar(angle - 0.5π, w) .- tip, - from_polar(angle + 0.5π, w) .- tip, - (0, 0), - ], -) - -const _shapes = KW( - :circle => makeshape(20), - :rect => makeshape(4, offset = -0.25), - :diamond => makeshape(4), - :utriangle => makeshape(3, offset = 0.5), - :dtriangle => makeshape(3, offset = -0.5), - :rtriangle => makeshape(3, offset = 0.0), - :ltriangle => makeshape(3, offset = 1.0), - :pentagon => makeshape(5), - :hexagon => makeshape(6), - :heptagon => makeshape(7), - :octagon => makeshape(8), - :cross => makecross(offset = -0.25), - :xcross => makecross(), - :vline => Shape([(0, 1), (0, -1)]), - :hline => Shape([(1, 0), (-1, 0)]), - :star4 => makestar(4), - :star5 => makestar(5), - :star6 => makestar(6), - :star7 => makestar(7), - :star8 => makestar(8), -) - -Shape(k::Symbol) = deepcopy(_shapes[k]) - -# ----------------------------------------------------------------------- - -# uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon -"return the centroid of a Shape" -function center(shape::Shape) - x, y = coords(shape) - n = length(x) - A, Cx, Cy = 0, 0, 0 - for i in 1:n - ip1 = i == n ? 1 : i + 1 - A += x[i] * y[ip1] - x[ip1] * y[i] - end - A *= 0.5 - for i in 1:n - ip1 = i == n ? 1 : i + 1 - m = (x[i] * y[ip1] - x[ip1] * y[i]) - Cx += (x[i] + x[ip1]) * m - Cy += (y[i] + y[ip1]) * m - end - Cx / 6A, Cy / 6A -end - -function scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) - sx, sy = coords(shape) - cx, cy = c - for i in eachindex(sx) - sx[i] = (sx[i] - cx) * x + cx - sy[i] = (sy[i] - cy) * y + cy - end - shape -end - -""" - scale(shape, x, y = x, c = center(shape)) - scale!(shape, x, y = x, c = center(shape)) - -Scale shape by a factor. -""" -scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) = - scale!(deepcopy(shape), x, y, c) - -function translate!(shape::Shape, x::Real, y::Real = x) - sx, sy = coords(shape) - for i in eachindex(sx) - sx[i] += x - sy[i] += y - end - shape -end - -""" - translate(shape, x, y = x) - translate!(shape, x, y = x) - -Translate a Shape in space. -""" -translate(shape::Shape, x::Real, y::Real = x) = translate!(deepcopy(shape), x, y) - -rotate_x(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = - ((x - centerx) * cos(θ) - (y - centery) * sin(θ) + centerx) - -rotate_y(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = - ((y - centery) * cos(θ) + (x - centerx) * sin(θ) + centery) - -rotate(x::Real, y::Real, θ::Real, c) = (rotate_x(x, y, θ, c...), rotate_y(x, y, θ, c...)) - -function rotate!(shape::Shape, θ::Real, c = center(shape)) - x, y = coords(shape) - for i in eachindex(x) - xi = rotate_x(x[i], y[i], θ, c...) - yi = rotate_y(x[i], y[i], θ, c...) - x[i], y[i] = xi, yi - end - shape -end - -"rotate an object in space" -function rotate(shape::Shape, θ::Real, c = center(shape)) - x, y = coords(shape) - x_new = rotate_x.(x, y, θ, c...) - y_new = rotate_y.(x, y, θ, c...) - Shape(x_new, y_new) -end - -# ----------------------------------------------------------------------- - -mutable struct Font - family::AbstractString - pointsize::Int - halign::Symbol - valign::Symbol - rotation::Float64 - color::Colorant -end - -""" - font(args...) -Create a Font from a list of features. Values may be specified either as -arguments (which are distinguished by type/value) or as keyword arguments. -# Arguments -- `family`: AbstractString. "serif" or "sans-serif" or "monospace" -- `pointsize`: Integer. Size of font in points -- `halign`: Symbol. Horizontal alignment (:hcenter, :left, or :right) -- `valign`: Symbol. Vertical alignment (:vcenter, :top, or :bottom) -- `rotation`: Real. Angle of rotation for text in degrees (use a non-integer type) -- `color`: Colorant or Symbol -# Examples -```julia-repl -julia> font(8) -julia> font(family="serif", halign=:center, rotation=45.0) -``` -""" -function font(args...; kw...) - # defaults - family = "sans-serif" - pointsize = 14 - halign = :hcenter - valign = :vcenter - rotation = 0 - color = colorant"black" - - for arg in args - T = typeof(arg) - @assert arg !== :match - - if T == Font - family = arg.family - pointsize = arg.pointsize - halign = arg.halign - valign = arg.valign - rotation = arg.rotation - color = arg.color - elseif arg === :center - halign = :hcenter - valign = :vcenter - elseif arg ∈ _haligns - halign = arg - elseif arg ∈ _valigns - valign = arg - elseif T <: Colorant - color = arg - elseif T <: Symbol || T <: AbstractString - try - color = parse(Colorant, string(arg)) - catch - family = string(arg) - end - elseif T <: Integer - pointsize = arg - elseif T <: Real - rotation = convert(Float64, arg) - else - @warn "Unused font arg: $arg ($T)" - end - end - - for sym in keys(kw) - if sym === :family - family = string(kw[sym]) - elseif sym === :pointsize - pointsize = kw[sym] - elseif sym === :halign - halign = kw[sym] - halign === :center && (halign = :hcenter) - @assert halign ∈ _haligns - elseif sym === :valign - valign = kw[sym] - valign === :center && (valign = :vcenter) - @assert valign ∈ _valigns - elseif sym === :rotation - rotation = kw[sym] - elseif sym === :color - col = kw[sym] - color = col isa Colorant ? col : parse(Colorant, col) - else - @warn "Unused font kwarg: $sym" - end - end - - Font(family, pointsize, halign, valign, rotation, color) -end - -function scalefontsize(k::Symbol, factor::Number) - f = default(k) - f = round(Int, factor * f) - default(k, f) -end - -""" - scalefontsizes(factor::Number) - -Scales all **current** font sizes by `factor`. For example `scalefontsizes(1.1)` increases all current font sizes by 10%. To reset to initial sizes, use `scalefontsizes()` -""" -function scalefontsizes(factor::Number) - for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) - scalefontsize(k, factor) - end - - for letter in (:x, :y, :z) - for k in keys(_initial_ax_fontsizes) - scalefontsize(get_attr_symbol(letter, k), factor) - end - end -end - -""" - scalefontsizes() - -Resets font sizes to initial default values. -""" -function scalefontsizes() - for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) - f = default(k) - if k in keys(_initial_fontsizes) - factor = f / _initial_fontsizes[k] - scalefontsize(k, 1.0 / factor) - end - end - - for letter in (:x, :y, :z) - for k in keys(_initial_ax_fontsizes) - if k in keys(_initial_fontsizes) - f = default(get_attr_symbol(letter, k)) - factor = f / _initial_fontsizes[k] - scalefontsize(get_attr_symbol(letter, k), 1.0 / factor) - end - end - end -end - -resetfontsizes() = scalefontsizes() - -"Wrap a string with font info" -struct PlotText - str::AbstractString - font::Font -end -PlotText(str) = PlotText(string(str), font()) - -""" - text(string, args...; kw...) - -Create a PlotText object wrapping a string with font info, for plot annotations. -`args` and `kw` are passed to `font`. -""" -text(t::PlotText) = t -text(t::PlotText, font::Font) = PlotText(t.str, font) -text(str::AbstractString, f::Font) = PlotText(str, f) -text(str, args...; kw...) = PlotText(string(str), font(args...; kw...)) - -Base.length(t::PlotText) = length(t.str) - -is_horizontal(t::PlotText) = abs(sind(t.font.rotation)) ≤ sind(45) - -# ----------------------------------------------------------------------- - -struct Stroke - width - color - alpha - style -end - -""" - stroke(args...; alpha = nothing) - -Define the properties of the stroke used in plotting lines -""" -function stroke(args...; alpha = nothing) - width = 1 - color = :black - style = :solid - - for arg in args - T = typeof(arg) - - # if arg in _allStyles - if allStyles(arg) - style = arg - elseif T <: Colorant - color = arg - elseif T <: Symbol || T <: AbstractString - try - color = parse(Colorant, string(arg)) - catch - end - elseif allAlphas(arg) - alpha = arg - elseif allReals(arg) - width = arg - else - @warn "Unused stroke arg: $arg ($(typeof(arg)))" - end - end - - Stroke(width, color, alpha, style) -end - -struct Brush - size # fillrange, markersize, or any other sizey attribute - color - alpha -end - -function brush(args...; alpha = nothing) - size = 1 - color = :black - - for arg in args - T = typeof(arg) - - if T <: Colorant - color = arg - elseif T <: Symbol || T <: AbstractString - try - color = parse(Colorant, string(arg)) - catch - end - elseif allAlphas(arg) - alpha = arg - elseif allReals(arg) - size = arg - else - @warn "Unused brush arg: $arg ($(typeof(arg)))" - end - end - - Brush(size, color, alpha) -end - -# ----------------------------------------------------------------------- - -mutable struct SeriesAnnotations - strs::AVec # the labels/names - font::Font - baseshape::Union{Shape,AVec{Shape},Nothing} - scalefactor::Tuple -end - -_text_label(lab::Tuple, font) = text(lab[1], font, lab[2:end]...) -_text_label(lab::PlotText, font) = lab -_text_label(lab, font) = text(lab, font) - -series_annotations(scalar) = series_annotations([scalar]) -series_annotations(anns::SeriesAnnotations) = anns -series_annotations(::Nothing) = nothing - -function series_annotations(anns::AMat{SeriesAnnotations}) - @assert size(anns, 1) == 1 "matrix of SeriesAnnotations must be a row vector" - anns -end - -function series_annotations(anns::AMat, outer_args...) - # Types that represent annotations for an entire series - whole_series = Union{AVec,Tuple{AVec,Vararg{Any}}} - - # whole_series types can only be in a row vector - if size(anns, 1) > 1 - for ann in Iterators.filter(ann -> ann isa whole_series, anns) - "Given series annotation must be the only element in its column:\n$ann" |> - ArgumentError |> - throw - end - end - - ann_vec = map(eachcol(anns)) do col - ann = first(col) isa whole_series ? first(col) : col - - # Override arguments from outer tuple with args from inner tuple - strs, inner_args = Iterators.peel(wraptuple(ann)) - series_annotations(strs, outer_args..., inner_args...) - end - - permutedims(ann_vec) -end - -function series_annotations(strs::AVec, args...) - fnt = font() - shp = nothing - scalefactor = 1, 1 - for arg in args - if isa(arg, Shape) || (isa(arg, AVec) && eltype(arg) == Shape) - shp = arg - elseif isa(arg, Font) - fnt = arg - elseif isa(arg, Symbol) && haskey(_shapes, arg) - shp = _shapes[arg] - elseif isa(arg, Number) - scalefactor = arg, arg - elseif is_2tuple(arg) - scalefactor = arg - elseif isa(arg, AVec) - strs = collect(zip(strs, arg)) - else - @warn "Unused SeriesAnnotations arg: $arg ($(typeof(arg)))" - end - end - SeriesAnnotations(map(s -> _text_label(s, fnt), strs), fnt, shp, scalefactor) -end - -function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) - anns = series[:series_annotations] - - if anns !== nothing && anns.baseshape !== nothing - # we use baseshape to overwrite the markershape attribute - # with a list of custom shapes for each - msw, msh = anns.scalefactor - msize = Float64[] - shapes = Vector{Shape}(undef, length(anns.strs)) - for i in eachindex(anns.strs) - str = _cycle(anns.strs, i) - - # get the width and height of the string (in mm) - sw, sh = text_size(str, anns.font.pointsize) - - # how much to scale the base shape? - # note: it's a rough assumption that the shape fills the unit box [-1, -1, 1, 1], - # so we scale the length-2 shape by 1/2 the total length - scalar = backend() == PyPlotBackend() ? 1.7 : 1.0 - xscale = 0.5to_pixels(sw) * scalar - yscale = 0.5to_pixels(sh) * scalar - - # we save the size of the larger direction to the markersize list, - # and then re-scale a copy of baseshape to match the w/h ratio - maxscale = max(xscale, yscale) - push!(msize, maxscale) - baseshape = _cycle(anns.baseshape, i) - shapes[i] = - scale(baseshape, msw * xscale / maxscale, msh * yscale / maxscale, (0, 0)) - end - series[:markershape] = shapes - series[:markersize] = msize - end - nothing -end - -mutable struct EachAnn - anns - x - y -end - -function Base.iterate(ea::EachAnn, i = 1) - (ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y)) && return - - tmp = _cycle(ea.anns.strs, i) - str, fnt = if isa(tmp, PlotText) - tmp.str, tmp.font - else - tmp, ea.anns.font - end - (_cycle(ea.x, i), _cycle(ea.y, i), str, fnt), i + 1 -end - -# ----------------------------------------------------------------------- -annotations(anns::AMat) = map(annotations, anns) -annotations(sa::SeriesAnnotations) = sa -annotations(anns::AVec) = anns -annotations(anns) = Any[anns] -annotations(::Nothing) = [] - -_annotationfont(sp::Subplot) = font(; - family = sp[:annotationfontfamily], - pointsize = sp[:annotationfontsize], - halign = sp[:annotationhalign], - valign = sp[:annotationvalign], - rotation = sp[:annotationrotation], - color = sp[:annotationcolor], -) - -_annotation(sp::Subplot, font, lab, pos...; alphabet = "abcdefghijklmnopqrstuvwxyz") = ( - pos..., - lab === :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : - _text_label(lab, font), -) - -assign_annotation_coord!(axis, x) = discrete_value!(axis, x)[1] -assign_annotation_coord!(axis, x::TimeType) = assign_annotation_coord!(axis, Dates.value(x)) - -_annotation_coords(pos::Symbol) = get(_positionAliases, pos, pos) -_annotation_coords(pos) = pos - -function _process_annotation_2d(sp::Subplot, x, y, lab, font = _annotationfont(sp)) - x = assign_annotation_coord!(sp[:xaxis], x) - y = assign_annotation_coord!(sp[:yaxis], y) - _annotation(sp, font, lab, x, y) -end - -_process_annotation_2d( - sp::Subplot, - pos::Union{Tuple,Symbol}, - lab, - font = _annotationfont(sp), -) = _annotation(sp, font, lab, _annotation_coords(pos)) - -function _process_annotation_3d(sp::Subplot, x, y, z, lab, font = _annotationfont(sp)) - x = assign_annotation_coord!(sp[:xaxis], x) - y = assign_annotation_coord!(sp[:yaxis], y) - z = assign_annotation_coord!(sp[:zaxis], z) - _annotation(sp, font, lab, x, y, z) -end - -_process_annotation_3d( - sp::Subplot, - pos::Union{Tuple,Symbol}, - lab, - font = _annotationfont(sp), -) = _annotation(sp, font, lab, _annotation_coords(pos)) - -function _process_annotation(sp::Subplot, ann, annotation_processor::Function) - ann = makevec.(ann) - [annotation_processor(sp, _cycle.(ann, i)...) for i in 1:maximum(length.(ann))] -end - -# Expand arrays of coordinates, positions and labels into individual annotations -# and make sure labels are of type PlotText -process_annotation(sp::Subplot, ann) = - _process_annotation(sp, ann, is3d(sp) ? _process_annotation_3d : _process_annotation_2d) - -function _relative_position(xmin, xmax, pos::Length{:pct}, scale::Symbol) - # !TODO Add more scales in the future (asinh, sqrt) ? - if scale === :log || scale === :ln - exp(log(xmin) + pos.value * log(xmax / xmin)) - elseif scale === :log10 - exp10(log10(xmin) + pos.value * log10(xmax / xmin)) - elseif scale === :log2 - exp2(log2(xmin) + pos.value * log2(xmax / xmin)) - else # :identity (linear scale) - xmin + pos.value * (xmax - xmin) - end -end - -# annotation coordinates in pct -const position_multiplier = Dict( - :N => (0.5, 0.9), - :NE => (0.9, 0.9), - :E => (0.9, 0.5), - :SE => (0.9, 0.1), - :S => (0.5, 0.1), - :SW => (0.1, 0.1), - :W => (0.1, 0.5), - :NW => (0.1, 0.9), - :topleft => (0.1, 0.9), - :topcenter => (0.5, 0.9), - :topright => (0.9, 0.9), - :bottomleft => (0.1, 0.1), - :bottomcenter => (0.5, 0.1), - :bottomright => (0.9, 0.1), -) - -# Give each annotation coordinates based on specified position -locate_annotation(sp::Subplot, rel::Tuple, label::PlotText) = ( - map(1:length(rel), (:x, :y, :z)) do i, letter - _relative_position( - axis_limits(sp, letter)..., - rel[i] * pct, - sp[get_attr_symbol(letter, :axis)][:scale], - ) - end..., - label, -) - -locate_annotation(sp::Subplot, x, y, label::PlotText) = (x, y, label) -locate_annotation(sp::Subplot, x, y, z, label::PlotText) = (x, y, z, label) -locate_annotation(sp::Subplot, pos::Symbol, label::PlotText) = - locate_annotation(sp, position_multiplier[pos], label) - -# ----------------------------------------------------------------------- - -function expand_extrema!(a::Axis, surf::Surface) - ex = a[:extrema] - foreach(x -> expand_extrema!(ex, x), surf.surf) - ex -end - -"For the case of representing a surface as a function of x/y... can possibly avoid allocations." -struct SurfaceFunction <: AbstractSurface - f::Function -end - -# ----------------------------------------------------------------------- - -# # I don't want to clash with ValidatedNumerics, but this would be nice: -# ..(a::T, b::T) = (a, b) - -# ----------------------------------------------------------------------- - -# style is :open or :closed (for now) -struct Arrow - style::Symbol - side::Symbol # :head (default), :tail, or :both - headlength::Float64 - headwidth::Float64 -end - -""" - arrow(args...) - -Define arrowheads to apply to lines - args are `style` (`:open` or `:closed`), -`side` (`:head`, `:tail` or `:both`), `headlength` and `headwidth` -""" -function arrow(args...) - style, side = :simple, :head - headlength = headwidth = 0.3 - setlength = false - for arg in args - T = typeof(arg) - if T == Symbol - if arg in (:head, :tail, :both) - side = arg - else - style = arg - end - elseif T <: Number - # first we apply to both, but if there's more, then only change width after the first number - headwidth = Float64(arg) - if !setlength - headlength = headwidth - end - setlength = true - elseif T <: Tuple && length(arg) == 2 - headlength, headwidth = Float64(arg[1]), Float64(arg[2]) - else - @warn "Skipped arrow arg $arg" - end - end - Arrow(style, side, headlength, headwidth) -end - -# allow for do-block notation which gets called on every valid start/end pair which -# we need to draw an arrow -function add_arrows(func::Function, x::AVec, y::AVec) - for i in 2:length(x) - xyprev = (x[i - 1], y[i - 1]) - xy = (x[i], y[i]) - if ok(xyprev) && ok(xy) - if i == length(x) || !ok(x[i + 1], y[i + 1]) - # add the arrow from xyprev to xy - func(xyprev, xy) - end - end - end -end - -# ----------------------------------------------------------------------- -"create a BezierCurve for plotting" -mutable struct BezierCurve{T<:Tuple} - control_points::Vector{T} -end - -function (bc::BezierCurve)(t::Real) - p = (0.0, 0.0) - n = length(bc.control_points) - 1 - for i in 0:n - p = p .+ bc.control_points[i + 1] .* binomial(n, i) .* (1 - t)^(n - i) .* t^i - end - p -end - -@deprecate curve_points coords - -coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = - map(curve, Base.range(first(range), stop = last(range), length = n)) - -function extrema_plus_buffer(v, buffmult = 0.2) - vmin, vmax = ignorenan_extrema(v) - vdiff = vmax - vmin - buffer = vdiff * buffmult - vmin - buffer, vmax + buffer -end - -### Legend - -@add_attributes subplot struct Legend - background_color = :match - foreground_color = :match - position = :best - title = nothing - font::Font = font(8) - title_font::Font = font(11) - column = 1 -end :match = ( - :legend_font_family, - :legend_font_color, - :legend_title_font_family, - :legend_title_font_color, -) diff --git a/src/consts.jl b/src/consts.jl deleted file mode 100644 index 764ac3c7a..000000000 --- a/src/consts.jl +++ /dev/null @@ -1,96 +0,0 @@ - -const _deprecated_attributes = Dict{Symbol,Symbol}(:orientation => :permute) -const _all_defaults = KW[_series_defaults, _plot_defaults, _subplot_defaults] - -const _initial_defaults = deepcopy(_all_defaults) -const _initial_axis_defaults = deepcopy(_axis_defaults) - -# add defaults for the letter versions -const _axis_defaults_byletter = KW() - -reset_axis_defaults_byletter!() = - for letter in (:x, :y, :z) - _axis_defaults_byletter[letter] = KW() - for (k, v) in _axis_defaults - _axis_defaults_byletter[letter][k] = v - end - end -reset_axis_defaults_byletter!() - -# to be able to reset font sizes to initial values -const _initial_plt_fontsizes = - Dict(:plot_titlefontsize => _plot_defaults[:plot_titlefontsize]) - -const _initial_sp_fontsizes = Dict( - :titlefontsize => _subplot_defaults[:titlefontsize], - :legend_font_pointsize => _subplot_defaults[:legend_font_pointsize], - :legend_title_font_pointsize => _subplot_defaults[:legend_title_font_pointsize], - :annotationfontsize => _subplot_defaults[:annotationfontsize], - :colorbar_tickfontsize => _subplot_defaults[:colorbar_tickfontsize], - :colorbar_titlefontsize => _subplot_defaults[:colorbar_titlefontsize], -) - -const _initial_ax_fontsizes = Dict( - :tickfontsize => _axis_defaults[:tickfontsize], - :guidefontsize => _axis_defaults[:guidefontsize], -) - -const _initial_fontsizes = - merge(_initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes) - -const _internal_args = [ - :plot_object, - :series_plotindex, - :series_index, - :markershape_to_add, - :letter, - :idxfilter, -] - -const _axis_args = Set(keys(_axis_defaults)) -const _series_args = Set(keys(_series_defaults)) -const _subplot_args = Set(keys(_subplot_defaults)) -const _plot_args = Set(keys(_plot_defaults)) - -const _magic_axis_args = [:axis, :tickfont, :guidefont, :grid, :minorgrid] -const _magic_subplot_args = - [:title_font, :legend_font, :legend_title_font, :plot_title_font, :colorbar_titlefont] -const _magic_series_args = [:line, :marker, :fill] -const _all_magic_args = - Set(union(_magic_axis_args, _magic_series_args, _magic_subplot_args)) - -const _all_axis_args = union(_axis_args, _magic_axis_args) -const _lettered_all_axis_args = - Set([Symbol(letter, kw) for letter in (:x, :y, :z) for kw in _all_axis_args]) -const _all_subplot_args = union(_subplot_args, _magic_subplot_args) -const _all_series_args = union(_series_args, _magic_series_args) -const _all_plot_args = _plot_args - -const _all_args = - union(_lettered_all_axis_args, _all_subplot_args, _all_series_args, _all_plot_args) - -# add all pluralized forms to the _keyAliases dict -for arg in _all_args - add_aliases(arg, makeplural(arg)) -end - -# fill symbol cache -for letter in (:x, :y, :z) - _attrsymbolcache[letter] = Dict{Symbol,Symbol}() - for k in _axis_args - # populate attribute cache - lk = Symbol(letter, k) - _attrsymbolcache[letter][k] = lk - # allow the underscore version too: xguide or x_guide - add_aliases(lk, Symbol(letter, "_", k)) - end - for k in (_magic_axis_args..., :(_discrete_indices)) - _attrsymbolcache[letter][k] = Symbol(letter, k) - end -end - -# add all non_underscored forms to the _keyAliases -add_non_underscore_aliases!(_keyAliases) - -_generate_doclist(attributes) = - replace(join(sort(collect(attributes)), "\n- "), "_" => "\\_") diff --git a/src/examples.jl b/src/examples.jl index 230368392..80b588934 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -179,7 +179,8 @@ const _examples = PlotExample[ PlotExample( # 13 "Marker types", quote - markers = filter(m -> m in Plots.supported_markers(), Plots._shape_keys) + markers = + filter(m -> m in Plots.supported_markers(), Plots.Commons._shape_keys) markers = permutedims(markers) n = length(markers) x = range(0, stop = 10, length = n + 2)[2:(end - 1)] @@ -1249,8 +1250,7 @@ const _examples = PlotExample[ # Some constants for PlotDocs and PlotReferenceImages _animation_examples = [2, 31] _backend_skips = Dict( - :gr => [], - :pyplot => [], + :gr => [25, 30], # TODO: add back when StatsPlots is available :plotlyjs => [ 21, 24, @@ -1330,7 +1330,8 @@ _backend_skips = Dict( ], ) _backend_skips[:plotly] = _backend_skips[:plotlyjs] -_backend_skips[:pythonplot] = _backend_skips[:pyplot] + +_backend_skips[:pythonplot] = Int[] # --------------------------------------------------------------------------------- # replace `f(args...)` with `f(rng, args...)` for `f ∈ (rand, randn)` @@ -1365,7 +1366,7 @@ function test_examples( Base.eval(m, quote using Random using Plots - Plots.debug!($debug) + Plots.Commons.debug!($debug) backend($(QuoteNode(pkgname))) rng = $rng rng === nothing || Random.seed!(rng, Plots.PLOTS_SEED) diff --git a/src/init.jl b/src/init.jl index aa58c2bb9..4372982b9 100644 --- a/src/init.jl +++ b/src/init.jl @@ -57,75 +57,55 @@ function __init__() ) end |> atreplinit - @static if !isdefined(Base, :get_extension) # COV_EXCL_LINE - @require FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" include( - normpath(@__DIR__, "..", "ext", "FileIOExt.jl"), - ) - @require GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" include( - normpath(@__DIR__, "..", "ext", "GeometryBasicsExt.jl"), - ) - @require IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" include( - normpath(@__DIR__, "..", "ext", "IJuliaExt.jl"), - ) - @require ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" include( - normpath(@__DIR__, "..", "ext", "ImageInTerminalExt.jl"), - ) - @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include( - normpath(@__DIR__, "..", "ext", "UnitfulExt.jl"), - ) - end - - _runtime_init(backend()) nothing end ################################################################## -backend() -include(_path(backend_name())) # COV_EXCL_START -@setup_workload begin - @debug backend_package_name() - n = length(_examples) - imports = sizehint!(Expr[], n) - examples = sizehint!(Expr[], 10n) - for i in setdiff(1:n, _backend_skips[backend_name()], _animation_examples) - _examples[i].external && continue - (imp = _examples[i].imports) === nothing || push!(imports, imp) - func = gensym(string(i)) - push!( - examples, - quote - $func() = begin # evaluate each example in a local scope - $(_examples[i].exprs) - $i == 1 || return # only for one example - fn = tempname() - pl = current() - show(devnull, pl) - # FIXME: pgfplotsx requires bug - backend_name() === :pgfplotsx && return - if backend_name() === :unicodeplots - savefig(pl, "$fn.txt") - return - end - showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") - showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") - if showable(MIME"image/svg+xml"(), pl) - show(IOBuffer(), MIME"image/svg+xml"(), pl) - end - nothing - end - $func() - end, - ) - end - withenv("GKSwstype" => "nul") do - @compile_workload begin - load_default_backend() - eval.(imports) - eval.(examples) - end - end - CURRENT_PLOT.nullableplot = nothing -end -# COV_EXCL_STOP +# TODO: revise and re-enable before release +# @setup_workload begin +# @debug backend_package_name() +# n = length(_examples) +# imports = sizehint!(Expr[], n) +# examples = sizehint!(Expr[], 10n) +# for i in setdiff(1:n, _backend_skips[backend_name()], _animation_examples) +# _examples[i].external && continue +# (imp = _examples[i].imports) === nothing || push!(imports, imp) +# func = gensym(string(i)) +# push!( +# examples, +# quote +# $func() = begin # evaluate each example in a local scope +# $(_examples[i].exprs) +# $i == 1 || return # only for one example +# fn = tempname() +# pl = current() +# show(devnull, pl) +# # FIXME: pgfplotsx requires bug +# backend_name() === :pgfplotsx && return +# if backend_name() === :unicodeplots +# savefig(pl, "$fn.txt") +# return +# end +# showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") +# showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") +# if showable(MIME"image/svg+xml"(), pl) +# show(IOBuffer(), MIME"image/svg+xml"(), pl) +# end +# nothing +# end +# $func() +# end, +# ) +# end +# withenv("GKSwstype" => "nul") do +# @compile_workload begin +# load_default_backend() +# eval.(imports) +# eval.(examples) +# end +# end +# CURRENT_PLOT.nullableplot = nothing +# end +# # COV_EXCL_STOP diff --git a/src/layouts.jl b/src/layouts.jl index c671bb8ba..2f80680f0 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -3,10 +3,6 @@ to_pixels(m::AbsoluteLength) = m.value / 0.254 -const _cbar_width = 5mm -const DEFAULT_BBOX = Ref(BoundingBox(0mm, 0mm, 0mm, 0mm)) -const DEFAULT_MINPAD = Ref((20mm, 5mm, 2mm, 10mm)) - origin(bbox::BoundingBox) = left(bbox) + width(bbox) / 2, top(bbox) + height(bbox) / 2 left(bbox::BoundingBox) = bbox.x0[1] top(bbox::BoundingBox) = bbox.x0[2] @@ -422,10 +418,10 @@ end # constructors # pass the layout arg through -layout_args(plotattributes::AKW) = layout_args(plotattributes[:layout]) +layout_attrs(plotattributes::AKW) = layout_attrs(plotattributes[:layout]) -function layout_args(plotattributes::AKW, n_override::Integer) - layout, n = layout_args(n_override, get(plotattributes, :layout, n_override)) +function layout_attrs(plotattributes::AKW, n_override::Integer) + layout, n = layout_attrs(n_override, get(plotattributes, :layout, n_override)) if n < n_override error( "When doing layout, n ($n) < n_override ($(n_override)). You're probably trying to force existing plots into a layout that doesn't fit them.", @@ -434,60 +430,60 @@ function layout_args(plotattributes::AKW, n_override::Integer) layout, n end -function layout_args(n::Integer) +function layout_attrs(n::Integer) nr, nc = compute_gridsize(n, -1, -1) GridLayout(nr, nc), n end -function layout_args(sztup::NTuple{2,Integer}) +function layout_attrs(sztup::NTuple{2,Integer}) nr, nc = sztup GridLayout(nr, nc), nr * nc end -layout_args(n_override::Integer, n::Integer) = layout_args(n) -layout_args(n, sztup::NTuple{2,Integer}) = layout_args(sztup) +layout_attrs(n_override::Integer, n::Integer) = layout_attrs(n) +layout_attrs(n, sztup::NTuple{2,Integer}) = layout_attrs(sztup) -function layout_args(n, sztup::Tuple{Colon,Integer}) +function layout_attrs(n, sztup::Tuple{Colon,Integer}) nc = sztup[2] nr = ceil(Int, n / nc) GridLayout(nr, nc), n end -function layout_args(n, sztup::Tuple{Integer,Colon}) +function layout_attrs(n, sztup::Tuple{Integer,Colon}) nr = sztup[1] nc = ceil(Int, n / nr) GridLayout(nr, nc), n end -function layout_args(sztup::NTuple{3,Integer}) +function layout_attrs(sztup::NTuple{3,Integer}) n, nr, nc = sztup nr, nc = compute_gridsize(n, nr, nc) GridLayout(nr, nc), n end -layout_args(nt::NamedTuple) = EmptyLayout(; nt...), 1 +layout_attrs(nt::NamedTuple) = EmptyLayout(; nt...), 1 -function layout_args(m::AbstractVecOrMat) +function layout_attrs(m::AbstractVecOrMat) sz = size(m) nr = first(sz) nc = get(sz, 2, 1) gl = GridLayout(nr, nc) for ci in CartesianIndices(m) - gl[ci] = layout_args(m[ci])[1] + gl[ci] = layout_attrs(m[ci])[1] end - layout_args(gl) + layout_attrs(gl) end # recursively get the size of the grid -layout_args(layout::GridLayout) = layout, calc_num_subplots(layout) +layout_attrs(layout::GridLayout) = layout, calc_num_subplots(layout) -layout_args(n_override::Integer, layout::Union{AbstractVecOrMat,GridLayout}) = - layout_args(layout) +layout_attrs(n_override::Integer, layout::Union{AbstractVecOrMat,GridLayout}) = + layout_attrs(layout) # ---------------------------------------------------------------------- function build_layout(args...) - layout, n = layout_args(args...) + layout, n = layout_attrs(args...) build_layout(layout, n, Array{Plot}(undef, 0)) end @@ -495,7 +491,7 @@ end function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) nr, nc = size(layout) subplots = Subplot[] - spmap = SubplotMap() + spmap = PlotsPlots.SubplotMap() empty = isempty(plts) i = 0 for r in 1:nr, c in 1:nc @@ -552,7 +548,7 @@ function link_axes!(axes::Axis...) a1 = axes[1] for i in 2:length(axes) a2 = axes[i] - expand_extrema!(a1, ignorenan_extrema(a2)) + expand_extrema!(a1, Axes.ignorenan_extrema(a2)) for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) a2[k] = a1[k] end diff --git a/src/legend.jl b/src/legend.jl index 74dfd66cd..86b7ddb60 100644 --- a/src/legend.jl +++ b/src/legend.jl @@ -1,3 +1,20 @@ +### Legend + +@add_attributes subplot struct Legend + background_color = :match + foreground_color = :match + position = :best + title = nothing + font::Font = font(8) + title_font::Font = font(11) + column = 1 +end :match = ( + :legend_font_family, + :legend_font_color, + :legend_title_font_family, + :legend_title_font_color, +) + """ ```julia legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax) @@ -55,3 +72,12 @@ legend_angle(leg::Symbol) = get( leg, (45, :inner), ) + +Commons._initial_sp_fontsizes[:legend_font_pointsize] = + _subplot_defaults[:legend_font_pointsize] +Commons._initial_sp_fontsizes[:legend_title_font_pointsize] = + _subplot_defaults[:legend_title_font_pointsize] +Commons._initial_fontsizes[:legend_font_pointsize] = + _subplot_defaults[:legend_font_pointsize] +Commons._initial_fontsizes[:legend_title_font_pointsize] = + _subplot_defaults[:legend_title_font_pointsize] diff --git a/src/output.jl b/src/output.jl index ce7a3f244..b63f18f90 100644 --- a/src/output.jl +++ b/src/output.jl @@ -1,5 +1,6 @@ +struct PlotsDisplay <: AbstractDisplay end -defaultOutputFormat(plt::Plot) = "png" +default_output_format(plt::Plot) = "png" function png(plt::Plot, fn) fn = addExtension(fn, "png") @@ -141,7 +142,7 @@ function savefig(plt::Plot, fn) # fn might be an `AbstractString` or an `Abstrac # get the extension _, ext = splitext(fn) ext = chop(ext, head = 1, tail = 0) - isempty(ext) && (ext = defaultOutputFormat(plt)) + isempty(ext) && (ext = default_output_format(plt)) # save it if haskey(_savemap, ext) @@ -178,7 +179,7 @@ end # --------------------------------------------------------- const _best_html_output_type = - KW(:pyplot => :png, :unicodeplots => :txt, :plotlyjs => :html, :plotly => :html) + KW(:pythonplot => :png, :unicodeplots => :txt, :plotlyjs => :html, :plotly => :html) # a backup for html... passes to svg or png depending on the html_output_format arg function _show(io::IO, ::MIME"text/html", plt::Plot) @@ -240,7 +241,7 @@ closeall() = closeall(backend()) # COV_EXCL_START -Base.showable(::MIME"text/html", plt::Plot{UnicodePlotsBackend}) = false # Pluto +# Base.showable(::MIME"text/html", plt::Plot{UnicodePlotsBackend}) = false # Pluto Base.show(io::IO, m::MIME"application/prs.juno.plotpane+html", plt::Plot) = showjuno(io, MIME("text/html"), plt) diff --git a/src/pipeline.jl b/src/pipeline.jl index 3babfb5ab..9222ea55a 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -10,7 +10,7 @@ function RecipesPipeline.warn_on_recipe_aliases!( ) pkeys = keys(plotattributes) for k in pkeys - if (dk = get(_keyAliases, k, nothing)) !== nothing + if (dk = get(Commons._keyAliases, k, nothing)) !== nothing kv = RecipesPipeline.pop_kw!(plotattributes, k) dk ∈ pkeys || (plotattributes[dk] = kv) end @@ -31,20 +31,19 @@ RecipesPipeline.split_attribute(plt::Plot, key, val::SeriesAnnotations, indices) ) ## Preprocessing attributes -function RecipesPipeline.preprocess_axis_args!(plt::Plot, plotattributes, letter) +function RecipesPipeline.preprocess_axis_attrs!(plt::Plot, plotattributes, letter) # Fix letter for seriestypes that are x only but data gets passed as y - if treats_y_as_x(get(plotattributes, :seriestype, :path)) && - get(plotattributes, :orientation, :vertical) === :vertical + if treats_y_as_x(get(plotattributes, :seriestype, :path)) letter = :x end plotattributes[:letter] = letter - RecipesPipeline.preprocess_axis_args!(plt, plotattributes) + RecipesPipeline.preprocess_axis_attrs!(plt, plotattributes) end -RecipesPipeline.is_axis_attribute(plt::Plot, attr) = is_axis_attr_noletter(attr) # in src/args.jl +RecipesPipeline.is_axis_attribute(plt::Plot, attr) = Commons.is_axis_attr_noletter(attr) # in src/args.jl -RecipesPipeline.is_subplot_attribute(plt::Plot, attr) = is_subplot_attr(attr) # in src/args.jl +RecipesPipeline.is_subplot_attribute(plt::Plot, attr) = Commons.is_subplot_attrs(attr) # in src/args.jl ## User recipes @@ -62,13 +61,13 @@ function RecipesPipeline.process_userrecipe!(plt::Plot, kw_list, kw) end function _preprocess_userrecipe(kw::AKW) - _add_markershape(kw) + Commons._add_markershape(kw) if get(kw, :permute, default(:permute)) !== :none l1, l2 = kw[:permute] - for k in _axis_args - k1 = _attrsymbolcache[l1][k] - k2 = _attrsymbolcache[l2][k] + for k in Commons._axis_attrs + k1 = Commons._attrsymbolcache[l1][k] + k2 = Commons._attrsymbolcache[l2][k] kwk = keys(kw) if k1 in kwk || k2 in kwk kw[k1], kw[k2] = get(kw, k2, default(k2)), get(kw, k1, default(k1)) @@ -140,7 +139,7 @@ RecipesPipeline.get_axis_limits(plt::Plot, letter) = axis_limits(plt[1], letter, ## Plot recipes -RecipesPipeline.type_alias(plt::Plot, st) = get(_typeAliases, st, st) +RecipesPipeline.type_alias(plt::Plot, st) = get(Commons._typeAliases, st, st) ## Plot setup @@ -200,7 +199,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) end # TODO: init subplots here - _update_plot_args(plt, plotattributes) + _update_plot_attrs(plt, plotattributes) if !plt.init plt.o = Base.invokelatest(_create_backend_figure, plt) @@ -258,14 +257,14 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # extract subplot/axis attributes from kw and add to sp_attr attr = KW() for (k, v) in collect(kw) - if is_subplot_attr(k) || is_axis_attr(k) + if Commons.is_subplot_attrs(k) || Commons.is_axis_attrs(k) v = pop!(kw, k) if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps) v = v[series_idx(kw_list, kw)] end attr[k] = v end - if is_axis_attr_noletter(k) + if Commons.is_axis_attr_noletter(k) v = pop!(kw, k) if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps) v = v[series_idx(kw_list, kw)] @@ -291,7 +290,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) else get(sp_attrs, sp, KW()) end - _update_subplot_args(plt, sp, attr, idx, false) + PlotsPlots._update_subplot_attrs(plt, sp, attr, idx, false) end # do we need to link any axes together? @@ -316,8 +315,7 @@ function _add_plot_title!(plt) subplot = Subplot(plt.backend, parent = plt.layout[1, 1]) plt.layout.grid[2, 1] = the_layout subplot.plt = plt - - top = plt.backend isa PyPlotBackend ? nothing : 0mm + top = plt.backend isa get(_backendType, :pythonplot, NoBackend) ? nothing : 0mm bot = 0mm plt[:force_minpad] = nothing, top, nothing, bot subplot[:subplot_index] = last(plt.subplots)[:subplot_index] + 1 @@ -345,7 +343,7 @@ function RecipesPipeline.slice_series_attributes!(plt::Plot, kw_list, kw) sp::Subplot = kw[:subplot] # in series attributes given as vector with one element per series, # select the value for current series - _slice_series_args!(kw, plt, sp, series_idx(kw_list, kw)) + _slice_series_attrs!(kw, plt, sp, series_idx(kw_list, kw)) nothing end @@ -383,8 +381,8 @@ end function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where {T} st::Symbol = plotattributes[:seriestype] sp::Subplot{T} = plotattributes[:subplot] - sp_idx = get_subplot_index(plt, sp) - _update_subplot_args(plt, sp, plotattributes, sp_idx, true) + sp_idx = PlotsPlots.get_subplot_index(plt, sp) + PlotsPlots._update_subplot_attrs(plt, sp, plotattributes, sp_idx, true) st = _override_seriestype_check(plotattributes, st) @@ -404,24 +402,6 @@ function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where {T} sp end -function _override_seriestype_check(plotattributes::AKW, st::Symbol) - # do we want to override the series type? - if !RecipesPipeline.is3d(st) && st ∉ (:contour, :contour3d, :quiver) - if (z = plotattributes[:z]) !== nothing && - size(plotattributes[:x]) == size(plotattributes[:y]) == size(z) - st = st === :scatter ? :scatter3d : :path3d - plotattributes[:seriestype] = st - end - end - st -end - -needs_any_3d_axes(sp::Subplot) = any( - RecipesPipeline.needs_3d_axes( - _override_seriestype_check(s.plotattributes, s.plotattributes[:seriestype]), - ) for s in series_list(sp) -) - function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol) # adjust extrema and discrete info if st === :image @@ -441,7 +421,7 @@ function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol) end function _add_the_series(plt, sp, plotattributes) - extra_kwargs = warn_on_unsupported_args(plt.backend, plotattributes) + extra_kwargs = warn_on_unsupported_attrs(plt.backend, plotattributes) if (kw = plt[:extra_kwargs]) isa AbstractDict plt[:extra_plot_kwargs] = get(kw, :plot, KW()) sp[:extra_kwargs] = get(kw, :subplot, KW()) diff --git a/src/plot.jl b/src/plot.jl index f16587489..73672a689 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -1,4 +1,5 @@ +struct PlaceHolder end mutable struct CurrentPlot nullableplot::Union{AbstractPlot,Nothing} end @@ -78,27 +79,27 @@ Pass any attribute to `plotattr` as a String to look up its docstring, e.g., `pl # Extended help ## Series attributes -- $(_generate_doclist(_all_series_args)) +- $(_generate_doclist(Commons._all_series_attrs)) ## Axis attributes Prepend these with the axis letter (x, y or z) -- $(_generate_doclist(_all_axis_args)) +- $(_generate_doclist(Commons._all_axis_attrs)) ## Subplot attributes -- $(_generate_doclist(_all_subplot_args)) +- $(_generate_doclist(Commons._all_subplot_attrs)) ## Plot attributes -- $(_generate_doclist(_all_plot_args)) +- $(_generate_doclist(Commons._all_plot_attrs)) """ function RecipesBase.plot(args...; kw...) @nospecialize # this creates a new plot with args/kw and sets it to be the current plot plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) + Plots.Commons.preprocess_attributes!(plotattributes) # create an empty Plot then process plt = Plot() - # plt.user_attr = plotattributes + # plt.user_attrs = plotattributes _plot!(plt, plotattributes, args) end @@ -118,7 +119,7 @@ function plot!( ) @nospecialize plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) + Plots.Commons.preprocess_attributes!(plotattributes) # build our plot vector from the args plts = Plot[plt1] @@ -127,7 +128,7 @@ function plot!( n = length(plts) # compute the layout - layout = layout_args(plotattributes, n)[1] + layout = layout_attrs(plotattributes, n)[1] num_sp = sum(length(p.subplots) for p in plts) # create a new plot object, with subplot list/map made of existing subplots. @@ -135,24 +136,24 @@ function plot!( # note: all subplots and series "belong" to this new plot... plt = Plot() - # TODO: build the user_attr dict by creating "Any matrices" for the args of each subplot + # TODO: build the user_attrs dict by creating "Any matrices" for the args of each subplot - # TODO: replace this with proper processing from a merged user_attr KW + # TODO: replace this with proper processing from a merged user_attrs KW # update plot args for p in plts plt.attr = merge(p.attr, plt.attr) # plt.attr preempts p.attr (for `twinx`) plt.n += p.n end plt[:size] = last(sort(getindex.(plts, :size), by = x -> x[1] * x[2])) - _update_plot_args(plt, plotattributes) + _update_plot_attrs(plt, plotattributes) # pass new plot to the backend plt.o = _create_backend_figure(plt) plt.init = true - series_attr = KW() + series_attrs = KW() for (k, v) in plotattributes - is_series_attr(k) && (series_attr[k] = pop!(plotattributes, k)) + Commons.is_series_attrs(k) && (series_attrs[k] = pop!(plotattributes, k)) end # create the layout @@ -170,8 +171,8 @@ function plot!( sp.plt = plt sp.attr[:subplot_index] = idx for series in serieslist - merge!(series.plotattributes, series_attr) - _slice_series_args!(series.plotattributes, plt, sp, cmdidx) + merge!(series.plotattributes, series_attrs) + _slice_series_attrs!(series.plotattributes, plt, sp, cmdidx) push!(plt.series_list, series) _series_added(plt, series) cmdidx += 1 @@ -181,7 +182,13 @@ function plot!( # first apply any args for the subplots for (idx, sp) in enumerate(plt.subplots) - _update_subplot_args(plt, sp, idx == ttl_idx ? KW() : plotattributes, idx, false) + PlotsPlots._update_subplot_attrs( + plt, + sp, + idx == ttl_idx ? KW() : plotattributes, + idx, + false, + ) end # finish up @@ -208,8 +215,8 @@ plot(plt::Plot, args...; kw...) = plot!(deepcopy(plt), args...; kw...) function plot!(plt::Plot, args...; kw...) @nospecialize plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) - # merge!(plt.user_attr, plotattributes) + Plots.Commons.preprocess_attributes!(plotattributes) + # merge!(plt.user_attrs, plotattributes) _plot!(plt, plotattributes, args) end diff --git a/src/plotattr.jl b/src/plotattr.jl index 69018dc64..788db52f6 100644 --- a/src/plotattr.jl +++ b/src/plotattr.jl @@ -10,7 +10,7 @@ attrtypes() = join(keys(_attribute_defaults), ", ") attributes(attrtype::Symbol) = sort(collect(keys(_attribute_defaults[attrtype]))) function lookup_aliases(attrtype::Symbol, attribute::Symbol) - attribute = get(_keyAliases, attribute, attribute) + attribute = get(Commons._keyAliases, attribute, attribute) attribute ∈ keys(_attribute_defaults[attrtype]) && return attribute error("There is no attribute named $attribute in $attrtype") end @@ -30,22 +30,22 @@ function plotattr() @warn "Fuzzy finding of attributes is disabled in notebooks." return end - attr = Symbol(JLFzf.inter_fzf(collect(Plots._all_args), "--read0", "--height=80%")) + attr = Symbol(JLFzf.inter_fzf(collect(Commons._all_attrs), "--read0", "--height=80%")) letter = "" - attrtype = if attr ∈ _all_series_args + attrtype = if attr ∈ Commons._all_series_attrs "Series" - elseif attr ∈ _all_subplot_args + elseif attr ∈ Commons._all_subplot_attrs "Subplot" - elseif attr ∈ _lettered_all_axis_args - if attr ∉ _all_axis_args + elseif attr ∈ Commons._lettered_all_axis_attrs + if attr ∉ Commons._all_axis_attrs letters = collect(String(attr)) letter = first(letters) attr = Symbol(join(letters[2:end])) end "Axis" - elseif attr ∈ _all_plot_args + elseif attr ∈ Commons._all_plot_attrs "Plot" - elseif attr ∈ _all_magic_args + elseif attr ∈ Commons._all_magic_attr "Magic" else "Unknown" @@ -69,7 +69,7 @@ end function plotattr(attribute::AbstractString) attribute = Symbol(attribute) - attribute = get(_keyAliases, attribute, attribute) + attribute = get(Commons._keyAliases, attribute, attribute) for (k, v) in _attribute_defaults attribute ∈ keys(v) && return plotattr(k, attribute) end @@ -83,7 +83,7 @@ function plotattr(attrtype::Symbol, attribute::Symbol) attribute = lookup_aliases(attrtype, attribute) type, desc = _arg_desc[attribute] def = _attribute_defaults[attrtype][attribute] - aliases = if (al = Plots.aliases(attribute)) |> length > 0 + aliases = if (al = Plots.Commons.aliases(attribute)) |> length > 0 "Aliases: " * string(Tuple(al)) * ".\n\n" else "" diff --git a/src/recipes.jl b/src/recipes.jl index c82a0ff01..c0fd54e98 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -28,9 +28,9 @@ end # get a list of all seriestypes function all_seriestypes() sts = Set{Symbol}(keys(_series_recipe_deps)) - for bsym in backends() - btype = _backendType[bsym] - sts = union(sts, Set{Symbol}(supported_seriestypes(btype()))) + for bsym in _initialized_backends + be = _backend_instance(bsym) + sts = union(sts, Set{Symbol}(supported_seriestypes(be))) end sts |> collect |> sort end @@ -373,7 +373,7 @@ end # for each line segment (point series with no NaNs), convert it into a bezier curve # where the points are the control points of the curve - for rng in iter_segments(args...) + for rng in PlotsSeries.iter_segments(args...) length(rng) < 2 && continue ts = range(0, stop = 1, length = npoints) nanappend!(newx, map(t -> bezier_value(_cycle(x, rng), t), ts)) @@ -408,7 +408,7 @@ end ywiden --> false procx, procy, xscale, yscale, _ = _preprocess_barlike(plotattributes, x, y) nx, ny = length(procx), length(procy) - axis = plotattributes[:subplot][isvertical(plotattributes) ? :xaxis : :yaxis] + axis = plotattributes[:subplot][:xaxis] cv = map(xi -> discrete_value!(plotattributes, :x, xi)[1], procx) procx = if nx == ny cv @@ -423,7 +423,7 @@ end # compute half-width of bars bw = plotattributes[:bar_width] hw = if bw === nothing - 0.5_bar_width * if nx > 1 + 0.5Commons._bar_width * if nx > 1 ignorenan_minimum(filter(x -> x > 0, diff(sort(procx)))) else 1 @@ -436,12 +436,12 @@ end if (fillto = plotattributes[:fillrange]) === nothing fillto = 0 end - if yscale in _logScales && !all(_is_positive, fillto) + if yscale in _log_scales && !all(_is_positive, fillto) # github.com/JuliaPlots/Plots.jl/issues/4502 # https://github.com/JuliaPlots/Plots.jl/issues/4774 T = float(eltype(y)) min_y = NaNMath.minimum(y) - base = _logScaleBases[yscale] + base = _log_scale_bases[yscale] baseline = floor_base(min_y, base) if min_y == baseline baseline /= base @@ -462,16 +462,10 @@ end end # widen limits out a bit - expand_extrema!(axis, scale_lims(ignorenan_extrema(xseg.pts)..., default_widen_factor)) - - # switch back - if !isvertical(plotattributes) - xseg, yseg = yseg, xseg - x, y = y, x - end - - # reset orientation - orientation := default(:orientation) + expand_extrema!( + axis, + Axes.scale_lims(ignorenan_extrema(xseg.pts)..., Axes.default_widen_factor), + ) # draw the bar shapes @series begin @@ -552,11 +546,11 @@ _scale_adjusted_values( ::Type{T}, V::AbstractVector, scale::Symbol, -) where {T<:AbstractFloat} = scale in _logScales ? _positive_else_nan.(T, V) : T.(V) +) where {T<:AbstractFloat} = scale in _log_scales ? _positive_else_nan.(T, V) : T.(V) _binbarlike_baseline(min_value::T, scale::Symbol) where {T<:Real} = - if scale in _logScales - isnan(min_value) ? T(1e-3) : floor_base(min_value, _logScaleBases[scale]) + if scale in _log_scales + isnan(min_value) ? T(1e-3) : floor_base(min_value, _log_scale_bases[scale]) else zero(T) end @@ -622,8 +616,8 @@ end @specialize function _stepbins_path(edge, weights, baseline::Real, xscale::Symbol, yscale::Symbol) - log_scale_x = xscale in _logScales - log_scale_y = yscale in _logScales + log_scale_x = xscale in _log_scales + log_scale_y = yscale in _log_scales nbins = length(eachindex(weights)) if length(eachindex(edge)) != nbins + 1 @@ -645,7 +639,7 @@ function _stepbins_path(edge, weights, baseline::Real, xscale::Symbol, yscale::S w, it_state_w = it_tuple_w if log_scale_x && a ≈ 0 - a = oftype(a, b / _logScaleBases[xscale]^3) + a = oftype(a, b / _log_scale_bases[xscale]^3) end if isnan(w) @@ -678,14 +672,10 @@ end @recipe function f(::Type{Val{:stepbins}}, x, y, z) # COV_EXCL_LINE @nospecialize - axis = plotattributes[:subplot][Plots.isvertical(plotattributes) ? :xaxis : :yaxis] edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, x, y) xpts, ypts = _stepbins_path(edge, weights, baseline, xscale, yscale) - if !isvertical(plotattributes) - xpts, ypts = ypts, xpts - end # create a secondary series for the markers if plotattributes[:markershape] !== :none @@ -1088,10 +1078,11 @@ end # --------------------------------------------------------------------------- # Error Bars -@attributes function error_style!(plotattributes::AKW) +Commons.@attributes function error_style!(plotattributes::AKW) # errorbar color should soley determined by markerstrokecolor - haskey(plotattributes, :marker_z) && reset_kw!(plotattributes, :marker_z) - haskey(plotattributes, :line_z) && reset_kw!(plotattributes, :line_z) + haskey(plotattributes, :marker_z) && + RecipesPipeline.reset_kw!(plotattributes, :marker_z) + haskey(plotattributes, :line_z) && RecipesPipeline.reset_kw!(plotattributes, :line_z) msc = if (msc = plotattributes[:markerstrokecolor]) === :match plotattributes[:subplot][:foreground_color_subplot] @@ -1143,7 +1134,7 @@ clamp_to_eps!(ary) = (replace!(x -> x <= 0.0 ? Base.eps(Float64) : x, ary); noth @nospecialize @recipe function f(::Type{Val{:xerror}}, x, y, z) # COV_EXCL_LINE - error_style!(plotattributes) + Commons.error_style!(plotattributes) markershape := :vline xerr = error_zipit(plotattributes[:xerror]) if z === nothing @@ -1160,7 +1151,7 @@ end @deps xerror path @recipe function f(::Type{Val{:yerror}}, x, y, z) # COV_EXCL_LINE - error_style!(plotattributes) + Commons.error_style!(plotattributes) markershape := :hline yerr = error_zipit(plotattributes[:yerror]) if z === nothing @@ -1177,7 +1168,7 @@ end @deps yerror path @recipe function f(::Type{Val{:zerror}}, x, y, z) # COV_EXCL_LINE - error_style!(plotattributes) + Commons.error_style!(plotattributes) markershape := :hline if z !== nothing zerr = error_zipit(plotattributes[:zerror]) @@ -1589,7 +1580,7 @@ end for c in axes(weights, 2) sx = vcat(weights[:, c], c == 1 ? zeros(n) : reverse(weights[:, c - 1])) sy = vcat(returns, reverse(returns)) - @series Plots.isvertical(plotattributes) ? (sx, sy) : (sy, sx) + @series (sx, sy) end end diff --git a/src/themes.jl b/src/themes.jl index 67d3569f6..e2feb711e 100644 --- a/src/themes.jl +++ b/src/themes.jl @@ -15,7 +15,7 @@ end function _theme(s::Symbol, defaults::AKW; kw...) # Reset to defaults to overwrite active theme - reset_defaults() + Commons.reset_defaults() # Set the theme's gradient as default if haskey(defaults, :colorgradient) @@ -41,11 +41,11 @@ end _color_functions = KW(:protanopic => protanopic, :deuteranopic => deuteranopic, :tritanopic => tritanopic) -_get_showtheme_args(thm::Symbol) = thm, identity -_get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func, identity) +_get_showtheme_attrs(thm::Symbol) = thm, identity +_get_showtheme_attrs(thm::Symbol, func::Symbol) = thm, get(_color_functions, func, identity) @recipe function showtheme(st::ShowTheme) - thm, cfunc = _get_showtheme_args(st.args...) + thm, cfunc = _get_showtheme_attrs(st.args...) defaults = PlotThemes._themes[thm].defaults # get the gradient @@ -60,7 +60,7 @@ _get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func for k in keys(defaults) k in (:colorgradient, :palette) && continue def = defaults[k] - arg = get(_keyAliases, k, k) + arg = get(Commons._keyAliases, k, k) plotattributes[arg] = if typeof(def) <: Colorant cfunc(RGB(def)) elseif eltype(def) <: Colorant diff --git a/src/types.jl b/src/types.jl deleted file mode 100644 index 6dc6067e2..000000000 --- a/src/types.jl +++ /dev/null @@ -1,186 +0,0 @@ - -# TODO: I declare lots of types here because of the lacking ability to do forward declarations in current Julia -# I should move these to the relevant files when something like "extern" is implemented - -const AVec = AbstractVector -const AMat = AbstractMatrix -const KW = Dict{Symbol,Any} -const AKW = AbstractDict{Symbol,Any} -const TicksArgs = - Union{AVec{T},Tuple{AVec{T},AVec{S}},Symbol} where {T<:Real,S<:AbstractString} - -struct PlotsDisplay <: AbstractDisplay end - -struct InputWrapper{T} - obj::T -end - -mutable struct Series - plotattributes::DefaultsDict -end - -# a single subplot -mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout - parent::AbstractLayout - series_list::Vector{Series} # arguments for each series - primary_series_count::Int # Number of primary series in the series list - minpad::Tuple # leftpad, toppad, rightpad, bottompad - bbox::BoundingBox # the canvas area which is available to this subplot - plotarea::BoundingBox # the part where the data goes - attr::DefaultsDict # args specific to this subplot - o # can store backend-specific data... like a pyplot ax - plt # the enclosing Plot object (can't give it a type because of no forward declarations) - - Subplot(::T; parent = RootLayout()) where {T<:AbstractBackend} = new{T}( - parent, - Series[], - 0, - DEFAULT_MINPAD[], - DEFAULT_BBOX[], - DEFAULT_BBOX[], - DefaultsDict(KW(), _subplot_defaults), - nothing, - nothing, - ) -end - -# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place -mutable struct Axis - sps::Vector{Subplot} - plotattributes::DefaultsDict -end - -mutable struct Extrema - emin::Float64 - emax::Float64 -end - -Extrema() = Extrema(Inf, -Inf) - -const SubplotMap = Dict{Any,Subplot} - -mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} - backend::T # the backend type - n::Int # number of series - attr::DefaultsDict # arguments for the whole plot - series_list::Vector{Series} # arguments for each series - o # the backend's plot object - subplots::Vector{Subplot} - spmap::SubplotMap # provide any label as a map to a subplot - layout::AbstractLayout - inset_subplots::Vector{Subplot} # list of inset subplots - init::Bool - - function Plot() - be = backend() - new{typeof(be)}( - be, - 0, - DefaultsDict(KW(), _plot_defaults), - Series[], - nothing, - Subplot[], - SubplotMap(), - EmptyLayout(), - Subplot[], - false, - ) - end - - function Plot(osp::Subplot) - plt = Plot() - plt.layout = GridLayout(1, 1) - sp = deepcopy(osp) # FIXME: fails `PlotlyJS` ? - plt.layout.grid[1, 1] = sp - # reset some attributes - sp.minpad = DEFAULT_MINPAD[] - sp.bbox = DEFAULT_BBOX[] - sp.plotarea = DEFAULT_BBOX[] - sp.plt = plt # change the enclosing plot - push!(plt.subplots, sp) - plt - end -end - -struct PlaceHolder end -const PlotOrSubplot = Union{Plot,Subplot} - -# ----------------------------------------------------------- - -wrap(obj::T) where {T} = InputWrapper{T}(obj) -Base.isempty(wrapper::InputWrapper) = false - -# ----------------------------------------------------------- -attr(series::Series, k::Symbol) = series.plotattributes[k] -attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) - -should_add_to_legend(series::Series) = - series.plotattributes[:primary] && - series.plotattributes[:label] != "" && - series.plotattributes[:seriestype] ∉ ( - :hexbin, - :bins2d, - :histogram2d, - :hline, - :vline, - :contour, - :contourf, - :contour3d, - :surface, - :wireframe, - :heatmap, - :image, - ) - -# ----------------------------------------------------------------------- -Base.iterate(plt::Plot) = iterate(plt.subplots) - -Base.getindex(plt::Plot, i::Union{Vector{<:Integer},Integer}) = plt.subplots[i] -Base.length(plt::Plot) = length(plt.subplots) -Base.lastindex(plt::Plot) = length(plt) - -Base.getindex(plt::Plot, r::Integer, c::Integer) = plt.layout[r, c] -Base.size(plt::Plot) = size(plt.layout) -Base.size(plt::Plot, i::Integer) = size(plt.layout)[i] -Base.ndims(plt::Plot) = 2 - -# clear out series list, but retain subplots -Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) - -# attr(plt::Plot, k::Symbol) = plt.attr[k] -# attr!(plt::Plot, v, k::Symbol) = (plt.attr[k] = v) - -Base.getindex(sp::Subplot, i::Union{Vector{<:Integer},Integer}) = series_list(sp)[i] -Base.lastindex(sp::Subplot) = length(series_list(sp)) - -Base.empty!(sp::Subplot) = empty!(sp.series_list) - -# ----------------------------------------------------------------------- - -Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}") - -""" - plotarea(subplot) - -Return the bounding box of a subplot. -""" -plotarea(sp::Subplot) = sp.plotarea -plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox) - -Base.size(sp::Subplot) = (1, 1) -Base.length(sp::Subplot) = 1 -Base.getindex(sp::Subplot, r::Int, c::Int) = sp - -leftpad(sp::Subplot) = sp.minpad[1] -toppad(sp::Subplot) = sp.minpad[2] -rightpad(sp::Subplot) = sp.minpad[3] -bottompad(sp::Subplot) = sp.minpad[4] - -get_subplot(plt::Plot, sp::Subplot) = sp -get_subplot(plt::Plot, i::Integer) = plt.subplots[i] -get_subplot(plt::Plot, k) = plt.spmap[k] -get_subplot(series::Series) = series.plotattributes[:subplot] - -get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x === sp, plt.subplots) - -series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] === sp, sp.plt.series_list) diff --git a/src/users.jl b/src/users.jl new file mode 100644 index 000000000..9e7274cd6 --- /dev/null +++ b/src/users.jl @@ -0,0 +1,4 @@ +# contains end user functions + +pgfx_preamble() = get_backend_module(:PGFPlotsX)[1].pgfx_preamble() +pgfx_preamble(pl) = get_backend_module(:PGFPlotsX)[1].pgfx_preamble(pl) diff --git a/src/utils.jl b/src/utils.jl index 74878824f..e79d790a4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,3 +1,4 @@ + # --------------------------------------------------------------- bool_env(x, default)::Bool = try @@ -35,10 +36,10 @@ to_nan(::Type{Float64}) = NaN to_nan(::Type{NTuple{2,Float64}}) = (NaN, NaN) to_nan(::Type{NTuple{3,Float64}}) = (NaN, NaN, NaN) -coords(segs::Segments{Float64}) = segs.pts -coords(segs::Segments{NTuple{2,Float64}}) = +Commons.coords(segs::Segments{Float64}) = segs.pts +Commons.coords(segs::Segments{NTuple{2,Float64}}) = (map(p -> p[1], segs.pts), map(p -> p[2], segs.pts)) -coords(segs::Segments{NTuple{3,Float64}}) = +Commons.coords(segs::Segments{NTuple{3,Float64}}) = (map(p -> p[1], segs.pts), map(p -> p[2], segs.pts), map(p -> p[3], segs.pts)) function Base.push!(segments::Segments{T}, vs...) where {T} @@ -53,187 +54,147 @@ function Base.push!(segments::Segments{T}, vs::AVec) where {T} segments end -struct SeriesSegment - # indexes of this segment 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 -struct NaNSegmentsIterator - args::Tuple - n1::Int - n2::Int -end - -function iter_segments(args...) - tup = Plots.wraptuple(args) - n1 = minimum(map(firstindex, tup)) - n2 = maximum(map(lastindex, tup)) - NaNSegmentsIterator(tup, n1, n2) -end +# Find minimal type that can contain NaN and x +# To allow use of NaN separated segments with categorical x axis -"floor number x in base b, note this is different from using Base.round(...; base=b) !" -floor_base(x, b) = round_base(x, b, RoundDown) +float_extended_type(x::AbstractArray{T}) where {T} = Union{T,Float64} +float_extended_type(x::AbstractArray{Real}) = Float64 -"ceil number x in base b" -ceil_base(x, b) = round_base(x, b, RoundUp) +function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) + pkg = plt.backend + globalIndex = plotattributes[:series_plotindex] + plotIndex = Commons._series_index(plotattributes, sp) + + Commons.aliases_and_autopick( + plotattributes, + :linestyle, + Commons._styleAliases, + supported_styles(pkg), + plotIndex, + ) + Commons.aliases_and_autopick( + plotattributes, + :markershape, + Commons._marker_aliases, + supported_markers(pkg), + plotIndex, + ) -round_base(x::T, b, ::RoundingMode{:Down}) where {T} = T(b^floor(log(b, x))) -round_base(x::T, b, ::RoundingMode{:Up}) where {T} = T(b^ceil(log(b, x))) + # update alphas + for asym in (:linealpha, :markeralpha, :fillalpha) + if plotattributes[asym] === nothing + plotattributes[asym] = plotattributes[:seriesalpha] + end + end + if plotattributes[:markerstrokealpha] === nothing + plotattributes[:markerstrokealpha] = plotattributes[:markeralpha] + end -ignorenan_min_max(::Any, ex) = ex -function ignorenan_min_max(x::AbstractArray{<:AbstractFloat}, ex::Tuple) - mn, mx = ignorenan_extrema(x) - NaNMath.min(ex[1], mn), NaNMath.max(ex[2], mx) -end + # update series color + scolor = plotattributes[:seriescolor] + stype = plotattributes[:seriestype] + plotattributes[:seriescolor] = scolor = get_series_color(scolor, sp, plotIndex, stype) -function series_segments(series::Series, seriestype::Symbol = :path; check = false) - x, y, z = series[:x], series[:y], series[:z] - (x === nothing || isempty(x)) && return UnitRange{Int}[] - - args = RecipesPipeline.is3d(series) ? (x, y, z) : (x, y) - nan_segments = collect(iter_segments(args...)) - - if check - scales = :xscale, :yscale, :zscale - for (n, s) in enumerate(args) - (scale = get(series, scales[n], :identity)) ∈ _logScales || continue - for (i, v) in enumerate(s) - if v <= 0 - @warn "Invalid negative or zero value $v found at series index $i for $scale based $(scales[n])" - @debug "" exception = (DomainError(v), stacktrace()) - break - end - end + # update other colors (`linecolor`, `markercolor`, `fillcolor`) <- for grep + for s in (:line, :marker, :fill) + csym, asym = Symbol(s, :color), Symbol(s, :alpha) + plotattributes[csym] = if plotattributes[csym] === :auto + plot_color(if Commons.has_black_border_for_default(stype) && s === :line + sp[:foreground_color_subplot] + else + scolor + end) + elseif plotattributes[csym] === :match + plot_color(scolor) + else + get_series_color(plotattributes[csym], sp, plotIndex, stype) end end - segments = if has_attribute_segments(series) - map(nan_segments) do r - if seriestype === :shape - warn_on_inconsistent_shape_attr(series, x, y, z, r) - (SeriesSegment(r, first(r)),) - elseif 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 |> Iterators.flatten + # update markerstrokecolor + plotattributes[:markerstrokecolor] = if plotattributes[:markerstrokecolor] === :match + plot_color(sp[:foreground_color_subplot]) + elseif plotattributes[:markerstrokecolor] === :auto + get_series_color(plotattributes[:markercolor], sp, plotIndex, stype) else - (SeriesSegment(r, 1) for r in nan_segments) + get_series_color(plotattributes[:markerstrokecolor], sp, plotIndex, stype) end - warn_on_attr_dim_mismatch(series, x, y, z, segments) - segments -end - -function warn_on_attr_dim_mismatch(series, x, y, z, segments) - isempty(segments) && return - seg_range = UnitRange( - minimum(map(seg -> first(seg.range), segments)), - maximum(map(seg -> last(seg.range), segments)), - ) - for attr in _segmenting_vector_attributes - if (v = get(series, attr, nothing)) isa AVec && eachindex(v) != seg_range - @warn "Indices $(eachindex(v)) of attribute `$attr` does not match data indices $seg_range." - if any(v -> !isnothing(v) && any(isnan, v), (x, y, z)) - @info """Data contains NaNs or missing values, and indices of `$attr` vector do not match data indices. - If you intend elements of `$attr` to apply to individual NaN-separated segments in the data, - pass each segment in a separate vector instead, and use a row vector for `$attr`. Legend entries - may be suppressed by passing an empty label. - For example, - plot([1:2,1:3], [[4,5],[3,4,5]], label=["y" ""], $attr=[1 2]) - """ - end - end + # if marker_z, fill_z or line_z are set, ensure we have a gradient + if plotattributes[:marker_z] !== nothing + Commons.ensure_gradient!(plotattributes, :markercolor, :markeralpha) + end + if plotattributes[:line_z] !== nothing + Commons.ensure_gradient!(plotattributes, :linecolor, :linealpha) + end + if plotattributes[:fill_z] !== nothing + Commons.ensure_gradient!(plotattributes, :fillcolor, :fillalpha) end -end -function warn_on_inconsistent_shape_attr(series, x, y, z, r) - for attr in _segmenting_vector_attributes - v = get(series, attr, nothing) - if v isa AVec && length(unique(v[r])) > 1 - @warn "Different values of `$attr` specified for different shape vertices. Only first one will be used." - break + # scatter plots don't have a line, but must have a shape + if plotattributes[:seriestype] in (:scatter, :scatterbins, :scatterhist, :scatter3d) + plotattributes[:linewidth] = 0 + if plotattributes[:markershape] === :none + plotattributes[:markershape] = :circle end end -end - -# helpers to figure out if there are NaN values in a list of array types -anynan(i::Int, args::Tuple) = any(a -> try - isnan(_cycle(a, i)) -catch MethodError - false -end, args) -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::NaNSegmentsIterator, nextidx::Int = itr.n1) - (i = findfirst(!anynan(itr.args), nextidx:(itr.n2))) === nothing && return - nextval = nextidx + i - 1 - j = findfirst(anynan(itr.args), nextval:(itr.n2)) - nextnan = j === nothing ? itr.n2 + 1 : nextval + j - 1 + # set label + plotattributes[:label] = Commons.label_to_string.(plotattributes[:label], globalIndex) - nextval:(nextnan - 1), nextnan + Commons._replace_linewidth(plotattributes) + plotattributes end -Base.IteratorSize(::NaNSegmentsIterator) = Base.SizeUnknown() # COV_EXCL_LINE - -# Find minimal type that can contain NaN and x -# To allow use of NaN separated segments with categorical x axis - -float_extended_type(x::AbstractArray{T}) where {T} = Union{T,Float64} -float_extended_type(x::AbstractArray{Real}) = Float64 - -# ------------------------------------------------------------------------------------ -_cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj -_cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj - -_cycle(v::AVec, idx::Int) = v[mod(idx, axes(v, 1))] -_cycle(v::AMat, idx::Int) = size(v, 1) == 1 ? v[end, mod(idx, axes(v, 2))] : v[:, mod(idx, axes(v, 2))] -_cycle(v, idx::Int) = v - -_cycle(v::AVec, indices::AVec{Int}) = map(i -> _cycle(v, i), indices) -_cycle(v::AMat, indices::AVec{Int}) = map(i -> _cycle(v, i), indices) -_cycle(v, indices::AVec{Int}) = fill(v, length(indices)) - -_cycle(cl::PlotUtils.AbstractColorList, idx::Int) = cl[mod1(idx, end)] -_cycle(cl::PlotUtils.AbstractColorList, idx::AVec{Int}) = cl[mod1.(idx, end)] - -_as_gradient(grad) = grad -_as_gradient(v::AbstractVector{<:Colorant}) = cgrad(v) -_as_gradient(cp::ColorPalette) = cgrad(cp, categorical = true) -_as_gradient(c::Colorant) = cgrad([c, c]) - -makevec(v::AVec) = v -makevec(v::T) where {T} = T[v] - -"duplicate a single value, or pass the 2-tuple through" -maketuple(x::Real) = (x, x) -maketuple(x::Tuple) = x - -RecipesPipeline.unzip(v) = Unzip.unzip(v) # COV_EXCL_LINE - -"collect into columns (convenience for `unzip` from `Unzip.jl`)" -unzip(v) = RecipesPipeline.unzip(v) +""" +1-row matrices will give an element +multi-row matrices will give a column +anything else is returned as-is +""" +function slice_arg(v::AMat, idx::Int) + isempty(v) && return v + c = mod1(idx, size(v, 2)) + m, n = axes(v) + size(v, 1) == 1 ? v[first(m), n[c]] : v[:, n[c]] +end +slice_arg(wrapper::InputWrapper, idx) = wrapper.obj +slice_arg(v::NTuple{2,AMat}, idx::Int) = slice_arg(v[1], idx), slice_arg(v[2], idx) +slice_arg(v, idx) = v -replaceAlias!(plotattributes::AKW, k::Symbol, aliases::Dict{Symbol,Symbol}) = - if haskey(aliases, k) - plotattributes[aliases[k]] = RecipesPipeline.pop_kw!(plotattributes, k) +""" +given an argument key `k`, extract the argument value for this index, +and set into plotattributes[k]. Matrices are sliced by column. +if nothing is set (or container is empty), return the existing value. +""" +function slice_arg!( + plotattributes_in, + plotattributes_out, + k::Symbol, + idx::Int, + remove_pair::Bool, +) + v = get(plotattributes_in, k, plotattributes_out[k]) + plotattributes_out[k] = if haskey(plotattributes_in, k) && k ∉ Commons._plot_attrs + slice_arg(v, idx) + else + v end + remove_pair && RecipesPipeline.reset_kw!(plotattributes_in, k) + nothing +end -replaceAliases!(plotattributes::AKW, aliases::Dict{Symbol,Symbol}) = - foreach(k -> replaceAlias!(plotattributes, k, aliases), collect(keys(plotattributes))) - -scale_inverse_scale_func(scale::Symbol) = ( - RecipesPipeline.scale_func(scale), - RecipesPipeline.inverse_scale_func(scale), - scale === :identity, +function _slice_series_attrs!( + plotattributes::AKW, + plt::Plot, + sp::Subplot, + commandIndex::Int, ) + for k in keys(_series_defaults) + haskey(plotattributes, k) && + slice_arg!(plotattributes, plotattributes, k, commandIndex, false) + end + plotattributes +end +# ----------------------------------------------------------------------------- function __heatmap_edges(v::AVec, isedges::Bool, ispolar::Bool) (n = length(v)) == 1 && return v[1] .+ [ispolar ? max(-v[1], -0.5) : -0.5, 0.5] @@ -326,14 +287,10 @@ isscalar(::Any) = false is_2tuple(v) = typeof(v) <: Tuple && length(v) == 2 -isvertical(plotattributes::AKW) = - get(plotattributes, :orientation, :vertical) in (:vertical, :v, :vert) -isvertical(series::Series) = isvertical(series.plotattributes) - -ticksType(ticks::AVec{<:Real}) = :ticks -ticksType(ticks::AVec{<:AbstractString}) = :labels -ticksType(ticks::Tuple{<:Union{AVec,Tuple},<:Union{AVec,Tuple}}) = :ticks_and_labels -ticksType(ticks) = :invalid +ticks_type(ticks::AVec{<:Real}) = :ticks +ticks_type(ticks::AVec{<:AbstractString}) = :labels +ticks_type(ticks::Tuple{<:Union{AVec,Tuple},<:Union{AVec,Tuple}}) = :ticks_and_labels +ticks_type(ticks) = :invalid limsType(lims::Tuple{<:Real,<:Real}) = :limits limsType(lims::Symbol) = lims === :auto ? :auto : :invalid @@ -372,28 +329,6 @@ function nanvcat(vs::AVec) v_out end -sort_3d_axes(x, y, z, letter) = - if letter === :x - x, y, z - elseif letter === :y - y, x, z - else - z, y, x - end - -axes_letters(sp, letter) = - if RecipesPipeline.is3d(sp) - sort_3d_axes(:x, :y, :z, letter) - else - letter === :x ? (:x, :y) : (:y, :x) - end - -handle_surface(z) = z -handle_surface(z::Surface) = permutedims(z.surf) - -ok(x::Number, y::Number, z::Number = 0) = isfinite(x) && isfinite(y) && isfinite(z) -ok(tup::Tuple) = ok(tup...) - # compute one side of a fill range from a ribbon function make_fillrange_side(y::AVec, rib) frs = zeros(axes(y)) @@ -449,187 +384,211 @@ xlims(sp_idx::Int = 1) = xlims(current(), sp_idx) ylims(sp_idx::Int = 1) = ylims(current(), sp_idx) zlims(sp_idx::Int = 1) = zlims(current(), sp_idx) -iscontour(series::Series) = series[:seriestype] in (:contour, :contour3d) -isfilledcontour(series::Series) = iscontour(series) && series[:fillrange] !== nothing +"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases." +function Commons.preprocess_attributes!(plotattributes::AKW) + Commons.replaceAliases!(plotattributes, Commons._keyAliases) -function contour_levels(series::Series, clims) - iscontour(series) || error("Not a contour series") - zmin, zmax = clims - levels = series[:levels] - if levels isa Integer - levels = range(zmin, stop = zmax, length = levels + 2) - isfilledcontour(series) || (levels = levels[2:(end - 1)]) + # handle axis args common to all axis + args = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :axis, ())) + showarg = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :showaxis, ())) + for arg in wraptuple((args..., showarg...)) + for letter in (:x, :y, :z) + process_axis_arg!(plotattributes, arg, letter) + end end - levels -end - -for comp in (:line, :fill, :marker) - compcolor = string(comp, :color) - get_compcolor = Symbol(:get_, compcolor) - comp_z = string(comp, :_z) - - compalpha = string(comp, :alpha) - get_compalpha = Symbol(:get_, compalpha) - - @eval begin - # defines `get_linecolor`, `get_fillcolor` and `get_markercolor` <- for grep - function $get_compcolor( - series, - cmin::Real, - cmax::Real, - i::Integer = 1, - s::Symbol = :identity, - ) - c = series[$Symbol($compcolor)] # series[:linecolor], series[:fillcolor], series[:markercolor] - z = series[$Symbol($comp_z)] # series[:line_z], series[:fill_z], series[:marker_z] - if z === nothing - isa(c, ColorGradient) ? c : plot_color(_cycle(c, i)) - else - grad = get_gradient(c) - if s === :identity - get(grad, z[i], (cmin, cmax)) - else - base = _logScaleBases[s] - get(grad, log(base, z[i]), (log(base, cmin), log(base, cmax))) - end + # handle axis args + for letter in (:x, :y, :z) + asym = get_attr_symbol(letter, :axis) + args = RecipesPipeline.pop_kw!(plotattributes, asym, ()) + if !(typeof(args) <: Axis) + for arg in wraptuple(args) + process_axis_arg!(plotattributes, arg, letter) end end + end - function $get_compcolor(series, i::Integer = 1, s::Symbol = :identity) - if series[$Symbol($comp_z)] === nothing - $get_compcolor(series, 0, 1, i, s) - else - $get_compcolor(series, get_clims(series[:subplot]), i, s) + # vline and others accesses the y argument but actually maps it to the x axis. + # Hence, we have to take care of formatters + if treats_y_as_x(get(plotattributes, :seriestype, :path)) + xformatter = get(plotattributes, :xformatter, :auto) + yformatter = get(plotattributes, :yformatter, :auto) + yformatter !== :auto && (plotattributes[:xformatter] = yformatter) + xformatter === :auto && + haskey(plotattributes, :yformatter) && + pop!(plotattributes, :yformatter) + end + + # handle grid args common to all axes + processGridArg! = Commons.process_grid_attr! + args = RecipesPipeline.pop_kw!(plotattributes, :grid, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + processGridArg!(plotattributes, arg, letter) + end + end + # handle individual axes grid args + for letter in (:x, :y, :z) + gridsym = get_attr_symbol(letter, :grid) + args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) + for arg in wraptuple(args) + processGridArg!(plotattributes, arg, letter) + end + end + # handle minor grid args common to all axes + args = RecipesPipeline.pop_kw!(plotattributes, :minorgrid, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + Commons.process_minor_grid_attr!(plotattributes, arg, letter) + end + end + # handle individual axes grid args + for letter in (:x, :y, :z) + gridsym = get_attr_symbol(letter, :minorgrid) + args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) + for arg in wraptuple(args) + Commons.process_minor_grid_attr!(plotattributes, arg, letter) + end + end + # handle font args common to all axes + for fontname in (:tickfont, :guidefont) + args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + Commons.process_font_attr!( + plotattributes, + get_attr_symbol(letter, fontname), + arg, + ) end end + end + # handle individual axes font args + for letter in (:x, :y, :z) + for fontname in (:tickfont, :guidefont) + args = RecipesPipeline.pop_kw!( + plotattributes, + get_attr_symbol(letter, fontname), + (), + ) + for arg in wraptuple(args) + Commons.process_font_attr!( + plotattributes, + get_attr_symbol(letter, fontname), + arg, + ) + end + end + end + # handle axes args + for k in Commons._axis_attrs + if haskey(plotattributes, k) && k !== :link + v = plotattributes[k] + for letter in (:x, :y, :z) + lk = get_attr_symbol(letter, k) + if !is_explicit(plotattributes, lk) + plotattributes[lk] = v + end + end + end + end - $get_compcolor(series, clims::NTuple{2,<:Number}, args...) = - $get_compcolor(series, clims[1], clims[2], args...) - - $get_compalpha(series, i::Integer = 1) = _cycle(series[$Symbol($compalpha)], i) + # fonts + for fontname in + (:titlefont, :legend_title_font, :plot_titlefont, :colorbar_titlefont, :legend_font) + args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) + for arg in wraptuple(args) + Commons.process_font_attr!(plotattributes, fontname, arg) + end end -end -function get_colorgradient(series::Series) - if (st = series[:seriestype]) in (:surface, :heatmap) || isfilledcontour(series) - series[:fillcolor] - elseif st in (:contour, :wireframe, :contour3d) - series[:linecolor] - elseif series[:marker_z] !== nothing - series[:markercolor] - elseif series[:line_z] !== nothing - series[:linecolor] - elseif series[:fill_z] !== nothing - series[:fillcolor] + # handle line args + for arg in wraptuple(RecipesPipeline.pop_kw!(plotattributes, :line, ())) + Commons.process_line_attr(plotattributes, arg) end -end -single_color(c, v = 0.5) = c -single_color(grad::ColorGradient, v = 0.5) = grad[v] + if haskey(plotattributes, :seriestype) && + haskey(Commons._typeAliases, plotattributes[:seriestype]) + plotattributes[:seriestype] = Commons._typeAliases[plotattributes[:seriestype]] + end -get_gradient(c) = cgrad() -get_gradient(cg::ColorGradient) = cg -get_gradient(cp::ColorPalette) = cgrad(cp, categorical = true) + # handle marker args... default to ellipse if shape not set + anymarker = false + for arg in wraptuple(get(plotattributes, :marker, ())) + Commons.process_marker_attr(plotattributes, arg) + anymarker = true + end + RecipesPipeline.reset_kw!(plotattributes, :marker) + if haskey(plotattributes, :markershape) + plotattributes[:markershape] = + Commons._replace_markershape(plotattributes[:markershape]) + if plotattributes[:markershape] === :none && + get(plotattributes, :seriestype, :path) in + (:scatter, :scatterbins, :scatterhist, :scatter3d) #the default should be :auto, not :none, so that :none can be set explicitly and would be respected + plotattributes[:markershape] = :circle + end + elseif anymarker + plotattributes[:markershape_to_add] = :circle # add it after _apply_recipe + end -get_linewidth(series, i::Integer = 1) = _cycle(series[:linewidth], i) -get_linestyle(series, i::Integer = 1) = _cycle(series[:linestyle], i) -get_fillstyle(series, i::Integer = 1) = _cycle(series[:fillstyle], i) + # handle fill + for arg in wraptuple(get(plotattributes, :fill, ())) + Commons.process_fill_attr(plotattributes, arg) + end + RecipesPipeline.reset_kw!(plotattributes, :fill) -get_markerstrokecolor(series, i::Integer = 1) = - let msc = series[:markerstrokecolor] - msc isa ColorGradient ? msc : _cycle(msc, i) + # handle series annotations + if haskey(plotattributes, :series_annotations) + plotattributes[:series_annotations] = + series_annotations(wraptuple(plotattributes[:series_annotations])...) end -get_markerstrokealpha(series, i::Integer = 1) = _cycle(series[:markerstrokealpha], i) -get_markerstrokewidth(series, i::Integer = 1) = _cycle(series[:markerstrokewidth], i) - -const _segmenting_vector_attributes = ( - :seriescolor, - :seriesalpha, - :linecolor, - :linealpha, - :linewidth, - :linestyle, - :fillcolor, - :fillalpha, - :fillstyle, - :markercolor, - :markeralpha, - :markersize, - :markerstrokecolor, - :markerstrokealpha, - :markerstrokewidth, - :markershape, -) + # convert into strokes and brushes -const _segmenting_array_attributes = :line_z, :fill_z, :marker_z - -# we want to check if a series needs to be split into segments just because -# of its attributes -# check relevant attributes if they have multiple inputs -has_attribute_segments(series::Series) = - any( - series[attr] isa AbstractVector && length(series[attr]) > 1 for - attr in _segmenting_vector_attributes - ) || any(series[attr] isa AbstractArray for attr in _segmenting_array_attributes) - -check_aspect_ratio(ar::AbstractVector) = nothing # for PyPlot -check_aspect_ratio(ar::Number) = nothing -check_aspect_ratio(ar::Symbol) = - ar in (:none, :equal, :auto) || throw(ArgumentError("Invalid `aspect_ratio` = $ar")) -check_aspect_ratio(ar::T) where {T} = - throw(ArgumentError("Invalid `aspect_ratio`::$T = $ar ")) - -function get_aspect_ratio(sp) - ar = sp[:aspect_ratio] - check_aspect_ratio(ar) - if ar === :auto - ar = :none - for series in series_list(sp) - if series[:seriestype] === :image - ar = :equal - end + if haskey(plotattributes, :arrow) + a = plotattributes[:arrow] + plotattributes[:arrow] = if a == true + arrow() + elseif a in (false, nothing, :none) + nothing + elseif !(typeof(a) <: Arrow || typeof(a) <: AbstractArray{Arrow}) + arrow(wraptuple(a)...) + else + a end end - ar isa Bool && (ar = Int(ar)) # NOTE: Bool <: ... <: Number - ar -end -get_size(series::Series) = get_size(series.plotattributes[:subplot]) -get_size(kw) = get(kw, :size, default(:size)) -get_size(plt::Plot) = get_size(plt.attr) -get_size(sp::Subplot) = get_size(sp.plt) + # legends - defaults are set in `src/components.jl` (see `@add_attributes`) + if haskey(plotattributes, :legend_position) + plotattributes[:legend_position] = + Commons.convert_legend_value(plotattributes[:legend_position]) + end + if haskey(plotattributes, :colorbar) + plotattributes[:colorbar] = Commons.convert_legend_value(plotattributes[:colorbar]) + end -get_thickness_scaling(kw) = get(kw, :thickness_scaling, default(:thickness_scaling)) -get_thickness_scaling(plt::Plot) = get_thickness_scaling(plt.attr) -get_thickness_scaling(sp::Subplot) = get_thickness_scaling(sp.plt) -get_thickness_scaling(series::Series) = - get_thickness_scaling(series.plotattributes[:subplot]) + # framestyle + if haskey(plotattributes, :framestyle) && + haskey(Commons._framestyle_aliases, plotattributes[:framestyle]) + plotattributes[:framestyle] = + Commons._framestyle_aliases[plotattributes[:framestyle]] + end -# --------------------------------------------------------------- -makekw(; kw...) = KW(kw) - -wraptuple(x::Tuple) = x -wraptuple(x) = (x,) - -trueOrAllTrue(f::Function, x::AbstractArray) = all(f, x) -trueOrAllTrue(f::Function, x) = f(x) - -allLineTypes(arg) = trueOrAllTrue(a -> get(_typeAliases, a, a) in _allTypes, arg) -allStyles(arg) = trueOrAllTrue(a -> get(_styleAliases, a, a) in _allStyles, arg) -allShapes(arg) = - (trueOrAllTrue(a -> get(_markerAliases, a, a) in _allMarkers || a isa Shape, arg)) -allAlphas(arg) = trueOrAllTrue( - a -> - (typeof(a) <: Real && a > 0 && a < 1) || ( - typeof(a) <: AbstractFloat && (a == zero(typeof(a)) || a == one(typeof(a))) - ), - arg, -) -allReals(arg) = trueOrAllTrue(a -> typeof(a) <: Real, arg) -allFunctions(arg) = trueOrAllTrue(a -> isa(a, Function), arg) + # contours + if haskey(plotattributes, :levels) + Commons.check_contour_levels(plotattributes[:levels]) + end -# --------------------------------------------------------------- + # warnings for moved recipes + st = get(plotattributes, :seriestype, :path) + if st in (:boxplot, :violin, :density) && + !haskey( + Base.loaded_modules, + Base.PkgId(Base.UUID("f3b207a7-027a-5e70-b257-86293d7955fd"), "StatsPlots"), + ) + @warn "seriestype $st has been moved to StatsPlots. To use: \`Pkg.add(\"StatsPlots\"); using StatsPlots\`" + end + nothing +end """ Allows temporary setting of backend and defaults for Plots. Settings apply only for the `do` block. Example: @@ -657,7 +616,6 @@ function with(f::Function, args...; scalefonts = nothing, kw...) end # save the backend - CURRENT_BACKEND.sym === :none && _pick_default_backend() oldbackend = CURRENT_BACKEND.sym for arg in args @@ -701,248 +659,6 @@ end # --------------------------------------------------------------- -const _debug = Ref(false) - -debug!(on = true) = _debug[] = on -debugshow(io, x) = show(io, x) -debugshow(io, x::AbstractArray) = print(io, summary(x)) - -function dumpdict(io::IO, plotattributes::AKW, prefix = "") - _debug[] || return - println(io) - prefix == "" || println(io, prefix, ":") - for k in sort(collect(keys(plotattributes))) - @printf(io, "%14s: ", k) - debugshow(io, plotattributes[k]) - println(io) - end - println(io) -end - -# ------------------------------------------------------- -# indexing notation - -Base.setindex!(plt::Plot, xy::NTuple{2}, i::Integer) = (setxy!(plt, xy, i); plt) -Base.setindex!(plt::Plot, xyz::Tuple{3}, i::Integer) = (setxyz!(plt, xyz, i); plt) - -# ------------------------------------------------------- -# operate on individual series - -Base.push!(series::Series, args...) = extend_series!(series, args...) -Base.append!(series::Series, args...) = extend_series!(series, args...) - -function extend_series!(series::Series, yi) - y = extend_series_data!(series, yi, :y) - x = extend_to_length!(series[:x], length(y)) - expand_extrema!(series[:subplot][:xaxis], x) - x, y -end - -extend_series!(series::Series, xi, yi) = - (extend_series_data!(series, xi, :x), extend_series_data!(series, yi, :y)) - -extend_series!(series::Series, xi, yi, zi) = ( - extend_series_data!(series, xi, :x), - extend_series_data!(series, yi, :y), - extend_series_data!(series, zi, :z), -) - -function extend_series_data!(series::Series, v, letter) - copy_series!(series, letter) - d = extend_by_data!(series[letter], v) - expand_extrema!(series[:subplot][get_attr_symbol(letter, :axis)], d) - d -end - -function copy_series!(series, letter) - plt = series[:plot_object] - for s in plt.series_list, l in (:x, :y, :z) - if (s !== series || l !== letter) && s[l] === series[letter] - series[letter] = copy(series[letter]) - end - end -end - -extend_to_length!(v::AbstractRange, n) = range(first(v), step = step(v), length = n) -function extend_to_length!(v::AbstractVector, n) - vmax = isempty(v) ? 0 : ignorenan_maximum(v) - extend_by_data!(v, vmax .+ (1:(n - length(v)))) -end -extend_by_data!(v::AbstractVector, x) = isimmutable(v) ? vcat(v, x) : push!(v, x) -extend_by_data!(v::AbstractVector, x::AbstractVector) = - isimmutable(v) ? vcat(v, x) : append!(v, x) - -# ------------------------------------------------------- - -function attr!(series::Series; kw...) - plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes - if haskey(_series_defaults, k) - series[k] = v - else - @warn "unused key $k in series attr" - end - end - _series_updated(series[:subplot].plt, series) - series -end - -function attr!(sp::Subplot; kw...) - plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes - if haskey(_subplot_defaults, k) - sp[k] = v - else - @warn "unused key $k in subplot attr" - end - end - sp -end - -# ------------------------------------------------------- -# push/append for one series - -Base.push!(plt::Plot, args::Real...) = push!(plt, 1, args...) -Base.push!(plt::Plot, i::Integer, args::Real...) = push!(plt.series_list[i], args...) -Base.append!(plt::Plot, args::AbstractVector) = append!(plt, 1, args...) -Base.append!(plt::Plot, i::Integer, args::Real...) = append!(plt.series_list[i], args...) - -# tuples -Base.push!(plt::Plot, t::Tuple) = push!(plt, 1, t...) -Base.push!(plt::Plot, i::Integer, t::Tuple) = push!(plt, i, t...) -Base.append!(plt::Plot, t::Tuple) = append!(plt, 1, t...) -Base.append!(plt::Plot, i::Integer, t::Tuple) = append!(plt, i, t...) - -# ------------------------------------------------------- -# push/append for all series - -# push y[i] to the ith series -function Base.push!(plt::Plot, y::AVec) - ny = length(y) - for i in 1:(plt.n) - push!(plt, i, y[mod1(i, ny)]) - end - plt -end - -# push y[i] to the ith series -# same x for each series -Base.push!(plt::Plot, x::Real, y::AVec) = push!(plt, [x], y) - -# push (x[i], y[i]) to the ith series -function Base.push!(plt::Plot, x::AVec, y::AVec) - nx = length(x) - ny = length(y) - for i in 1:(plt.n) - push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)]) - end - plt -end - -# push (x[i], y[i], z[i]) to the ith series -function Base.push!(plt::Plot, x::AVec, y::AVec, z::AVec) - nx = length(x) - ny = length(y) - nz = length(z) - for i in 1:(plt.n) - push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)], z[mod1(i, nz)]) - end - plt -end - -# --------------------------------------------------------------- - -# Some conversion functions -# note: I borrowed these conversion constants from Compose.jl's Measure - -inch2px(inches::Real) = float(inches * PX_PER_INCH) -px2inch(px::Real) = float(px / PX_PER_INCH) -inch2mm(inches::Real) = float(inches * MM_PER_INCH) -mm2inch(mm::Real) = float(mm / MM_PER_INCH) -px2mm(px::Real) = float(px * MM_PER_PX) -mm2px(mm::Real) = float(mm / MM_PER_PX) - -"Smallest x in plot" -xmin(plt::Plot) = ignorenan_minimum([ - ignorenan_minimum(series.plotattributes[:x]) for series in plt.series_list -]) -"Largest x in plot" -xmax(plt::Plot) = ignorenan_maximum([ - ignorenan_maximum(series.plotattributes[:x]) for series in plt.series_list -]) - -"Extrema of x-values in plot" -ignorenan_extrema(plt::Plot) = (xmin(plt), xmax(plt)) - -# --------------------------------------------------------------- -# get fonts from objects: - -plottitlefont(p::Plot) = font(; - family = p[:plot_titlefontfamily], - pointsize = p[:plot_titlefontsize], - valign = p[:plot_titlefontvalign], - halign = p[:plot_titlefonthalign], - rotation = p[:plot_titlefontrotation], - color = p[:plot_titlefontcolor], -) - -colorbartitlefont(sp::Subplot) = font(; - family = sp[:colorbar_titlefontfamily], - pointsize = sp[:colorbar_titlefontsize], - valign = sp[:colorbar_titlefontvalign], - halign = sp[:colorbar_titlefonthalign], - rotation = sp[:colorbar_titlefontrotation], - color = sp[:colorbar_titlefontcolor], -) - -titlefont(sp::Subplot) = font(; - family = sp[:titlefontfamily], - pointsize = sp[:titlefontsize], - valign = sp[:titlefontvalign], - halign = sp[:titlefonthalign], - rotation = sp[:titlefontrotation], - color = sp[:titlefontcolor], -) - -legendfont(sp::Subplot) = font(; - family = sp[:legend_font_family], - pointsize = sp[:legend_font_pointsize], - valign = sp[:legend_font_valign], - halign = sp[:legend_font_halign], - rotation = sp[:legend_font_rotation], - color = sp[:legend_font_color], -) - -legendtitlefont(sp::Subplot) = font(; - family = sp[:legend_title_font_family], - pointsize = sp[:legend_title_font_pointsize], - valign = sp[:legend_title_font_valign], - halign = sp[:legend_title_font_halign], - rotation = sp[:legend_title_font_rotation], - color = sp[:legend_title_font_color], -) - -tickfont(ax::Axis) = font(; - family = ax[:tickfontfamily], - pointsize = ax[:tickfontsize], - valign = ax[:tickfontvalign], - halign = ax[:tickfonthalign], - rotation = ax[:tickfontrotation], - color = ax[:tickfontcolor], -) - -guidefont(ax::Axis) = font(; - family = ax[:guidefontfamily], - pointsize = ax[:guidefontsize], - valign = ax[:guidefontvalign], - halign = ax[:guidefonthalign], - rotation = ax[:guidefontrotation], - color = ax[:guidefontcolor], -) - -# --------------------------------------------------------------- # converts unicode scientific notation, as returned by Showoff, # to a tex-like format (supported by gr, pyplot, and pgfplots). @@ -1036,7 +752,7 @@ end function straightline_data(series, expansion_factor = 1) sp = series[:subplot] - xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) + xl, yl = (xlims(sp), ylims(sp)) # handle axes scales xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) @@ -1080,7 +796,7 @@ end function shape_data(series, expansion_factor = 1) sp = series[:subplot] - xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) + xl, yl = (xlims(sp), ylims(sp)) # handle axes scales xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) @@ -1132,12 +848,6 @@ function mesh3d_triangles(x, y, z, cns::AbstractVector{NTuple{3,Int}}) X, Y, Z end -# cache joined symbols so they can be looked up instead of constructed each time -const _attrsymbolcache = Dict{Symbol,Dict{Symbol,Symbol}}() - -get_attr_symbol(letter::Symbol, keyword::String) = get_attr_symbol(letter, Symbol(keyword)) -get_attr_symbol(letter::Symbol, keyword::Symbol) = _attrsymbolcache[letter][keyword] - texmath2unicode(s::AbstractString, pat = r"\$([^$]+)\$") = replace(s, pat => m -> UnicodeFun.to_latex(m[2:(length(m) - 1)])) @@ -1173,7 +883,7 @@ end _argument_description(s::Symbol) = if s ∈ keys(_arg_desc) - aliases = if (al = Plots.aliases(s)) |> length > 0 + aliases = if (al = Plots.Commons.aliases(s)) |> length > 0 " Aliases: " * string(Tuple(al)) * '.' else "" @@ -1250,6 +960,9 @@ macro ext_imp_use(imp_use::QuoteNode, mod::Symbol, args...) Expr(imp_use.value, ex) |> esc end +_generate_doclist(attributes) = + replace(join(sort(collect(attributes)), "\n- "), "_" => "\\_") + # for UnitfulExt - cannot reside in `UnitfulExt` (macro) function protectedstring end # COV_EXCL_LINE @@ -1272,3 +985,10 @@ end # for `PGFPlotsx` together with `UnitfulExt` function pgfx_sanitize_string end # COV_EXCL_LINE + +function extrema_plus_buffer(v, buffmult = 0.2) + vmin, vmax = ignorenan_extrema(v) + vdiff = vmax - vmin + buffer = vdiff * buffmult + vmin - buffer, vmax + buffer +end diff --git a/test/runtests.jl b/test/runtests.jl index f568f6d5e..cd1cedb6f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,6 @@ import Plots: PLOTS_SEED, Plot, with import SentinelArrays: ChainedVector import GeometryBasics import OffsetArrays -import ImageMagick import FreeType # for `unicodeplots` import LibGit2 import Aqua @@ -21,29 +20,49 @@ using FileIO using Plots using Dates using Test -using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 - -# get `Preferences` set backend, if any -const PREVIOUS_DEFAULT_BACKEND = load_preference(Plots, "default_backend") # NOTE: don't use `plotly` (test hang, not surprised), test only the backends used in the docs -const TEST_BACKENDS = - :gr, :unicodeplots, :pythonplot, :pgfplotsx, :plotlyjs, :gaston, :inspectdr +const TEST_BACKENDS = let + var = get(ENV, "PLOTS_TEST_BACKENDS", nothing) + if var !== nothing + Symbol.(lowercase.(strip.(split(var, ",")))) + else + [ + :gr, + :unicodeplots, + # :pythonplot, # currently segfaults + :pgfplotsx, + :plotlyjs, + # :gaston, # currently doesn't precompile (on julia v1.10) + # :inspectdr # currently doesn't precompile + ] + end +end # initial load - required for `should_warn_on_unsupported` -unicodeplots() -pgfplotsx() -plotlyjs() -plotly() -hdf5() + +import GR +import UnicodePlots +import PythonPlot +import PGFPlotsX +import PlotlyJS +# import Gaston +# initialize all backends +for be in TEST_BACKENDS + getproperty(Plots, be)() +end gr() is_auto() = Plots.bool_env("VISUAL_REGRESSION_TESTS_AUTO", "false") is_pkgeval() = Plots.bool_env("JULIA_PKGEVAL", "false") is_ci() = Plots.bool_env("CI", "false") +if !is_ci() + @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 +end + for name in ( - "quality", + # "quality", # Persistent tasks cannot resolve versions "misc", "utils", "args", @@ -55,13 +74,12 @@ for name in ( "components", "shorthands", "recipes", - "unitful", - "hdf5plots", + # "unitful", # many fail + # "hdf5plots", "pgfplotsx", "plotly", - "animations", - "output", - "preferences", + # "animations", # some failing + # "output", # some plotly failing "backends", ) @testset "$name" begin @@ -73,9 +91,3 @@ for name in ( include("test_$name.jl") end end - -if PREVIOUS_DEFAULT_BACKEND === nothing - delete_preferences!(Plots, "default_backend") # restore the absence of a preference -else - Plots.set_default_backend!(PREVIOUS_DEFAULT_BACKEND) # reset to previous state -end diff --git a/test/test_args.jl b/test/test_args.jl index 30e337a02..569d4fe3a 100644 --- a/test/test_args.jl +++ b/test/test_args.jl @@ -91,9 +91,9 @@ end end @testset "aliases" begin - @test :legend in aliases(:legend_position) - Plots.add_non_underscore_aliases!(Plots._typeAliases) - Plots.add_axes_aliases(:ticks, :tick) + @test :legend in Plots.Commons.aliases(:legend_position) + Plots.Commons.add_non_underscore_aliases!(Plots.Commons._typeAliases) + Plots.Commons.add_axes_aliases(:ticks, :tick) end @userplot MatrixHeatmap diff --git a/test/test_axes.jl b/test/test_axes.jl index 62092ac5b..edb8911ae 100644 --- a/test/test_axes.jl +++ b/test/test_axes.jl @@ -4,12 +4,12 @@ @test typeof(axis) == Plots.Axis @test Plots.discrete_value!(axis, "HI") == (0.5, 1) @test Plots.discrete_value!(axis, :yo) == (1.5, 2) - @test Plots.ignorenan_extrema(axis) == (0.5, 1.5) + @test Plots.Axes.ignorenan_extrema(axis) == (0.5, 1.5) @test axis[:discrete_map] == Dict{Any,Any}(:yo => 2, "HI" => 1) Plots.discrete_value!(axis, map(i -> "x$i", 1:5)) Plots.discrete_value!(axis, map(i -> "x$i", 0:2)) - @test Plots.ignorenan_extrema(axis) == (0.5, 7.5) + @test Plots.Axes.ignorenan_extrema(axis) == (0.5, 7.5) # github.com/JuliaPlots/Plots.jl/issues/4375 for lab in ("foo", :foo) @@ -50,7 +50,7 @@ end @testset "Showaxis" begin - for value in Plots._allShowaxisArgs + for value in Plots.Commons._all_showaxis_attrs @test plot(1:5, showaxis = value)[1][:yaxis][:showaxis] isa Bool end @test plot(1:5, showaxis = :y)[1][:yaxis][:showaxis] @@ -82,7 +82,8 @@ end end @testset "Axis limits" begin - default_widen(from, to) = Plots.scale_lims(from, to, Plots.default_widen_factor) + default_widen(from, to) = + Plots.Axes.scale_lims(from, to, Plots.Axes.default_widen_factor) pl = plot(1:5, xlims = :symmetric, widen = false) @test Plots.xlims(pl) == (-5, 5) @@ -149,9 +150,9 @@ end end @testset "Axis-aliases" begin - @test haskey(Plots._keyAliases, :xguideposition) - @test haskey(Plots._keyAliases, :x_guide_position) - @test !haskey(Plots._keyAliases, :xguide_position) + @test haskey(Plots.Commons._keyAliases, :xguideposition) + @test haskey(Plots.Commons._keyAliases, :x_guide_position) + @test !haskey(Plots.Commons._keyAliases, :xguide_position) pl = plot(1:2, xl = "x label") @test pl[1][:xaxis][:guide] === "x label" pl = plot(1:2, xrange = (0, 3)) @@ -208,7 +209,7 @@ end @testset "scale_lims!" begin let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.scale_lims!(:x, 1.1) + Plots.Axes.scale_lims!(:x, 1.1) @test first(xlims(pl)) < first(xl) @test last(xlims(pl)) > last(xl) @test ylims(pl) == yl @@ -216,7 +217,7 @@ end let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.scale_lims!(pl, 1.1) + Plots.PlotsPlots.scale_lims!(pl, 1.1) @test first(xlims(pl)) < first(xl) @test last(xlims(pl)) > last(xl) @test first(ylims(pl)) < first(yl) @@ -226,7 +227,7 @@ end @testset "reset_extrema!" begin pl = plot(1:2) - Plots.reset_extrema!(pl[1]) + Plots.Axes.reset_extrema!(pl[1]) ax = pl[1][:xaxis] @test Plots.expand_extrema!(ax, nothing) == ax[:extrema] @test Plots.expand_extrema!(ax, true) == ax[:extrema] @@ -242,9 +243,9 @@ end # FIXME in 2.0: this is awful to read, because `minorticks` represent the number of `intervals` for minor_intervals in (:auto, :none, nothing, false, true, 0, 1, 2, 3, 4, 5) n_minor_ticks_per_major = if minor_intervals isa Bool - minor_intervals ? Plots.DEFAULT_MINOR_INTERVALS[] - 1 : 0 + minor_intervals ? Plots.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 elseif minor_intervals === :auto - Plots.DEFAULT_MINOR_INTERVALS[] - 1 + Plots.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 elseif minor_intervals === :none || minor_intervals isa Nothing 0 else diff --git a/test/test_backends.jl b/test/test_backends.jl index d6e1c3c04..bc8e83dc6 100644 --- a/test/test_backends.jl +++ b/test/test_backends.jl @@ -86,7 +86,7 @@ function image_comparison_tests( imports = something(example.imports, :()) exprs = quote - Plots.debug!($debug) + Plots.Commons.debug!($debug) backend($(QuoteNode(pkg))) theme(:default) rng = StableRNG(Plots.PLOTS_SEED) @@ -130,10 +130,6 @@ with(:plotlyjs) do image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:plotlyjs]) end -with(:pyplot) do - image_comparison_facts(:pyplot, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:pyplot]) -end - with(:pgfplotsx) do image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:pgfplotsx]) end @@ -141,7 +137,7 @@ end @testset "UnicodePlots" begin with(:unicodeplots) do - @test backend() == Plots.UnicodePlotsBackend() + @test backend() == Plots._backend_instance(:unicodeplots) io = IOContext(IOBuffer(), :color => true) @@ -183,16 +179,19 @@ end end const blacklist = if VERSION.major == 1 && VERSION.minor ∈ (9, 10) - [41] # FIXME: github.com/JuliaLang/julia/issues/47261 + [ + 25, + 30, # FIXME: remove, when StatsPlots supports Plots v2 + 41, + ] # FIXME: github.com/JuliaLang/julia/issues/47261 else [] end -push!(blacklist, 50) # NOTE: remove when github.com/jheinen/GR.jl/issues/507 is resolved @testset "GR - reference images" begin with(:gr) do # NOTE: use `ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true;` to automatically replace reference images - @test backend() == Plots.GRBackend() + @test backend() == Plots._backend_instance(:gr) @test backend_name() === :gr image_comparison_facts( :gr, @@ -202,14 +201,14 @@ push!(blacklist, 50) # NOTE: remove when github.com/jheinen/GR.jl/issues/507 i end end -is_pkgeval() || @testset "PlotlyJS" begin - with(:plotlyjs) do - @test backend() == Plots.PlotlyJSBackend() - pl = plot(rand(10)) - @test pl isa Plot - @test_broken display(pl) isa Nothing - end -end +# is_pkgeval() || @testset "PlotlyJS" begin +# with(:plotlyjs) do +# @test backend() == Plots.PlotlyJSBackend() +# pl = plot(rand(10)) +# @test pl isa Plot +# @test display(pl) isa Nothing +# end +# end is_pkgeval() || @testset "Examples" begin callback(m, pkgname, i) = begin @@ -222,7 +221,8 @@ is_pkgeval() || @testset "Examples" begin ) @test filesize(fn) > 1_000 end - Sys.islinux() && for be in TEST_BACKENDS + # TODO: check whats up with those who are filtered + Sys.islinux() && for be in filter(∉((:plotlyjs, :gaston)), TEST_BACKENDS) skip = vcat(Plots._backend_skips[be], blacklist) Plots.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage closeall() diff --git a/test/test_components.jl b/test/test_components.jl index 971f4e519..026e3ac3b 100644 --- a/test/test_components.jl +++ b/test/test_components.jl @@ -1,9 +1,12 @@ @testset "Shapes" begin + get_xs = Plots.Shapes.get_xs + get_ys = Plots.Shapes.get_ys + vertices = Plots.Shapes.vertices @testset "Type" begin square = Shape([(0, 0.0), (1, 0.0), (1, 1.0), (0, 1.0)]) - @test Plots.get_xs(square) == [0, 1, 1, 0] - @test Plots.get_ys(square) == [0, 0, 1, 1] - @test Plots.vertices(square) == [(0, 0), (1, 0), (1, 1), (0, 1)] + @test get_xs(square) == [0, 1, 1, 0] + @test get_ys(square) == [0, 0, 1, 1] + @test vertices(square) == [(0, 0), (1, 0), (1, 1), (0, 1)] @test isa(square, Shape{Int64,Float64}) @test coords(square) isa Tuple{Vector{S},Vector{T}} where {T,S} @test Shape(:circle) isa Shape @@ -12,7 +15,7 @@ ys = view([6 4 7; 9 9 9], 1, :) tri = Shape(xs, ys) @test isa(tri, Shape{Float64,Int64}) - @test Plots.vertices(tri) == [(0.0, 6), (1.0, 4), (2.0, 7)] + @test vertices(tri) == [(0.0, 6), (1.0, 4), (2.0, 7)] end @testset "Copy" begin @@ -80,8 +83,8 @@ star_scaled = Plots.scale(star, 0.5) Plots.scale!(star, 0.5) - @test Plots.get_xs(star) == Plots.get_xs(star_scaled) - @test Plots.get_ys(star) == Plots.get_ys(star_scaled) + @test get_xs(star) == get_xs(star_scaled) + @test get_ys(star) == get_ys(star_scaled) @test Plots.extrema_plus_buffer([1, 2], 0.1) == (0.9, 2.1) end @@ -166,7 +169,7 @@ end annotate!(sp = 2, (0.03, 0.95), text("Cats&Dogs", :left)) end - for scale in Plots._logScales + for scale in Plots._log_scales pl = plot(xlim = (1, 10), xscale = scale) annotate!(pl, (0.5, 0.5), "hello") end @@ -218,6 +221,9 @@ end end @testset "Series Annotations" begin + get_xs = Plots.Shapes.get_xs + get_ys = Plots.Shapes.get_ys + vertices = Plots.Shapes.vertices square = Shape([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]) @test_logs (:warn, "Unused SeriesAnnotations arg: triangle (Symbol)") begin pl = plot( @@ -308,7 +314,7 @@ end end @testset "Bezier" begin - curve = Plots.BezierCurve([(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) + curve = Plots.BezierCurves.BezierCurve([(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) @test curve(0.75) == (0.75, 0.375) @test length(coords(curve, 10)) == 10 end diff --git a/test/test_contours.jl b/test/test_contours.jl index 3c7672733..948fd3657 100644 --- a/test/test_contours.jl +++ b/test/test_contours.jl @@ -1,25 +1,29 @@ @testset "check_contour_levels" begin - @test Plots.check_contour_levels(2) === nothing - @test Plots.check_contour_levels(-1.0:0.2:10.0) === nothing - @test Plots.check_contour_levels([-100, -2, -1, 0, 1, 2, 100]) === nothing - @test_throws ArgumentError Plots.check_contour_levels(1.0) - @test_throws ArgumentError Plots.check_contour_levels((1, 2, 3)) - @test_throws ArgumentError Plots.check_contour_levels(-3) + let check_contour_levels = Plots.Commons.check_contour_levels + @test check_contour_levels(2) === nothing + @test check_contour_levels(-1.0:0.2:10.0) === nothing + @test check_contour_levels([-100, -2, -1, 0, 1, 2, 100]) === nothing + @test_throws ArgumentError check_contour_levels(1.0) + @test_throws ArgumentError check_contour_levels((1, 2, 3)) + @test_throws ArgumentError check_contour_levels(-3) + end end -@testset "Plots.preprocess_attributes!" begin +@testset "Plots.Commons.preprocess_attributes!" begin function equal_after_pipeline(kw) kw′ = deepcopy(kw) - Plots.preprocess_attributes!(kw′) + Plots.Commons.preprocess_attributes!(kw′) kw == kw′ end @test equal_after_pipeline(KW(:levels => 1)) @test equal_after_pipeline(KW(:levels => 1:10)) @test equal_after_pipeline(KW(:levels => [1.0, 3.0, 5.0])) - @test_throws ArgumentError Plots.preprocess_attributes!(KW(:levels => 1.0)) - @test_throws ArgumentError Plots.preprocess_attributes!(KW(:levels => (1, 2, 3))) - @test_throws ArgumentError Plots.preprocess_attributes!(KW(:levels => -3)) + @test_throws ArgumentError Plots.Commons.preprocess_attributes!(KW(:levels => 1.0)) + @test_throws ArgumentError Plots.Commons.preprocess_attributes!( + KW(:levels => (1, 2, 3)), + ) + @test_throws ArgumentError Plots.Commons.preprocess_attributes!(KW(:levels => -3)) end @testset "contour[f]" begin diff --git a/test/test_layouts.jl b/test/test_layouts.jl index 2bdc2fbcb..8b1510a37 100644 --- a/test/test_layouts.jl +++ b/test/test_layouts.jl @@ -85,7 +85,7 @@ end @test gl isa Plots.GridLayout @test length(gl) == 1 @test size(gl) == (1, 1) - @test Plots.layout_args(gl) == (gl, 1) + @test Plots.layout_attrs(gl) == (gl, 1) @test size(pl, 1) == 2 @test size(pl, 2) == 2 diff --git a/test/test_misc.jl b/test/test_misc.jl index 9559b4fc7..f0506a3d9 100644 --- a/test/test_misc.jl +++ b/test/test_misc.jl @@ -21,7 +21,7 @@ end @testset "NoFail" begin with(:unicodeplots) do - @test backend() == Plots.UnicodePlotsBackend() + @test backend() == Plots._backend_instance(:unicodeplots) dsp = TextDisplay(IOContext(IOBuffer(), :color => true)) @@ -128,13 +128,6 @@ end value.(m) end - @testset "orientation" begin - for f in (histogram, barhist, stephist, scatterhist), o in (:vertical, :horizontal) - sp = f(data, orientation = o).subplots[1] - @test sp.attr[:title] == (o ≡ :vertical ? "x" : "y") - end - end - @testset "$f" for f in (hline, hspan) @test f(data).subplots[1].attr[:title] == "y" end @@ -169,11 +162,11 @@ end for i in axes(data4, 1) for attribute in (:fillrange, :ribbon) nt = NamedTuple{tuple(attribute)} - get_attr(pl) = pl[1][i][attribute] - @test plot(data4; nt(0)...) |> get_attr == 0 - @test plot(data4; nt(Ref([1, 2]))...) |> get_attr == [1.0, 2.0] - @test plot(data4; nt(Ref([1 2]))...) |> get_attr == (iseven(i) ? 2 : 1) - @test plot(data4; nt(Ref(mat))...) |> get_attr == [2(i - 1) + 1, 2i] + get_attrs(pl) = pl[1][i][attribute] + @test plot(data4; nt(0)...) |> get_attrs == 0 + @test plot(data4; nt(Ref([1, 2]))...) |> get_attrs == [1.0, 2.0] + @test plot(data4; nt(Ref([1 2]))...) |> get_attrs == (iseven(i) ? 2 : 1) + @test plot(data4; nt(Ref(mat))...) |> get_attrs == [2(i - 1) + 1, 2i] end @test sp[i][:ribbon] == ([2(i - 1) + 1, 2i], [2(i - 1) + 1, 2i]) end @@ -218,14 +211,14 @@ end end @testset "docstring" begin - @test occursin("label", Plots._generate_doclist(Plots._all_series_args)) + @test occursin("label", Plots._generate_doclist(Plots.Commons._all_series_attrs)) end -@testset "wrap" begin +@testset "protect" begin # not sure what is intended here ... - wrapped = wrap([:red, :blue]) - @test !isempty(wrapped) - @test scatter(1:2, color = wrapped) isa Plots.Plot + protected = protect([:red, :blue]) + @test !isempty(protected) + @test scatter(1:2, color = protected) isa Plots.Plot end @testset "group" begin @@ -246,7 +239,7 @@ with(:gr) do io = PipeBuffer() x = y = range(-3, 3, length = 10) extra_kwargs = Dict( - :series => Dict(:display_option => Plots.GR.OPTION_SHADED_MESH), + :series => Dict(:display_option => GR.OPTION_SHADED_MESH), :subplot => Dict(:legend_hfactor => 2), :plot => Dict(:foo => nothing), ) diff --git a/test/test_output.jl b/test/test_output.jl index 12f3270a3..75cacad06 100644 --- a/test/test_output.jl +++ b/test/test_output.jl @@ -30,7 +30,7 @@ macro test_save(fmt) end with(:gr) do - @test Plots.defaultOutputFormat(plot()) == "png" + @test Plots.default_output_format(plot()) == "png" @test Plots.addExtension("foo", "bar") == "foo.bar" @test_save :png @@ -41,7 +41,9 @@ end with(:unicodeplots) do @test_save :txt - if Plots.UnicodePlots.get_font_face() ≢ nothing + get_font_face = + Base.get_extension(Plots, :PlotsUnicodePlotsExt).UnicodePlots.get_font_face + if get_font_face() ≢ nothing @test_save :png end end @@ -69,13 +71,13 @@ if Sys.islinux() && Sys.which("pdflatex") ≢ nothing @test_save :pdf end - with(:pythonplot) do - @test_save :pdf - @test_save :png - @test_save :svg - @test_save :eps - @test_save :ps - end + # with(:pythonplot) do + # @test_save :pdf + # @test_save :png + # @test_save :svg + # @test_save :eps + # @test_save :ps + # end end #= diff --git a/test/test_pgfplotsx.jl b/test/test_pgfplotsx.jl index 5d178603a..8eb602e6e 100644 --- a/test/test_pgfplotsx.jl +++ b/test/test_pgfplotsx.jl @@ -1,4 +1,5 @@ using Test, Plots, Unitful, LaTeXStrings +import PGFPlotsX function create_plot(args...; kwargs...) pl = plot(args...; kwargs...) @@ -12,7 +13,7 @@ end function get_pgf_axes(pl) Plots._update_plot_object(pl) - Plots.pgfx_axes(pl.o) + Plots.get_backend_module(:PGFPlotsX)[1].pgfx_axes(pl.o) end with(:pgfplotsx) do @@ -108,7 +109,7 @@ with(:pgfplotsx) do @testset "Marker types" begin markers = filter((m -> begin m in Plots.supported_markers() - end), Plots._shape_keys) + end), Plots.Commons._shape_keys) markers = reshape(markers, 1, length(markers)) n = length(markers) x = (range(0, stop = 10, length = n + 2))[2:(end - 1)] diff --git a/test/test_preferences.jl b/test/test_preferences.jl deleted file mode 100644 index 033087001..000000000 --- a/test/test_preferences.jl +++ /dev/null @@ -1,59 +0,0 @@ - -@testset "Preferences" begin - Plots.set_default_backend!() # start with empty preferences - - withenv("PLOTS_DEFAULT_BACKEND" => "invalid") do - @test_logs (:warn, r".*is not a supported backend") Plots.load_default_backend() - end - @test_logs (:warn, r".*is not a supported backend") backend(:invalid) - - @test Plots.load_default_backend() == Plots.GRBackend() - - withenv("PLOTS_DEFAULT_BACKEND" => "unicodeplots") do - @test_logs (:info, r".*environment variable") Plots.diagnostics(devnull) - @test Plots.load_default_backend() == Plots.UnicodePlotsBackend() - end - - @test Plots.load_default_backend() == Plots.GRBackend() - @test Plots.backend_package_name() === :GR - @test Plots.backend_name() === :gr - - @test_logs (:info, r".*fallback") Plots.diagnostics(devnull) - - @test Plots.merge_with_base_supported([:annotations, :guide]) isa Set - @test Plots.CurrentBackend(:gr).sym === :gr - - @test_logs (:warn, r".*is not compatible with") Plots.set_default_backend!(:invalid) - - @testset "persistent backend" begin - # this test mimics a restart, which is needed after a preferences change - Plots.set_default_backend!(:unicodeplots) - script = tempname() - write( - script, - """ - using Pkg, Test; io = (devnull, stdout)[1] # toggle for debugging - Pkg.activate(; temp = true, io) - Pkg.develop(; path = "$(escape_string(pkgdir(Plots)))", io) - Pkg.add("UnicodePlots"; io) # checked by Plots - using Plots - res = @testset "Prefs" begin - @test_logs (:info, r".*Preferences") Plots.diagnostics(io) - @test backend() == Plots.UnicodePlotsBackend() - end - exit(res.n_passed == 2 ? 0 : 1) - """, - ) - @test success(run(```$(Base.julia_cmd()) $script```)) - end - - is_pkgeval() || for be in TEST_BACKENDS - (Sys.isapple() && be === :gaston) && continue # FIXME: hangs - (Sys.iswindows() && be === :plotlyjs && is_ci()) && continue # OutOfMemory - @test_logs Plots.set_default_backend!(be) # test the absence of warnings - rm.(Base.find_all_in_cache_path(Base.module_keys[Plots])) # make sure the compiled cache is removed - @test success(run(```$(Base.julia_cmd()) -e 'using Plots'```)) # test default precompilation - end - - Plots.set_default_backend!() # clear `Preferences` key -end diff --git a/test/test_recipes.jl b/test/test_recipes.jl index 34273b172..b4ac1e368 100644 --- a/test/test_recipes.jl +++ b/test/test_recipes.jl @@ -93,10 +93,13 @@ end end @testset "coverage" begin + # TODO: that should cover all seriestypes without the need to have the extension loaded + # currently uses plotly seriestypes only @test :surface in Plots.all_seriestypes() - @test Plots.seriestype_supported(Plots.UnicodePlotsBackend(), :surface) === :native - @test Plots.seriestype_supported(Plots.UnicodePlotsBackend(), :hspan) === :recipe - @test Plots.seriestype_supported(Plots.NoBackend(), :line) === :no + unicode_instance = Plots._backend_instance(:unicodeplots) + @test Plots.seriestype_supported(unicode_instance, :surface) === :native + @test Plots.seriestype_supported(unicode_instance, :hspan) === :recipe + @test Plots.seriestype_supported(Plots.NoBackend(), :line) === :native end with(:gr) do diff --git a/test/test_utils.jl b/test/test_utils.jl index e597c90c2..e093b7b98 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -15,13 +15,13 @@ @test isequal(collect(zip(Plots.unzip(z)...)), z) @test isequal(collect(zip(Plots.unzip(GeometryBasics.Point.(z))...)), z) end - op1 = Plots.process_clims((1.0, 2.0)) - op2 = Plots.process_clims((1, 2.0)) + op1 = Plots.Colorbars.process_clims((1.0, 2.0)) + op2 = Plots.Colorbars.process_clims((1, 2.0)) data = randn(100, 100) @test op1(data) == op2(data) - @test Plots.process_clims(nothing) == - Plots.process_clims(missing) == - Plots.process_clims(:auto) + @test Plots.Colorbars.process_clims(nothing) == + Plots.Colorbars.process_clims(missing) == + Plots.Colorbars.process_clims(:auto) @test (==)( Plots.texmath2unicode( @@ -49,12 +49,12 @@ @test Plots.nansplit([1, 2, NaN, 3, 4]) == [[1.0, 2.0], [3.0, 4.0]] @test Plots.nanvcat([1, NaN]) |> length == 4 - @test Plots.inch2px(1) isa AbstractFloat - @test Plots.px2inch(1) isa AbstractFloat - @test Plots.inch2mm(1) isa AbstractFloat - @test Plots.mm2inch(1) isa AbstractFloat - @test Plots.px2mm(1) isa AbstractFloat - @test Plots.mm2px(1) isa AbstractFloat + @test Plots.PlotMeasures.inch2px(1) isa AbstractFloat + @test Plots.PlotMeasures.px2inch(1) isa AbstractFloat + @test Plots.PlotMeasures.inch2mm(1) isa AbstractFloat + @test Plots.PlotMeasures.mm2inch(1) isa AbstractFloat + @test Plots.PlotMeasures.px2mm(1) isa AbstractFloat + @test Plots.PlotMeasures.mm2px(1) isa AbstractFloat pl = plot() @test xlims() isa Tuple @@ -66,17 +66,15 @@ @test plot(-1:10, xscale = :log10) isa Plots.Plot - Plots.makekw(foo = 1, bar = 2) isa Dict - ###################### - Plots.debug!(true) + Plots.Commons.debug!(true) io = PipeBuffer() - Plots.debugshow(io, nothing) - Plots.debugshow(io, [1]) + Plots.Commons.debugshow(io, nothing) + Plots.Commons.debugshow(io, [1]) pl = plot(1:2) - Plots.dumpdict(devnull, first(pl.series_list).plotattributes) + Plots.Commons.dumpdict(devnull, first(pl.series_list).plotattributes) show(devnull, pl[1][:xaxis]) # bounding boxes @@ -84,7 +82,7 @@ show(devnull, plot(1:2)) end - Plots.debug!(false) + Plots.Commons.debug!(false) ###################### let pl = plot(1) @@ -104,12 +102,12 @@ push!(pl, 1:2, 2:3, 3:4) pl = plot([1, 2, 3], [4, 5, 6]) - @test Plots.xmin(pl) == 1 - @test Plots.xmax(pl) == 3 - @test Plots.ignorenan_extrema(pl) == (1, 3) + @test Plots.PlotsPlots.xmin(pl) == 1 + @test Plots.PlotsPlots.xmax(pl) == 3 + @test Plots.Commons.ignorenan_extrema(pl) == (1, 3) - @test Plots.get_attr_symbol(:x, "lims") === :xlims - @test Plots.get_attr_symbol(:x, :lims) === :xlims + @test Plots.Commons.get_attr_symbol(:x, "lims") === :xlims + @test Plots.Commons.get_attr_symbol(:x, :lims) === :xlims @test contains(Plots._document_argument(:bar_position), "bar_position") @@ -118,11 +116,11 @@ @test Plots.limsType(:auto) === :auto @test Plots.limsType(NaN) === :invalid - @test Plots.ticksType([1, 2]) === :ticks - @test Plots.ticksType(["1", "2"]) === :labels - @test Plots.ticksType(([1, 2], ["1", "2"])) === :ticks_and_labels - @test Plots.ticksType(((1, 2), ("1", "2"))) === :ticks_and_labels - @test Plots.ticksType(:undefined) === :invalid + @test Plots.ticks_type([1, 2]) === :ticks + @test Plots.ticks_type(["1", "2"]) === :labels + @test Plots.ticks_type(([1, 2], ["1", "2"])) === :ticks_and_labels + @test Plots.ticks_type(((1, 2), ("1", "2"))) === :ticks_and_labels + @test Plots.ticks_type(:undefined) === :invalid pl = plot(1:2, 1:2, 1:2, proj_type = :ortho) @test Plots.isortho(first(pl.subplots)) @@ -132,23 +130,23 @@ let pl = plot(1:2) series = first(pl.series_list) label = "fancy label" - attr!(series; label) + Plots.PlotsSeries.attr!(series; label) @test series[:label] == label - @test Plots.attr(series, :label) == label + @test Plots.PlotsSeries.attr(series, :label) == label label = "another label" - attr!(series, label, :label) - @test Plots.attr(series, :label) == label + Plots.PlotsSeries.attr!(series, label, :label) + @test Plots.PlotsSeries.attr(series, :label) == label sp = first(pl.subplots) title = "fancy title" - attr!(sp; title) + Plots.Subplots.attr!(sp; title) @test sp[:title] == title end end @testset "NaN-separated Segments" begin - segments(args...) = collect(iter_segments(args...)) + segments(args...) = collect(Plots.PlotsSeries.iter_segments(args...)) nan10 = fill(NaN, 10) @test segments(11:20) == [1:10] @@ -301,7 +299,7 @@ end pl = heatmap(rand(10, 10); xscale = :log10, yscale = :log10) @test show(devnull, pl) isa Nothing - pl = plot(Shape([(1, 1), (2, 1), (2, 2), (1, 2)]); xscale = :log10) + pl = plot(Plots.Shape([(1, 1), (2, 1), (2, 2), (1, 2)]); xscale = :log10) @test show(devnull, pl) isa Nothing end end From b1811baa5bec6c7243c6bd2ca0207611ea343425 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Mon, 1 Apr 2024 02:59:58 +0200 Subject: [PATCH 02/85] create `PlotsBase` (#4913) * rework extensions * need to fix `test_backends` * test `Plots` pass * fix test * restore tests * format * update --- .github/workflows/ci.yml | 61 +- .gitignore | 1 + PlotsBase/Project.toml | 140 +++++ {ext => PlotsBase/ext}/FileIOExt.jl | 10 +- .../gr.jl => PlotsBase/ext/GRExt.jl | 280 ++++++++- .../gaston.jl => PlotsBase/ext/GastonExt.jl | 199 ++++++- {ext => PlotsBase/ext}/GeometryBasicsExt.jl | 8 +- .../ext/HDF5Ext.jl | 234 +++++--- {ext => PlotsBase/ext}/IJuliaExt.jl | 21 +- PlotsBase/ext/ImageInTerminalExt.jl | 31 + .../ext/PGFPlotsXExt.jl | 282 ++++++++- PlotsBase/ext/PlotlyJSExt.jl | 115 ++++ .../ext/PlotlyKaleidoExt.jl | 13 +- .../ext/PythonPlotExt.jl | 340 +++++++++-- .../ext/UnicodePlotsExt.jl | 189 +++++- {ext => PlotsBase/ext}/UnitfulExt.jl | 47 +- {src => PlotsBase/src}/Annotations.jl | 14 +- {src => PlotsBase/src}/Arrows.jl | 2 +- {src => PlotsBase/src}/Axes.jl | 47 +- {src => PlotsBase/src}/BezierCurves.jl | 4 +- {src => PlotsBase/src}/Colorbars.jl | 16 +- {src => PlotsBase/src}/Commons/Commons.jl | 25 +- {src => PlotsBase/src}/Commons/aliases.jl | 0 {src => PlotsBase/src}/Commons/attrs.jl | 28 +- .../src}/Commons/postprocess_attrs.jl | 0 {src => PlotsBase/src}/Fonts.jl | 8 +- {src => PlotsBase/src}/PlotMeasures.jl | 0 PlotsBase/src/PlotsBase.jl | 204 +++++++ {src => PlotsBase/src}/PlotsPlots.jl | 56 +- {src => PlotsBase/src}/Series.jl | 41 +- {src => PlotsBase/src}/Shapes.jl | 8 +- {src => PlotsBase/src}/Strokes.jl | 4 +- {src => PlotsBase/src}/Subplots.jl | 50 +- {src => PlotsBase/src}/Surfaces.jl | 8 +- {src => PlotsBase/src}/Ticks.jl | 6 +- {src => PlotsBase/src}/abstract_backend.jl | 44 +- {src => PlotsBase/src}/alignment.jl | 0 {src => PlotsBase/src}/animation.jl | 8 +- {src => PlotsBase/src}/arg_desc.jl | 0 {src => PlotsBase/src}/axes_utils.jl | 0 {src => PlotsBase/src}/backends/nobackend.jl | 0 {src => PlotsBase/src}/backends/plotly.jl | 104 ++-- {src => PlotsBase/src}/backends/web.jl | 0 {src => PlotsBase/src}/examples.jl | 102 ++-- PlotsBase/src/init.jl | 61 ++ {src => PlotsBase/src}/layouts.jl | 0 {src => PlotsBase/src}/legend.jl | 0 {src => PlotsBase/src}/output.jl | 2 +- {src => PlotsBase/src}/pipeline.jl | 2 +- {src => PlotsBase/src}/plot.jl | 8 +- {src => PlotsBase/src}/plotattr.jl | 2 +- {src => PlotsBase/src}/recipes.jl | 12 +- {src => PlotsBase/src}/shorthands.jl | 0 {src => PlotsBase/src}/themes.jl | 0 {src => PlotsBase/src}/users.jl | 0 {src => PlotsBase/src}/utils.jl | 31 +- {test => PlotsBase/test}/.gitignore | 0 PlotsBase/test/runtests.jl | 79 +++ {test => PlotsBase/test}/test_animations.jl | 10 +- {test => PlotsBase/test}/test_args.jl | 14 +- {test => PlotsBase/test}/test_axes.jl | 121 ++-- {test => PlotsBase/test}/test_backends.jl | 67 +-- {test => PlotsBase/test}/test_components.jl | 90 +-- {test => PlotsBase/test}/test_contours.jl | 14 +- {test => PlotsBase/test}/test_dates.jl | 6 +- {test => PlotsBase/test}/test_defaults.jl | 18 +- {test => PlotsBase/test}/test_hdf5plots.jl | 6 +- {test => PlotsBase/test}/test_layouts.jl | 58 +- {test => PlotsBase/test}/test_misc.jl | 104 ++-- {test => PlotsBase/test}/test_output.jl | 78 ++- {test => PlotsBase/test}/test_pgfplotsx.jl | 83 +-- {test => PlotsBase/test}/test_plotly.jl | 30 +- {test => PlotsBase/test}/test_quality.jl | 4 +- {test => PlotsBase/test}/test_recipes.jl | 38 +- {test => PlotsBase/test}/test_shorthands.jl | 2 +- {test => PlotsBase/test}/test_unitful.jl | 115 ++-- {test => PlotsBase/test}/test_utils.jl | 182 +++--- Project.toml | 129 +---- ext/ImageInTerminalExt.jl | 31 - ext/PlotsGRExt/PlotsGRExt.jl | 54 -- ext/PlotsGRExt/initialization.jl | 200 ------- ext/PlotsGastonExt/PlotsGastonExt.jl | 16 - ext/PlotsGastonExt/initialization.jl | 145 ----- ext/PlotsHDF5Ext/hdf5.jl | 529 ----------------- ext/PlotsInspectDR/PlotsInspectDR.jl | 1 - ext/PlotsInspectDR/inspectdr.jl | 543 ------------------ ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl | 60 -- ext/PlotsPGFPlotsXExt/initialization.jl | 218 ------- ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl | 12 - ext/PlotsPlotlyJSExt/initialization.jl | 53 -- ext/PlotsPlotlyJSExt/plotlyjs.jl | 52 -- ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl | 84 --- ext/PlotsPythonPlotExt/initialization.jl | 192 ------- .../PlotsUnicodePlotsExt.jl | 41 -- ext/PlotsUnicodePlotsExt/initialization.jl | 118 ---- src/Plots.jl | 308 ++++------ src/init.jl | 111 ---- test/preferences.jl | 75 +++ test/runtests.jl | 104 +--- 99 files changed, 3115 insertions(+), 3948 deletions(-) create mode 100644 PlotsBase/Project.toml rename {ext => PlotsBase/ext}/FileIOExt.jl (78%) rename ext/PlotsGRExt/gr.jl => PlotsBase/ext/GRExt.jl (91%) rename ext/PlotsGastonExt/gaston.jl => PlotsBase/ext/GastonExt.jl (82%) rename {ext => PlotsBase/ext}/GeometryBasicsExt.jl (79%) rename ext/PlotsHDF5Ext/PlotsHDF5Ext.jl => PlotsBase/ext/HDF5Ext.jl (78%) rename {ext => PlotsBase/ext}/IJuliaExt.jl (70%) create mode 100644 PlotsBase/ext/ImageInTerminalExt.jl rename ext/PlotsPGFPlotsXExt/pgfplotsx.jl => PlotsBase/ext/PGFPlotsXExt.jl (88%) create mode 100644 PlotsBase/ext/PlotlyJSExt.jl rename ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl => PlotsBase/ext/PlotlyKaleidoExt.jl (58%) rename ext/PlotsPythonPlotExt/pythonplot.jl => PlotsBase/ext/PythonPlotExt.jl (85%) rename ext/PlotsUnicodePlotsExt/unicodeplots.jl => PlotsBase/ext/UnicodePlotsExt.jl (71%) rename {ext => PlotsBase/ext}/UnitfulExt.jl (89%) rename {src => PlotsBase/src}/Annotations.jl (96%) rename {src => PlotsBase/src}/Arrows.jl (98%) rename {src => PlotsBase/src}/Axes.jl (90%) rename {src => PlotsBase/src}/BezierCurves.jl (82%) rename {src => PlotsBase/src}/Colorbars.jl (94%) rename {src => PlotsBase/src}/Commons/Commons.jl (95%) rename {src => PlotsBase/src}/Commons/aliases.jl (100%) rename {src => PlotsBase/src}/Commons/attrs.jl (98%) rename {src => PlotsBase/src}/Commons/postprocess_attrs.jl (100%) rename {src => PlotsBase/src}/Fonts.jl (98%) rename {src => PlotsBase/src}/PlotMeasures.jl (100%) create mode 100644 PlotsBase/src/PlotsBase.jl rename {src => PlotsBase/src}/PlotsPlots.jl (86%) rename {src => PlotsBase/src}/Series.jl (89%) rename {src => PlotsBase/src}/Shapes.jl (96%) rename {src => PlotsBase/src}/Strokes.jl (94%) rename {src => PlotsBase/src}/Subplots.jl (85%) rename {src => PlotsBase/src}/Surfaces.jl (71%) rename {src => PlotsBase/src}/Ticks.jl (96%) rename {src => PlotsBase/src}/abstract_backend.jl (80%) rename {src => PlotsBase/src}/alignment.jl (100%) rename {src => PlotsBase/src}/animation.jl (97%) rename {src => PlotsBase/src}/arg_desc.jl (100%) rename {src => PlotsBase/src}/axes_utils.jl (100%) rename {src => PlotsBase/src}/backends/nobackend.jl (100%) rename {src => PlotsBase/src}/backends/plotly.jl (95%) rename {src => PlotsBase/src}/backends/web.jl (100%) rename {src => PlotsBase/src}/examples.jl (95%) create mode 100644 PlotsBase/src/init.jl rename {src => PlotsBase/src}/layouts.jl (100%) rename {src => PlotsBase/src}/legend.jl (100%) rename {src => PlotsBase/src}/output.jl (99%) rename {src => PlotsBase/src}/pipeline.jl (99%) rename {src => PlotsBase/src}/plot.jl (97%) rename {src => PlotsBase/src}/plotattr.jl (97%) rename {src => PlotsBase/src}/recipes.jl (99%) rename {src => PlotsBase/src}/shorthands.jl (100%) rename {src => PlotsBase/src}/themes.jl (100%) rename {src => PlotsBase/src}/users.jl (100%) rename {src => PlotsBase/src}/utils.jl (97%) rename {test => PlotsBase/test}/.gitignore (100%) create mode 100644 PlotsBase/test/runtests.jl rename {test => PlotsBase/test}/test_animations.jl (93%) rename {test => PlotsBase/test}/test_args.jl (90%) rename {test => PlotsBase/test}/test_axes.jl (66%) rename {test => PlotsBase/test}/test_backends.jl (75%) rename {test => PlotsBase/test}/test_components.jl (77%) rename {test => PlotsBase/test}/test_contours.jl (78%) rename {test => PlotsBase/test}/test_dates.jl (87%) rename {test => PlotsBase/test}/test_defaults.jl (88%) rename {test => PlotsBase/test}/test_hdf5plots.jl (87%) rename {test => PlotsBase/test}/test_layouts.jl (65%) rename {test => PlotsBase/test}/test_misc.jl (76%) rename {test => PlotsBase/test}/test_output.jl (59%) rename {test => PlotsBase/test}/test_pgfplotsx.jl (86%) rename {test => PlotsBase/test}/test_plotly.jl (66%) rename {test => PlotsBase/test}/test_quality.jl (83%) rename {test => PlotsBase/test}/test_recipes.jl (78%) rename {test => PlotsBase/test}/test_shorthands.jl (98%) rename {test => PlotsBase/test}/test_unitful.jl (84%) rename {test => PlotsBase/test}/test_utils.jl (51%) delete mode 100644 ext/ImageInTerminalExt.jl delete mode 100644 ext/PlotsGRExt/PlotsGRExt.jl delete mode 100644 ext/PlotsGRExt/initialization.jl delete mode 100644 ext/PlotsGastonExt/PlotsGastonExt.jl delete mode 100644 ext/PlotsGastonExt/initialization.jl delete mode 100644 ext/PlotsHDF5Ext/hdf5.jl delete mode 100644 ext/PlotsInspectDR/PlotsInspectDR.jl delete mode 100644 ext/PlotsInspectDR/inspectdr.jl delete mode 100644 ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl delete mode 100644 ext/PlotsPGFPlotsXExt/initialization.jl delete mode 100644 ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl delete mode 100644 ext/PlotsPlotlyJSExt/initialization.jl delete mode 100644 ext/PlotsPlotlyJSExt/plotlyjs.jl delete mode 100644 ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl delete mode 100644 ext/PlotsPythonPlotExt/initialization.jl delete mode 100644 ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl delete mode 100644 ext/PlotsUnicodePlotsExt/initialization.jl delete mode 100644 src/init.jl create mode 100644 test/preferences.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18cffdc97..4dae99f4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ name: ci on: + pull_request: push: branches: [master] - pull_request: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -22,27 +22,42 @@ jobs: JULIA_CONDAPKG_BACKEND: "MicroMamba" MPLBACKEND: "agg" name: Julia ${{ matrix.version }} - ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.version == 'nightly' }} strategy: fail-fast: false matrix: version: - - '1.9' # (minimal declared julia compat in `Project.toml`) - - '1.10' # latest stable + - '1.6' # LTS (minimal declared julia compat in `Project.toml`) + - '1' # latest stable + experimental: + - false os: [ubuntu-latest, windows-latest, macos-latest] arch: [x64] include: - # - os: ubuntu-latest - # prefix: xvfb-run - # version: '1.9' # only test intermediate release on `ubuntu` to spare resources - # - os: ubuntu-latest - # prefix: xvfb-run - # version: '~1.11.0-0' # upcoming julia version, next `rc` - os: ubuntu-latest + experimental: false + prefix: xvfb-run # julia-actions/julia-runtest/blob/master/README.md + - os: ubuntu-latest + experimental: false + prefix: xvfb-run + version: '1.7' # only test intermediate release on `ubuntu` to spare resources + - os: ubuntu-latest + experimental: false + prefix: xvfb-run + version: '1.8' # only test intermediate release on `ubuntu` to spare resources + - os: ubuntu-latest + experimental: false + prefix: xvfb-run + version: '1.9' # only test intermediate release on `ubuntu` to spare resources + - os: ubuntu-latest + experimental: true + prefix: xvfb-run + version: '~1.11.0-0' # upcoming julia version, next `rc` + - os: ubuntu-latest + experimental: true prefix: xvfb-run version: 'nightly' - allow_failure: true # `nightly` often breaks steps: - uses: actions/checkout@v4 @@ -62,19 +77,14 @@ jobs: with: version: ${{ matrix.version }} - uses: julia-actions/cache@v1 - - name: Use local RecipesBase/RecipesPipeline - shell: julia --project=@. --color=yes {0} - run: | - using Pkg - Pkg.develop([(; path="./RecipesBase"), (; path="./RecipesPipeline")]) - uses: julia-actions/julia-buildpkg@latest - - name: Run upstream RecipesBase & RecipesPipeline tests + - name: Run upstream RecipesBase, RecipesPipeline tests shell: julia --project=@. --color=yes {0} run: | using Pkg foreach(("RecipesBase", "RecipesPipeline")) do name - Pkg.test(name; coverage=true) + Pkg.develop(path=name); Pkg.test(name; coverage=true) end - name: Install conda based matplotlib @@ -90,7 +100,9 @@ jobs: v"3.4.29" => ">=11.1,<12.1", v"3.4.30" => ">=12.1,<13.1", v"3.4.31" => ">=13.1,<14.1", - # ... keep this up-to-date with gcc 14 + v"3.4.32" => ">=14.1,<15.1", + v"3.4.33" => ">=15.1,<16.1", + # ... keep this up-to-date with gcc 16 )[Base.BinaryPlatforms.detect_libstdcxx_version()] ("libgcc-ng$specs", "libstdcxx-ng$specs") else @@ -98,6 +110,15 @@ jobs: end CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) CondaPkg.status() + + - name: Run upstream PlotsBase tests + shell: julia --project=@. --color=yes {0} + run: | + using Pkg + foreach(("PlotsBase",)) do name + Pkg.develop(path=name); Pkg.test(name; coverage=true) + end + - uses: julia-actions/julia-runtest@latest timeout-minutes: 60 with: @@ -118,7 +139,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') with: directories: RecipesBase/src,RecipesPipeline/src,src - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 if: startsWith(matrix.os, 'ubuntu') with: file: lcov.info diff --git a/.gitignore b/.gitignore index 48ae0045c..609972e94 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ test/tmpplotsave.hdf5 /.benchmarkci /benchmark/*.json .vscode/ +.CondaPkg/ diff --git a/PlotsBase/Project.toml b/PlotsBase/Project.toml new file mode 100644 index 000000000..192ef1cf0 --- /dev/null +++ b/PlotsBase/Project.toml @@ -0,0 +1,140 @@ +name = "PlotsBase" +uuid = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +version = "1.41.0" + +[deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +JLFzf = "1019f520-868f-41f5-a6de-eb00f4b6a39c" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" +Scratch = "6c6a2e73-6563-6170-7368-637461726353" +Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1" +UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" +Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" + +[weakdeps] +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +Gaston = "4b11ee91-296f-5714-9832-002c20994614" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" +PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" +PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" +PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" +PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[extensions] +FileIOExt = "FileIO" +GRExt = "GR" +GastonExt = "Gaston" +GeometryBasicsExt = "GeometryBasics" +HDF5Ext = "HDF5" +IJuliaExt = "IJulia" +ImageInTerminalExt = "ImageInTerminal" +PGFPlotsXExt = "PGFPlotsX" +PlotlyJSExt = "PlotlyJS" +PlotlyKaleidoExt = "PlotlyKaleido" +PythonPlotExt = "PythonPlot" +UnicodePlotsExt = "UnicodePlots" +UnitfulExt = "Unitful" + +[compat] +Base64 = "1" +Contour = "0.5 - 0.6" +Dates = "1" +FFMPEG = "0.2 - 0.4" +FixedPointNumbers = "0.6 - 0.8" +GR = "0.69.5 - 0.73" +Gaston = "1" +HDF5 = "0.16 - 0.17" +JLFzf = "0.1" +JSON = "0.21, 1" +LaTeXStrings = "1" +Latexify = "0.14 - 0.16" +Measures = "0.3" +NaNMath = "0.3, 1" +PGFPlotsX = "1" +Pkg = "1" +PlotThemes = "2, 3" +PlotUtils = "1" +PlotlyJS = "0.18" +PlotlyKaleido = "2.2.2" +Printf = "1" +PythonPlot = "1" +Random = "1" +REPL = "1" +RecipesBase = "1.3.1" +RecipesPipeline = "1" +Reexport = "0.2, 1" +RelocatableFolders = "0.3, 1" +Scratch = "1" +Showoff = "0.3.1, 1" +SparseArrays = "1" +Statistics = "1" +StatsBase = "0.33 - 0.34" +UnicodeFun = "0.4" +UnicodePlots = "3" +UnitfulLatexify = "1" +Unzip = "0.1 - 0.2" +UUIDs = "1" +julia = "1.6" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" +FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +Gaston = "4b11ee91-296f-5714-9832-002c20994614" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" +PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" +PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" +PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" +RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" +SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" + +[targets] +test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "GR", "Gtk", "HDF5", "Images", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyKaleido", "PythonPlot", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] diff --git a/ext/FileIOExt.jl b/PlotsBase/ext/FileIOExt.jl similarity index 78% rename from ext/FileIOExt.jl rename to PlotsBase/ext/FileIOExt.jl index d70ab80d5..19c022a49 100644 --- a/ext/FileIOExt.jl +++ b/PlotsBase/ext/FileIOExt.jl @@ -1,7 +1,7 @@ module FileIOExt -import Plots: Plots, Plot, @ext_imp_use -@ext_imp_use :import FileIO +import PlotsBase: PlotsBase, Plot +import FileIO _fileio_load(@nospecialize(filename::AbstractString)) = FileIO.load(filename::AbstractString) @@ -12,7 +12,7 @@ function _show_pdfbackends(io::IO, ::MIME"image/png", plt::Plot) fn = tempname() # first save a pdf file - Plots.pdf(plt, fn) + PlotsBase.pdf(plt, fn) # load that pdf into a FileIO Stream s = _fileio_load("$fn.pdf") @@ -28,10 +28,10 @@ end # Possibly need to create another extension that has both pgfplotsx and showio # delete for now, as testing for pgfplotsx is hard; TODO restore later at @2.0 # for be in ( -# Plots.PGFPlotsBackend, # NOTE: I guess this can be removed in Plots@2.0 +# PlotsBase.PGFPlotsBackend, # NOTE: I guess this can be removed in PlotsBase@2.0 # ) # showable(MIME"image/png"(), Plot{be}) && continue -# @eval Plots._show(io::IO, mime::MIME"image/png", plt::Plot{$be}) = +# @eval PlotsBase._show(io::IO, mime::MIME"image/png", plt::Plot{$be}) = # _show_pdfbackends(io, mime, plt) # end diff --git a/ext/PlotsGRExt/gr.jl b/PlotsBase/ext/GRExt.jl similarity index 91% rename from ext/PlotsGRExt/gr.jl rename to PlotsBase/ext/GRExt.jl index 238ba31e5..5bc1e7f67 100644 --- a/ext/PlotsGRExt/gr.jl +++ b/PlotsBase/ext/GRExt.jl @@ -1,3 +1,224 @@ +module GRExt + +import PlotsBase: PlotsBase, _cycle +import RecipesPipeline +import NaNMath +import GR + +import PlotsBase.Colorbars: cbar_gradient, cbar_fill, cbar_lines + +using PlotsBase.PlotMeasures +using PlotsBase.Annotations +using PlotsBase.PlotsSeries +using PlotsBase.PlotsPlots +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Commons +using PlotsBase.Arrows +using PlotsBase.Shapes +using PlotsBase.Colors +using PlotsBase.Fonts +using PlotsBase.Fonts +using PlotsBase.Ticks +using PlotsBase.Axes + +const package_str = "GR" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct GRBackend <: PlotsBase.AbstractBackend end + +get_concrete_backend() = GRBackend # opposite to abstract + +function __init__() + @debug "Initializing GR backend in PlotsBase; run `gr()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[GRBackend] = sym + + push!(PlotsBase._initialized_backends, sym) +end +# Make GR know to Plots +PlotsBase.backend_name(::GRBackend) = sym +PlotsBase.backend_package_name(::GRBackend) = PlotsBase.backend_package_name(sym) + +const _gr_attrs = PlotsBase.merge_with_base_supported([ + :annotations, + :annotationrotation, + :annotationhalign, + :annotationfontsize, + :annotationfontfamily, + :annotationcolor, + :annotationvalign, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :legend_foreground_color, + :foreground_color_grid, + :foreground_color_axis, + :foreground_color_text, + :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :fillstyle, + :bins, + :layout, + :title, + :window_title, + :guide, + :widen, + :lims, + :ticks, + :scale, + :flip, + :titlefontfamily, + :titlefontsize, + :titlefonthalign, + :titlefontvalign, + :titlefontrotation, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_halign, + :legend_font_valign, + :legend_font_rotation, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfonthalign, + :tickfontvalign, + :tickfontrotation, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefonthalign, + :guidefontvalign, + :guidefontrotation, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_titlefont, + :colorbar_titlefontsize, + :colorbar_titlefontrotation, + :colorbar_titlefontcolor, + :colorbar_entry, + :colorbar_scale, + :clims, + :fill, + :fill_z, + :fontfamily, + :fontfamily_subplot, + :line_z, + :marker_z, + :legend_column, + :legend_font, + :legend_title, + :legend_title_font_color, + :legend_title_font_family, + :legend_title_font_rotation, + :legend_title_font_pointsize, + :legend_title_font_valigm, + :levels, + :line, + :ribbon, + :quiver, + :overwrite_figure, + :plot_title, + :plot_titlefontcolor, + :plot_titlefontfamily, + :plot_titlefontrotation, + :plot_titlefontsize, + :plot_titlelocation, + :plot_titlevspan, + :polar, + :aspect_ratio, + :normalize, + :weights, + :inset_subplots, + :bar_width, + :arrow, + :framestyle, + :tick_direction, + :camera, + :contour_labels, + :connections, + :axis, + :thickness_scaling, + :minorgrid, + :minorgridalpha, + :minorgridlinewidth, + :minorgridstyle, + :minorticks, + :mirror, + :rotation, + :showaxis, + :tickfonthalign, + :formatter, + :mirror, + :guidefont, +]) +const _gr_seriestypes = [ + :path, + :scatter, + :straightline, + :heatmap, + :image, + :contour, + :path3d, + :scatter3d, + :surface, + :wireframe, + :mesh3d, + :volume, + :shape, +] +const _gr_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _gr_markers = vcat(Commons._all_markers, :pixel) +const _gr_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::GRBackend, $s::Symbol) = $s in $v + PlotsBase.$f2(::GRBackend) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + +PlotsBase.is_marker_supported(::GRBackend, shape::Shape) = true # https://github.com/jheinen/GR.jl - significant contributions by @jheinen @@ -123,7 +344,7 @@ xposition(vp::GRViewport, pos) = vp.xmin + pos * width(vp) yposition(vp::GRViewport, pos) = vp.ymin + pos * height(vp) # -------------------------------------------------------------------------------------- -gr_is3d(st) = RecipesPipeline.is3d(st) +gr_is3d(st) = PlotsBase.is3d(st) gr_color(c, ::Type) = gr_color(RGBA(c), RGB) gr_color(c) = gr_color(c, color_type(c)) @@ -453,10 +674,10 @@ end function gr_viewport_from_bbox(sp::Subplot{GRBackend}, bb::BoundingBox, w, h, vp_canvas) viewport = GRViewport( - vp_canvas.xmax * (left(bb) / w), - vp_canvas.xmax * (right(bb) / w), - vp_canvas.ymax * (1 - bottom(bb) / h), - vp_canvas.ymax * (1 - top(bb) / h), + vp_canvas.xmax * (PlotsBase.left(bb) / w), + vp_canvas.xmax * (PlotsBase.right(bb) / w), + vp_canvas.ymax * (1 - PlotsBase.bottom(bb) / h), + vp_canvas.ymax * (1 - PlotsBase.top(bb) / h), ) hascolorbar(sp) && (viewport.xmax -= 0.1(1 + 0.5gr_is3d(sp))) viewport @@ -753,7 +974,7 @@ function gr_get_ticks_size(ticks, rot) end function labelfunc(scale::Symbol, backend::GRBackend) - texfunc = labelfunc_tex(scale) + texfunc = PlotsBase.labelfunc_tex(scale) # replace dash with \minus (U+2212) label -> replace(texfunc(label), "-" => "−") end @@ -790,7 +1011,7 @@ function gr_axis_width(sp, axis) w end -function _update_min_padding!(sp::Subplot{GRBackend}) +function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) dpi = sp.plt[:thickness_scaling] width, height = sp_size = get_size(sp) @@ -928,11 +1149,11 @@ function gr_viewport_bbox(vp, sp, color) end function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport) - _update_min_padding!(sp) + PlotsBase._update_min_padding!(sp) # the viewports for this subplot and the whole plot - vp_sp = gr_viewport_from_bbox(sp, bbox(sp), w, h, vp_canvas) - vp_plt = gr_viewport_from_bbox(sp, plotarea(sp), w, h, vp_canvas) + vp_sp = gr_viewport_from_bbox(sp, PlotsBase.bbox(sp), w, h, vp_canvas) + vp_plt = gr_viewport_from_bbox(sp, PlotsBase.plotarea(sp), w, h, vp_canvas) # update plot viewport leg = gr_get_legend_geometry(vp_plt, sp) @@ -974,7 +1195,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport) # add annotations for ann in sp[:annotations] - x, y = if is3d(sp) + x, y = if PlotsBase.is3d(sp) x, y, z, val = locate_annotation(sp, ann...) GR.setwindow(-1, 1, -1, 1) gr_w3tondc(x, y, z) @@ -1143,7 +1364,7 @@ function gr_legend_pos(sp::Subplot, leg, vp) return gr_legend_pos(lp, vp) end - leg_str = string(_guess_best_legend_position(lp, sp)) + leg_str = string(PlotsBase._guess_best_legend_position(lp, sp)) xpos = if occursin("left", leg_str) vp.xmin + if occursin("outer", leg_str) @@ -1395,8 +1616,8 @@ function gr_draw_axes(sp, vp) azimuth, elevation = sp[:camera] GR.setwindow3d(x_min, x_max, y_min, y_max, z_min, z_max) - fov = (isortho(sp) || isautop(sp)) ? NaN : 30 - cam = (isortho(sp) || isautop(sp)) ? 0 : NaN + fov = (PlotsBase.isortho(sp) || PlotsBase.isautop(sp)) ? NaN : 30 + cam = (PlotsBase.isortho(sp) || PlotsBase.isautop(sp)) ? 0 : NaN GR.setspace3d(-90 + azimuth, 90 - elevation, fov, cam) gr_set_projectiontype(sp) @@ -1422,7 +1643,7 @@ function gr_draw_axes(sp, vp) end function gr_draw_axis(sp, letter, vp) - ax = axis_drawing_info(sp, letter) + ax = PlotsBase.axis_drawing_info(sp, letter) axis = sp[get_attr_symbol(letter, :axis)] # draw segments @@ -1439,7 +1660,7 @@ function gr_draw_axis(sp, letter, vp) end function gr_draw_axis_3d(sp, letter, vp) - ax = axis_drawing_info_3d(sp, letter) + ax = PlotsBase.axis_drawing_info_3d(sp, letter) axis = sp[get_attr_symbol(letter, :axis)] # draw segments @@ -1723,9 +1944,9 @@ function gr_add_series(sp, series) if ispolar(sp) && z === nothing extrema_r = gr_y_axislims(sp) if frng !== nothing - _, frng = convert_to_polar(x, frng, extrema_r) + _, frng = PlotsBase.convert_to_polar(x, frng, extrema_r) end - x, y = convert_to_polar(x, y, extrema_r) + x, y = PlotsBase.convert_to_polar(x, y, extrema_r) end # add custom frame shapes to markershape? @@ -1738,7 +1959,7 @@ function gr_add_series(sp, series) clims = gr_clims(sp, series) if (st = series[:seriestype]) in (:path, :scatter, :straightline) if st === :straightline - x, y = straightline_data(series) + x, y = PlotsBase.straightline_data(series) end gr_draw_segments(series, x, y, nothing, frng, clims) if series[:markershape] !== :none @@ -1761,7 +1982,8 @@ function gr_add_series(sp, series) GR.gr3.clear() elseif st === :heatmap # `z` is already transposed, so we need to reverse before passing its size. - x, y = heatmap_edges(x, xscale, y, yscale, reverse(size(z)), ispolar(series)) + x, y = + PlotsBase.heatmap_edges(x, xscale, y, yscale, reverse(size(z)), ispolar(series)) gr_draw_heatmap(series, x, y, z, clims) elseif st === :image gr_draw_image(series, x, y, z, clims) @@ -1794,7 +2016,7 @@ function gr_draw_segments(series, x, y, z, fillrange, clims) (x === nothing || length(x) ≤ 1) && return if fillrange !== nothing # prepare fill-in GR.setfillintstyle(GR.INTSTYLE_SOLID) - fr_from, fr_to = is_2tuple(fillrange) ? fillrange : (y, fillrange) + fr_from, fr_to = PlotsBase.is_2tuple(fillrange) ? fillrange : (y, fillrange) end # draw the line(s) @@ -1864,7 +2086,7 @@ function gr_draw_markers( end function gr_draw_shapes(series, clims) - x, y = shape_data(series) + x, y = PlotsBase.shape_data(series) for segment in series_segments(series, :shape) i, rng = segment.attr_index, segment.range if length(rng) > 1 @@ -1991,7 +2213,9 @@ function gr_draw_heatmap(series, x, y, z, clims) GR.setspace(clims..., 0, 90) w, h = length(x) - 1, length(y) - 1 sp = series[:subplot] - if !ispolar(series) && is_uniformly_spaced(x) && is_uniformly_spaced(y) + if !ispolar(series) && + PlotsBase.is_uniformly_spaced(x) && + PlotsBase.is_uniformly_spaced(y) # For uniformly spaced data use GR.drawimage, which can be # much faster than GR.nonuniformcellarray, especially for # pdf output, and also supports alpha values. @@ -2049,8 +2273,8 @@ for (mime, fmt) in ( "application/postscript" => "ps", "image/svg+xml" => "svg", ) - @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) - dpi_factor = $fmt == "png" ? plt[:dpi] / DPI : 1 + @eval function PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) + dpi_factor = $fmt == "png" ? plt[:dpi] / PlotsBase.DPI : 1 filepath = tempname() * "." * $fmt # workaround windows bug github.com/JuliaLang/julia/issues/46989 touch(filepath) @@ -2068,7 +2292,7 @@ for (mime, fmt) in ( end end -function Plots._display(plt::Plot{GRBackend}) +function PlotsBase._display(plt::Plot{GRBackend}) if plt[:display_type] === :inline filepath = tempname() * ".pdf" GR.emergencyclosegks() @@ -2093,4 +2317,6 @@ function Plots._display(plt::Plot{GRBackend}) end end -closeall(::GRBackend) = GR.emergencyclosegks() +PlotsBase.closeall(::GRBackend) = GR.emergencyclosegks() + +end # module diff --git a/ext/PlotsGastonExt/gaston.jl b/PlotsBase/ext/GastonExt.jl similarity index 82% rename from ext/PlotsGastonExt/gaston.jl rename to PlotsBase/ext/GastonExt.jl index c6eeb3878..27fb87a50 100644 --- a/ext/PlotsGastonExt/gaston.jl +++ b/PlotsBase/ext/GastonExt.jl @@ -1,14 +1,171 @@ +module GastonExt + +import RecipesPipeline +import PlotsBase: PlotsBase, ticks_type +import PlotUtils +import Gaston + +using PlotsBase.PlotMeasures +using PlotsBase.PlotsSeries +using PlotsBase.PlotsPlots +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Commons +using PlotsBase.Ticks +using PlotsBase.Fonts +using PlotsBase.Axes + +const package_str = "Gaston" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct GastonBackend <: PlotsBase.AbstractBackend end +const T = GastonBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) +end + +PlotsBase.backend_name(::T) = sym +PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) + +const _gaston_attrs = PlotsBase.merge_with_base_supported([ + :annotations, + # :background_color_legend, + # :background_color_inside, + # :background_color_outside, + # :foreground_color_legend, + # :foreground_color_grid, :foreground_color_axis, + # :foreground_color_text, :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + # :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :markerstrokestyle, + # :fillrange, :fillcolor, :fillalpha, + # :bins, + # :bar_width, :bar_edges, + :title, + :window_title, + :guide, + :guide_position, + :widen, + :lims, + :ticks, + :scale, + :flip, + :rotation, + :tickfont, + :guidefont, + :legendfont, + :grid, + :legend, + # :colorbar, :colorbar_title, + # :fill_z, :line_z, :marker_z, :levels, + # :ribbon, + :quiver, + :arrow, + # :orientation, :overwrite_figure, + :polar, + # :normalize, :weights, :contours, + :aspect_ratio, + :tick_direction, + # :framestyle, + # :camera, + # :contour_labels, + :connections, +]) + +const _gaston_seriestypes = [ + :path, + :path3d, + :scatter, + :steppre, + :stepmid, + :steppost, + :ysticks, + :xsticks, + :contour, + :shape, + :straightline, + :scatter3d, + :contour3d, + :wireframe, + :heatmap, + :surface, + :mesh3d, + :image, +] + +const _gaston_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] + +const _gaston_markers = [ + :none, + :auto, + :pixel, + :cross, + :xcross, + :+, + :x, + :star5, + :rect, + :circle, + :utriangle, + :dtriangle, + :diamond, + :pentagon, + # :hline, + # :vline, +] + +const _gaston_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::T, $s::Symbol) = $s in $v + PlotsBase.$f2(::T) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + # https://github.com/mbaz/Gaston. -should_warn_on_unsupported(::GastonBackend) = false +PlotsBase.should_warn_on_unsupported(::GastonBackend) = false # Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{GastonBackend}) +function PlotsBase._create_backend_figure(plt::Plot{GastonBackend}) state_handle = Gaston.nexthandle() # for now all the figures will be kept plt.o = Gaston.newfigure(state_handle) end -function _before_layout_calcs(plt::Plot{GastonBackend}) +function PlotsBase._before_layout_calcs(plt::Plot{GastonBackend}) # initialize all the subplots first plt.o.subplots = Gaston.SubPlot[] @@ -38,9 +195,9 @@ function _before_layout_calcs(plt::Plot{GastonBackend}) nothing end -_update_min_padding!(sp::Subplot{GastonBackend}) = sp.minpad = 0mm, 0mm, 0mm, 0mm +PlotsBase._update_min_padding!(sp::Subplot{GastonBackend}) = sp.minpad = 0mm, 0mm, 0mm, 0mm -function _update_plot_object(plt::Plot{GastonBackend}) +function PlotsBase._update_plot_object(plt::Plot{GastonBackend}) # respect the layout ratio dat = gaston_multiplot_pos_size(plt.layout, (0, 0, 1, 1)) gaston_multiplot_pos_size!(dat) @@ -49,16 +206,16 @@ end for (mime, term) in ( "application/eps" => "epscairo", - "image/eps" => "epslatex", + "image/eps" => "epscairo", "application/pdf" => "pdfcairo", "application/postscript" => "postscript", "image/png" => "png", "image/svg+xml" => "svg", "text/latex" => "tikz", - "application/x-tex" => "epslatex", + "application/x-tex" => "cairolatex", "text/plain" => "dumb", ) - @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend}) + @eval function PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend}) term = String($term) tmpfile = tempname() * ".$term" if plt.o !== nothing @@ -79,7 +236,7 @@ for (mime, term) in ( end end -_display(plt::Plot{GastonBackend}) = display(plt.o) +PlotsBase._display(plt::Plot{GastonBackend}) = display(plt.o) # -------------------------------------------- # These functions are gaston specific @@ -88,8 +245,8 @@ _display(plt::Plot{GastonBackend}) = display(plt.o) function gaston_saveopts(plt::Plot{GastonBackend}) saveopts = ["size " * join(plt[:size], ',')] - # scale all plot elements to match Plots.jl DPI standard - scaling = plt[:dpi] / Plots.DPI + # scale all plot elements to match PlotsBase.jl DPI standard + scaling = plt[:dpi] / PlotsBase.DPI push!( saveopts, @@ -112,7 +269,7 @@ function gaston_get_subplots(n, plt_subplots, layout) nr, nc = size(layout) sps = Array{Any}(nothing, nr, nc) for r in 1:nr, c in 1:nc # NOTE: col major - sps[r, c] = if (l = layout[r, c]) isa GridLayout + sps[r, c] = if (l = layout[r, c]) isa PlotsBase.GridLayout n, sub = gaston_get_subplots(n, plt_subplots, l) size(sub) == (1, 1) ? only(sub) : sub else @@ -171,7 +328,7 @@ function gaston_multiplot_pos_size(layout, parent_xy_wh) # width and height (pct) are multiplicative (parent) w = layout.widths[c].value * parent_xy_wh[3] h = layout.heights[r].value * parent_xy_wh[4] - if isa(l, EmptyLayout) + if isa(l, PlotsBase.EmptyLayout) dat[r, c] = (c - 1) * w, (r - 1) * h, w, h, nothing else # previous position (origin) @@ -181,7 +338,7 @@ function gaston_multiplot_pos_size(layout, parent_xy_wh) prev_c isa Array && (prev_c = prev_c[end, end]) x = prev_c !== nothing ? prev_c[1] + prev_c[3] : parent_xy_wh[1] y = prev_r !== nothing ? prev_r[2] + prev_r[4] : parent_xy_wh[2] - dat[r, c] = if l isa GridLayout + dat[r, c] = if l isa PlotsBase.GridLayout sub = gaston_multiplot_pos_size(l, (x, y, w, h)) size(sub) == (1, 1) ? only(sub) : sub else @@ -242,7 +399,7 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) length(y) == size(z, 1) + 1 && (y = (y[1:(end - 1)] + y[2:end]) / 2) end if st === :mesh3d - x, y, z = mesh3d_triangles(x, y, z, series[:connections]) + x, y, z = PlotsBase.mesh3d_triangles(x, y, z, series[:connections]) elseif st === :surface if ndims(x) == ndims(y) == ndims(z) == 1 # must reinterpret 1D data for `pm3d` (points are ordered) @@ -357,7 +514,7 @@ function gaston_seriesconf!( elseif st === :quiver curveconf *= "w vectors filled" else - @warn "Plots(Gaston): $st is not implemented yet" + @warn "PlotsBase(Gaston): $st is not implemented yet" end [curveconf, extra_curves...] @@ -617,7 +774,7 @@ end function gaston_set_legend!(axesconf, sp, any_label) if (lp = sp[:legend_position]) ∉ (:none, :inline) && any_label - leg_str = string(_guess_best_legend_position(lp, sp)) + leg_str = string(PlotsBase._guess_best_legend_position(lp, sp)) pos = occursin("outer", leg_str) ? "outside " : "inside " pos *= if occursin("top", leg_str) @@ -701,14 +858,14 @@ function gaston_marker(marker, alpha) marker === :dtriangle && return filled ? 11 : 10 marker === :diamond && return filled ? 13 : 12 marker === :pentagon && return filled ? 15 : 14 - # @debug "Plots(Gaston): unsupported marker $marker" + # @debug "PlotsBase(Gaston): unsupported marker $marker" 1 end function gaston_color(col, alpha = 0) col = single_color(col) # in case of gradients - col = alphacolor(col, gaston_alpha(alpha)) # add a default alpha if non existent - "rgbcolor '#$(hex(col, :aarrggbb))'" + col = PlotUtils.alphacolor(col, gaston_alpha(alpha)) # add a default alpha if non existent + "rgbcolor '#$(PlotUtils.hex(col, :aarrggbb))'" end function gaston_linestyle(style) @@ -725,3 +882,5 @@ function gaston_enclose_tick_string(tick_string) base, power = split(tick_string, '^') "$base^{$power}" end + +end # module diff --git a/ext/GeometryBasicsExt.jl b/PlotsBase/ext/GeometryBasicsExt.jl similarity index 79% rename from ext/GeometryBasicsExt.jl rename to PlotsBase/ext/GeometryBasicsExt.jl index a46d2a65b..deea3de17 100644 --- a/ext/GeometryBasicsExt.jl +++ b/PlotsBase/ext/GeometryBasicsExt.jl @@ -1,11 +1,11 @@ module GeometryBasicsExt -import Plots: Plots, @ext_imp_use, @recipe +import RecipesBase: @recipe +import PlotsBase: AVec import RecipesPipeline +import GeometryBasics import Unzip -@ext_imp_use :import GeometryBasics - RecipesPipeline.unzip(points::AbstractVector{<:GeometryBasics.Point}) = Unzip.unzip(Tuple.(points)) RecipesPipeline.unzip(points::AbstractVector{GeometryBasics.Point{N,T}}) where {N,T} = @@ -14,7 +14,7 @@ RecipesPipeline.unzip(points::AbstractVector{GeometryBasics.Point{N,T}}) where { # ----------------------------------------- # Lists of tuples and GeometryBasics.Points # ----------------------------------------- -@recipe f(v::Plots.AVec{<:GeometryBasics.Point}) = RecipesPipeline.unzip(v) +@recipe f(v::AVec{<:GeometryBasics.Point}) = RecipesPipeline.unzip(v) @recipe f(p::GeometryBasics.Point) = [p] # Special case for 4-tuples in :ohlc series end # module diff --git a/ext/PlotsHDF5Ext/PlotsHDF5Ext.jl b/PlotsBase/ext/HDF5Ext.jl similarity index 78% rename from ext/PlotsHDF5Ext/PlotsHDF5Ext.jl rename to PlotsBase/ext/HDF5Ext.jl index a9e886799..1aeaf7783 100644 --- a/ext/PlotsHDF5Ext/PlotsHDF5Ext.jl +++ b/PlotsBase/ext/HDF5Ext.jl @@ -1,72 +1,166 @@ -module PlotsHDF5Ext +module HDF5Ext -import Plots: Plot, HDF5Backend, _display, _show, closeall +import HDF5: HDF5, Group, Dataset -#= - -# HDF5 Plots: Save/replay plots to/from HDF5 - -# Usage -Write to .hdf5 file using: - p = plot(...) - Plots.hdf5plot_write(p, "plotsave.hdf5") - -Read from .hdf5 file using: - pyplot() # Must first select backend - pread = Plots.hdf5plot_read("plotsave.hdf5") - display(pread) - -# TODO - 1. Support more features. - - GridLayout known not to be working. - 2. Improve error handling. - - Will likely crash if file format is off. - 3. Save data in a folder parallel to "plot". - - Will make it easier for users to locate data. - - Use HDF5 reference to link data? - 4. Develop an actual versioned file format. - - Should have some form of backward compatibility. - - Should be reliable for archival purposes. - 5. Fix construction of plot object with hdf5plot_read. - - Layout doesn't seem to get transferred well (ex: `Plots._examples[40]`). - - Not building object correctly when backends do not natively support - a certain feature (ex: :steppre) - - No support for CategoricalArrays.* structures. But they appear to be - brought into `Plots._examples[25,30]` through DataFrames.jl - so we can't - really reference them in this code. -=# +import PlotUtils: PlotUtils, Colors +import PlotUtils.ColorSchemes: ColorScheme +import PlotUtils.Colors: Colorant +import RecipesPipeline -""" - _hdf5_implementation +import RecipesPipeline.datetimeformatter +import PlotUtils.ColorPalette, + PlotUtils.CategoricalColorGradient, PlotUtils.ContinuousColorGradient +import PlotsBase: + PlotsBase, Surface, Arrow, GridLayout, RootLayout, Font, PlotText, SeriesAnnotations +import PlotsBase: BoundingBox, Length, Plot, DefaultsDict, plot, plot! -Create module (namespace) for implementing HDF5 "plots". -(Avoid name collisions, while keeping names short) -""" -module _hdf5_implementation # Tools required to implements HDF5 "plots" +using PlotsBase.PlotsSeries +using PlotsBase.Subplots +using PlotsBase.Commons +using PlotsBase.Shapes +using PlotsBase.Axes import Dates -# Plots.jl imports HDF5 to main: -import ..HDF5 -import ..HDF5: Group, Dataset - -import ..Colors, ..Colorant -import ..PlotUtils.ColorSchemes.ColorScheme - -import ..HDF5Backend, .._current_plots_version -import ..HDF5PLOT_MAP_STR2TELEM, ..HDF5PLOT_MAP_TELEM2STR -import ..HDF5Plot_PlotRef, ..HDF5PLOT_PLOTREF -import ..BoundingBox, ..Extrema, ..Length -import ..RecipesPipeline.datetimeformatter -import ..PlotUtils.ColorPalette, - ..PlotUtils.CategoricalColorGradient, ..PlotUtils.ContinuousColorGradient -import ..Surface, ..Shape, ..Arrow -import ..GridLayout, ..RootLayout -import ..Font, ..PlotText, ..SeriesAnnotations -import ..Axis, ..Subplot, ..Plot -import ..AKW, ..KW, ..DefaultsDict -import .._axis_defaults -import ..plot, ..plot! +const package_str = "HDF5" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct HDF5Backend <: PlotsBase.AbstractBackend end +const T = HDF5Backend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) +end + +PlotsBase.backend_name(::T) = sym +PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) + +const _hdf5_attr = PlotsBase.merge_with_base_supported([ + :annotations, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :foreground_color_grid, + :legend_foreground_color, + :foreground_color_title, + :foreground_color_axis, + :foreground_color_border, + :foreground_color_guide, + :foreground_color_text, + :label, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :bins, + :bar_width, + :bar_edges, + :bar_position, + :title, + :titlelocation, + :titlefont, + :window_title, + :guide, + :widen, + :lims, + :ticks, + :scale, + :flip, + :rotation, + :tickfont, + :guidefont, + :legendfont, + :grid, + :legend, + :colorbar, + :marker_z, + :line_z, + :fill_z, + :levels, + :ribbon, + :quiver, + :arrow, + :orientation, + :overwrite_figure, + :polar, + :normalize, + :weights, + :contours, + :aspect_ratio, + :clims, + :inset_subplots, + :dpi, + :colorbar_title, +]) +const _hdf5_seriestype = [ + :path, + :steppre, + :stepmid, + :steppost, + :shape, + :straightline, + :scatter, + :hexbin, + :heatmap, + :image, + :contour, + :contour3d, + :path3d, + :scatter3d, + :surface, + :wireframe, +] +const _hdf5_style = [:auto, :solid, :dash, :dot, :dashdot] +const _hdf5_marker = vcat(PlotsBase.Commons._all_markers, :pixel) +const _hdf5_scale = [:identity, :ln, :log2, :log10] + +#= +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::HDF5Backend, $s::Symbol) = $s in $v + PlotsBase.$f2(::HDF5Backend) = sort(collect($v)) + end |> eval +end +=# + +## results in: +# PlotsBase.is_attr_supported(::HDF5Backend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::HDF5Backend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::HDF5Backend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + +# Additional constants +# Dict has problems using "Types" as keys. Initialize in "_initialize_backend": +const HDF5PLOT_MAP_STR2TELEM = Dict{String,Type}() +const HDF5PLOT_MAP_TELEM2STR = Dict{Type,String}() + +# Don't really like this global variable... Very hacky +mutable struct HDF5Plot_PlotRef + ref::Union{Plot,Nothing} +end +const HDF5PLOT_PLOTREF = HDF5Plot_PlotRef(nothing) # Types that already have built-in HDF5 support (just write out natively): const HDF5_SupportedTypes = Union{Number,String} @@ -318,7 +412,7 @@ function hdf5plot_write( name::String = "_unnamed", ) HDF5.h5open(path, "w") do file - HDF5.write_dataset(file, "VERSION_INFO", string(_current_plots_version)) + HDF5.write_dataset(file, "VERSION_INFO", string(PlotsBase._current_plots_version)) grp = HDF5.create_group(file, h5plotpath(name)) _write(grp, plt) end @@ -430,7 +524,7 @@ end # 1st arg appears to be ref to subplots. Seems to work without it. _read(::Type{Axis}, grp::Group) = - Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), _axis_defaults)) + Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), PlotsBase._axis_defaults)) # Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. _read(::Type{Subplot}, grp::Group) = @@ -482,9 +576,7 @@ hdf5plot_read(path::AbstractString; name::String = "_unnamed") = return _read_plot(grp) end -end # module _hdf5_implementation - -# Implement Plots.jl backend interface for HDF5Backend +# Implement PlotsBase.jl backend interface for HDF5Backend is_marker_supported(::HDF5Backend, shape::Shape) = true @@ -520,15 +612,13 @@ function _update_plot_object(plt::Plot{HDF5Backend}) end # Display/show the plot (open a GUI window, or browser page, for example). function _display(plt::Plot{HDF5Backend}) msg = "HDF5 interface does not support `display()` function." - msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." + msg *= "\nUse `PlotsBase.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." @warn msg return end # Interface actually required to use HDF5Backend -hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString) = - _hdf5_implementation.hdf5plot_write(plt, path) -hdf5plot_write(path::AbstractString) = _hdf5_implementation.hdf5plot_write(current(), path) -hdf5plot_read(path::AbstractString) = _hdf5_implementation.hdf5plot_read(path) +hdf5plot_write(path::AbstractString) = hdf5plot_write(current(), path) + end # module diff --git a/ext/IJuliaExt.jl b/PlotsBase/ext/IJuliaExt.jl similarity index 70% rename from ext/IJuliaExt.jl rename to PlotsBase/ext/IJuliaExt.jl index 5322c155b..b94020260 100644 --- a/ext/IJuliaExt.jl +++ b/PlotsBase/ext/IJuliaExt.jl @@ -1,16 +1,17 @@ module IJuliaExt -import Plots: @ext_imp_use, Plots, Plot +import PlotsBase: PlotsBase, Plot using Base64 +# NOTE: cannot use import IJulia const IJulia = Base.require(Base.PkgId(Base.UUID("7073ff75-c697-5162-941a-fcdaad2a7d2a"), "IJulia")) function _init_ijulia_plotting() # IJulia is more stable with local file - Plots._use_local_plotlyjs[] = - Plots._plotly_local_file_path[] === nothing ? false : - isfile(Plots._plotly_local_file_path[]) + PlotsBase._use_local_plotlyjs[] = + PlotsBase._plotly_local_file_path[] === nothing ? false : + isfile(PlotsBase._plotly_local_file_path[]) ENV["MPLBACKEND"] = "Agg" end @@ -24,15 +25,15 @@ frontends like jupyterlab and nteract. """ _ijulia__extra_mime_info!(plt::Plot, out::Dict) = out -function _ijulia__extra_mime_info!(plt::Plot{Plots.PlotlyJSBackend}, out::Dict) +function _ijulia__extra_mime_info!(plt::Plot{PlotsBase.PlotlyJSBackend}, out::Dict) out["application/vnd.plotly.v1+json"] = - Dict(:data => Plots.plotly_series(plt), :layout => Plots.plotly_layout(plt)) + Dict(:data => PlotsBase.plotly_series(plt), :layout => PlotsBase.plotly_layout(plt)) out end -function _ijulia__extra_mime_info!(plt::Plot{Plots.PlotlyBackend}, out::Dict) +function _ijulia__extra_mime_info!(plt::Plot{PlotsBase.PlotlyBackend}, out::Dict) out["application/vnd.plotly.v1+json"] = - Dict(:data => Plots.plotly_series(plt), :layout => Plots.plotly_layout(plt)) + Dict(:data => PlotsBase.plotly_series(plt), :layout => PlotsBase.plotly_layout(plt)) out end @@ -40,7 +41,7 @@ function _ijulia_display_dict(plt::Plot) output_type = Symbol(plt.attr[:html_output_format]) if output_type === :auto output_type = - get(Plots._best_html_output_type, Plots.backend_name(plt.backend), :svg) + get(PlotsBase._best_html_output_type, PlotsBase.backend_name(plt.backend), :svg) end out = Dict() if output_type === :txt @@ -71,7 +72,7 @@ if IJulia.inited end # IJulia only... inline display -function Plots.inline(plt::Plot = Plots.current()) +function PlotsBase.inline(plt::Plot = PlotsBase.current()) IJulia.clear_output(true) display(IJulia.InlineDisplay(), plt) end diff --git a/PlotsBase/ext/ImageInTerminalExt.jl b/PlotsBase/ext/ImageInTerminalExt.jl new file mode 100644 index 000000000..7dd49ca83 --- /dev/null +++ b/PlotsBase/ext/ImageInTerminalExt.jl @@ -0,0 +1,31 @@ +module ImageInTerminalExt + +import ImageInTerminal +import PlotsBase + +if ImageInTerminal.ENCODER_BACKEND[] == :Sixel + get!(ENV, "GKSwstype", "nul") # disable `gr` output, we display in the terminal instead + for be in ( + PlotsBase.GRBackend, + PlotsBase.PythonPlotBackend, + # PlotsBase.UnicodePlotsBackend, # better and faster as MIME("text/plain") in terminal + PlotsBase.PGFPlotsXBackend, + PlotsBase.PlotlyJSBackend, + PlotsBase.PlotlyBackend, + PlotsBase.GastonBackend, + PlotsBase.InspectDRBackend, + ) + @eval function Base.display(::PlotsBase.PlotsDisplay, plt::PlotsBase.Plot{$be}) + PlotsBase.prepare_output(plt) + buf = PipeBuffer() + show(buf, MIME("image/png"), plt) + display( + ImageInTerminal.TerminalGraphicDisplay(stdout), + MIME("image/png"), + read(buf), + ) + end + end +end + +end # module diff --git a/ext/PlotsPGFPlotsXExt/pgfplotsx.jl b/PlotsBase/ext/PGFPlotsXExt.jl similarity index 88% rename from ext/PlotsPGFPlotsXExt/pgfplotsx.jl rename to PlotsBase/ext/PGFPlotsXExt.jl index 8268841af..e72abe26f 100644 --- a/ext/PlotsPGFPlotsXExt/pgfplotsx.jl +++ b/PlotsBase/ext/PGFPlotsXExt.jl @@ -1,3 +1,243 @@ +module PGFPlotsXExt + +import PlotsBase: PlotsBase, pgfx_sanitize_string +import PlotUtils: PlotUtils, ColorGradient +import LaTeXStrings: LaTeXString +import Printf: @sprintf +import UUIDs: uuid4 +import RecipesPipeline +import PGFPlotsX +import Latexify +import Contour + +using PlotsBase.PlotMeasures +using PlotsBase.Annotations +using PlotsBase.PlotsSeries +using PlotsBase.PlotsPlots +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Surfaces +using PlotsBase.Commons +using PlotsBase.Colors +using PlotsBase.Shapes +using PlotsBase.Arrows +using PlotsBase.Fonts +using PlotsBase.Ticks +using PlotsBase.Axes + +const package_str = "PGFPlotsX" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct PGFPlotsXBackend <: PlotsBase.AbstractBackend end +const T = PGFPlotsXBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) +end + +PlotsBase.backend_name(::T) = sym +PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) + +const _pgfplotsx_attrs = PlotsBase.merge_with_base_supported([ + :annotations, + :annotationrotation, + :annotationhalign, + :annotationfontsize, + :annotationfontfamily, + :annotationcolor, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :legend_foreground_color, + :foreground_color_grid, + :foreground_color_axis, + :foreground_color_text, + :foreground_color_border, + :label, + :seriescolor, + :seriesalpha, + :line, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :bins, + :layout, + :title, + :window_title, + :guide, + :widen, + :lims, + :ticks, + :scale, + :flip, + :titlefontfamily, + :titlefontsize, + :titlefonthalign, + :titlefontvalign, + :titlefontrotation, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_halign, + :legend_font_valign, + :legend_font_rotation, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfonthalign, + :tickfontvalign, + :tickfontrotation, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefonthalign, + :guidefontvalign, + :guidefontrotation, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_titlefontsize, + :colorbar_titlefontcolor, + :colorbar_titlefontrotation, + :colorbar_entry, + :fill, + :fill_z, + :line_z, + :marker_z, + :levels, + :legend_column, + :legend_title, + :legend_title_font_color, + :legend_title_font_pointsize, + :ribbon, + :quiver, + :orientation, + :overwrite_figure, + :polar, + :plot_title, + :plot_titlefontcolor, + :plot_titlefontrotation, + :plot_titlefontsize, + :plot_titlevspan, + :aspect_ratio, + :normalize, + :weights, + :inset_subplots, + :bar_width, + :arrow, + :framestyle, + :tick_direction, + :thickness_scaling, + :camera, + :contour_labels, + :connections, + :thickness_scaling, + :axis, + :draw_arrow, + :minorgrid, + :minorgridalpha, + :minorgridlinewidth, + :minorgridstyle, + :minorticks, + :mirror, + :rotation, + :showaxis, + :tickfontrotation, + :draw_arrow, +]) +const _pgfplotsx_seriestypes = [ + :path, + :scatter, + :straightline, + :path3d, + :scatter3d, + :surface, + :wireframe, + :heatmap, + :mesh3d, + :contour, + :contour3d, + :quiver, + :shape, + :steppre, + :stepmid, + :steppost, + :ysticks, + :xsticks, +] +const _pgfplotsx_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _pgfplotsx_markers = [ + :none, + :auto, + :circle, + :rect, + :diamond, + :utriangle, + :dtriangle, + :ltriangle, + :rtriangle, + :cross, + :xcross, + :x, + :+, + :star5, + :star6, + :pentagon, + :hline, + :vline, +] +const _pgfplotsx_scales = [:identity, :ln, :log2, :log10] +PlotsBase.is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true + +# additional constants +const _pgfplotsx_series_ids = KW() + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::T, $s::Symbol) = $s in $v + PlotsBase.$f2(::T) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + const Options = PGFPlotsX.Options const Table = PGFPlotsX.Table @@ -43,7 +283,7 @@ end pgfx_axes(pgfx_plot::PGFPlotsXPlot) = pgfx_plot.the_plot.elements[1].elements -pgfx_preamble() = pgfx_preamble(current()) +pgfx_preamble() = pgfx_preamble(PlotsBase.current()) function pgfx_preamble(pgfx_plot::Plot{PGFPlotsXBackend}) old_flag = pgfx_plot.attr[:tex_output_standalone] pgfx_plot.attr[:tex_output_standalone] = true @@ -81,7 +321,8 @@ curly(obj) = "{$(string(obj))}" latex_formatter(formatter::Symbol) = formatter in (:plain, :latex) ? formatter : :latex latex_formatter(formatter::Function) = formatter -labelfunc(scale::Symbol, backend::PGFPlotsXBackend) = labelfunc_tex(scale) +PlotsBase.labelfunc(scale::Symbol, backend::PGFPlotsXBackend) = + PlotsBase.labelfunc_tex(scale) pgfx_halign(k) = (left = "left", hcenter = "center", center = "center", right = "right")[k] @@ -112,9 +353,9 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) end for sp in plt.subplots - bb2 = bbox(sp) + bb2 = PlotsBase.bbox(sp) dx, dy = bb2.x0 - sp_w, sp_h = width(bb2), height(bb2) + sp_w, sp_h = PlotsBase.width(bb2), PlotsBase.height(bb2) if sp[:subplot_index] == plt[:plot_titleindex] x = dx + sp_w / 2 - 10mm # FIXME: get rid of magic constant y = dy + sp_h / 2 @@ -653,9 +894,9 @@ pgfx_series_arguments(series, opt) = elseif RecipesPipeline.is3d(st) opt[:x], opt[:y], opt[:z] elseif st === :straightline - straightline_data(series) + PlotsBase.straightline_data(series) elseif st === :shape - shape_data(series) + PlotsBase.shape_data(series) elseif ispolar(series) theta, r = opt[:x], opt[:y] rad2deg.(theta), r @@ -880,7 +1121,7 @@ function pgfx_filllegend!(series_opt, opt) end # Generates a colormap for pgfplots based on a ColorGradient -pgfx_colormap(cl::PlotUtils.AbstractColorList) = pgfx_colormap(color_list(cl)) +pgfx_colormap(cl::PlotUtils.AbstractColorList) = pgfx_colormap(PlotUtils.color_list(cl)) pgfx_colormap(v::Vector{<:Colorant}) = join(map(c -> @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)), v), '\n') pgfx_colormap(cg::ColorGradient) = join( @@ -1050,7 +1291,7 @@ function pgfx_fillrange_series!(axis, series, series_func, i, fillrange, rng) elseif ispolar(series) rad2deg.(opt[:x][rng]), opt[:y][rng] elseif series[:seriestype] === :straightline - straightline_data(series) + PlotsBase.straightline_data(series) else opt[:x][rng], opt[:y][rng] end @@ -1348,7 +1589,7 @@ end # display calls this and then _display, its called 3 times for plot(1:5) # Set the (left, top, right, bottom) minimum padding around the plot area # to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{PGFPlotsXBackend}) +function PlotsBase._update_min_padding!(sp::Subplot{PGFPlotsXBackend}) sp.minpad = if (leg = sp[:legend_position]) in (:best, :outertopright, :outerright, :outerbottomright) || @@ -1359,20 +1600,29 @@ function _update_min_padding!(sp::Subplot{PGFPlotsXBackend}) end end -_create_backend_figure(plt::Plot{PGFPlotsXBackend}) = plt.o = PGFPlotsXPlot() +PlotsBase._create_backend_figure(plt::Plot{PGFPlotsXBackend}) = plt.o = PGFPlotsXPlot() -_series_added(plt::Plot{PGFPlotsXBackend}, series::Series) = plt.o.is_created = false +PlotsBase._series_added(plt::Plot{PGFPlotsXBackend}, series::Series) = + plt.o.is_created = false -_update_plot_object(plt::Plot{PGFPlotsXBackend}) = plt.o(plt) +PlotsBase._update_plot_object(plt::Plot{PGFPlotsXBackend}) = plt.o(plt) for mime in ("application/pdf", "image/svg+xml", "image/png") - @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{PGFPlotsXBackend}) + @eval function PlotsBase._show( + io::IO, + mime::MIME{Symbol($mime)}, + plt::Plot{PGFPlotsXBackend}, + ) plt.o.was_shown = true show(io, mime, plt.o.the_plot) end end -function _show(io::IO, mime::MIME{Symbol("application/x-tex")}, plt::Plot{PGFPlotsXBackend}) +function PlotsBase._show( + io::IO, + mime::MIME{Symbol("application/x-tex")}, + plt::Plot{PGFPlotsXBackend}, +) plt.o.was_shown = true PGFPlotsX.print_tex( io, @@ -1381,7 +1631,9 @@ function _show(io::IO, mime::MIME{Symbol("application/x-tex")}, plt::Plot{PGFPlo ) end -function _display(plt::Plot{PGFPlotsXBackend}) +function PlotsBase._display(plt::Plot{PGFPlotsXBackend}) plt.o.was_shown = true display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot) end + +end # module diff --git a/PlotsBase/ext/PlotlyJSExt.jl b/PlotsBase/ext/PlotlyJSExt.jl new file mode 100644 index 000000000..27f06a4c1 --- /dev/null +++ b/PlotsBase/ext/PlotlyJSExt.jl @@ -0,0 +1,115 @@ +module PlotlyJSExt + +import PlotsBase: PlotsBase, Plot, isijulia +using PlotsBase.PlotsPlots +using PlotsBase.Commons +using PlotsBase.Plotly + +import PlotlyJS: PlotlyJS, WebIO + +# unrolling the old # init_backend macro by hand case by case +# this is not a macro for the backend maintainers and explicit control +const package_str = "PlotlyJS" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct PlotlyJSBackend <: PlotsBase.AbstractBackend end +const T = PlotlyJSBackend + +get_concrete_backend() = T # opposite to abstract + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) +end + +PlotsBase.backend_name(::T) = sym +PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) + +const _plotlyjs_attrs = PlotsBase.Plotly._plotly_attrs +const _plotlyjs_seriestypes = PlotsBase.Plotly._plotly_seriestypes +const _plotlyjs_styles = PlotsBase.Plotly._plotly_styles +const _plotlyjs_markers = PlotsBase.Plotly._plotly_markers +const _plotlyjs_scales = PlotsBase.Plotly._plotly_scales + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::T, $s::Symbol) = $s in $v + PlotsBase.$f2(::T) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- +# https://github.com/JuliaPlots/PlotlyJS.jl + +# ------------------------------------------------------------------------------ + +function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) + plt[:overwrite_figure] && PlotsBase.closeall() + plt.o = PlotlyJS.plot() + traces = PlotlyJS.GenericTrace[] + for series_dict in plotly_series(plt) + plotly_type = pop!(series_dict, :type) + series_dict[:transpose] = false + push!(traces, PlotlyJS.GenericTrace(plotly_type; series_dict...)) + end + PlotlyJS.addtraces!(plt.o, traces...) + layout = plotly_layout(plt) + w, h = plt[:size] + PlotlyJS.relayout!(plt.o, layout, width = w, height = h) + plt.o +end + +# ------------------------------------------------------------------------------ + +for (mime, fmt) in ( + "application/pdf" => "pdf", + "image/png" => "png", + "image/svg+xml" => "svg", + "image/eps" => "eps", +) + @eval PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyJSBackend}) = + PlotlyJS.savefig(io, plotlyjs_syncplot(plt), format = $fmt) +end + +# Use the Plotly implementation for json and html: +PlotsBase._show( + io::IO, + mime::MIME"application/vnd.plotly.v1+json", + plt::Plot{PlotlyJSBackend}, +) = plotly_show_js(io, plt) + +PlotsBase.html_head(plt::Plot{PlotlyJSBackend}) = PlotsBase.Plotly.plotly_html_head(plt) +PlotsBase.html_body(plt::Plot{PlotlyJSBackend}) = PlotsBase.Plotly.plotly_html_body(plt) + +PlotsBase._show(io::IO, ::MIME"text/html", plt::Plot{PlotlyJSBackend}) = + write(io, PlotsBase.embeddable_html(plt)) + +PlotsBase._display(plt::Plot{PlotlyJSBackend}) = display(plotlyjs_syncplot(plt)) + +WebIO.render(plt::Plot{PlotlyJSBackend}) = WebIO.render(plotlyjs_syncplot(plt)) + +PlotsBase.closeall(::PlotlyJSBackend) = + if !PlotsBase.isplotnull() && isa(PlotsBase.current().o, PlotlyJS.SyncPlot) + close(PlotsBase.current().o) + end + +Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyJSBackend}) = true + +end # module diff --git a/ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl b/PlotsBase/ext/PlotlyKaleidoExt.jl similarity index 58% rename from ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl rename to PlotsBase/ext/PlotlyKaleidoExt.jl index 7e6f022a2..bff882427 100644 --- a/ext/PlotsPlotlyKaleidoExt/PlotsPlotlyKaleidoExt.jl +++ b/PlotsBase/ext/PlotlyKaleidoExt.jl @@ -1,11 +1,10 @@ -module PlotsPlotlyKaleidoExt +module PlotlyKaleidoExt -using PlotlyKaleido - -using Plots: Plots, Plot, PlotlyBackend, plotly_show_js -import Plots: _show +import PlotsBase: PlotsBase, Plot, PlotlyBackend +import PlotlyKaleido function __init__() + ccall(:jl_generating_output, Cint, ()) == 1 && return PlotlyKaleido.start() atexit() do PlotlyKaleido.kill_kaleido() @@ -18,10 +17,10 @@ for (mime, fmt) in ( "image/svg+xml" => "svg", "image/eps" => "eps", ) - @eval Plots._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyBackend}) = + @eval PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyBackend}) = PlotlyKaleido.savefig( io, - sprint(io -> plotly_show_js(io, plt)), + sprint(io -> PlotsBase.plotly_show_js(io, plt)), height = plt[:size][2], width = plt[:size][1], format = $fmt, diff --git a/ext/PlotsPythonPlotExt/pythonplot.jl b/PlotsBase/ext/PythonPlotExt.jl similarity index 85% rename from ext/PlotsPythonPlotExt/pythonplot.jl rename to PlotsBase/ext/PythonPlotExt.jl index 000dccc97..5b37e0bd6 100644 --- a/ext/PlotsPythonPlotExt/pythonplot.jl +++ b/PlotsBase/ext/PythonPlotExt.jl @@ -1,6 +1,231 @@ +module PythonPlotExt + +import RecipesPipeline +import PythonPlot +import NaNMath + +import PlotsBase +import PlotsBase.PlotUtils: PlotUtils, ColorGradient, plot_color, color_list, cgrad +import PlotsBase.Commons: Commons, single_color + +using PlotsBase.PlotMeasures +using PlotsBase.Annotations +using PlotsBase.PlotsSeries +using PlotsBase.PlotsPlots +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Commons +using PlotsBase.Colors +using PlotsBase.Arrows +using PlotsBase.Shapes +using PlotsBase.Fonts +using PlotsBase.Ticks +using PlotsBase.Axes + +const package_str = "PythonPlot" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct PythonPlotBackend <: PlotsBase.AbstractBackend end +const T = PythonPlotBackend + +get_concrete_backend() = T + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) + + if PythonPlot.version < v"3.4" + @warn """You are using Matplotlib $(PythonPlot.version), which is no longer + officially supported by the Plots community. To ensure smooth PlotsBase.jl + integration update your Matplotlib library to a version ≥ 3.4.0 + """ + end + + PythonCall.pycopy!(mpl, PythonCall.pyimport("matplotlib")) + PythonCall.pycopy!(mpl_toolkits, PythonCall.pyimport("mpl_toolkits")) + PythonCall.pycopy!(numpy, PythonCall.pyimport("numpy")) + PythonCall.pyimport("mpl_toolkits.axes_grid1") + numpy.seterr(invalid = "ignore") + PythonPlot.ioff() # we don't want every command to update the figure +end + +PlotsBase.backend_name(::T) = sym +PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) + +const _pythonplot_attrs = PlotsBase.merge_with_base_supported([ + :annotations, + :legend_background_color, + :background_color_inside, + :background_color_outside, + :foreground_color_grid, + :legend_foreground_color, + :foreground_color_title, + :foreground_color_axis, + :foreground_color_border, + :foreground_color_guide, + :foreground_color_text, + :label, + :linecolor, + :linestyle, + :linewidth, + :linealpha, + :markershape, + :markercolor, + :markersize, + :markeralpha, + :markerstrokewidth, + :markerstrokecolor, + :markerstrokealpha, + :fillrange, + :fillcolor, + :fillalpha, + :fillstyle, + :bins, + :bar_width, + :bar_edges, + :bar_position, + :title, + :titlelocation, + :titlefont, + :window_title, + :guide, + :guide_position, + :widen, + :lims, + :ticks, + :scale, + :flip, + :rotation, + :titlefontfamily, + :titlefontsize, + :titlefontcolor, + :legend_font_family, + :legend_font_pointsize, + :legend_font_color, + :tickfontfamily, + :tickfontsize, + :tickfontcolor, + :guidefontfamily, + :guidefontsize, + :guidefontcolor, + :grid, + :gridalpha, + :gridstyle, + :gridlinewidth, + :legend_position, + :legend_title, + :colorbar, + :colorbar_title, + :colorbar_entry, + :colorbar_ticks, + :colorbar_tickfontfamily, + :colorbar_tickfontsize, + :colorbar_tickfonthalign, + :colorbar_tickfontvalign, + :colorbar_tickfontrotation, + :colorbar_tickfontcolor, + :colorbar_titlefontcolor, + :colorbar_titlefontsize, + :colorbar_scale, + :marker_z, + :line, + :line_z, + :fill, + :fill_z, + :fontfamily, + :fontfamily_subplot, + :legend_column, + :legend_font, + :legend_title, + :legend_title_font_color, + :legend_title_font_family, + :legend_title_font_pointsize, + :levels, + :ribbon, + :quiver, + :arrow, + :orientation, + :overwrite_figure, + :polar, + :normalize, + :weights, + :contours, + :aspect_ratio, + :clims, + :inset_subplots, + :dpi, + :stride, + :framestyle, + :tick_direction, + :camera, + :contour_labels, + :connections, +]) + +const _pythonplot_seriestypes = [ + :path, + :steppre, + :stepmid, + :steppost, + :shape, + :straightline, + :scatter, + :hexbin, + :heatmap, + :image, + :contour, + :contour3d, + :path3d, + :scatter3d, + :mesh3d, + :surface, + :wireframe, +] + +const _pythonplot_styles = [:auto, :solid, :dash, :dot, :dashdot] +const _pythonplot_markers = vcat(Commons._all_markers, :pixel) +const _pythonplot_scales = [:identity, :ln, :log2, :log10] + +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::T, $s::Symbol) = $s in $v + PlotsBase.$f2(::T) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + # github.com/stevengj/PythonPlot.jl -is_marker_supported(::PythonPlotBackend, shape::Shape) = true +const PythonCall = PythonPlot.PythonCall +const mpl = PythonCall.pynew() # PythonCall.pyimport("matplotlib") +const mpl_toolkits = PythonCall.pynew() # PythonCall.pyimport("mpl_toolkits") +const numpy = PythonCall.pynew() # PythonCall.pyimport("numpy") + +const pyisnone = if isdefined(PythonCall, :pyisnone) + PythonCall.pyisnone +else + PythonCall.Core.pyisnone +end + +PlotsBase.is_marker_supported(::PythonPlotBackend, shape::Shape) = true # problem: github.com/tbreloff/Plots.jl/issues/308 # solution: hack from @stevengj: github.com/JuliaPy/PyPlot.jl/pull/223#issuecomment-229747768 @@ -17,7 +242,7 @@ for k in (:linthresh, :base, :label) end _py_handle_surface(v) = v -_py_handle_surface(z::Surface) = z.surf +_py_handle_surface(z::PlotsBase.Surface) = z.surf _py_color(s) = _py_color(parse(Colorant, string(s))) _py_color(c::Colorant) = [red(c), green(c), blue(c), alpha(c)] # NOTE: returning a tuple fails `PythonPlot` @@ -142,7 +367,7 @@ get_locator_and_formatter(vals::AVec) = mpl.ticker.FixedLocator(eachindex(vals)), mpl.ticker.FixedFormatter(vals) labelfunc(scale::Symbol, backend::PythonPlotBackend) = - PythonPlot.LaTeXStrings.latexstring ∘ labelfunc_tex(scale) + PythonPlot.LaTeXStrings.latexstring ∘ PlotsBase.labelfunc_tex(scale) _py_mask_nans(z) = PythonPlot.pycall(numpy.ma.masked_invalid, z) @@ -152,7 +377,7 @@ function fix_xy_lengths!(plt::Plot{PythonPlotBackend}, series::Series) if (x = series[:x]) !== nothing y = series[:y] nx, ny = length(x), length(y) - if !(get(series.plotattributes, :z, nothing) isa Surface || nx == ny) + if !(get(series.plotattributes, :z, nothing) isa PlotsBase.Surface || nx == ny) if nx < ny series[:x] = map(i -> Float64(x[mod1(i, nx)]), 1:ny) else @@ -209,16 +434,16 @@ to_str(x) = PythonCall.pyconvert(String, x) # compute a bounding box (with origin top-left), however PythonPlot gives coords with origin bottom-left function _py_bbox(obj) - PythonCall.pyisnone(obj) && return _py_bbox(nothing) + pyisnone(obj) && return _py_bbox(nothing) fl, fr, fb, ft = bb = _py_extents(obj.get_figure()) l, r, b, t = ex = _py_extents(obj) # @show obj bb ex x0, y0, width, height = l * px, (ft - t) * px, (r - l) * px, (t - b) * px # @show width height - BoundingBox(x0, y0, width, height) + PlotsBase.BoundingBox(x0, y0, width, height) end -_py_bbox(::Nothing) = BoundingBox(0mm, 0mm) +_py_bbox(::Nothing) = PlotsBase.BoundingBox(0mm, 0mm) # get the bounding box of the union of the objects function _py_bbox(v::AVec) @@ -267,8 +492,8 @@ _py_thickness_scale(plt::Plot{PythonPlotBackend}, ptsz) = ptsz * plt[:thickness_ # --------------------------------------------------------------------------- # Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{PythonPlotBackend}) - w, h = map(s -> px2inch(s * plt[:dpi] / Plots.DPI), plt[:size]) +function PlotsBase._create_backend_figure(plt::Plot{PythonPlotBackend}) + w, h = map(s -> PlotMeasures.px2inch(s * plt[:dpi] / PlotsBase.DPI), plt[:size]) # reuse the current figure? plt[:overwrite_figure] ? PythonPlot.gcf() : PythonPlot.figure() end @@ -317,9 +542,9 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # ax = getAxis(plt, series) x, y, z = (_py_handle_surface(series[letter]) for letter in (:x, :y, :z)) if st === :straightline - x, y = straightline_data(series) + x, y = PlotsBase.straightline_data(series) elseif st === :shape - x, y = shape_data(series) + x, y = PlotsBase.shape_data(series) end # make negative radii positive and flip the angle (PythonPlot ignores negative radii) @@ -330,7 +555,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end end - xyargs = st ∈ _3dTypes ? (x, y, z) : (x, y) + xyargs = st ∈ Commons._3dTypes ? (x, y, z) : (x, y) # handle zcolor and get c/cmap needs_colorbar = hascolorbar(sp) @@ -351,8 +576,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # pass in an integer value as an arg, but a levels list as a keyword arg levels = series[:levels] - levelargs = isscalar(levels) ? levels : () - isvector(levels) && (extrakw[:levels] = levels) + levelargs = PlotsBase.isscalar(levels) ? levels : () + PlotsBase.isvector(levels) && (extrakw[:levels] = levels) # add custom frame shapes to markershape? series_annotations_shapes!(series, :xy) @@ -424,8 +649,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) ax.scatter( args...; zorder = zorder + 0.5, - marker = _py_marker(_cycle(series[:markershape], i)), - s = _py_thickness_scale(plt, _cycle(series[:markersize], i)) .^ 2, + marker = _py_marker(PlotsBase._cycle(series[:markershape], i)), + s = _py_thickness_scale(plt, PlotsBase._cycle(series[:markersize], i)) .^ 2, facecolors = _py_color( get_markercolor(series, i, cbar_scale), get_markeralpha(series, i), @@ -519,7 +744,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) aspect, ) |> push_h elseif st === :heatmap - x, y = heatmap_edges(x, xaxis[:scale], y, yaxis[:scale], size(z)) + x, y = PlotsBase.heatmap_edges(x, xaxis[:scale], y, yaxis[:scale], size(z)) expand_extrema!(xaxis, x) expand_extrema!(yaxis, y) ax.pcolormesh( @@ -543,7 +768,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) map(inds -> map(i -> [x[i], y[i], z[i]], inds), cns) elseif cns isa NTuple{3,<:AbstractVector{<:Integer}} # Only triangles - connections have to be 0-based (indexing) - X, Y, Z = mesh3d_triangles(x, y, z, cns) + X, Y, Z = PlotsBase.mesh3d_triangles(x, y, z, cns) ntris = length(cns[1]) polys = sizehint!(Matrix{eltype(x)}[], ntris) for n in 1:ntris @@ -699,9 +924,9 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) f, dim1, dim2 = :fill_between, x[rng], y[rng] n = length(dim1) args = if typeof(fillrange) <: Union{Real,AVec} - dim1, _cycle(fillrange, rng), dim2 - elseif is_2tuple(fillrange) - dim1, _cycle(fillrange[1], rng), _cycle(fillrange[2], rng) + dim1, PlotsBase._cycle(fillrange, rng), dim2 + elseif PlotsBase.is_2tuple(fillrange) + dim1, PlotsBase._cycle(fillrange[1], rng), PlotsBase._cycle(fillrange[2], rng) end la = get_linealpha(series, i) @@ -751,7 +976,7 @@ function _py_set_ticks(sp, ax, ticks, letter) return end - tick_values, tick_labels = if (ttype = ticks_type(ticks)) === :ticks + tick_values, tick_labels = if (ttype = PlotsBase.ticks_type(ticks)) === :ticks ticks, [] elseif ttype === :ticks_and_labels ticks @@ -777,7 +1002,8 @@ function _py_compute_axis_minval(sp::Subplot, axis::Axis) end function _py_set_scale(ax, sp::Subplot, scale::Symbol, letter::Symbol) - scale ∈ supported_scales() || return @warn "Unhandled scale value in PythonPlot: $scale" + scale ∈ PlotsBase.supported_scales() || + return @warn "Unhandled scale value in PythonPlot: $scale" scl, kw = if scale === :identity "linear", KW() else @@ -823,12 +1049,12 @@ end _py_hide_spines(ax) = foreach(spine -> getproperty(ax.spines, string(spine)).set_visible(false), ax.spines) -function _before_layout_calcs(plt::Plot{PythonPlotBackend}) +function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) # update the fig w, h = plt[:size] fig = plt.o fig.clear() - fig.set_size_inches(w / DPI, h / DPI, forward = true) + fig.set_size_inches(w / PlotsBase.DPI, h / PlotsBase.DPI, forward = true) fig.set_facecolor(_py_color(plt[:background_color_outside])) fig.set_dpi(plt[:dpi]) @@ -1057,7 +1283,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend}) ticks = framestyle === :none ? nothing : get_ticks(sp, axis) has_major_ticks = ticks !== :none && ticks !== nothing && ticks !== false - has_major_ticks &= if (ttype = ticks_type(ticks)) === :ticks + has_major_ticks &= if (ttype = PlotsBase.ticks_type(ticks)) === :ticks length(ticks) > 0 elseif ttype === :ticks_and_labels tcs, labs = ticks @@ -1226,16 +1452,17 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend}) end expand_padding!(padding, bb, plotbb) = - if ispositive(width(bb)) && ispositive(height(bb)) - padding[1] = max(padding[1], left(plotbb) - left(bb)) - padding[2] = max(padding[2], top(plotbb) - top(bb)) - padding[3] = max(padding[3], right(bb) - right(plotbb)) - padding[4] = max(padding[4], bottom(bb) - bottom(plotbb)) + if PlotsBase.ispositive(PlotsBase.width(bb)) && + PlotsBase.ispositive(PlotsBase.height(bb)) + padding[1] = max(padding[1], PlotsBase.left(plotbb) - PlotsBase.left(bb)) + padding[2] = max(padding[2], PlotsBase.top(plotbb) - PlotsBase.top(bb)) + padding[3] = max(padding[3], PlotsBase.right(bb) - PlotsBase.right(plotbb)) + padding[4] = max(padding[4], PlotsBase.bottom(bb) - PlotsBase.bottom(plotbb)) end # Set the (left, top, right, bottom) minimum padding around the plot area # to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{PythonPlotBackend}) +function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) (ax = sp.o) === nothing && return sp.minpad plotbb = _py_bbox(ax) @@ -1273,7 +1500,7 @@ function _update_min_padding!(sp::Subplot{PythonPlotBackend}) # add ∈ the user-specified margin padding .+= [sp[:left_margin], sp[:top_margin], sp[:right_margin], sp[:bottom_margin]] - sp.minpad = Tuple((Plots.DPI / sp.plt[:dpi]) .* padding) + sp.minpad = Tuple((PlotsBase.DPI / sp.plt[:dpi]) .* padding) end # ----------------------------------------------------------------- @@ -1316,12 +1543,18 @@ function _py_legend_pos(pos::Tuple{<:Real,Symbol}) s, c = sincosd(pos[1]) .* (pos[2] === :outer ? -1 : 1) yanchors = "lower", "center", "upper" xanchors = "left", "center", "right" - join([yanchors[legend_anchor_index(s)], xanchors[legend_anchor_index(c)]], ' ') + join( + [ + yanchors[PlotsBase.legend_anchor_index(s)], + xanchors[PlotsBase.legend_anchor_index(c)], + ], + ' ', + ) end # legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax) _py_legend_bbox(pos::Tuple{<:Real,Symbol}) = - legend_pos_from_angle(pos[1], 0.0, 0.5, 1.0, 0.0, 0.5, 1.0) + PlotsBase.legend_pos_from_angle(pos[1], 0.0, 0.5, 1.0, 0.0, 0.5, 1.0) _py_legend_bbox(pos) = pos function _py_add_legend(plt::Plot, sp::Subplot, ax) @@ -1387,7 +1620,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) solid_joinstyle = "miter", dash_capstyle = "butt", dash_joinstyle = "miter", - marker = _py_marker(_cycle(series[:markershape], 1)), + marker = _py_marker(PlotsBase._cycle(series[:markershape], 1)), markersize = _py_thickness_scale(plt, 0.8sp[:legend_font_pointsize]), markeredgecolor = _py_color( single_color(get_markerstrokecolor(series)), @@ -1412,7 +1645,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) # if anything was added, call ax.legend and set the colors isempty(handles) && return - leg = legend_angle(leg) + leg = PlotsBase.legend_angle(leg) ncol = if (lc = sp[:legend_column]) < 0 nseries elseif lc > 1 @@ -1463,26 +1696,29 @@ end # Use the bounding boxes (and methods left/top/right/bottom/width/height) `sp.bbox` and `sp.plotarea` to # position the subplot in the backend. -function _update_plot_object(plt::Plot{PythonPlotBackend}) +function PlotsBase._update_plot_object(plt::Plot{PythonPlotBackend}) for sp in plt.subplots (ax = sp.o) === nothing && return figw, figh = sp.plt[:size] .* px # ax.set_position signature: `[left, bottom, width, height]` - bbox_to_pcts(sp.plotarea, figw, figh) |> ax.set_position + PlotsBase.bbox_to_pcts(sp.plotarea, figw, figh) |> ax.set_position if haskey(sp.attr, :cbar_ax) && RecipesPipeline.is3d(sp) # 2D plots are completely handled by axis dividers bb = sp.attr[:cbar_bbox] # this is the bounding box of just the colors of the colorbar (not labels) pad = 2mm - cb_bbox = BoundingBox( - right(sp.bbox) - 2width(bb) - 2pad, # x0 - top(sp.bbox) + pad, # y0 - width(bb), # width - height(sp.bbox) - 2pad, # height + cb_bbox = PlotsBase.BoundingBox( + PlotsBase.right(sp.bbox) - 2PlotsBase.width(bb) - 2pad, # x0 + PlotsBase.top(sp.bbox) + pad, # y0 + PlotsBase.width(bb), # width + PlotsBase.height(sp.bbox) - 2pad, # height ) - get(sp[:extra_kwargs], "3d_colorbar_axis", bbox_to_pcts(cb_bbox, figw, figh)) |> - sp.attr[:cbar_ax].set_position + get( + sp[:extra_kwargs], + "3d_colorbar_axis", + PlotsBase.bbox_to_pcts(cb_bbox, figw, figh), + ) |> sp.attr[:cbar_ax].set_position end end PythonPlot.draw() @@ -1491,7 +1727,7 @@ end # ----------------------------------------------------------------- # display/output -_display(plt::Plot{PythonPlotBackend}) = plt.o.show() +PlotsBase._display(plt::Plot{PythonPlotBackend}) = plt.o.show() for (mime, fmt) in ( "application/eps" => "eps", @@ -1502,7 +1738,11 @@ for (mime, fmt) in ( "image/svg+xml" => "svg", "application/x-tex" => "pgf", ) - @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PythonPlotBackend}) + @eval function PlotsBase._show( + io::IO, + ::MIME{Symbol($mime)}, + plt::Plot{PythonPlotBackend}, + ) fig = plt.o fig.canvas.print_figure( io, @@ -1515,4 +1755,6 @@ for (mime, fmt) in ( end end -closeall(::PythonPlotBackend) = PythonPlot.close("all") +PlotsBase.closeall(::PythonPlotBackend) = PythonPlot.close("all") + +end # module diff --git a/ext/PlotsUnicodePlotsExt/unicodeplots.jl b/PlotsBase/ext/UnicodePlotsExt.jl similarity index 71% rename from ext/PlotsUnicodePlotsExt/unicodeplots.jl rename to PlotsBase/ext/UnicodePlotsExt.jl index 469045f49..b517e2176 100644 --- a/ext/PlotsUnicodePlotsExt/unicodeplots.jl +++ b/PlotsBase/ext/UnicodePlotsExt.jl @@ -1,3 +1,139 @@ +module UnicodePlotsExt + +import PlotsBase: PlotsBase, texmath2unicode +import RecipesPipeline +import UnicodePlots + +using PlotsBase.PlotMeasures +using PlotsBase.PlotsSeries +using PlotsBase.Annotations +using PlotsBase.PlotsPlots +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Commons +using PlotsBase.Shapes +using PlotsBase.Arrows +using PlotsBase.Colors +using PlotsBase.Fonts +using PlotsBase.Ticks +using PlotsBase.Axes + +const package_str = "UnicodePlots" +const str = lowercase(package_str) +const sym = Symbol(str) + +struct UnicodePlotsBackend <: PlotsBase.AbstractBackend end +const T = UnicodePlotsBackend + +get_concrete_backend() = UnicodePlotsBackend # opposite to abstract + +function __init__() + @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." + PlotsBase._backendType[sym] = get_concrete_backend() + PlotsBase._backendSymbol[T] = sym + + push!(PlotsBase._initialized_backends, sym) +end +PlotsBase.backend_name(::UnicodePlotsBackend) = sym +PlotsBase.backend_package_name(::UnicodePlotsBackend) = PlotsBase.backend_package_name(sym) + +const _unicodeplots_attrs = PlotsBase.merge_with_base_supported([ + :annotations, + :bins, + :guide, + :widen, + :grid, + :label, + :layout, + :legend, + :legend_title_font_color, + :lims, + :line, + :linealpha, + :linecolor, + :linestyle, + :markershape, + :plot_title, + :quiver, + :arrow, + :seriesalpha, + :seriescolor, + :scale, + :flip, + :title, + # :marker_z, + :line_z, +]) +const _unicodeplots_seriestypes = [ + :path, + :path3d, + :scatter, + :scatter3d, + :straightline, + # :bar, + :shape, + :histogram2d, + :heatmap, + :contour, + # :contour3d, + :image, + :spy, + :surface, + :wireframe, + :mesh3d, +] +const _unicodeplots_styles = [:auto, :solid] +const _unicodeplots_markers = [ + :none, + :auto, + :pixel, + # vvvvvvvvvv shapes + :circle, + :rect, + :star5, + :diamond, + :hexagon, + :cross, + :xcross, + :utriangle, + :dtriangle, + :rtriangle, + :ltriangle, + :pentagon, + # :heptagon, + # :octagon, + :star4, + :star6, + # :star7, + :star8, + :vline, + :hline, + :+, + :x, +] +const _unicodeplots_scales = [:identity, :ln, :log2, :log10] +# ----------------------------------------------------------------------------- +# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods +# defined in abstract_backend.jl + +for s in (:attr, :seriestype, :marker, :style, :scale) + f1 = Symbol("is_", s, "_supported") + f2 = Symbol("supported_", s, "s") + v = Symbol("_$(str)_", s, "s") + quote + PlotsBase.$f1(::UnicodePlotsBackend, $s::Symbol) = $s in $v + PlotsBase.$f2(::UnicodePlotsBackend) = sort(collect($v)) + end |> eval +end + +## results in: +# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool +# ... +# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} +# ... +# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} +# ----------------------------------------------------------------------------- + # https://github.com/JuliaPlots/UnicodePlots.jl const _canvas_map = ( @@ -10,9 +146,9 @@ const _canvas_map = ( dot = UnicodePlots.DotCanvas, ) -should_warn_on_unsupported(::UnicodePlotsBackend) = false +PlotsBase.should_warn_on_unsupported(::UnicodePlotsBackend) = false -function _before_layout_calcs(plt::Plots.Plot{UnicodePlotsBackend}) +function PlotsBase._before_layout_calcs(plt::PlotsBase.Plot{UnicodePlotsBackend}) plt.o = UnicodePlots.Plot[] up_width = UnicodePlots.DEFAULT_WIDTH[] up_height = UnicodePlots.DEFAULT_HEIGHT[] @@ -40,7 +176,7 @@ function _before_layout_calcs(plt::Plots.Plot{UnicodePlotsBackend}) # create a plot window with xlim/ylim set, # but the X/Y vectors are outside the bounds canvas = if (up_c = get(sp_kw, :canvas, :auto)) ≡ :auto - isijulia() ? :ascii : :braille + PlotsBase.isijulia() ? :ascii : :braille else up_c end @@ -49,7 +185,7 @@ function _before_layout_calcs(plt::Plots.Plot{UnicodePlotsBackend}) if plot_3d :none # no plots border in 3d (consistency with other backends) else - isijulia() ? :ascii : :solid + PlotsBase.isijulia() ? :ascii : :solid end else up_b @@ -167,9 +303,9 @@ function addUnicodeSeries!( # get the series data and label x, y = if st ≡ :straightline - straightline_data(series) + PlotsBase.straightline_data(series) elseif st ≡ :shape - shape_data(series) + PlotsBase.shape_data(series) else series[:x], series[:y] end @@ -195,7 +331,7 @@ function addUnicodeSeries!( z = Array(series[:z]) if st ≡ :contour isfilledcontour(series) && - @warn "Plots(UnicodePlots): filled contour is not implemented" + @warn "PlotsBase(UnicodePlots): filled contour is not implemented" return UnicodePlots.contourplot(x, y, z; kw..., levels = series[:levels]) elseif st ≡ :heatmap return UnicodePlots.heatmap(z; fix_ar = fix_ar, kw...) @@ -218,7 +354,7 @@ function addUnicodeSeries!( elseif st ≡ :mesh3d return UnicodePlots.lineplot!( up, - mesh3d_triangles(x, y, series[:z], series[:connections])..., + PlotsBase.mesh3d_triangles(x, y, series[:z], series[:connections])..., ) end @@ -230,7 +366,7 @@ function addUnicodeSeries!( func = UnicodePlots.scatterplot! series_kw = (; marker = series[:markershape]) else - throw(ArgumentError("Plots(UnicodePlots): series type $st not supported")) + throw(ArgumentError("PlotsBase(UnicodePlots): series type $st not supported")) end label = addlegend ? series[:label] : "" @@ -266,7 +402,7 @@ end function unsupported_layout_error() """ - Plots(UnicodePlots): complex nested layout is currently unsupported. + PlotsBase(UnicodePlots): complex nested layout is currently unsupported. Consider using plain `UnicodePlots` commands and `grid` from Term.jl as an alternative. """ |> ArgumentError |> @@ -276,19 +412,23 @@ end # ------------------------------------------------------------------------------------------ -function _show(io::IO, ::MIME"image/png", plt::Plots.Plot{UnicodePlotsBackend}) +function PlotsBase._show( + io::IO, + ::MIME"image/png", + plt::PlotsBase.Plot{UnicodePlotsBackend}, +) applicable(UnicodePlots.save_image, io) || - "Plots(UnicodePlots): saving to `.png` requires `import FreeType, FileIO`" |> + "PlotsBase(UnicodePlots): saving to `.png` requires `import FreeType, FileIO`" |> ArgumentError |> throw - prepare_output(plt) + PlotsBase.prepare_output(plt) nr, nc = size(plt.layout) s1, s2 = map(_ -> zeros(Int, nr, nc), 1:2) canvas_type = nothing imgs = [] sps = 0 for r in 1:nr, c in 1:nc - if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) + if (l = plt.layout[r, c]) isa PlotsBase.GridLayout && size(l) != (1, 1) unsupported_layout_error() else img = UnicodePlots.png_image(plt.o[sps += 1]; pixelsize = 32) @@ -299,7 +439,7 @@ function _show(io::IO, ::MIME"image/png", plt::Plots.Plot{UnicodePlotsBackend}) end end if canvas_type ≡ nothing - @warn "Plots(UnicodePlots) failed to render `png` from plot (font issue)." + @warn "PlotsBase(UnicodePlots) failed to render `png` from plot (font issue)." else m1 = maximum(s1; dims = 2) m2 = maximum(s2; dims = 1) @@ -321,12 +461,17 @@ function _show(io::IO, ::MIME"image/png", plt::Plots.Plot{UnicodePlotsBackend}) nothing end -Base.show(plt::Plots.Plot{UnicodePlotsBackend}) = show(stdout, plt) -Base.show(io::IO, plt::Plots.Plot{UnicodePlotsBackend}) = _show(io, MIME("text/plain"), plt) +Base.show(plt::PlotsBase.Plot{UnicodePlotsBackend}) = show(stdout, plt) +Base.show(io::IO, plt::PlotsBase.Plot{UnicodePlotsBackend}) = + PlotsBase._show(io, MIME("text/plain"), plt) # NOTE: _show(...) must be kept for Base.showable (src/output.jl) -function _show(io::IO, ::MIME"text/plain", plt::Plots.Plot{UnicodePlotsBackend}) - prepare_output(plt) +function PlotsBase._show( + io::IO, + ::MIME"text/plain", + plt::PlotsBase.Plot{UnicodePlotsBackend}, +) + PlotsBase.prepare_output(plt) nr, nc = size(plt.layout) if nr == 1 && nc == 1 # fast path n = length(plt.o) @@ -345,7 +490,7 @@ function _show(io::IO, ::MIME"text/plain", plt::Plots.Plot{UnicodePlotsBackend}) for r in 1:nr lmax = 0 for c in 1:nc - if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) + if (l = plt.layout[r, c]) isa PlotsBase.GridLayout && size(l) != (1, 1) unsupported_layout_error() else if get(l.attr, :blank, false) @@ -386,7 +531,9 @@ function _show(io::IO, ::MIME"text/plain", plt::Plots.Plot{UnicodePlotsBackend}) end # we only support MIME"text/plain", hence display(...) falls back to plain-text on stdout -function _display(plt::Plots.Plot{UnicodePlotsBackend}) +function PlotsBase._display(plt::PlotsBase.Plot{UnicodePlotsBackend}) show(stdout, plt) println(stdout) end + +end # module diff --git a/ext/UnitfulExt.jl b/PlotsBase/ext/UnitfulExt.jl similarity index 89% rename from ext/UnitfulExt.jl rename to PlotsBase/ext/UnitfulExt.jl index 3eb13aabc..ede2e5713 100644 --- a/ext/UnitfulExt.jl +++ b/PlotsBase/ext/UnitfulExt.jl @@ -3,9 +3,22 @@ module UnitfulExt -import Plots: Plots, @ext_imp_use, @recipe, PlotText, Subplot, AVec, AMat, Axis +import Unitful: + Unitful, + Quantity, + unit, + ustrip, + dimension, + Units, + NoUnits, + LogScaled, + logunit, + MixedUnits, + Level, + Gain, + uconvert +import PlotsBase: PlotsBase, @recipe, PlotText, Subplot, AVec, AMat, Axis import RecipesBase -@ext_imp_use :import Unitful Quantity unit ustrip Unitful dimension Units NoUnits LogScaled logunit MixedUnits Level Gain uconvert import LaTeXStrings: LaTeXString import Latexify: latexify using UnitfulLatexify @@ -129,7 +142,7 @@ Attribute fixing function fixaspectratio!(attr, u, axisletter) aspect_ratio = get(attr, :aspect_ratio, :auto) if aspect_ratio in (:auto, :none) - # Keep the default behavior (let Plots figure it out) + # Keep the default behavior (let PlotsBase figure it out) return end if aspect_ratio === :equal @@ -142,7 +155,7 @@ function fixaspectratio!(attr, u, axisletter) On the first pass, `axisletter` is `:x`, so `aspect_ratio` is converted to `u"m/s"/u"m" = u"s^-1"`. On the second pass, `axisletter` is `:y`, so `aspect_ratio` becomes `u"s^-1"*u"s" = 1`. If at this point `aspect_ratio` is *not* unitless, an error has been - made, and the default aspect ratio fixing of Plots throws a `DimensionError` as it tries + made, and the default aspect ratio fixing of PlotsBase throws a `DimensionError` as it tries to compare `0 < 1u"m"`. =======================================================================================# if axisletter === :y @@ -196,7 +209,7 @@ struct UnitfulString{S,U} <: AbstractProtectedString content::S unit::U end -# Minimum required AbstractString interface to work with Plots +# Minimum required AbstractString interface to work with PlotsBase const S = AbstractProtectedString Base.iterate(n::S) = iterate(n.content) Base.iterate(n::S, i::Integer) = iterate(n.content, i) @@ -206,7 +219,7 @@ Base.isvalid(n::S, i::Integer) = isvalid(n.content, i) Base.pointer(n::S) = pointer(n.content) Base.pointer(n::S, i::Integer) = pointer(n.content, i) -Plots.protectedstring(s) = ProtectedString(s) +PlotsBase.protectedstring(s) = ProtectedString(s) #===================================== Append unit to labels when appropriate @@ -218,7 +231,7 @@ append_unit_if_needed!(attr, key, u) = append_unit_if_needed!(attr, key, label::ProtectedString, u) = nothing append_unit_if_needed!(attr, key, label::UnitfulString, u) = nothing function append_unit_if_needed!(attr, key, label::Nothing, u) - attr[key] = if attr[:plot_object].backend == Plots._backend_instance(:pgfplotsx) + attr[key] = if attr[:plot_object].backend == PlotsBase._backend_instance(:pgfplotsx) UnitfulString(LaTeXString(latexify(u)), u) else UnitfulString(string(u), u) @@ -226,7 +239,7 @@ function append_unit_if_needed!(attr, key, label::Nothing, u) end function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString} isempty(label) && return attr[key] = UnitfulString(label, u) - if attr[:plot_object].backend == Plots._backend_instance(:pgfplotsx) + if attr[:plot_object].backend == PlotsBase._backend_instance(:pgfplotsx) attr[key] = UnitfulString( LaTeXString( format_unit_label( @@ -287,7 +300,7 @@ getaxisunit(a::Axis) = getaxisunit(a[:guide]) #============== Fix annotations ===============# -function Plots.locate_annotation( +function PlotsBase.locate_annotation( sp::Subplot, x::MissingOrQuantity, y::MissingOrQuantity, @@ -297,7 +310,7 @@ function Plots.locate_annotation( yunit = getaxisunit(sp.attr[:yaxis]) (_ustrip(xunit, x), _ustrip(yunit, y), label) end -function Plots.locate_annotation( +function PlotsBase.locate_annotation( sp::Subplot, x::MissingOrQuantity, y::MissingOrQuantity, @@ -309,23 +322,23 @@ function Plots.locate_annotation( zunit = getaxisunit(sp.attr[:zaxis]) (_ustrip(xunit, x), _ustrip(yunit, y), _ustrip(zunit, z), label) end -function Plots.locate_annotation( +function PlotsBase.locate_annotation( sp::Subplot, rel::NTuple{N,<:MissingOrQuantity}, label, ) where {N} units = getaxisunit(sp.attr[:xaxis], sp.attr[:yaxis], sp.attr[:zaxis]) - Plots.locate_annotation(sp, _ustrip.(zip(units, rel)), label) + PlotsBase.locate_annotation(sp, _ustrip.(zip(units, rel)), label) end #==================# # ticks and limits # #==================# -Plots._transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Quantity} = +PlotsBase._transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Quantity} = _ustrip.(getaxisunit(axis), ticks) -Plots.Axes.process_limits(lims::AbstractArray{T}, axis) where {T<:Quantity} = +PlotsBase.Axes.process_limits(lims::AbstractArray{T}, axis) where {T<:Quantity} = _ustrip.(getaxisunit(axis), lims) -Plots.Axes.process_limits(lims::Tuple{S,T}, axis) where {S<:Quantity,T<:Quantity} = +PlotsBase.Axes.process_limits(lims::Tuple{S,T}, axis) where {S<:Quantity,T<:Quantity} = _ustrip.(getaxisunit(axis), lims) function _ustrip(u, x) @@ -338,8 +351,8 @@ function _unit(x) unit(x) end -function Plots.pgfx_sanitize_string(s::UnitfulString) - UnitfulString(Plots.pgfx_sanitize_string(s.content), s.unit) +function PlotsBase.pgfx_sanitize_string(s::UnitfulString) + UnitfulString(PlotsBase.pgfx_sanitize_string(s.content), s.unit) end end # module diff --git a/src/Annotations.jl b/PlotsBase/src/Annotations.jl similarity index 96% rename from src/Annotations.jl rename to PlotsBase/src/Annotations.jl index fe1e838ed..2c8c8a1c7 100644 --- a/src/Annotations.jl +++ b/PlotsBase/src/Annotations.jl @@ -1,13 +1,13 @@ # internal module module Annotations -using ..Plots.Commons -using ..Plots.Dates -using ..Plots.Fonts: Font, PlotText, text, font -using ..Plots.Shapes: Shape, _shapes -using ..Plots: Series, Subplot, TimeType, Length -using ..Plots.PlotMeasures: pct -using ..Plots: is_2tuple, is3d, discrete_value! +using ..PlotsBase.Commons +using ..PlotsBase.Dates +using ..PlotsBase.Fonts: Font, PlotText, text, font +using ..PlotsBase.Shapes: Shape, _shapes +using ..PlotsBase.PlotMeasures: pct +using ..PlotsBase: Series, Subplot, TimeType, Length +using ..PlotsBase: is_2tuple, is3d, discrete_value! export EachAnn, series_annotations, series_annotations_shapes!, diff --git a/src/Arrows.jl b/PlotsBase/src/Arrows.jl similarity index 98% rename from src/Arrows.jl rename to PlotsBase/src/Arrows.jl index 13465aee2..8d058d3c2 100644 --- a/src/Arrows.jl +++ b/PlotsBase/src/Arrows.jl @@ -1,6 +1,6 @@ module Arrows -using ..Plots.Commons +using ..PlotsBase.Commons export Arrow, arrow, add_arrows # style is :open or :closed (for now) diff --git a/src/Axes.jl b/PlotsBase/src/Axes.jl similarity index 90% rename from src/Axes.jl rename to PlotsBase/src/Axes.jl index d239b52d4..4e604fcba 100644 --- a/src/Axes.jl +++ b/PlotsBase/src/Axes.jl @@ -1,14 +1,13 @@ - module Axes -export Axis, tickfont, guidefont, widen_factor, scale_inverse_scale_func -export sort_3d_axes, axes_letters, process_axis_arg! -import Plots: get_ticks -using Plots: Plots, RecipesPipeline, Subplot, DefaultsDict, TimeType -using Plots.Commons: _axis_defaults_byletter, _all_axis_attrs, dumpdict -using Plots.Commons -using Plots.Ticks -using Plots.Fonts +export Axis, Extrema, tickfont, guidefont, widen_factor, scale_inverse_scale_func +export sort_3d_axes, axes_letters, process_axis_arg!, has_ticks +import PlotsBase: get_ticks +using PlotsBase: PlotsBase, RecipesPipeline, Subplot, DefaultsDict, TimeType +using PlotsBase.Commons: _axis_defaults_byletter, _all_axis_attrs, dumpdict +using PlotsBase.Commons +using PlotsBase.Ticks +using PlotsBase.Fonts const default_widen_factor = Ref(1.06) const _widen_seriestypes = ( @@ -163,8 +162,8 @@ function Commons.axis_limits( !(aspect_ratio === :none || RecipesPipeline.is3d(:sp)) ) aspect_ratio = aspect_ratio isa Number ? aspect_ratio : 1 - area = Plots.plotarea(sp) - plot_ratio = Plots.height(area) / Plots.width(area) + area = PlotsBase.plotarea(sp) + plot_ratio = PlotsBase.height(area) / PlotsBase.width(area) dist = amax - amin factor = if letter === :x @@ -252,11 +251,11 @@ If `letter` is omitted, all axes are affected. """ function scale_lims!(sp::Subplot, letter, factor) axis = get_axis(sp, letter) - from, to = Plots.get_sp_lims(sp, letter) + from, to = PlotsBase.get_sp_lims(sp, letter) axis[:lims] = scale_lims(from, to, factor, axis[:scale]) end -scale_lims!(factor::Number) = scale_lims!(Plots.current(), factor) -scale_lims!(letter::Symbol, factor) = scale_lims!(Plots.current(), letter, factor) +scale_lims!(factor::Number) = scale_lims!(PlotsBase.current(), factor) +scale_lims!(letter::Symbol, factor) = scale_lims!(PlotsBase.current(), letter, factor) #---------------------------------------------------------------------- function process_axis_arg!(plotattributes::AKW, arg, letter = "") T = typeof(arg) @@ -304,7 +303,7 @@ function process_axis_arg!(plotattributes::AKW, arg, letter = "") end end -has_ticks(axis::Axis) = get(axis, :ticks, nothing) |> Plots.Ticks._has_ticks +has_ticks(axis::Axis) = get(axis, :ticks, nothing) |> PlotsBase.Ticks._has_ticks # update an Axis object with magic args and keywords function attr!(axis::Axis, args...; kw...) @@ -313,7 +312,7 @@ function attr!(axis::Axis, args...; kw...) foreach(arg -> process_axis_arg!(plotattributes, arg), args) # then preprocess keyword arguments - Plots.Commons.preprocess_attributes!(KW(kw)) + PlotsBase.Commons.preprocess_attributes!(KW(kw)) # then override for any keywords... only those keywords that already exists in plotattributes for (k, v) in kw @@ -370,14 +369,14 @@ function _update_axis( # first get the args without the letter: `tickfont = font(10)` # note: we don't pop because we want this to apply to all axes! (delete after all have finished) if haskey(plotattributes_in, k) - kw[k] = Plots.slice_arg(plotattributes_in[k], subplot_index) + kw[k] = PlotsBase.slice_arg(plotattributes_in[k], subplot_index) end # then get those args that were passed with a leading letter: `xlabel = "X"` lk = get_attr_symbol(letter, k) if haskey(plotattributes_in, lk) - kw[k] = Plots.slice_arg(plotattributes_in[lk], subplot_index) + kw[k] = PlotsBase.slice_arg(plotattributes_in[lk], subplot_index) end end @@ -431,19 +430,19 @@ function reset_extrema!(sp::Subplot) end end -function Plots.expand_extrema!(ex::Extrema, v::Number) +function PlotsBase.expand_extrema!(ex::Extrema, v::Number) ex.emin = isfinite(v) ? min(v, ex.emin) : ex.emin ex.emax = isfinite(v) ? max(v, ex.emax) : ex.emax ex end -Plots.expand_extrema!(axis::Axis, v::Number) = expand_extrema!(axis[:extrema], v) +PlotsBase.expand_extrema!(axis::Axis, v::Number) = expand_extrema!(axis[:extrema], v) # these shouldn't impact the extrema -Plots.expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] -Plots.expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] +PlotsBase.expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] +PlotsBase.expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] -function Plots.expand_extrema!( +function PlotsBase.expand_extrema!( axis::Axis, v::Tuple{MIN,MAX}, ) where {MIN<:Number,MAX<:Number} @@ -452,7 +451,7 @@ function Plots.expand_extrema!( ex.emax = isfinite(v[2]) ? max(v[2], ex.emax) : ex.emax ex end -function Plots.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} +function PlotsBase.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} ex = axis[:extrema]::Extrema foreach(vi -> expand_extrema!(ex, vi), v) ex diff --git a/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl similarity index 82% rename from src/BezierCurves.jl rename to PlotsBase/src/BezierCurves.jl index 115cc6a15..cf9eb5119 100644 --- a/src/BezierCurves.jl +++ b/PlotsBase/src/BezierCurves.jl @@ -1,6 +1,6 @@ module BezierCurves -import ..Plots +import ..PlotsBase "create a BezierCurve for plotting" mutable struct BezierCurve{T<:Tuple} @@ -16,7 +16,7 @@ function (bc::BezierCurve)(t::Real) p end -Plots.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = +PlotsBase.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = map(curve, Base.range(first(range), stop = last(range), length = n)) end diff --git a/src/Colorbars.jl b/PlotsBase/src/Colorbars.jl similarity index 94% rename from src/Colorbars.jl rename to PlotsBase/src/Colorbars.jl index 94bdb5e26..cfac48642 100644 --- a/src/Colorbars.jl +++ b/PlotsBase/src/Colorbars.jl @@ -2,13 +2,13 @@ module Colorbars export colorbar_style, get_clims, update_clims, hascolorbar, get_colorbar_ticks, _update_subplot_colorbars -using Plots.Commons: Commons, NaNMath, ignorenan_extrema -using Plots.PlotsSeries -using Plots.Subplots: Subplot, series_list -using Plots.Surfaces: AbstractSurface -using Plots.Ticks -using Plots.Ticks: _transform_ticks -import Plots.Commons.get_clims +using PlotsBase.Commons: Commons, NaNMath, ignorenan_extrema +using PlotsBase.PlotsSeries +using PlotsBase.Subplots: Subplot, series_list +using PlotsBase.Surfaces: AbstractSurface +using PlotsBase.Ticks +using PlotsBase.Ticks: _transform_ticks +import PlotsBase.Commons.get_clims # These functions return an operator for use in `get_clims(::Seres, op)` process_clims(lims::Tuple{<:Number,<:Number}) = @@ -68,7 +68,7 @@ function update_clims( end """ - update_clims(::Series, op=Plots.ignorenan_extrema) + update_clims(::Series, op=PlotsBase.ignorenan_extrema) Finds the limits for the colorbar by taking the "z-values" for the series and passing them into `op`, which must return the tuple `(zmin, zmax)`. The default op is the extrema of the finite values of the input. The value is stored as a series property, which is retrieved by `get_clims`. diff --git a/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl similarity index 95% rename from src/Commons/Commons.jl rename to PlotsBase/src/Commons/Commons.jl index f98929227..5d995153d 100644 --- a/src/Commons/Commons.jl +++ b/PlotsBase/src/Commons/Commons.jl @@ -2,7 +2,7 @@ module Commons export AVec, AMat, KW, AKW, TicksArgs -export Plots, PLOTS_SEED +export PlotsBase, PLOTS_SEED export _haligns, _valigns, _cbar_width # Functions export get_subplot, @@ -41,15 +41,15 @@ export anynan, #exports from args.jl export default, wraptuple, merge_with_base_supported -using Plots: Plots, Printf, NaNMath, cgrad -import Plots: RecipesPipeline -using Plots.Colors: Colorant, @colorant_str -using Plots.ColorTypes: alpha -using Plots.Measures: mm, BoundingBox -using Plots.PlotUtils: PlotUtils, ColorPalette, plot_color, isdark, ColorGradient -using Plots.RecipesBase -using Plots: DEFAULT_LINEWIDTH -using Plots: Statistics +using PlotsBase: PlotsBase, Printf, NaNMath, cgrad +import PlotsBase: RecipesPipeline +using PlotsBase.Colors: Colorant, @colorant_str +using PlotsBase.ColorTypes: alpha +using PlotsBase.Measures: mm, BoundingBox +using PlotsBase.PlotUtils: PlotUtils, ColorPalette, plot_color, isdark, ColorGradient +using PlotsBase.RecipesBase +using PlotsBase: DEFAULT_LINEWIDTH +using PlotsBase: Statistics const AVec = AbstractVector const AMat = AbstractMatrix @@ -111,7 +111,8 @@ all_styles(arg) = true_or_all_true(a -> get(Commons._styleAliases, a, a) in Commons._all_styles, arg) all_shapes(arg) = (true_or_all_true( a -> - get(Commons._marker_aliases, a, a) in Commons._all_markers || a isa Plots.Shape, + get(Commons._marker_aliases, a, a) in Commons._all_markers || + a isa PlotsBase.Shape, arg, )) all_alphas(arg) = true_or_all_true( @@ -140,7 +141,7 @@ function _override_seriestype_check(plotattributes::AKW, st::Symbol) end "These should only be needed in frontend modules" -Plots.@ScopeModule( +PlotsBase.@ScopeModule( Frontend, Commons, _subplot_defaults, diff --git a/src/Commons/aliases.jl b/PlotsBase/src/Commons/aliases.jl similarity index 100% rename from src/Commons/aliases.jl rename to PlotsBase/src/Commons/aliases.jl diff --git a/src/Commons/attrs.jl b/PlotsBase/src/Commons/attrs.jl similarity index 98% rename from src/Commons/attrs.jl rename to PlotsBase/src/Commons/attrs.jl index 551d9d047..5f453a477 100644 --- a/src/Commons/attrs.jl +++ b/PlotsBase/src/Commons/attrs.jl @@ -166,7 +166,7 @@ const _shape_keys = Symbol[ :x, ] -const _all_markers = vcat(:none, :auto, _shape_keys) #sort(collect(keys(_shapes)))) +const _all_markers = vcat(:none, :auto, _shape_keys) # sort(collect(keys(_shapes)))) const _marker_aliases = Dict{Symbol,Symbol}( :n => :none, :no => :none, @@ -662,7 +662,7 @@ is_series_attrs(k) = k in _all_series_attrs is_axis_attrs(k) = Symbol(chop(string(k); head = 1, tail = 0)) in _all_axis_attrs is_axis_attr_noletter(k) = k in _all_axis_attrs -RecipesBase.is_key_supported(k::Symbol) = Plots.is_attr_supported(k) +RecipesBase.is_key_supported(k::Symbol) = PlotsBase.is_attr_supported(k) # ----------------------------------------------------------------------------- include("aliases.jl") @@ -737,7 +737,7 @@ default(plotattributes::AKW, k::Symbol) = get(plotattributes, k, default(k)) function reset_defaults() foreach(merge!, _all_defaults, _initial_defaults) merge!(_axis_defaults, _initial_axis_defaults) - Plots.Fonts.resetfontsizes() + PlotsBase.Fonts.resetfontsizes() reset_axis_defaults_byletter!() end @@ -766,7 +766,7 @@ function process_line_attr(plotattributes::AKW, arg) elseif all_styles(arg) plotattributes[:linestyle] = arg - elseif typeof(arg) <: Plots.Stroke + elseif typeof(arg) <: PlotsBase.Stroke arg.width === nothing || (plotattributes[:linewidth] = arg.width) arg.color === nothing || ( plotattributes[:linecolor] = @@ -775,7 +775,7 @@ function process_line_attr(plotattributes::AKW, arg) arg.alpha === nothing || (plotattributes[:linealpha] = arg.alpha) arg.style === nothing || (plotattributes[:linestyle] = arg.style) - elseif typeof(arg) <: Plots.Brush + elseif typeof(arg) <: PlotsBase.Brush arg.size === nothing || (plotattributes[:fillrange] = arg.size) arg.color === nothing || ( plotattributes[:fillcolor] = @@ -784,7 +784,7 @@ function process_line_attr(plotattributes::AKW, arg) arg.alpha === nothing || (plotattributes[:fillalpha] = arg.alpha) arg.style === nothing || (plotattributes[:fillstyle] = arg.style) - elseif typeof(arg) <: Plots.Arrow || arg in (:arrow, :arrows) + elseif typeof(arg) <: PlotsBase.Arrow || arg in (:arrow, :arrows) plotattributes[:arrow] = arg # linealpha @@ -810,7 +810,7 @@ function process_marker_attr(plotattributes::AKW, arg) elseif all_styles(arg) plotattributes[:markerstrokestyle] = arg - elseif typeof(arg) <: Plots.Stroke + elseif typeof(arg) <: PlotsBase.Stroke arg.width === nothing || (plotattributes[:markerstrokewidth] = arg.width) arg.color === nothing || ( plotattributes[:markerstrokecolor] = @@ -819,7 +819,7 @@ function process_marker_attr(plotattributes::AKW, arg) arg.alpha === nothing || (plotattributes[:markerstrokealpha] = arg.alpha) arg.style === nothing || (plotattributes[:markerstrokestyle] = arg.style) - elseif typeof(arg) <: Plots.Brush + elseif typeof(arg) <: PlotsBase.Brush arg.size === nothing || (plotattributes[:markersize] = arg.size) arg.color === nothing || ( plotattributes[:markercolor] = @@ -847,7 +847,7 @@ end function process_fill_attr(plotattributes::AKW, arg) # fr = get(plotattributes, :fillrange, 0) - if typeof(arg) <: Plots.Brush + if typeof(arg) <: PlotsBase.Brush arg.size === nothing || (plotattributes[:fillrange] = arg.size) arg.color === nothing || ( plotattributes[:fillcolor] = @@ -885,7 +885,7 @@ function process_grid_attr!(plotattributes::AKW, arg, letter) elseif all_styles(arg) plotattributes[get_attr_symbol(letter, :gridstyle)] = arg - elseif typeof(arg) <: Plots.Stroke + elseif typeof(arg) <: PlotsBase.Stroke arg.width === nothing || (plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg.width) arg.color === nothing || ( @@ -923,7 +923,7 @@ function process_minor_grid_attr!(plotattributes::AKW, arg, letter) plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg plotattributes[get_attr_symbol(letter, :minorgrid)] = true - elseif typeof(arg) <: Plots.Stroke + elseif typeof(arg) <: PlotsBase.Stroke arg.width === nothing || (plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg.width) arg.color === nothing || ( @@ -964,7 +964,7 @@ end # TODO: this is neccessary while old and new font names coexist and should be standard after the transition fontname = Symbol(fontname, :_) end - if T <: Plots.Font + if T <: PlotsBase.Font Symbol(fontname, :family) --> arg.family # TODO: this is neccessary in the transition from old fontsize to new font_pointsize and should be removed when it is completed @@ -1213,7 +1213,7 @@ macro add_attributes(level, expr, match_table) insert_block.args, Expr( :(=), - Expr(:ref, Expr(:call, getfield, Plots, field), QuoteNode(exp_key)), + Expr(:ref, Expr(:call, getfield, PlotsBase, field), QuoteNode(exp_key)), value, ), :($add_aliases($(QuoteNode(exp_key)), $(QuoteNode(pl_key)))), @@ -1249,7 +1249,7 @@ function _splitdef!(blk, key_dict) var = lhs.args[1] type = lhs.args[2] if @isdefined type - for field in fieldnames(getproperty(Plots, type)) + for field in fieldnames(getproperty(PlotsBase, type)) key_dict[Symbol(var, "_", field)] = :(getfield($(ei.args[2]), $(QuoteNode(field)))) end diff --git a/src/Commons/postprocess_attrs.jl b/PlotsBase/src/Commons/postprocess_attrs.jl similarity index 100% rename from src/Commons/postprocess_attrs.jl rename to PlotsBase/src/Commons/postprocess_attrs.jl diff --git a/src/Fonts.jl b/PlotsBase/src/Fonts.jl similarity index 98% rename from src/Fonts.jl rename to PlotsBase/src/Fonts.jl index c2b6edb05..965c15611 100644 --- a/src/Fonts.jl +++ b/PlotsBase/src/Fonts.jl @@ -1,11 +1,11 @@ module Fonts -using Plots.Colors -using Plots.Commons -using Plots.Commons: +using PlotsBase.Colors +using PlotsBase.Commons +using PlotsBase.Commons: _initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes, _initial_fontsizes # keep in mind: these will be reexported and are public API -export font, scalefontsizes, resetfontsizes, text, is_horizontal, Font +export font, scalefontsizes, resetfontsizes, text, is_horizontal, Font, PlotText mutable struct Font family::AbstractString diff --git a/src/PlotMeasures.jl b/PlotsBase/src/PlotMeasures.jl similarity index 100% rename from src/PlotMeasures.jl rename to PlotsBase/src/PlotMeasures.jl diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl new file mode 100644 index 000000000..c78fef6f6 --- /dev/null +++ b/PlotsBase/src/PlotsBase.jl @@ -0,0 +1,204 @@ +module PlotsBase + +if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel")) + @eval Base.Experimental.@optlevel 1 +end +if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_methods")) + @eval Base.Experimental.@max_methods 1 +end + +using Pkg, Dates, Printf, Statistics, Base64, LinearAlgebra, SparseArrays, Random +using Reexport, RelocatableFolders +using Base.Meta +@reexport using RecipesBase +@reexport using PlotThemes +@reexport using PlotUtils + +import RecipesBase: plot, plot!, animate, is_explicit, grid +import RecipesPipeline: + RecipesPipeline, + inverse_scale_func, + datetimeformatter, + AbstractSurface, + group_as_matrix, # for StatsPlots + dateformatter, + timeformatter, + needs_3d_axes, + DefaultsDict, + explicitkeys, + scale_func, + is_surface, + Formatted, + reset_kw!, + SliceIt, + pop_kw!, + Volume, + is3d +import UnicodeFun +import StatsBase +import Downloads +import Showoff +import Unzip +import JLFzf +import JSON + +#! format: off +export + grid, + bbox, + plotarea, + KW, + + theme, + protect, + plot, + plot!, + attr!, + + current, + default, + with, + twinx, + twiny, + + pie, + pie!, + plot3d, + plot3d!, + + title!, + annotate!, + + xlims, + ylims, + zlims, + + savefig, + png, + gui, + inline, + closeall, + + backend, + backends, + backend_name, + backend_object, + + text, + font, + stroke, + brush, + OHLC, + arrow, + Shape, + cgrad, + + frame, + gif, + mov, + mp4, + webm, + animate, + @animate, + @gif, + @P_str, + Animation, + + test_examples, + coords, + + translate, + translate!, + rotate, + rotate!, + center, + plotattr, + scalefontsizes, + resetfontsizes +#! format: on +import Measures +include("PlotMeasures.jl") +using .PlotMeasures +import .PlotMeasures: Length, AbsoluteLength, Measure, width, height +# --------------------------------------------------------- +macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) + Expr( + :module, + true, + mod, + Expr( + :block, + Expr( + :import, + Expr( + :(:), + Expr(:., :., :., parent), + (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., + ), + ), + Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...), + ), + ) |> esc +end +import NaNMath +include("Commons/Commons.jl") +using .Commons +using .Commons.Frontend +# --------------------------------------------------------- +include("Fonts.jl") +@reexport using .Fonts +using .Fonts: Font, PlotText +include("Ticks.jl") +using .Ticks +include("Series.jl") +using .PlotsSeries +include("Subplots.jl") +using .Subplots +import .Subplots: plotarea, plotarea!, leftpad, toppad, bottompad, rightpad +include("Axes.jl") +using .Axes +include("Surfaces.jl") +include("Colorbars.jl") +using .Colorbars +include("PlotsPlots.jl") +using .PlotsPlots +include("layouts.jl") +# --------------------------------------------------------- +include("utils.jl") +using .Surfaces +include("axes_utils.jl") +include("legend.jl") +include("Shapes.jl") +using .Shapes +using .Shapes: Shape, _shapes, rotate! +include("Annotations.jl") +using .Annotations +using .Annotations: SeriesAnnotations, process_annotation +include("Arrows.jl") +using .Arrows +include("Strokes.jl") +using .Strokes +using .Strokes: Stroke, Brush +include("BezierCurves.jl") +using .BezierCurves +include("themes.jl") +include("plot.jl") +include("pipeline.jl") +include("arg_desc.jl") +include("recipes.jl") +include("animation.jl") +include("examples.jl") +include("plotattr.jl") +include("backends/nobackend.jl") +include("abstract_backend.jl") +include("alignment.jl") +const CURRENT_BACKEND = CurrentBackend(:none) +include("output.jl") +include("shorthands.jl") +include("backends/web.jl") +include("backends/plotly.jl") +using .Plotly +include("init.jl") +include("users.jl") + +end diff --git a/src/PlotsPlots.jl b/PlotsBase/src/PlotsPlots.jl similarity index 86% rename from src/PlotsPlots.jl rename to PlotsBase/src/PlotsPlots.jl index 48f9d983b..25a561201 100644 --- a/src/PlotsPlots.jl +++ b/PlotsBase/src/PlotsPlots.jl @@ -7,25 +7,25 @@ export Plot, ignorenan_extrema, protect, InputWrapper -import Plots.Axes: _update_axis, scale_lims! -import Plots.Commons: ignorenan_extrema, _cycle -import Plots.Ticks: get_ticks -using Plots: - Plots, +import PlotsBase.Axes: _update_axis, scale_lims! +import PlotsBase.Commons: ignorenan_extrema, _cycle +import PlotsBase.Ticks: get_ticks +using PlotsBase: + PlotsBase, AbstractPlot, AbstractBackend, DefaultsDict, Series, AbstractLayout, RecipesPipeline -using Plots.PlotMeasures -using Plots.Colorbars: _update_subplot_colorbars -using Plots.Subplots: Subplot, _update_subplot_colors, _update_margins -using Plots.Axes: Axis, get_axis -using Plots.PlotUtils: get_color_palette -using Plots.Commons -using Plots.Commons.Frontend -using Plots.Fonts: font +using PlotsBase.PlotMeasures +using PlotsBase.Colorbars: _update_subplot_colorbars +using PlotsBase.Subplots: Subplot, _update_subplot_colors, _update_margins +using PlotsBase.Axes: Axis, get_axis +using PlotsBase.PlotUtils: get_color_palette +using PlotsBase.Commons +using PlotsBase.Commons.Frontend +using PlotsBase.Fonts: font const SubplotMap = Dict{Any,Subplot} mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} @@ -41,16 +41,16 @@ mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} init::Bool function Plot() - be = Plots.backend() + be = PlotsBase.backend() new{typeof(be)}( be, 0, - DefaultsDict(KW(), Plots._plot_defaults), + DefaultsDict(KW(), PlotsBase._plot_defaults), Series[], nothing, Subplot[], SubplotMap(), - Plots.EmptyLayout(), + PlotsBase.EmptyLayout(), Subplot[], false, ) @@ -58,7 +58,7 @@ mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} function Plot(osp::Subplot) plt = Plot() - plt.layout = Plots.GridLayout(1, 1) + plt.layout = PlotsBase.GridLayout(1, 1) sp = deepcopy(osp) # FIXME: fails `PlotlyJS` ? plt.layout.grid[1, 1] = sp # reset some attributes @@ -175,15 +175,15 @@ Base.ndims(plt::Plot) = 2 # clear out series list, but retain subplots Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) -Plots.get_subplot(plt::Plot, sp::Subplot) = sp -Plots.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] -Plots.get_subplot(plt::Plot, k) = plt.spmap[k] -Plots.series_list(plt::Plot) = plt.series_list +PlotsBase.get_subplot(plt::Plot, sp::Subplot) = sp +PlotsBase.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] +PlotsBase.get_subplot(plt::Plot, k) = plt.spmap[k] +PlotsBase.series_list(plt::Plot) = plt.series_list get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x === sp, plt.subplots) -Plots.RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = +PlotsBase.RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = Commons.preprocess_attributes!(plotattributes) plottitlefont(p::Plot) = font(; @@ -197,8 +197,8 @@ plottitlefont(p::Plot) = font(; # update attr from an input dictionary function _update_plot_attrs(plt::Plot, plotattributes_in::AKW) - for (k, v) in Plots._plot_defaults - Plots.slice_arg!(plotattributes_in, plt.attr, k, 1, true) + for (k, v) in PlotsBase._plot_defaults + PlotsBase.slice_arg!(plotattributes_in, plt.attr, k, 1, true) end # handle colors @@ -218,7 +218,7 @@ function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol) nothing end -function Plots.Axes._update_axis( +function PlotsBase.Axes._update_axis( plt::Plot, sp::Subplot, plotattributes_in::AKW, @@ -235,7 +235,7 @@ function Plots.Axes._update_axis( axis[:ticks] = axis[:ticks] ? :auto : nothing end - Plots.Axes._update_axis_colors(axis) + PlotsBase.Axes._update_axis_colors(axis) _update_axis_links(plt, axis, letter) nothing end @@ -252,7 +252,7 @@ function _update_subplot_attrs( # grab those args which apply to this subplot for k in keys(_subplot_defaults) - Plots.slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair) + PlotsBase.slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair) end _update_subplot_colors(sp) @@ -277,7 +277,7 @@ function _update_subplot_attrs( end end - Plots.Subplots._update_subplot_periphery(sp, anns) + PlotsBase.Subplots._update_subplot_periphery(sp, anns) end function scale_lims!(plt::Plot, letter, factor) diff --git a/src/Series.jl b/PlotsBase/src/Series.jl similarity index 89% rename from src/Series.jl rename to PlotsBase/src/Series.jl index cd66546f1..411e82549 100644 --- a/src/Series.jl +++ b/PlotsBase/src/Series.jl @@ -20,11 +20,11 @@ export get_linestyle, get_fillalpha, get_markercolor, get_markeralpha -import Plots.Commons: get_subplot, _series_defaults -using Plots.Commons -using Plots.Commons: get_gradient -using Plots.PlotUtils: ColorGradient, plot_color -using Plots: Plots, DefaultsDict, RecipesPipeline, get_attr_symbol, KW +import PlotsBase.Commons: get_subplot, _series_defaults +using PlotsBase.Commons +using PlotsBase.Commons: get_gradient +using PlotsBase.PlotUtils: ColorGradient, plot_color +using PlotsBase: PlotsBase, DefaultsDict, RecipesPipeline, get_attr_symbol, KW mutable struct Series plotattributes::DefaultsDict @@ -41,7 +41,7 @@ attr(series::Series, k::Symbol) = series.plotattributes[k] attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) function attr!(series::Series; kw...) plotattributes = KW(kw) - Plots.Commons.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) for (k, v) in plotattributes if haskey(_series_defaults, k) series[k] = v @@ -49,7 +49,7 @@ function attr!(series::Series; kw...) @warn "unused key $k in series attr" end end - Plots._series_updated(series[:subplot].plt, series) + PlotsBase._series_updated(series[:subplot].plt, series) series end @@ -71,9 +71,9 @@ should_add_to_legend(series::Series) = :image, ) -Plots.get_subplot(series::Series) = series.plotattributes[:subplot] -Plots.RecipesPipeline.is3d(series::Series) = RecipesPipeline.is3d(series.plotattributes) -Plots.ispolar(series::Series) = Plots.ispolar(series.plotattributes[:subplot]) +PlotsBase.get_subplot(series::Series) = series.plotattributes[:subplot] +PlotsBase.RecipesPipeline.is3d(series::Series) = RecipesPipeline.is3d(series.plotattributes) +PlotsBase.ispolar(series::Series) = PlotsBase.ispolar(series.plotattributes[:subplot]) # ------------------------------------------------------- # operate on individual series @@ -225,10 +225,11 @@ struct NaNSegmentsIterator end function Base.iterate(itr::NaNSegmentsIterator, nextidx::Int = itr.n1) - (i = findfirst(!Plots.Commons.anynan(itr.args), nextidx:(itr.n2))) === nothing && return + (i = findfirst(!PlotsBase.Commons.anynan(itr.args), nextidx:(itr.n2))) === nothing && + return nextval = nextidx + i - 1 - j = findfirst(Plots.Commons.anynan(itr.args), nextval:(itr.n2)) + j = findfirst(PlotsBase.Commons.anynan(itr.args), nextval:(itr.n2)) nextnan = j === nothing ? itr.n2 + 1 : nextval + j - 1 nextval:(nextnan - 1), nextnan @@ -237,7 +238,7 @@ end Base.IteratorSize(::NaNSegmentsIterator) = Base.SizeUnknown() # COV_EXCL_LINE function iter_segments(args...) - tup = Plots.wraptuple(args) + tup = PlotsBase.wraptuple(args) n1 = minimum(map(firstindex, tup)) n2 = maximum(map(lastindex, tup)) NaNSegmentsIterator(tup, n1, n2) @@ -249,10 +250,10 @@ end has_attribute_segments(series::Series) = any( series[attr] isa AbstractVector && length(series[attr]) > 1 for - attr in Plots.Commons._segmenting_vector_attributes + attr in PlotsBase.Commons._segmenting_vector_attributes ) || any( series[attr] isa AbstractArray for - attr in Plots.Commons._segmenting_array_attributes + attr in PlotsBase.Commons._segmenting_array_attributes ) function series_segments(series::Series, seriestype::Symbol = :path; check = false) @@ -265,7 +266,7 @@ function series_segments(series::Series, seriestype::Symbol = :path; check = fal if check scales = :xscale, :yscale, :zscale for (n, s) in enumerate(args) - (scale = get(series, scales[n], :identity)) ∈ Plots.Commons._log_scales || + (scale = get(series, scales[n], :identity)) ∈ PlotsBase.Commons._log_scales || continue for (i, v) in enumerate(s) if v <= 0 @@ -302,8 +303,8 @@ function warn_on_attr_dim_mismatch(series, x, y, z, segments) minimum(map(seg -> first(seg.range), segments)), maximum(map(seg -> last(seg.range), segments)), ) - for attr in Plots.Commons._segmenting_vector_attributes - if (v = get(series, attr, nothing)) isa Plots.Commons.AVec && + for attr in PlotsBase.Commons._segmenting_vector_attributes + if (v = get(series, attr, nothing)) isa PlotsBase.Commons.AVec && eachindex(v) != seg_range @warn "Indices $(eachindex(v)) of attribute `$attr` does not match data indices $seg_range." if any(v -> !isnothing(v) && any(isnan, v), (x, y, z)) @@ -320,9 +321,9 @@ function warn_on_attr_dim_mismatch(series, x, y, z, segments) end function warn_on_inconsistent_shape_attrs(series, x, y, z, r) - for attr in Plots.Commons._segmenting_vector_attributes + for attr in PlotsBase.Commons._segmenting_vector_attributes v = get(series, attr, nothing) - if v isa Plots.Commons.AVec && length(unique(v[r])) > 1 + if v isa PlotsBase.Commons.AVec && length(unique(v[r])) > 1 @warn "Different values of `$attr` specified for different shape vertices. Only first one will be used." break end diff --git a/src/Shapes.jl b/PlotsBase/src/Shapes.jl similarity index 96% rename from src/Shapes.jl rename to PlotsBase/src/Shapes.jl index bd0544526..81b412ec0 100644 --- a/src/Shapes.jl +++ b/PlotsBase/src/Shapes.jl @@ -1,7 +1,7 @@ module Shapes -using Plots: Plots, RecipesPipeline -using Plots.Commons +using PlotsBase: PlotsBase, RecipesPipeline +using PlotsBase.Commons # keep in mind: these will be reexported and are public API export Shape, @@ -54,9 +54,9 @@ get_ys(shape::Shape) = shape.y vertices(shape::Shape) = collect(zip(shape.x, shape.y)) "return the vertex points from a Shape or Segments object" -Plots.coords(shape::Shape) = shape.x, shape.y +PlotsBase.coords(shape::Shape) = shape.x, shape.y -Plots.coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shapes)) +PlotsBase.coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shapes)) "get an array of tuples of points on a circle with radius `r`" partialcircle(start_θ, end_θ, n = 20, r = 1) = diff --git a/src/Strokes.jl b/PlotsBase/src/Strokes.jl similarity index 94% rename from src/Strokes.jl rename to PlotsBase/src/Strokes.jl index 8398a7896..5fcb8a5b3 100644 --- a/src/Strokes.jl +++ b/PlotsBase/src/Strokes.jl @@ -1,8 +1,8 @@ module Strokes export stroke, brush, Stroke, Brush -using Plots.Colors: Colorant -using Plots.Commons: all_alphas, all_reals, all_styles +using PlotsBase.Colors: Colorant +using PlotsBase.Commons: all_alphas, all_reals, all_styles struct Stroke width color diff --git a/src/Subplots.jl b/PlotsBase/src/Subplots.jl similarity index 85% rename from src/Subplots.jl rename to PlotsBase/src/Subplots.jl index d87f695d8..3abbf6fc0 100644 --- a/src/Subplots.jl +++ b/PlotsBase/src/Subplots.jl @@ -13,22 +13,22 @@ export Subplot, leftpad, bottompad, rightpad -import Plots.Ticks: get_ticks -using Plots: - Plots, +import PlotsBase.Ticks: get_ticks +using PlotsBase: + PlotsBase, RecipesPipeline, Series, AbstractBackend, AbstractLayout, BoundingBox, DefaultsDict -using Plots.RecipesPipeline: RecipesPipeline, Surface, Volume -using Plots.PlotUtils: get_color_palette -using Plots.Commons -using Plots.Commons.Frontend -using Plots.Commons: convert_legend_value, like_surface -using Plots.Fonts -using Plots.PlotMeasures +using PlotsBase.RecipesPipeline: RecipesPipeline, Surface, Volume +using PlotsBase.PlotUtils: get_color_palette +using PlotsBase.Commons +using PlotsBase.Commons.Frontend +using PlotsBase.Commons: convert_legend_value, like_surface +using PlotsBase.Fonts +using PlotsBase.PlotMeasures # a single subplot mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout @@ -42,7 +42,7 @@ mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout o # can store backend-specific data... like a pyplot ax plt # the enclosing Plot object (can't give it a type because of no forward declarations) - Subplot(::T; parent = Plots.RootLayout()) where {T<:AbstractBackend} = new{T}( + Subplot(::T; parent = PlotsBase.RootLayout()) where {T<:AbstractBackend} = new{T}( parent, Series[], 0, @@ -96,7 +96,7 @@ bottompad(sp::Subplot) = sp.minpad[4] function attr!(sp::Subplot; kw...) plotattributes = KW(kw) - Plots.Commons.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) for (k, v) in plotattributes if haskey(_subplot_defaults, k) sp[k] = v @@ -107,9 +107,9 @@ function attr!(sp::Subplot; kw...) sp end -Plots.series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] === sp, sp.plt.series_list) -Plots.RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" -Plots.ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar" +PlotsBase.series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] === sp, sp.plt.series_list) +PlotsBase.RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" +PlotsBase.ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar" get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) @@ -117,12 +117,12 @@ get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) # and assigns a color automatically get_series_color(c, sp::Subplot, n::Int, seriestype) = if c === :auto - like_surface(seriestype) ? Plots.cgrad() : _cycle(sp[:color_palette], n) + like_surface(seriestype) ? PlotsBase.cgrad() : _cycle(sp[:color_palette], n) elseif isa(c, Int) _cycle(sp[:color_palette], c) else c - end |> Plots.plot_color + end |> PlotsBase.plot_color get_series_color(c::AbstractArray, sp::Subplot, n::Int, seriestype) = map(x -> get_series_color(x, sp, n, seriestype), c) @@ -167,7 +167,7 @@ function _update_subplot_periphery(sp::Subplot, anns::AVec) # extend annotations, and ensure we always have a (x,y,PlotText) tuple newanns = [] for ann in vcat(anns, sp[:annotations]) - append!(newanns, Plots.process_annotation(sp, ann)) + append!(newanns, PlotsBase.process_annotation(sp, ann)) end sp.attr[:annotations] = newanns @@ -197,7 +197,7 @@ end _update_margins(sp::Subplot) = for sym in (:margin, :left_margin, :top_margin, :right_margin, :bottom_margin) if (margin = get(sp.attr, sym, nothing)) isa Tuple - # transform e.g. (1, :mm) => 1 * Plots.mm + # transform e.g. (1, :mm) => 1 * PlotsBase.mm sp.attr[sym] = margin[1] * getfield(@__MODULE__, margin[2]) end end @@ -208,7 +208,7 @@ needs_any_3d_axes(sp::Subplot) = any( ) for s in series_list(sp) ) -function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) +function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) # first expand for the data for letter in (:x, :y, :z) @@ -224,7 +224,7 @@ function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) end axis = sp[get_attr_symbol(letter, :axis)] - if isa(data, Plots.Volume) + if isa(data, PlotsBase.Volume) expand_extrema!(sp[:xaxis], data.x_extents) expand_extrema!(sp[:yaxis], data.y_extents) expand_extrema!(sp[:zaxis], data.z_extents) @@ -241,7 +241,7 @@ function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) # correspond to the same x-value) plotattributes[letter], plotattributes[get_attr_symbol(letter, :(_discrete_indices))] = - Plots.discrete_value!(axis, data) + PlotsBase.discrete_value!(axis, data) expand_extrema!(axis, plotattributes[letter]) end end @@ -267,7 +267,7 @@ function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) if (bw = plotattributes[:bar_width]) === nothing pos = filter(>(0), diff(sort(data))) - plotattributes[:bar_width] = bw = _bar_width * ignorenan_minimum(pos) + plotattributes[:bar_width] = bw = Commons._bar_width * ignorenan_minimum(pos) end axis = sp.attr[get_attr_symbol(dsym, :axis)] expand_extrema!(axis, ignorenan_maximum(data) + 0.5maximum(bw)) @@ -280,12 +280,12 @@ function Plots.expand_extrema!(sp::Subplot, plotattributes::AKW) data = plotattributes[letter] axis = sp[get_attr_symbol(letter, :axis)] scale = get(plotattributes, get_attr_symbol(letter, :scale), :identity) - expand_extrema!(axis, Plots.heatmap_edges(data, scale)) + expand_extrema!(axis, PlotsBase.heatmap_edges(data, scale)) end end end -function Plots.expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) +function PlotsBase.expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) expand_extrema!(sp[:xaxis], (xmin, xmax)) expand_extrema!(sp[:yaxis], (ymin, ymax)) end diff --git a/src/Surfaces.jl b/PlotsBase/src/Surfaces.jl similarity index 71% rename from src/Surfaces.jl rename to PlotsBase/src/Surfaces.jl index 87c915d47..90c2c7794 100644 --- a/src/Surfaces.jl +++ b/PlotsBase/src/Surfaces.jl @@ -2,12 +2,12 @@ module Surfaces export SurfaceFunction, Surface -import Plots: Plots, expand_extrema!, Commons -using Plots.Axes: Axis +import PlotsBase: PlotsBase, expand_extrema!, Commons +using PlotsBase.Axes: Axis using RecipesPipeline: AbstractSurface, Surface -using Plots.Commons +using PlotsBase.Commons -function Plots.expand_extrema!(a::Axis, surf::Surface) +function PlotsBase.expand_extrema!(a::Axis, surf::Surface) ex = a[:extrema] foreach(x -> expand_extrema!(ex, x), surf.surf) ex diff --git a/src/Ticks.jl b/PlotsBase/src/Ticks.jl similarity index 96% rename from src/Ticks.jl rename to PlotsBase/src/Ticks.jl index 01999f041..af37828f5 100644 --- a/src/Ticks.jl +++ b/PlotsBase/src/Ticks.jl @@ -1,8 +1,8 @@ module Ticks -export get_ticks, _has_ticks, _transform_ticks, get_minor_ticks -using Plots.Commons -using Plots.Dates +export get_ticks, _has_ticks, _transform_ticks, get_minor_ticks, no_minor_intervals +using PlotsBase.Commons +using PlotsBase.Dates const DEFAULT_MINOR_INTERVALS = Ref(5) # 5 intervals -> 4 ticks diff --git a/src/abstract_backend.jl b/PlotsBase/src/abstract_backend.jl similarity index 80% rename from src/abstract_backend.jl rename to PlotsBase/src/abstract_backend.jl index 10cfafdbf..ba6530f33 100644 --- a/src/abstract_backend.jl +++ b/PlotsBase/src/abstract_backend.jl @@ -4,13 +4,39 @@ const _plots_compats = _plots_project.compat const _backendSymbol = Dict{DataType,Symbol}(NoBackend => :none) const _backendType = Dict{Symbol,DataType}(:none => NoBackend) -const _backend_packages = (gr = :GR, unicodeplots = :UnicodePlots, pgfplotsx = :PGFPlotsX, pythonplot = :PythonPlot, plotly = nothing, plotlyjs = :PlotlyJS, inspectdr = :InspectDR, gaston = :Gaston, hdf5 = :HDF5) +const _backend_packages = (gaston = :Gaston, gr = :GR, unicodeplots = :UnicodePlots, pgfplotsx = :PGFPlotsX, pythonplot = :PythonPlot, plotly = :Plotly, plotlyjs = :PlotlyJS, hdf5 = :HDF5) const _initialized_backends = Set{Symbol}() const _backends = keys(_backend_packages) const _plots_deps = let toml = Pkg.TOML.parsefile(normpath(@__DIR__, "..", "Project.toml")) merge(toml["deps"], toml["extras"]) end + +function _check_installed(backend::Union{Module,AbstractString,Symbol}; warn = true) + sym = Symbol(lowercase(string(backend))) + if warn && !haskey(_backend_packages, sym) + @warn "backend `$sym` is not compatible with `Plots`." + return + end + # lowercase -> CamelCase, falling back to the given input for `PlotlyBase` ... + str = string(get(_backend_packages, sym, backend)) + str == "Plotly" && (str *= "Base") # FIXME: `Plots` inconsistency, `plotly` should be named `plotlybase` + # check supported + if warn && !haskey(_plots_compats, str) + @warn "backend `$str` is not compatible with `Plots`." + return + end + # check installed + pkg_id = Base.identify_package(str) + version = if pkg_id === nothing + nothing + else + get(Pkg.dependencies(), pkg_id.uuid, (; version = nothing)).version + end + version === nothing && @warn "backend `$str` is not installed." + version +end + _create_backend_figure(plt::Plot) = nothing _initialize_subplot(plt::Plot, sp::Subplot) = nothing @@ -29,6 +55,8 @@ mutable struct CurrentBackend pkg::AbstractBackend end +CurrentBackend(sym::Symbol) = CurrentBackend(sym, _backend_instance(sym)) + """ Returns the current plotting package name. Initializes package on first call. """ @@ -40,7 +68,7 @@ backends() = _backends backend_name() = CURRENT_BACKEND.sym _backend_instance(sym::Symbol)::AbstractBackend = _backendType[sym]() -backend_package_name(sym::Symbol = backend_name()) = _backend_packages[sym] +backend_package_name(sym::Symbol = backend_name()) = get(_backend_packages, sym, :None) # Traits to be implemented by the extensions backend_name(::AbstractBackend) = @info "`backend_name(::Backend) not implemented." @@ -73,17 +101,13 @@ backend(sym::Symbol) = backend() end else - error("Unsupported backend $sym") + @error "Unsupported backend $sym" end function get_backend_module(name::Symbol) - ext_name = Symbol("Plots", name, "Ext") - ext = Base.get_extension(@__MODULE__, ext_name) + ext = Base.get_extension(@__MODULE__, Symbol(name, "Ext")) if !isnothing(ext) - module_name = ext - # Concrete as opposed to abstract - ConcreteBackend = ext.get_concrete_backend() - return (module_name, ConcreteBackend) + return ext, ext.get_concrete_backend() else @error "Extension $name is not loaded yet, run `import $name` to load it" return nothing @@ -128,7 +152,7 @@ function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) Set{Symbol}() end extra_kwargs = Dict{Symbol,Any}() - for k in Plots.explicitkeys(plotattributes) + for k in PlotsBase.explicitkeys(plotattributes) (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue k in Commons._suppress_warnings && continue if ismissing(default(k)) diff --git a/src/alignment.jl b/PlotsBase/src/alignment.jl similarity index 100% rename from src/alignment.jl rename to PlotsBase/src/alignment.jl diff --git a/src/animation.jl b/PlotsBase/src/animation.jl similarity index 97% rename from src/animation.jl rename to PlotsBase/src/animation.jl index 2b0847963..434c747b1 100644 --- a/src/animation.jl +++ b/PlotsBase/src/animation.jl @@ -229,16 +229,16 @@ function _animate(forloop::Expr, args...; type::Symbol = :none) push!(block.args, :( if $filterexpr - Plots.frame($animsym) + PlotsBase.frame($animsym) end )) push!(block.args, :($countersym += 1)) # add a final call to `gif(anim)`? retval = if type === :gif - :(Plots.gif($animsym; $(animationsKwargs...))) + :(PlotsBase.gif($animsym; $(animationsKwargs...))) elseif type === :apng - :(Plots.apng($animsym; $(animationsKwargs...))) + :(PlotsBase.apng($animsym; $(animationsKwargs...))) else animsym end @@ -246,7 +246,7 @@ function _animate(forloop::Expr, args...; type::Symbol = :none) # full expression: quote $freqassert # if filtering, check frequency is an Integer > 0 - $animsym = Plots.Animation() # init animation object + $animsym = PlotsBase.Animation() # init animation object let $countersym = 1 # init iteration counter $forloop # for loop, saving a frame after each iteration end diff --git a/src/arg_desc.jl b/PlotsBase/src/arg_desc.jl similarity index 100% rename from src/arg_desc.jl rename to PlotsBase/src/arg_desc.jl diff --git a/src/axes_utils.jl b/PlotsBase/src/axes_utils.jl similarity index 100% rename from src/axes_utils.jl rename to PlotsBase/src/axes_utils.jl diff --git a/src/backends/nobackend.jl b/PlotsBase/src/backends/nobackend.jl similarity index 100% rename from src/backends/nobackend.jl rename to PlotsBase/src/backends/nobackend.jl diff --git a/src/backends/plotly.jl b/PlotsBase/src/backends/plotly.jl similarity index 95% rename from src/backends/plotly.jl rename to PlotsBase/src/backends/plotly.jl index d4b7a4b7d..9a97182cf 100644 --- a/src/backends/plotly.jl +++ b/PlotsBase/src/backends/plotly.jl @@ -1,38 +1,37 @@ # https://plot.ly/javascript/getting-started module Plotly -export PlotlyBackend, plotly_show_js, plotly_series, plotly_layout, embeddable_html - -using UUIDs -using Statistics: mean -using Plots: bbox_to_pcts, labelfunc_tex, is_2tuple, ticks_type, recursive_merge -using Plots.Annotations -using Plots.Axes -using Plots.Colorbars -using Plots.Colors: Colorant -using Plots.Commons -using Plots.Fonts -using Plots.Fonts: PlotText -using Plots.PlotMeasures -using Plots.PlotsPlots -using Plots.PlotsSeries -using Plots.PlotUtils: PlotUtils, ColorGradient, rgba_string, rgb_string -using Plots.RecipesPipeline: RecipesPipeline -using Plots.Subplots -using Plots.Surfaces -using Plots.Ticks -import Plots: labelfunc, _show, _display, default_output_format -import Plots: backend_name, backend_package_name - -struct PlotlyBackend <: Plots.AbstractBackend end -Plots._backendType[:plotly] = PlotlyBackend -Plots._backendSymbol[PlotlyBackend] = :plotly - -push!(Plots._initialized_backends, :plotly) -backend_name(::PlotlyBackend) = :plotly -backend_package_name(::PlotlyBackend) = backend_package_name(:plotly) - -const _plotly_attrs = merge_with_base_supported([ +export PlotlyBackend, plotly_show_js, plotly_series, plotly_layout, html_head, html_body + +import RecipesPipeline +import Statistics +import UUIDs +import JSON + +using PlotsBase.Annotations +using PlotsBase.Axes +using PlotsBase.Colorbars +using PlotsBase.Colors: Colorant +using PlotsBase.Commons +using PlotsBase.Fonts +using PlotsBase.Fonts: PlotText +using PlotsBase.PlotMeasures +using PlotsBase.PlotsPlots +using PlotsBase.PlotsSeries +using PlotsBase.PlotUtils: PlotUtils, ColorGradient, rgba_string, rgb_string +using PlotsBase.Subplots +using PlotsBase.Surfaces +using PlotsBase.Ticks + +struct PlotlyBackend <: PlotsBase.AbstractBackend end +PlotsBase._backendType[:plotly] = PlotlyBackend +PlotsBase._backendSymbol[PlotlyBackend] = :plotly + +push!(PlotsBase._initialized_backends, :plotly) +PlotsBase.backend_name(::PlotlyBackend) = :plotly +PlotsBase.backend_package_name(::PlotlyBackend) = PlotsBase.backend_package_name(:plotly) + +const _plotly_attrs = PlotsBase.merge_with_base_supported([ :annotations, :legend_background_color, :background_color_inside, @@ -180,21 +179,21 @@ const _plotly_markers = [ ] const _plotly_scales = [:identity, :log10] -default_output_format(plt::Plot{PlotlyBackend}) = "html" +PlotsBase.default_output_format(plt::Plot{PlotlyBackend}) = "html" for s in (:attr, :seriestype, :marker, :style, :scale) f1 = Symbol("is_", s, "_supported") f2 = Symbol("supported_", s, "s") v = Symbol("_plotly_", s, "s") eval(quote - Plots.$f1(::PlotlyBackend, $s::Symbol) = $s in $v - Plots.$f2(::PlotlyBackend) = sort(collect($v)) + PlotsBase.$f1(::PlotlyBackend, $s::Symbol) = $s in $v + PlotsBase.$f2(::PlotlyBackend) = sort(collect($v)) end) end # ---------------------------------------------------------------- function labelfunc(scale::Symbol, backend::PlotlyBackend) - texfunc = labelfunc_tex(scale) + texfunc = PlotsBase.labelfunc_tex(scale) x -> begin tex_x = texfunc(x) sup_x = replace(tex_x, r"\^{(.*)}" => s"\1") @@ -291,7 +290,7 @@ end # this method gets the start/end in percentage of the canvas for this axis direction function plotly_domain(sp::Subplot) figw, figh = sp.plt[:size] - pcts = bbox_to_pcts(sp.plotarea, figw * px, figh * px) + pcts = PlotsBase.bbox_to_pcts(sp.plotarea, figw * px, figh * px) pcts = plotly_apply_aspect_ratio(sp, sp.plotarea, pcts) x_domain = [pcts[1], pcts[1] + pcts[3]] y_domain = [pcts[2], pcts[2] + pcts[4]] @@ -345,8 +344,8 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing) # ticks if axis[:ticks] !== :native - ticks = get_ticks(sp, axis) - ttype = ticks_type(ticks) + ticks = PlotsBase.get_ticks(sp, axis) + ttype = PlotsBase.ticks_type(ticks) if ttype === :ticks ax[:tickmode] = "array" ax[:tickvals] = ticks @@ -520,7 +519,8 @@ function plotly_layout(plt::Plot) plotattributes_out[:hovermode] = "none" end - plotattributes_out = recursive_merge(plotattributes_out, plt.attr[:extra_plot_kwargs]) + plotattributes_out = + PlotsBase.recursive_merge(plotattributes_out, plt.attr[:extra_plot_kwargs]) end function plotly_add_legend!(plotattributes_out::KW, sp::Subplot) @@ -717,7 +717,7 @@ plotly_convert_to_datetime(x::AbstractArray, formatter::Function) = map(xi -> isfinite(xi) ? string(Dates.today(), " ", formatter(xi)) : missing, x) else error( - "Invalid DateTime formatter. Expected Plots.datetime/date/time formatter but got $formatter", + "Invalid DateTime formatter. Expected PlotsBase.datetime/date/time formatter but got $formatter", ) end @@ -771,7 +771,7 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:colorbar] = plotly_colorbar(sp) - if is_2tuple(clims) && all(!isnan, clims) + if PlotsBase.is_2tuple(clims) && all(!isnan, clims) plotattributes_out[:zmin], plotattributes_out[:zmax] = clims end @@ -935,7 +935,7 @@ function plotly_colorbar(sp::Subplot) x_domain, y_domain = plotly_domain(sp) plot_attribute = KW( :title => sp[:colorbar_title], - :y => mean(y_domain), + :y => Statistics.mean(y_domain), :len => diff(y_domain)[1], :x => x_domain[2], ) @@ -1241,10 +1241,10 @@ html_head(plt::Plot{PlotlyBackend}) = plotly_html_head(plt) html_body(plt::Plot{PlotlyBackend}) = plotly_html_body(plt) plotly_url() = - if _use_local_dependencies[] - "file:///" * _plotly_local_file_path[] + if PlotsBase._use_local_dependencies[] + "file:///$(PlotsBase._plotly_local_file_path[])" else - "https://cdn.plot.ly/$_plotly_min_js_filename" + "https://cdn.plot.ly/$(PlotsBase._plotly_min_js_filename)" end function plotly_html_head(plt::Plot) @@ -1264,7 +1264,7 @@ function plotly_html_head(plt::Plot) "\n\t\t" end - if isijulia() + if PlotsBase.isijulia() mathjax_head else "$mathjax_head" @@ -1278,7 +1278,7 @@ function plotly_html_body(plt, style = nothing) end requirejs_prefix = requirejs_suffix = "" - if isijulia() + if PlotsBase.isijulia() # require.js adds .js automatically plotly_no_ext = plotly_url() |> splitext |> first @@ -1315,10 +1315,12 @@ plotly_show_js(io::IO, plot::Plot) = Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyBackend}) = true -_show(io::IO, ::MIME"application/vnd.plotly.v1+json", plot::Plot{PlotlyBackend}) = +PlotsBase._show(io::IO, ::MIME"application/vnd.plotly.v1+json", plot::Plot{PlotlyBackend}) = plotly_show_js(io, plot) -_show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = write(io, embeddable_html(plt)) +PlotsBase._show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = + write(io, PlotsBase.embeddable_html(plt)) + +PlotsBase._display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) -_display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) end # module diff --git a/src/backends/web.jl b/PlotsBase/src/backends/web.jl similarity index 100% rename from src/backends/web.jl rename to PlotsBase/src/backends/web.jl diff --git a/src/examples.jl b/PlotsBase/src/examples.jl similarity index 95% rename from src/examples.jl rename to PlotsBase/src/examples.jl index 80b588934..96c40facc 100644 --- a/src/examples.jl +++ b/PlotsBase/src/examples.jl @@ -24,7 +24,7 @@ const _examples = PlotExample[ PlotExample( # 1 "Lines", "A simple line plot of the columns.", - :(plot(Plots.fakedata(50, 5), w = 3)), + :(plot(PlotsBase.fakedata(50, 5), w = 3)), ), PlotExample( # 2 "Functions, adding data, and animations", @@ -71,7 +71,7 @@ const _examples = PlotExample[ scatter!( y, zcolor = abs.(y .- 0.5), - m = (:heat, 0.8, Plots.stroke(1, :green)), + m = (:heat, 0.8, PlotsBase.stroke(1, :green)), ms = 10 * abs.(y .- 0.5) .+ 4, lab = "grad", ) @@ -132,7 +132,7 @@ const _examples = PlotExample[ [rand(10), rand(20)], color = [:black :orange], line = (:dot, 4), - marker = ([:hex :d], 12, 0.8, Plots.stroke(3, :gray)), + marker = ([:hex :d], 12, 0.8, PlotsBase.stroke(3, :gray)), ) end, ), @@ -164,7 +164,7 @@ const _examples = PlotExample[ "Line styles", quote styles = filter( - s -> s in Plots.supported_styles(), + s -> s in PlotsBase.supported_styles(), [:solid, :dash, :dot, :dashdot, :dashdotdot], ) styles = reshape(styles, 1, length(styles)) # Julia 0.6 unfortunately gives an error when transposing symbol vectors @@ -179,8 +179,10 @@ const _examples = PlotExample[ PlotExample( # 13 "Marker types", quote - markers = - filter(m -> m in Plots.supported_markers(), Plots.Commons._shape_keys) + markers = filter( + m -> m in PlotsBase.supported_markers(), + PlotsBase.Commons._shape_keys, + ) markers = permutedims(markers) n = length(markers) x = range(0, stop = 10, length = n + 2)[2:(end - 1)] @@ -232,7 +234,7 @@ const _examples = PlotExample[ """, quote plot( - Plots.fakedata(100, 10), + PlotsBase.fakedata(100, 10), layout = 4, palette = cgrad.([:grays :blues :heat :lightrainbow]), bg_inside = [:orange :pink :darkblue :black], @@ -244,7 +246,7 @@ const _examples = PlotExample[ :(using Random), quote Random.seed!(111) - plot!(Plots.fakedata(100, 10)) + plot!(PlotsBase.fakedata(100, 10)) end, ), PlotExample( # 19 @@ -284,7 +286,7 @@ const _examples = PlotExample[ method `text(string, attrs...)`. This wraps font and color attributes and allows you to set text styling. `text` may also be a tuple `(string, attrs...)` of arguments which are passed - to `Plots.text`. + to `PlotsBase.text`. `annotate!(ann)` is shorthand for `plot!(; annotation=ann)`, and `annotate!(x, y, txt)` for `plot!(; annotation=(x,y,txt))`. @@ -295,7 +297,11 @@ const _examples = PlotExample[ """, quote y = rand(10) - plot(y, annotations = (3, y[3], Plots.text("this is #3", :left)), leg = false) + plot( + y, + annotations = (3, y[3], PlotsBase.text("this is #3", :left)), + leg = false, + ) # single vector of annotation tuples annotate!([ (5, y[5], ("this is #5", 16, :red, :center)), @@ -313,14 +319,14 @@ const _examples = PlotExample[ "map", "to", "series", - Plots.text("data", :green), + PlotsBase.text("data", :green), ], ) end, ), PlotExample( # 21 "Custom Markers", - """A `Plots.Shape` is a light wrapper around vertices of a polygon. For supported + """A `PlotsBase.Shape` is a light wrapper around vertices of a polygon. For supported backends, pass arbitrary polygons as the marker shapes. Note: The center is (0,0) and the size is expected to be rougly the area of the unit circle. """, @@ -395,7 +401,7 @@ const _examples = PlotExample[ 0.1ts .* map(sin, ts), z, zcolor = reverse(z), - m = (10, 0.8, :blues, Plots.stroke(0)), + m = (10, 0.8, :blues, PlotsBase.stroke(0)), leg = false, cbar = true, w = 5, @@ -454,7 +460,7 @@ const _examples = PlotExample[ ), PlotExample( # 29 "Layouts, margins, label rotation, title location", - :(using Plots.PlotMeasures), # for Measures, e.g. mm and px + :(using PlotsBase.PlotMeasures), # for Measures, e.g. mm and px quote plot( rand(100, 6), @@ -787,7 +793,7 @@ const _examples = PlotExample[ ylabel = "y", zlabel = "z", legend = :none, - margin = 2Plots.mm, + margin = 2PlotsBase.mm, ) end, ), @@ -831,7 +837,7 @@ const _examples = PlotExample[ z; projection = :polar, color = :cividis, - right_margin = 2Plots.mm, + right_margin = 2PlotsBase.mm, ) end, ), @@ -983,7 +989,7 @@ const _examples = PlotExample[ plot( plots..., layout = (@layout [_ ° _; ° ° °; ° ° °]), - margin = 0Plots.px, + margin = 0PlotsBase.px, ) end end, @@ -1198,7 +1204,7 @@ const _examples = PlotExample[ ), legs, ) - w, h = Plots._plot_defaults[:size] + w, h = PlotsBase._plot_defaults[:size] with(scalefonts = 0.5, size = (2w, 2h)) do plot(leg_plots()..., leg_plots(legend_column = -1)...; layout = (6, 3)) end @@ -1230,7 +1236,7 @@ const _examples = PlotExample[ ), legs, ) - w, h = Plots._plot_defaults[:size] + w, h = PlotsBase._plot_defaults[:size] with(scalefonts = 0.5, size = (2w, 2h)) do plot(leg_plots()..., leg_plots(legend_column = -1)...; layout = (6, 3)) end @@ -1276,30 +1282,6 @@ _backend_skips = Dict( 56, # custom bar plot 62, # fillstyle unsupported ], - :inspectdr => [ - 4, - 6, - 10, - 22, - 24, - 28, - 30, - 38, - 43, - 45, - 47, - 48, - 49, - 50, - 51, - 55, - 56, - 60, - 62, - 63, - 64, - 65, - ], :unicodeplots => [ 5, # limits issue 6, # embedded images supported, but requires `using ImageInTerminal`, disable for docs @@ -1340,13 +1322,33 @@ replace_rand(ex) = ex function replace_rand(ex::Expr) expr = Expr(ex.head) foreach(arg -> push!(expr.args, replace_rand(arg)), ex.args) - if Meta.isexpr(ex, :call) && ex.args[1] ∈ (:rand, :randn, :(Plots.fakedata)) - pushfirst!(expr.args, ex.args[1]) + if Meta.isexpr(ex, :call) && first(ex.args) ∈ (:rand, :randn, :(PlotsBase.fakedata)) + pushfirst!(expr.args, first(ex.args)) expr.args[2] = :rng end expr end +replace_module(ex) = ex + +function replace_module(ex::Expr) + if Meta.isexpr(ex, :import) || Meta.isexpr(ex, :using) + expr = Expr(ex.head) + for arg in ex.args + mod = last(arg.args) + new_arg = if Meta.isexpr(arg, :.) + mod ≡ :PlotsBase ? arg : Expr(:., :PlotsBase, mod) + else + arg + end + push!(expr.args, new_arg) + end + else + expr = ex + end + expr +end + # make and display one plot test_examples(i::Integer; kw...) = test_examples(backend_name(), i; kw...) @@ -1365,21 +1367,21 @@ function test_examples( # prevent leaking variables (esp. functions) directly into Plots namespace Base.eval(m, quote using Random - using Plots - Plots.Commons.debug!($debug) + using PlotsBase + PlotsBase.Commons.debug!($debug) backend($(QuoteNode(pkgname))) rng = $rng - rng === nothing || Random.seed!(rng, Plots.PLOTS_SEED) + rng === nothing || Random.seed!(rng, PlotsBase.PLOTS_SEED) theme(:default) end) (imp = _examples[i].imports) === nothing || Base.eval(m, imp) exprs = _examples[i].exprs - rng === nothing || (exprs = Plots.replace_rand(exprs)) + rng === nothing || (exprs = PlotsBase.replace_rand(exprs)) Base.eval(m, exprs) disp && Base.eval(m, :(gui(current()))) callback === nothing || callback(m, pkgname, i) - m.Plots.current() + m.PlotsBase.current() end # generate all plots and create a dict mapping idx --> plt diff --git a/PlotsBase/src/init.jl b/PlotsBase/src/init.jl new file mode 100644 index 000000000..bac48ea2b --- /dev/null +++ b/PlotsBase/src/init.jl @@ -0,0 +1,61 @@ +using Scratch +using REPL + +const _plotly_local_file_path = Ref{Union{Nothing,String}}(nothing) +# use fixed version of Plotly instead of the latest one for stable dependency +# see github.com/JuliaPlots/Plots.jl/pull/2779 +const _plotly_min_js_filename = "plotly-2.6.3.min.js" + +const _use_local_dependencies = Ref(false) +const _use_local_plotlyjs = Ref(false) + +_plots_defaults() = + if isdefined(Main, :PLOTS_DEFAULTS) + copy(Dict{Symbol,Any}(Main.PLOTS_DEFAULTS)) + else + Dict{Symbol,Any}() + end + +function _plots_theme_defaults() + user_defaults = _plots_defaults() + theme(pop!(user_defaults, :theme, :default); user_defaults...) +end + +function _plots_plotly_defaults() + if bool_env("PLOTS_HOST_DEPENDENCY_LOCAL", "false") + _plotly_local_file_path[] = + fn = joinpath(@get_scratch!("plotly"), _plotly_min_js_filename) + isfile(fn) || + Downloads.download("https://cdn.plot.ly/$(_plotly_min_js_filename)", fn) + _use_local_plotlyjs[] = true + end + _use_local_dependencies[] = _use_local_plotlyjs[] +end + +function __init__() + _plots_theme_defaults() + _plots_plotly_defaults() + + insert!( + Base.Multimedia.displays, + findlast( + x -> x isa Base.TextDisplay || x isa REPL.REPLDisplay, + Base.Multimedia.displays, + ) + 1, + PlotsDisplay(), + ) + + i -> + begin + while PlotsDisplay() in Base.Multimedia.displays + popdisplay(PlotsDisplay()) + end + insert!( + Base.Multimedia.displays, + findlast(x -> x isa REPL.REPLDisplay, Base.Multimedia.displays) + 1, + PlotsDisplay(), + ) + end |> atreplinit + + nothing +end diff --git a/src/layouts.jl b/PlotsBase/src/layouts.jl similarity index 100% rename from src/layouts.jl rename to PlotsBase/src/layouts.jl diff --git a/src/legend.jl b/PlotsBase/src/legend.jl similarity index 100% rename from src/legend.jl rename to PlotsBase/src/legend.jl diff --git a/src/output.jl b/PlotsBase/src/output.jl similarity index 99% rename from src/output.jl rename to PlotsBase/src/output.jl index b63f18f90..c9bab7865 100644 --- a/src/output.jl +++ b/PlotsBase/src/output.jl @@ -250,7 +250,7 @@ Base.show(io::IO, m::MIME"application/prs.juno.plotpane+html", plt::Plot) = function showjuno(io::IO, m, plt) dpi = plt[:dpi] - plt[:dpi] = get(io, :juno_dpi_ratio, 1) * Plots.DPI + plt[:dpi] = get(io, :juno_dpi_ratio, 1) * PlotsBase.DPI prepare_output(plt) try diff --git a/src/pipeline.jl b/PlotsBase/src/pipeline.jl similarity index 99% rename from src/pipeline.jl rename to PlotsBase/src/pipeline.jl index 9222ea55a..93764ebbb 100644 --- a/src/pipeline.jl +++ b/PlotsBase/src/pipeline.jl @@ -149,7 +149,7 @@ function RecipesPipeline.plot_setup!(plt::Plot, plotattributes, kw_list) nothing end -function RecipesPipeline.process_sliced_series_attributes!(plt::Plots.Plot, kw_list) +function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, kw_list) # determine global extrema xe = ye = ze = NaN, NaN for kw in kw_list diff --git a/src/plot.jl b/PlotsBase/src/plot.jl similarity index 97% rename from src/plot.jl rename to PlotsBase/src/plot.jl index 73672a689..963eea86c 100644 --- a/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -95,7 +95,7 @@ function RecipesBase.plot(args...; kw...) @nospecialize # this creates a new plot with args/kw and sets it to be the current plot plotattributes = KW(kw) - Plots.Commons.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) # create an empty Plot then process plt = Plot() @@ -119,7 +119,7 @@ function plot!( ) @nospecialize plotattributes = KW(kw) - Plots.Commons.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) # build our plot vector from the args plts = Plot[plt1] @@ -215,7 +215,7 @@ plot(plt::Plot, args...; kw...) = plot!(deepcopy(plt), args...; kw...) function plot!(plt::Plot, args...; kw...) @nospecialize plotattributes = KW(kw) - Plots.Commons.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) # merge!(plt.user_attrs, plotattributes) _plot!(plt, plotattributes, args) end @@ -296,7 +296,7 @@ julia> plot(pl.subplots[2]) # extract 2nd subplot as a standalone plot """ function plot(sp::Subplot, args...; kw...) @nospecialize - plt = Plots.Plot(sp) + plt = PlotsBase.Plot(sp) plot(plt, PlaceHolder(), PlaceHolder(), args...; kw...) end diff --git a/src/plotattr.jl b/PlotsBase/src/plotattr.jl similarity index 97% rename from src/plotattr.jl rename to PlotsBase/src/plotattr.jl index 788db52f6..972dcf2e3 100644 --- a/src/plotattr.jl +++ b/PlotsBase/src/plotattr.jl @@ -83,7 +83,7 @@ function plotattr(attrtype::Symbol, attribute::Symbol) attribute = lookup_aliases(attrtype, attribute) type, desc = _arg_desc[attribute] def = _attribute_defaults[attrtype][attribute] - aliases = if (al = Plots.Commons.aliases(attribute)) |> length > 0 + aliases = if (al = PlotsBase.Commons.aliases(attribute)) |> length > 0 "Aliases: " * string(Tuple(al)) * ".\n\n" else "" diff --git a/src/recipes.jl b/PlotsBase/src/recipes.jl similarity index 99% rename from src/recipes.jl rename to PlotsBase/src/recipes.jl index c0fd54e98..0431d2826 100644 --- a/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -22,7 +22,7 @@ function seriestype_supported(pkg::AbstractBackend, st::Symbol) end macro deps(st, args...) - :(Plots.series_recipe_dependencies($(quot(st)), $(map(quot, args)...))) + :(PlotsBase.series_recipe_dependencies($(quot(st)), $(map(quot, args)...))) end # get a list of all seriestypes @@ -502,7 +502,7 @@ end @deps bar shape # --------------------------------------------------------------------------- -# Plots Heatmap +# PlotsBase Heatmap @recipe function f(::Type{Val{:plots_heatmap}}, x, y, z) # COV_EXCL_LINE xe, ye = heatmap_edges(x), heatmap_edges(y) m, n = size(z.surf) @@ -862,7 +862,7 @@ end _preprocess_binlike(plotattributes, h.edges[1], h.weights) xerror --> diff(h.edges[1]) / 2 seriestype := :scatter - (Plots._bin_centers(edge), weights) + (PlotsBase._bin_centers(edge), weights) else (h.edges[1], h.weights) end @@ -891,13 +891,13 @@ end end end - x := Plots._bin_centers(edge_x) - y := Plots._bin_centers(edge_y) + x := PlotsBase._bin_centers(edge_x) + y := PlotsBase._bin_centers(edge_y) z := Surface(permutedims(float_weights)) seriestype := :heatmap () end -Plots.@deps bins2d heatmap +PlotsBase.@deps bins2d heatmap @recipe function f(::Type{Val{:histogram2d}}, x, y, z) # COV_EXCL_LINE h = _make_hist( diff --git a/src/shorthands.jl b/PlotsBase/src/shorthands.jl similarity index 100% rename from src/shorthands.jl rename to PlotsBase/src/shorthands.jl diff --git a/src/themes.jl b/PlotsBase/src/themes.jl similarity index 100% rename from src/themes.jl rename to PlotsBase/src/themes.jl diff --git a/src/users.jl b/PlotsBase/src/users.jl similarity index 100% rename from src/users.jl rename to PlotsBase/src/users.jl diff --git a/src/utils.jl b/PlotsBase/src/utils.jl similarity index 97% rename from src/utils.jl rename to PlotsBase/src/utils.jl index e79d790a4..ef38e1baa 100644 --- a/src/utils.jl +++ b/PlotsBase/src/utils.jl @@ -1,12 +1,6 @@ # --------------------------------------------------------------- -bool_env(x, default)::Bool = - try - return parse(Bool, get(ENV, x, default)) - catch e - @warn e - return false - end +bool_env(x, default::String = "0")::Bool = tryparse(Bool, get(ENV, x, default)) treats_y_as_x(seriestype) = seriestype in (:vline, :vspan, :histogram, :barhist, :stephist, :scatterhist) @@ -591,7 +585,7 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end """ -Allows temporary setting of backend and defaults for Plots. Settings apply only for the `do` block. Example: +Allows temporary setting of backend and defaults for PlotsBase. Settings apply only for the `do` block. Example: ``` with(:gr, size=(400,400), type=:histogram) do plot(rand(10)) @@ -620,7 +614,14 @@ function with(f::Function, args...; scalefonts = nothing, kw...) for arg in args # change backend? - arg in backends() && backend(arg) + if arg isa Symbol + if arg ∈ backends() + if (pkg = backend_package_name(arg)) ≢ nothing # :plotly + @eval Main import $pkg + end + backend(arg) + end + end # TODO: generalize this strategy to allow args as much as possible # as in: with(:gr, :scatter, :legend, :grid) do; ...; end @@ -883,7 +884,7 @@ end _argument_description(s::Symbol) = if s ∈ keys(_arg_desc) - aliases = if (al = Plots.Commons.aliases(s)) |> length > 0 + aliases = if (al = PlotsBase.Commons.aliases(s)) |> length > 0 " Aliases: " * string(Tuple(al)) * '.' else "" @@ -950,16 +951,6 @@ function _guess_best_legend_position(lp::Symbol, plt) _guess_best_legend_position(xlims(plt), ylims(plt), plt) end -macro ext_imp_use(imp_use::QuoteNode, mod::Symbol, args...) - dots = ntuple(_ -> :., isdefined(Base, :get_extension) ? 1 : 3) - ex = if length(args) > 0 - Expr(:(:), Expr(dots..., mod), Expr.(:., args)...) - else - Expr(dots..., mod) - end - Expr(imp_use.value, ex) |> esc -end - _generate_doclist(attributes) = replace(join(sort(collect(attributes)), "\n- "), "_" => "\\_") diff --git a/test/.gitignore b/PlotsBase/test/.gitignore similarity index 100% rename from test/.gitignore rename to PlotsBase/test/.gitignore diff --git a/PlotsBase/test/runtests.jl b/PlotsBase/test/runtests.jl new file mode 100644 index 000000000..2d8048ad1 --- /dev/null +++ b/PlotsBase/test/runtests.jl @@ -0,0 +1,79 @@ +const TEST_PACKAGES = + strip.( + split( + get( + ENV, + "PLOTSBASE_TEST_PACKAGES", + "GR,UnicodePlots,PythonPlot,PGFPlotsX,PlotlyJS,Gaston", + ), + ",", + ) + ) +const TEST_BACKENDS = Symbol.(lowercase.(TEST_PACKAGES)) + +using PlotsBase + +# initialize all backends +for pkg in TEST_PACKAGES + @eval import $(Symbol(pkg)) # trigger extension + getproperty(PlotsBase, Symbol(lowercase(pkg)))() +end +gr() + +import Unitful: m, s, cm, DimensionError +import PlotsBase: PLOTS_SEED, Plot, with +import SentinelArrays: ChainedVector +import GeometryBasics +import OffsetArrays +import FreeType # for `unicodeplots` +import LibGit2 +import Aqua +import JSON + +using VisualRegressionTests +using RecipesPipeline +using FilePathsBase +using LaTeXStrings +using RecipesBase +using TestImages +using Unitful +using FileIO +using Dates +using Test + +is_auto() = PlotsBase.bool_env("VISUAL_REGRESSION_TESTS_AUTO") +is_pkgeval() = PlotsBase.bool_env("JULIA_PKGEVAL") +is_ci() = PlotsBase.bool_env("CI") + +is_ci() || @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 + +for name in ( + "quality", + "misc", + "utils", + "args", + "defaults", + "dates", + "axes", + "layouts", + "contours", + "components", + "shorthands", + "recipes", + "unitful", + "hdf5plots", # broken ? + "pgfplotsx", + "plotly", + "animations", + "output", + "backends", +) + @testset "$name" begin + # skip the majority of tests if we only want to update reference images or under `PkgEval` (timeout limit) + if is_auto() || is_pkgeval() + name != "backends" && continue + end + gr() # reset to default backend (safer) + include("test_$name.jl") + end +end diff --git a/test/test_animations.jl b/PlotsBase/test/test_animations.jl similarity index 93% rename from test/test_animations.jl rename to PlotsBase/test/test_animations.jl index cc1025053..068e2e4ce 100644 --- a/test/test_animations.jl +++ b/PlotsBase/test/test_animations.jl @@ -40,7 +40,7 @@ end @test filesize(mov(anim, show_msg = false).filename) > 10_000 @test filesize(mp4(anim, show_msg = false).filename) > 10_000 @test filesize(webm(anim, show_msg = false).filename) > 10_000 - @test filesize(Plots.apng(anim, show_msg = false).filename) > 10_000 + @test filesize(PlotsBase.apng(anim, show_msg = false).filename) > 10_000 @gif for i in 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) @@ -72,7 +72,7 @@ end end, ) - anim = Plots.@apng for i in 1:n + anim = PlotsBase.@apng for i in 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end every 5 @test showable(MIME("image/png"), anim) @@ -104,7 +104,7 @@ end @testset "animate" begin anim = animate([1:2, 2:3]; show_msg = false, fps = 1 // 10) - @test anim isa Plots.AnimatedGif + @test anim isa PlotsBase.AnimatedGif @test showable(MIME("image/gif"), anim) fn = tempname() * ".apng" @@ -122,6 +122,6 @@ end @testset "coverage" begin @test animate([1:2, 2:3]; variable_palette = true, show_msg = false) isa - Plots.AnimatedGif - @test Plots.FrameIterator([1:2, 2:3]).every == 1 + PlotsBase.AnimatedGif + @test PlotsBase.FrameIterator([1:2, 2:3]).every == 1 end diff --git a/test/test_args.jl b/PlotsBase/test/test_args.jl similarity index 90% rename from test/test_args.jl rename to PlotsBase/test/test_args.jl index 569d4fe3a..1e0da0f8e 100644 --- a/test/test_args.jl +++ b/PlotsBase/test/test_args.jl @@ -1,4 +1,4 @@ -using Plots, Dates, Test +using PlotsBase, Dates, Test struct Foo{T} x::Vector{T} y::Vector{T} @@ -64,8 +64,8 @@ end end @testset "@add_attributes" begin - Font = Plots.Font - Plots.@add_attributes subplot struct Legend + Font = PlotsBase.Font + PlotsBase.@add_attributes subplot struct Legend background_color = :match foreground_color = :match position = :best @@ -91,9 +91,9 @@ end end @testset "aliases" begin - @test :legend in Plots.Commons.aliases(:legend_position) - Plots.Commons.add_non_underscore_aliases!(Plots.Commons._typeAliases) - Plots.Commons.add_axes_aliases(:ticks, :tick) + @test :legend in PlotsBase.Commons.aliases(:legend_position) + PlotsBase.Commons.add_non_underscore_aliases!(PlotsBase.Commons._typeAliases) + PlotsBase.Commons.add_axes_aliases(:ticks, :tick) end @userplot MatrixHeatmap @@ -118,7 +118,7 @@ end p1 = plot(ts, 100randn(24)) vline!(p1, [now()]) @test p1[1][:yaxis][:formatter] == :auto - @test p1[1][:xaxis][:formatter] == Plots.datetimeformatter + @test p1[1][:xaxis][:formatter] == PlotsBase.datetimeformatter p2 = plot(rand(4) .* 10^6, rand(4) .* 10^6, xformatter = :plain, yformatter = :plain) vline!(p2, [10^6]) @test p2[1][:yaxis][:formatter] == :plain diff --git a/test/test_axes.jl b/PlotsBase/test/test_axes.jl similarity index 66% rename from test/test_axes.jl rename to PlotsBase/test/test_axes.jl index edb8911ae..abda149df 100644 --- a/test/test_axes.jl +++ b/PlotsBase/test/test_axes.jl @@ -1,15 +1,15 @@ @testset "Axes" begin pl = plot() axis = pl.subplots[1][:xaxis] - @test typeof(axis) == Plots.Axis - @test Plots.discrete_value!(axis, "HI") == (0.5, 1) - @test Plots.discrete_value!(axis, :yo) == (1.5, 2) - @test Plots.Axes.ignorenan_extrema(axis) == (0.5, 1.5) + @test typeof(axis) == PlotsBase.Axis + @test PlotsBase.discrete_value!(axis, "HI") == (0.5, 1) + @test PlotsBase.discrete_value!(axis, :yo) == (1.5, 2) + @test PlotsBase.Axes.ignorenan_extrema(axis) == (0.5, 1.5) @test axis[:discrete_map] == Dict{Any,Any}(:yo => 2, "HI" => 1) - Plots.discrete_value!(axis, map(i -> "x$i", 1:5)) - Plots.discrete_value!(axis, map(i -> "x$i", 0:2)) - @test Plots.Axes.ignorenan_extrema(axis) == (0.5, 7.5) + PlotsBase.discrete_value!(axis, map(i -> "x$i", 1:5)) + PlotsBase.discrete_value!(axis, map(i -> "x$i", 0:2)) + @test PlotsBase.Axes.ignorenan_extrema(axis) == (0.5, 7.5) # github.com/JuliaPlots/Plots.jl/issues/4375 for lab in ("foo", :foo) @@ -17,40 +17,41 @@ show(devnull, pl) end - @test Plots.labelfunc_tex(:log10)(1) == "10^{1}" - @test Plots.labelfunc_tex(:log2)(1) == "2^{1}" - @test Plots.labelfunc_tex(:ln)(1) == "e^{1}" + @test PlotsBase.labelfunc_tex(:log10)(1) == "10^{1}" + @test PlotsBase.labelfunc_tex(:log2)(1) == "2^{1}" + @test PlotsBase.labelfunc_tex(:ln)(1) == "e^{1}" - @test Plots.get_labels(:auto, 1:3, :identity) == ["1", "2", "3"] - @test Plots.get_labels(:scientific, float.(500:500:1500), :identity) == + @test PlotsBase.get_labels(:auto, 1:3, :identity) == ["1", "2", "3"] + @test PlotsBase.get_labels(:scientific, float.(500:500:1500), :identity) == ["5.00×10^{2}", "1.00×10^{3}", "1.50×10^{3}"] - @test Plots.get_labels(:engineering, float.(500:500:1500), :identity) == + @test PlotsBase.get_labels(:engineering, float.(500:500:1500), :identity) == ["500.×10^{0}", "1.00×10^{3}", "1.50×10^{3}"] - @test Plots.get_labels(:latex, 1:3, :identity) == ["\$1\$", "\$2\$", "\$3\$"] + @test PlotsBase.get_labels(:latex, 1:3, :identity) == ["\$1\$", "\$2\$", "\$3\$"] # GR is used during tests and it correctly overrides labelfunc(), but PGFPlotsX did not with(:pgfplotsx) do - @test Plots.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] + @test PlotsBase.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] end - @test Plots.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] - @test Plots.get_labels(:auto, 1:3, :log2) == ["2^{1}", "2^{2}", "2^{3}"] - @test Plots.get_labels(:auto, 1:3, :ln) == ["e^{1}", "e^{2}", "e^{3}"] - @test Plots.get_labels(:latex, 1:3, :log10) == + @test PlotsBase.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] + @test PlotsBase.get_labels(:auto, 1:3, :log2) == ["2^{1}", "2^{2}", "2^{3}"] + @test PlotsBase.get_labels(:auto, 1:3, :ln) == ["e^{1}", "e^{2}", "e^{3}"] + @test PlotsBase.get_labels(:latex, 1:3, :log10) == ["\$10^{1}\$", "\$10^{2}\$", "\$10^{3}\$"] - @test Plots.get_labels(:latex, 1:3, :log2) == ["\$2^{1}\$", "\$2^{2}\$", "\$2^{3}\$"] - @test Plots.get_labels(:latex, 1:3, :ln) == ["\$e^{1}\$", "\$e^{2}\$", "\$e^{3}\$"] + @test PlotsBase.get_labels(:latex, 1:3, :log2) == + ["\$2^{1}\$", "\$2^{2}\$", "\$2^{3}\$"] + @test PlotsBase.get_labels(:latex, 1:3, :ln) == ["\$e^{1}\$", "\$e^{2}\$", "\$e^{3}\$"] - @test Plots.get_labels(x -> 1e3x, 1:3, :identity) == ["1000", "2000", "3000"] - @test Plots.get_labels(x -> 1e3x, 1:3, :log10) == ["10^{4}", "10^{5}", "10^{6}"] - @test Plots.get_labels(x -> 8x, 1:3, :log2) == ["2^{4}", "2^{5}", "2^{6}"] - @test Plots.get_labels(x -> ℯ * x, 1:3, :ln) == ["e^{2}", "e^{3}", "e^{4}"] - @test Plots.get_labels(x -> string(x, " MB"), 1:3, :identity) == + @test PlotsBase.get_labels(x -> 1e3x, 1:3, :identity) == ["1000", "2000", "3000"] + @test PlotsBase.get_labels(x -> 1e3x, 1:3, :log10) == ["10^{4}", "10^{5}", "10^{6}"] + @test PlotsBase.get_labels(x -> 8x, 1:3, :log2) == ["2^{4}", "2^{5}", "2^{6}"] + @test PlotsBase.get_labels(x -> ℯ * x, 1:3, :ln) == ["e^{2}", "e^{3}", "e^{4}"] + @test PlotsBase.get_labels(x -> string(x, " MB"), 1:3, :identity) == ["1.0 MB", "2.0 MB", "3.0 MB"] - @test Plots.get_labels(x -> string(x, " MB"), 1:3, :log10) == + @test PlotsBase.get_labels(x -> string(x, " MB"), 1:3, :log10) == ["10.0 MB", "100.0 MB", "1000.0 MB"] end @testset "Showaxis" begin - for value in Plots.Commons._all_showaxis_attrs + for value in PlotsBase.Commons._all_showaxis_attrs @test plot(1:5, showaxis = value)[1][:yaxis][:showaxis] isa Bool end @test plot(1:5, showaxis = :y)[1][:yaxis][:showaxis] @@ -66,9 +67,9 @@ end p1 = plot('A':'M', 1:13) p2 = plot('A':'Z', 1:26) p3 = plot('A':'Z', 1:26, ticks = :all) - @test Plots.get_ticks(p1[1], p1[1][:xaxis])[2] == string.('A':'M') - @test Plots.get_ticks(p2[1], p2[1][:xaxis])[2] == string.('C':3:'Z') - @test Plots.get_ticks(p3[1], p3[1][:xaxis])[2] == string.('A':'Z') + @test PlotsBase.get_ticks(p1[1], p1[1][:xaxis])[2] == string.('A':'M') + @test PlotsBase.get_ticks(p2[1], p2[1][:xaxis])[2] == string.('C':3:'Z') + @test PlotsBase.get_ticks(p3[1], p3[1][:xaxis])[2] == string.('A':'Z') end @testset "Ticks getter functions" begin @@ -83,31 +84,31 @@ end @testset "Axis limits" begin default_widen(from, to) = - Plots.Axes.scale_lims(from, to, Plots.Axes.default_widen_factor) + PlotsBase.Axes.scale_lims(from, to, PlotsBase.Axes.default_widen_factor) pl = plot(1:5, xlims = :symmetric, widen = false) - @test Plots.xlims(pl) == (-5, 5) + @test PlotsBase.xlims(pl) == (-5, 5) pl = plot(1:3) - @test Plots.xlims(pl) == default_widen(1, 3) + @test PlotsBase.xlims(pl) == default_widen(1, 3) pl = plot([1.05, 2.0, 2.95], ylims = :round) - @test Plots.ylims(pl) == (1, 3) + @test PlotsBase.ylims(pl) == (1, 3) for x in (1:3, -10:10), xlims in ((1, 5), [1, 5]) pl = plot(x; xlims) - @test Plots.xlims(pl) == (1, 5) + @test PlotsBase.xlims(pl) == (1, 5) pl = plot(x; xlims, widen = true) - @test Plots.xlims(pl) == default_widen(1, 5) + @test PlotsBase.xlims(pl) == default_widen(1, 5) end pl = plot(1:5, lims = :symmetric, widen = false) - @test Plots.xlims(pl) == Plots.ylims(pl) == (-5, 5) + @test PlotsBase.xlims(pl) == PlotsBase.ylims(pl) == (-5, 5) for xlims in (0, 0.0, false, true, plot()) pl = plot(1:5; xlims) plims = - @test_logs (:warn, r"Invalid limits for x axis") match_mode = :any Plots.xlims( + @test_logs (:warn, r"Invalid limits for x axis") match_mode = :any PlotsBase.xlims( pl, ) @test plims == default_widen(1, 5) @@ -116,20 +117,20 @@ end @testset "#4379" begin for ylims in ((-5, :auto), [-5, :auto]) pl = plot([-2, 3], ylims = ylims, widen = false) - @test Plots.ylims(pl) == (-5.0, 3.0) + @test PlotsBase.ylims(pl) == (-5.0, 3.0) end for ylims in ((:auto, 4), [:auto, 4]) pl = plot([-2, 3], ylims = ylims, widen = false) - @test Plots.ylims(pl) == (-2.0, 4.0) + @test PlotsBase.ylims(pl) == (-2.0, 4.0) end for xlims in ((-3, :auto), [-3, :auto]) pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false) - @test Plots.xlims(pl) == (-3.0, 3.0) + @test PlotsBase.xlims(pl) == (-3.0, 3.0) end for xlims in ((:auto, 4), [:auto, 4]) pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false) - @test Plots.xlims(pl) == (-2.0, 4.0) + @test PlotsBase.xlims(pl) == (-2.0, 4.0) end end end @@ -140,19 +141,19 @@ end end @testset "Twinx" begin - pl = plot(1:10, margin = 2Plots.cm) + pl = plot(1:10, margin = 2PlotsBase.cm) twpl = twinx(pl) pl! = plot!(twpl, -(1:10)) - @test twpl[:right_margin] == 2Plots.cm - @test twpl[:left_margin] == 2Plots.cm - @test twpl[:top_margin] == 2Plots.cm - @test twpl[:bottom_margin] == 2Plots.cm + @test twpl[:right_margin] == 2PlotsBase.cm + @test twpl[:left_margin] == 2PlotsBase.cm + @test twpl[:top_margin] == 2PlotsBase.cm + @test twpl[:bottom_margin] == 2PlotsBase.cm end @testset "Axis-aliases" begin - @test haskey(Plots.Commons._keyAliases, :xguideposition) - @test haskey(Plots.Commons._keyAliases, :x_guide_position) - @test !haskey(Plots.Commons._keyAliases, :xguide_position) + @test haskey(PlotsBase.Commons._keyAliases, :xguideposition) + @test haskey(PlotsBase.Commons._keyAliases, :x_guide_position) + @test !haskey(PlotsBase.Commons._keyAliases, :xguide_position) pl = plot(1:2, xl = "x label") @test pl[1][:xaxis][:guide] === "x label" pl = plot(1:2, xrange = (0, 3)) @@ -209,7 +210,7 @@ end @testset "scale_lims!" begin let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.Axes.scale_lims!(:x, 1.1) + PlotsBase.Axes.scale_lims!(:x, 1.1) @test first(xlims(pl)) < first(xl) @test last(xlims(pl)) > last(xl) @test ylims(pl) == yl @@ -217,7 +218,7 @@ end let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.PlotsPlots.scale_lims!(pl, 1.1) + PlotsBase.PlotsPlots.scale_lims!(pl, 1.1) @test first(xlims(pl)) < first(xl) @test last(xlims(pl)) > last(xl) @test first(ylims(pl)) < first(yl) @@ -227,10 +228,10 @@ end @testset "reset_extrema!" begin pl = plot(1:2) - Plots.Axes.reset_extrema!(pl[1]) + PlotsBase.Axes.reset_extrema!(pl[1]) ax = pl[1][:xaxis] - @test Plots.expand_extrema!(ax, nothing) == ax[:extrema] - @test Plots.expand_extrema!(ax, true) == ax[:extrema] + @test PlotsBase.expand_extrema!(ax, nothing) == ax[:extrema] + @test PlotsBase.expand_extrema!(ax, true) == ax[:extrema] end @testset "no labels" begin @@ -243,9 +244,9 @@ end # FIXME in 2.0: this is awful to read, because `minorticks` represent the number of `intervals` for minor_intervals in (:auto, :none, nothing, false, true, 0, 1, 2, 3, 4, 5) n_minor_ticks_per_major = if minor_intervals isa Bool - minor_intervals ? Plots.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 + minor_intervals ? PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 elseif minor_intervals === :auto - Plots.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 + PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 elseif minor_intervals === :none || minor_intervals isa Nothing 0 else @@ -254,9 +255,9 @@ end pl = plot(1:4; minorgrid = true, minorticks = minor_intervals) sp = first(pl) for axis in (:xaxis, :yaxis) - ticks = Plots.get_ticks(sp, sp[axis], update = false) + ticks = PlotsBase.get_ticks(sp, sp[axis], update = false) n_expected_minor_ticks = (length(first(ticks)) - 1) * n_minor_ticks_per_major - minor_ticks = Plots.get_minor_ticks(sp, sp[axis], ticks) + minor_ticks = PlotsBase.get_minor_ticks(sp, sp[axis], ticks) n_minor_ticks = if minor_intervals isa Bool if minor_intervals length(minor_ticks) diff --git a/test/test_backends.jl b/PlotsBase/test/test_backends.jl similarity index 75% rename from test/test_backends.jl rename to PlotsBase/test/test_backends.jl index bc8e83dc6..b30111944 100644 --- a/test/test_backends.jl +++ b/PlotsBase/test/test_backends.jl @@ -7,10 +7,10 @@ ci_tol() = "1e-1" end -const TESTS_MODULE = Module(:PlotsTestsModule) +const TESTS_MODULE = Module(:PlotsBaseTestModule) const PLOTS_IMG_TOL = parse(Float64, get(ENV, "PLOTS_IMG_TOL", is_ci() ? ci_tol() : "1e-5")) -Base.eval(TESTS_MODULE, :(using Random, StableRNGs, Plots)) +Base.eval(TESTS_MODULE, :(using Random, StableRNGs, PlotsBase)) reference_dir(args...) = if (ref_dir = get(ENV, "PLOTS_REFERENCE_DIR", nothing)) !== nothing @@ -35,7 +35,7 @@ function checkout_reference_dir(dn::AbstractString) sleep(20i) end end - if (ver = Plots._current_plots_version).prerelease |> isempty + if (ver = PlotsBase._current_plots_version).prerelease |> isempty try tag = LibGit2.GitObject(repo, "v$ver") hash = string(LibGit2.target(tag)) @@ -76,21 +76,21 @@ function image_comparison_tests( sigma = [1, 1], tol = 1e-2, ) - example = Plots._examples[idx] + example = PlotsBase._examples[idx] @info "Testing plot: $pkg:$idx:$(example.header)" - ver = Plots._current_plots_version + ver = PlotsBase._current_plots_version ver = VersionNumber(ver.major, ver.minor, ver.patch) reffn = reference_file(pkg, ver, idx) newfn = joinpath(reference_path(pkg, ver), ref_name(idx) * ".png") imports = something(example.imports, :()) exprs = quote - Plots.Commons.debug!($debug) + PlotsBase.Commons.debug!($debug) backend($(QuoteNode(pkg))) theme(:default) - rng = StableRNG(Plots.PLOTS_SEED) - $(Plots.replace_rand(example.exprs)) + rng = StableRNG(PlotsBase.PLOTS_SEED) + $(PlotsBase.replace_rand(example.exprs)) end @debug imports exprs @@ -112,7 +112,7 @@ function image_comparison_facts( sigma = [1, 1], # number of pixels to "blur" tol = 1e-2, # acceptable error (percent) ) - for i in setdiff(1:length(Plots._examples), skip) + for i in setdiff(1:length(PlotsBase._examples), skip) if only === nothing || i in only @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) end @@ -123,21 +123,21 @@ end #= with(:gr) do - image_comparison_facts(:gr, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:gr]) + image_comparison_facts(:gr, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:gr]) end with(:plotlyjs) do - image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:plotlyjs]) + image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:plotlyjs]) end with(:pgfplotsx) do - image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:pgfplotsx]) + image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:pgfplotsx]) end =# @testset "UnicodePlots" begin with(:unicodeplots) do - @test backend() == Plots._backend_instance(:unicodeplots) + @test backend() == PlotsBase._backend_instance(:unicodeplots) io = IOContext(IOBuffer(), :color => true) @@ -149,13 +149,16 @@ end @test show(io, pl) isa Nothing pl = plot([1, 2], [3, 4]) - annotate!(pl, [(1.5, 3.2, Plots.text("Test", :red, :center))]) + annotate!(pl, [(1.5, 3.2, PlotsBase.text("Test", :red, :center))]) hline!(pl, [3.1]) @test show(io, pl) isa Nothing pl = plot([Dates.Date(2019, 1, 1), Dates.Date(2019, 2, 1)], [3, 4]) hline!(pl, [3.1]) - annotate!(pl, [(Dates.Date(2019, 1, 15), 3.2, Plots.text("Test", :red, :center))]) + annotate!( + pl, + [(Dates.Date(2019, 1, 15), 3.2, PlotsBase.text("Test", :red, :center))], + ) @test show(io, pl) isa Nothing pl = plot([Dates.Date(2019, 1, 1), Dates.Date(2019, 2, 1)], [3, 4]) @@ -191,40 +194,40 @@ end @testset "GR - reference images" begin with(:gr) do # NOTE: use `ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true;` to automatically replace reference images - @test backend() == Plots._backend_instance(:gr) + @test backend() == PlotsBase._backend_instance(:gr) @test backend_name() === :gr image_comparison_facts( :gr, tol = PLOTS_IMG_TOL, - skip = vcat(Plots._backend_skips[:gr], blacklist), + skip = vcat(PlotsBase._backend_skips[:gr], blacklist), ) end end -# is_pkgeval() || @testset "PlotlyJS" begin -# with(:plotlyjs) do -# @test backend() == Plots.PlotlyJSBackend() -# pl = plot(rand(10)) -# @test pl isa Plot -# @test display(pl) isa Nothing -# end -# end +is_pkgeval() || @testset "PlotlyJS" begin + with(:plotlyjs) do + PlotlyJSExt = Base.get_extension(PlotsBase, :PlotlyJSExt) + @test backend() == PlotlyJSExt.PlotlyJSBackend() + pl = plot(rand(10)) + @test pl isa Plot + @test display(pl) isa Nothing + end +end is_pkgeval() || @testset "Examples" begin callback(m, pkgname, i) = begin - pl = m.Plots.current() - save_func = (; pgfplotsx = m.Plots.pdf, unicodeplots = m.Plots.txt) # fastest `savefig` for each backend + pl = m.PlotsBase.current() + save_func = (; pgfplotsx = m.PlotsBase.pdf, unicodeplots = m.PlotsBase.txt) # fastest `savefig` for each backend fn = Base.invokelatest( - get(save_func, pkgname, m.Plots.png), + get(save_func, pkgname, m.PlotsBase.png), pl, tempname() * ref_name(i), ) @test filesize(fn) > 1_000 end - # TODO: check whats up with those who are filtered - Sys.islinux() && for be in filter(∉((:plotlyjs, :gaston)), TEST_BACKENDS) - skip = vcat(Plots._backend_skips[be], blacklist) - Plots.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage + Sys.islinux() && for be in TEST_BACKENDS + skip = vcat(PlotsBase._backend_skips[be], blacklist) + PlotsBase.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage closeall() end end diff --git a/test/test_components.jl b/PlotsBase/test/test_components.jl similarity index 77% rename from test/test_components.jl rename to PlotsBase/test/test_components.jl index 026e3ac3b..501c43bab 100644 --- a/test/test_components.jl +++ b/PlotsBase/test/test_components.jl @@ -1,7 +1,7 @@ @testset "Shapes" begin - get_xs = Plots.Shapes.get_xs - get_ys = Plots.Shapes.get_ys - vertices = Plots.Shapes.vertices + get_xs = PlotsBase.Shapes.get_xs + get_ys = PlotsBase.Shapes.get_ys + vertices = PlotsBase.Shapes.vertices @testset "Type" begin square = Shape([(0, 0.0), (1, 0.0), (1, 1.0), (0, 1.0)]) @test get_xs(square) == [0, 1, 1, 0] @@ -27,7 +27,7 @@ @testset "Center" begin square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)]) - @test Plots.center(square) == (0.5, 0.5) + @test PlotsBase.center(square) == (0.5, 0.5) end @testset "Translate" begin @@ -35,10 +35,10 @@ squareUp = Shape([(0, 1), (1, 1), (1, 2), (0, 2)]) squareUpRight = Shape([(1, 1), (2, 1), (2, 2), (1, 2)]) - @test Plots.translate(square, 0, 1).x == squareUp.x - @test Plots.translate(square, 0, 1).y == squareUp.y + @test PlotsBase.translate(square, 0, 1).x == squareUp.x + @test PlotsBase.translate(square, 0, 1).y == squareUp.y - @test Plots.center(translate!(square, 1)) == (1.5, 1.5) + @test PlotsBase.center(translate!(square, 1)) == (1.5, 1.5) end @testset "Rotate" begin @@ -50,7 +50,7 @@ square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)]) # make a new, rotated square - square2 = Plots.rotate(square, -2) + square2 = PlotsBase.rotate(square, -2) @test square2.x ≈ coordsRotated2[1, :] @test square2.y ≈ coordsRotated2[2, :] @@ -71,22 +71,22 @@ end @testset "Misc" begin - @test Plots.weave([1, 3], [2, 4]) == collect(1:4) - @test Plots.makeshape(3) isa Plots.Shape - @test Plots.makestar(3) isa Plots.Shape - @test Plots.makecross() isa Plots.Shape - @test Plots.makearrowhead(10.0) isa Plots.Shape + @test PlotsBase.weave([1, 3], [2, 4]) == collect(1:4) + @test PlotsBase.makeshape(3) isa PlotsBase.Shape + @test PlotsBase.makestar(3) isa PlotsBase.Shape + @test PlotsBase.makecross() isa PlotsBase.Shape + @test PlotsBase.makearrowhead(10.0) isa PlotsBase.Shape - @test Plots.rotate(1.0, 2.0, 5.0, (0, 0)) isa Tuple + @test PlotsBase.rotate(1.0, 2.0, 5.0, (0, 0)) isa Tuple - star = Plots.makestar(3) - star_scaled = Plots.scale(star, 0.5) + star = PlotsBase.makestar(3) + star_scaled = PlotsBase.scale(star, 0.5) - Plots.scale!(star, 0.5) + PlotsBase.scale!(star, 0.5) @test get_xs(star) == get_xs(star_scaled) @test get_ys(star) == get_ys(star_scaled) - @test Plots.extrema_plus_buffer([1, 2], 0.1) == (0.9, 2.1) + @test PlotsBase.extrema_plus_buffer([1, 2], 0.1) == (0.9, 2.1) end end @@ -113,11 +113,11 @@ end end @testset "Text" begin - t = Plots.PlotText("foo") + t = PlotsBase.PlotText("foo") @test length(t) == 3 - f = Plots.font() - @test Plots.PlotText(nothing).str == "nothing" + f = PlotsBase.font() + @test PlotsBase.PlotText(nothing).str == "nothing" @test text(t).str == "foo" @test text(t, f).str == "foo" @test text("bar", f).str == "bar" @@ -126,32 +126,32 @@ end for rotation in -180:5:180 t = text("foo"; rotation) if abs(rotation) ≤ 45 || abs(rotation) ≥ 135 - @test Plots.is_horizontal(t) + @test PlotsBase.is_horizontal(t) else - @test !Plots.is_horizontal(t) + @test !PlotsBase.is_horizontal(t) end end end @testset "Annotations" begin - ann = Plots.series_annotations(missing) + ann = PlotsBase.series_annotations(missing) - @test Plots.series_annotations(["1" "2"; "3" "4"]) isa AbstractMatrix - @test Plots.series_annotations(10).strs[1].str == "10" - @test Plots.series_annotations(nothing) === nothing - @test Plots.series_annotations(ann) == ann + @test PlotsBase.series_annotations(["1" "2"; "3" "4"]) isa AbstractMatrix + @test PlotsBase.series_annotations(10).strs[1].str == "10" + @test PlotsBase.series_annotations(nothing) === nothing + @test PlotsBase.series_annotations(ann) == ann - @test Plots.annotations(["1" "2"; "3" "4"]) isa AbstractMatrix - @test Plots.annotations(ann) == ann - @test Plots.annotations([ann]) == [ann] - @test Plots.annotations(nothing) == [] + @test PlotsBase.annotations(["1" "2"; "3" "4"]) isa AbstractMatrix + @test PlotsBase.annotations(ann) == ann + @test PlotsBase.annotations([ann]) == [ann] + @test PlotsBase.annotations(nothing) == [] - t = Plots.text("foo") + t = PlotsBase.text("foo") sp = plot(1)[1] - @test Plots.locate_annotation(sp, 1, 2, t) == (1, 2, t) - @test Plots.locate_annotation(sp, 1, 2, 3, t) == (1, 2, 3, t) - @test Plots.locate_annotation(sp, (0.1, 0.2), t) isa Tuple - @test Plots.locate_annotation(sp, (0.1, 0.2, 0.3), t) isa Tuple + @test PlotsBase.locate_annotation(sp, 1, 2, t) == (1, 2, t) + @test PlotsBase.locate_annotation(sp, 1, 2, 3, t) == (1, 2, 3, t) + @test PlotsBase.locate_annotation(sp, (0.1, 0.2), t) isa Tuple + @test PlotsBase.locate_annotation(sp, (0.1, 0.2, 0.3), t) isa Tuple # see github.com/JuliaPlots/Plots.jl/issues/4073 anns = [(["x", "y"], [10, 20], :hexagon) (["a", "b"], [3, 4], :circle)] @@ -169,7 +169,7 @@ end annotate!(sp = 2, (0.03, 0.95), text("Cats&Dogs", :left)) end - for scale in Plots._log_scales + for scale in PlotsBase._log_scales pl = plot(xlim = (1, 10), xscale = scale) annotate!(pl, (0.5, 0.5), "hello") end @@ -201,29 +201,29 @@ end :zguidefontsize, ] # get initial font sizes - initialSizes = [Plots.default(s) for s in sizesToCheck] + initialSizes = [PlotsBase.default(s) for s in sizesToCheck] #scale up font sizes scalefontsizes(2) # get initial font sizes - doubledSizes = [Plots.default(s) for s in sizesToCheck] + doubledSizes = [PlotsBase.default(s) for s in sizesToCheck] @test doubledSizes == initialSizes * 2 # reset font sizes resetfontsizes() - finalSizes = [Plots.default(s) for s in sizesToCheck] + finalSizes = [PlotsBase.default(s) for s in sizesToCheck] @test finalSizes == initialSizes end end @testset "Series Annotations" begin - get_xs = Plots.Shapes.get_xs - get_ys = Plots.Shapes.get_ys - vertices = Plots.Shapes.vertices + get_xs = PlotsBase.Shapes.get_xs + get_ys = PlotsBase.Shapes.get_ys + vertices = PlotsBase.Shapes.vertices square = Shape([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]) @test_logs (:warn, "Unused SeriesAnnotations arg: triangle (Symbol)") begin pl = plot( @@ -314,7 +314,7 @@ end end @testset "Bezier" begin - curve = Plots.BezierCurves.BezierCurve([(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) + curve = PlotsBase.BezierCurves.BezierCurve([(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) @test curve(0.75) == (0.75, 0.375) @test length(coords(curve, 10)) == 10 end diff --git a/test/test_contours.jl b/PlotsBase/test/test_contours.jl similarity index 78% rename from test/test_contours.jl rename to PlotsBase/test/test_contours.jl index 948fd3657..5ec0072c8 100644 --- a/test/test_contours.jl +++ b/PlotsBase/test/test_contours.jl @@ -1,5 +1,5 @@ @testset "check_contour_levels" begin - let check_contour_levels = Plots.Commons.check_contour_levels + let check_contour_levels = PlotsBase.Commons.check_contour_levels @test check_contour_levels(2) === nothing @test check_contour_levels(-1.0:0.2:10.0) === nothing @test check_contour_levels([-100, -2, -1, 0, 1, 2, 100]) === nothing @@ -9,21 +9,21 @@ end end -@testset "Plots.Commons.preprocess_attributes!" begin +@testset "Commons.preprocess_attributes!" begin function equal_after_pipeline(kw) kw′ = deepcopy(kw) - Plots.Commons.preprocess_attributes!(kw′) + PlotsBase.Commons.preprocess_attributes!(kw′) kw == kw′ end @test equal_after_pipeline(KW(:levels => 1)) @test equal_after_pipeline(KW(:levels => 1:10)) @test equal_after_pipeline(KW(:levels => [1.0, 3.0, 5.0])) - @test_throws ArgumentError Plots.Commons.preprocess_attributes!(KW(:levels => 1.0)) - @test_throws ArgumentError Plots.Commons.preprocess_attributes!( + @test_throws ArgumentError PlotsBase.Commons.preprocess_attributes!(KW(:levels => 1.0)) + @test_throws ArgumentError PlotsBase.Commons.preprocess_attributes!( KW(:levels => (1, 2, 3)), ) - @test_throws ArgumentError Plots.Commons.preprocess_attributes!(KW(:levels => -3)) + @test_throws ArgumentError PlotsBase.Commons.preprocess_attributes!(KW(:levels => -3)) end @testset "contour[f]" begin @@ -39,7 +39,7 @@ end @testset "Default number" begin @test contour(x, y, z)[1][1].plotattributes[:levels] == - Plots._series_defaults[:levels] + PlotsBase._series_defaults[:levels] end @testset "Number" begin diff --git a/test/test_dates.jl b/PlotsBase/test/test_dates.jl similarity index 87% rename from test/test_dates.jl rename to PlotsBase/test/test_dates.jl index 68db3cfe4..031c3931a 100644 --- a/test/test_dates.jl +++ b/PlotsBase/test/test_dates.jl @@ -9,8 +9,8 @@ ref_ylims = (y[1], y[end]) ref_xlims = (x[1].instant.periods.value, x[end].instant.periods.value) - @test Plots.ylims(pl) == ref_ylims - @test Plots.xlims(pl) == ref_xlims + @test PlotsBase.ylims(pl) == ref_ylims + @test PlotsBase.xlims(pl) == ref_xlims end @testset "Date xlims" begin @@ -31,5 +31,5 @@ end ref_xlims = map(date -> date.instant.periods.value, span) pl = plot(x, y, xlims = span, widen = false) - @test Plots.xlims(pl) == ref_xlims + @test PlotsBase.xlims(pl) == ref_xlims end diff --git a/test/test_defaults.jl b/PlotsBase/test/test_defaults.jl similarity index 88% rename from test/test_defaults.jl rename to PlotsBase/test/test_defaults.jl index d96f82152..f75fff7af 100644 --- a/test/test_defaults.jl +++ b/PlotsBase/test/test_defaults.jl @@ -1,18 +1,18 @@ const PLOTS_DEFAULTS = Dict(:theme => :wong2, :fontfamily => :palantino) -Plots._plots_theme_defaults() +PlotsBase._plots_theme_defaults() @testset "Loading theme" begin pl = plot(1:5) @test pl[1][1][:seriescolor] == RGBA(colorant"black") - @test Plots.guidefont(pl[1][:xaxis]).family == "palantino" + @test PlotsBase.guidefont(pl[1][:xaxis]).family == "palantino" end empty!(PLOTS_DEFAULTS) -Plots._plots_theme_defaults() +PlotsBase._plots_theme_defaults() @testset "default" begin default(fillrange = 0) - @test Plots._series_defaults[:fillrange] == 0 + @test PlotsBase._series_defaults[:fillrange] == 0 pl = plot(1:5) @test pl[1][1][:fillrange] == 0 @test_nowarn default(legendfont = font(5)) @@ -81,7 +81,7 @@ end #remember settings plot(legend_font_pointsize = 20) sp = plot!(label = "R")[1] - @test Plots.legendfont(sp).pointsize == 20 + @test PlotsBase.legendfont(sp).pointsize == 20 #setting whole font sp = plot( @@ -90,15 +90,15 @@ end legend_font_halign = :left, foreground_color_subplot = :red, )[1] - @test Plots.legendfont(sp).pointsize == 12 - @test Plots.legendfont(sp).halign === :left + @test PlotsBase.legendfont(sp).pointsize == 12 + @test PlotsBase.legendfont(sp).halign === :left # match mechanism @test sp[:legend_font_color] == colorant"black" - @test Plots.legendfont(sp).color == colorant"black" + @test PlotsBase.legendfont(sp).color == colorant"black" @test sp[:foreground_color_subplot] == RGBA(colorant"red") # magic invocation @test_nowarn sp = plot(; legendfont = 12)[1] @test sp[:legend_font_pointsize] == 12 - @test Plots.legendfont(sp).pointsize == 12 + @test PlotsBase.legendfont(sp).pointsize == 12 end diff --git a/test/test_hdf5plots.jl b/PlotsBase/test/test_hdf5plots.jl similarity index 87% rename from test/test_hdf5plots.jl rename to PlotsBase/test/test_hdf5plots.jl index 8430acc9f..c9bf158d0 100644 --- a/test/test_hdf5plots.jl +++ b/PlotsBase/test/test_hdf5plots.jl @@ -1,14 +1,16 @@ +using HDF5 + @testset "HDF5_Plots" begin fname = tempname() * ".hdf5" hdf5() x = 1:10 pl = plot(x, x .^ 2) # create some plot - Plots.hdf5plot_write(pl, fname) + PlotsBase.hdf5plot_write(pl, fname) # read back file gr() # choose some fast backend likely to work in test environment - pread = Plots.hdf5plot_read(fname) + pread = PlotsBase.hdf5plot_read(fname) # make sure data made it through @test pl.subplots[1].series_list[1][:x] == pread.subplots[1].series_list[1][:x] diff --git a/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl similarity index 65% rename from test/test_layouts.jl rename to PlotsBase/test/test_layouts.jl index 8b1510a37..b98df9b4e 100644 --- a/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -73,67 +73,67 @@ end pl = plot(map(plot, 1:4)..., layout = (2, 2)) sp = pl[end] - @test sp isa Plots.Subplot + @test sp isa PlotsBase.Subplot @test size(sp) == (1, 1) @test length(sp) == 1 @test sp[1, 1] == sp - @test Plots.get_subplot(pl, UInt32(4)) == sp - @test Plots.series_list(sp) |> first |> Plots.get_subplot isa Plots.Subplot - @test Plots.get_subplot(pl, keys(pl.spmap) |> first) isa Plots.Subplot + @test PlotsBase.get_subplot(pl, UInt32(4)) == sp + @test PlotsBase.series_list(sp) |> first |> PlotsBase.get_subplot isa PlotsBase.Subplot + @test PlotsBase.get_subplot(pl, keys(pl.spmap) |> first) isa PlotsBase.Subplot gl = pl[2, 2] - @test gl isa Plots.GridLayout + @test gl isa PlotsBase.GridLayout @test length(gl) == 1 @test size(gl) == (1, 1) - @test Plots.layout_attrs(gl) == (gl, 1) + @test PlotsBase.layout_attrs(gl) == (gl, 1) @test size(pl, 1) == 2 @test size(pl, 2) == 2 @test size(pl) == (2, 2) @test ndims(pl) == 2 - @test pl[1][end] isa Plots.Series + @test pl[1][end] isa PlotsBase.Series io = devnull show(io, pl[1]) - @test Plots.getplot(pl) == pl - @test Plots.getattr(pl) == pl.attr - @test Plots.backend_object(pl) == pl.o + @test PlotsBase.getplot(pl) == pl + @test PlotsBase.getattr(pl) == pl.attr + @test PlotsBase.backend_object(pl) == pl.o @test occursin("Plot", string(pl)) print(io, pl) - @test Plots.to_pixels(1Plots.mm) isa AbstractFloat - @test Plots.ispositive(1Plots.mm) - @test size(Plots.DEFAULT_BBOX[]) == (0Plots.mm, 0Plots.mm) - show(io, Plots.DEFAULT_BBOX[]) + @test PlotsBase.to_pixels(1PlotsBase.mm) isa AbstractFloat + @test PlotsBase.ispositive(1PlotsBase.mm) + @test size(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm) + show(io, PlotsBase.DEFAULT_BBOX[]) show(io, pl.layout) - @test Plots.make_measure_hor(1Plots.mm) == 1Plots.mm - @test Plots.make_measure_vert(1Plots.mm) == 1Plots.mm + @test PlotsBase.make_measure_hor(1PlotsBase.mm) == 1PlotsBase.mm + @test PlotsBase.make_measure_vert(1PlotsBase.mm) == 1PlotsBase.mm - @test Plots.parent(pl.layout) isa Plots.RootLayout - show(io, Plots.parent_bbox(pl.layout)) + @test PlotsBase.parent(pl.layout) isa PlotsBase.RootLayout + show(io, PlotsBase.parent_bbox(pl.layout)) - rl = Plots.RootLayout() + rl = PlotsBase.RootLayout() show(io, rl) @test parent(rl) === nothing - @test Plots.parent_bbox(rl) == Plots.DEFAULT_BBOX[] - @test Plots.bbox(rl) == Plots.DEFAULT_BBOX[] - @test Plots.origin(Plots.DEFAULT_BBOX[]) == (0Plots.mm, 0Plots.mm) + @test PlotsBase.parent_bbox(rl) == PlotsBase.DEFAULT_BBOX[] + @test PlotsBase.bbox(rl) == PlotsBase.DEFAULT_BBOX[] + @test PlotsBase.origin(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm) for h_anchor in (:left, :right, :hcenter), v_anchor in (:top, :bottom, :vcenter) - @test Plots.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa Plots.BoundingBox + @test PlotsBase.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa PlotsBase.BoundingBox end - el = Plots.EmptyLayout() - @test Plots.update_position!(el) === nothing + el = PlotsBase.EmptyLayout() + @test PlotsBase.update_position!(el) === nothing @test size(el) == (0, 0) @test length(el) == 0 @test el[1, 1] === nothing - @test Plots.left(el) == 0Plots.mm - @test Plots.top(el) == 0Plots.mm - @test Plots.right(el) == 0Plots.mm - @test Plots.bottom(el) == 0Plots.mm + @test PlotsBase.left(el) == 0PlotsBase.mm + @test PlotsBase.top(el) == 0PlotsBase.mm + @test PlotsBase.right(el) == 0PlotsBase.mm + @test PlotsBase.bottom(el) == 0PlotsBase.mm plot(map(plot, 1:4)..., layout = (2, :)) plot(map(plot, 1:4)..., layout = (:, 2)) diff --git a/test/test_misc.jl b/PlotsBase/test/test_misc.jl similarity index 76% rename from test/test_misc.jl rename to PlotsBase/test/test_misc.jl index f0506a3d9..6a53aad68 100644 --- a/test/test_misc.jl +++ b/PlotsBase/test/test_misc.jl @@ -2,26 +2,26 @@ @testset "Infrastructure" begin @test_nowarn JSON.Parser.parse( - String(read(joinpath(dirname(pathof(Plots)), "..", ".zenodo.json"))), + String(read(joinpath(dirname(pathof(PlotsBase)), "..", "..", ".zenodo.json"))), ) end @testset "Plotly standalone" begin - @test Plots._plotly_local_file_path[] ≡ nothing - temp = Plots._use_local_dependencies[] + @test PlotsBase._plotly_local_file_path[] ≡ nothing + temp = PlotsBase._use_local_dependencies[] withenv("PLOTS_HOST_DEPENDENCY_LOCAL" => true) do - Plots._plots_plotly_defaults() - @test Plots._plotly_local_file_path[] isa String - @test isfile(Plots._plotly_local_file_path[]) - @test Plots._use_local_dependencies[] = true + PlotsBase._plots_plotly_defaults() + @test PlotsBase._plotly_local_file_path[] isa String + @test isfile(PlotsBase._plotly_local_file_path[]) + @test PlotsBase._use_local_dependencies[] = true end - Plots._plotly_local_file_path[] = nothing - Plots._use_local_dependencies[] = temp + PlotsBase._plotly_local_file_path[] = nothing + PlotsBase._use_local_dependencies[] = temp end @testset "NoFail" begin with(:unicodeplots) do - @test backend() == Plots._backend_instance(:unicodeplots) + @test backend() == PlotsBase._backend_instance(:unicodeplots) dsp = TextDisplay(IOContext(IOBuffer(), :color => true)) @@ -43,7 +43,7 @@ end @testset "bar" begin p = bar([3, 2, 1], [1, 2, 3]) - @test p isa Plots.Plot + @test p isa PlotsBase.Plot @test display(dsp, p) isa Nothing end @@ -64,23 +64,23 @@ end end @testset "bool_env" begin - @test Plots.bool_env("FOO", "true") - @test Plots.bool_env("FOO", "1") - @test !Plots.bool_env("FOO", "false") - @test !Plots.bool_env("FOO", "0") + @test PlotsBase.bool_env("FOO", "true") + @test PlotsBase.bool_env("FOO", "1") + @test !PlotsBase.bool_env("FOO", "false") + @test !PlotsBase.bool_env("FOO", "0") end @testset "Themes" begin - @test showtheme(:dark) isa Plots.Plot + @test showtheme(:dark) isa PlotsBase.Plot end @testset "maths" begin - @test Plots.floor_base(15.0, 10.0) ≈ 10 - @test Plots.ceil_base(15.0, 10.0) ≈ 10^2 - @test Plots.floor_base(4.2, 2.0) ≈ 2^2 - @test Plots.ceil_base(4.2, 2.0) ≈ 2^3 - @test Plots.floor_base(1.5 * ℯ, ℯ) ≈ ℯ - @test Plots.ceil_base(1.5 * ℯ, ℯ) ≈ ℯ^2 + @test PlotsBase.floor_base(15.0, 10.0) ≈ 10 + @test PlotsBase.ceil_base(15.0, 10.0) ≈ 10^2 + @test PlotsBase.floor_base(4.2, 2.0) ≈ 2^2 + @test PlotsBase.ceil_base(4.2, 2.0) ≈ 2^3 + @test PlotsBase.floor_base(1.5 * ℯ, ℯ) ≈ ℯ + @test PlotsBase.ceil_base(1.5 * ℯ, ℯ) ≈ ℯ^2 end @testset "plotattr" begin @@ -97,21 +97,23 @@ end str = join(readlines(tmp), "") @test occursin("seriestype", str) @test occursin("Plot attributes", str) - @test Plots.attrtypes() == "Series, Subplot, Plot, Axis" + @test PlotsBase.attrtypes() == "Series, Subplot, Plot, Axis" end @testset "legend" begin @test isa( - Plots.legend_pos_from_angle(20, 0.0, 0.5, 1.0, 0.0, 0.5, 1.0), + PlotsBase.legend_pos_from_angle(20, 0.0, 0.5, 1.0, 0.0, 0.5, 1.0), NTuple{2,<:AbstractFloat}, ) - @test Plots.legend_anchor_index(-1) == 1 - @test Plots.legend_anchor_index(+0) == 2 - @test Plots.legend_anchor_index(+1) == 3 - - @test Plots.legend_angle(:foo_bar) == (45, :inner) - @test Plots.legend_angle(20.0) == Plots.legend_angle((20.0, :inner)) == (20.0, :inner) - @test Plots.legend_angle((20.0, 10.0)) == (20.0, 10.0) + @test PlotsBase.legend_anchor_index(-1) == 1 + @test PlotsBase.legend_anchor_index(+0) == 2 + @test PlotsBase.legend_anchor_index(+1) == 3 + + @test PlotsBase.legend_angle(:foo_bar) == (45, :inner) + @test PlotsBase.legend_angle(20.0) == + PlotsBase.legend_angle((20.0, :inner)) == + (20.0, :inner) + @test PlotsBase.legend_angle((20.0, 10.0)) == (20.0, 10.0) end @testset "axis letter" begin @@ -204,21 +206,24 @@ end end @testset "Measures" begin - @test 1Plots.mm * 0.1Plots.pct == 0.1Plots.mm - @test 0.1Plots.pct * 1Plots.mm == 0.1Plots.mm - @test 1Plots.mm / 0.1Plots.pct == 10Plots.mm - @test 0.1Plots.pct / 1Plots.mm == 10Plots.mm + @test 1PlotsBase.mm * 0.1PlotsBase.pct == 0.1PlotsBase.mm + @test 0.1PlotsBase.pct * 1PlotsBase.mm == 0.1PlotsBase.mm + @test 1PlotsBase.mm / 0.1PlotsBase.pct == 10PlotsBase.mm + @test 0.1PlotsBase.pct / 1PlotsBase.mm == 10PlotsBase.mm end @testset "docstring" begin - @test occursin("label", Plots._generate_doclist(Plots.Commons._all_series_attrs)) + @test occursin( + "label", + PlotsBase._generate_doclist(PlotsBase.Commons._all_series_attrs), + ) end @testset "protect" begin # not sure what is intended here ... protected = protect([:red, :blue]) @test !isempty(protected) - @test scatter(1:2, color = protected) isa Plots.Plot + @test scatter(1:2, color = protected) isa PlotsBase.Plot end @testset "group" begin @@ -227,11 +232,11 @@ end b = repeat(["low", "high"], inner = 2, outer = 3) c = repeat(1:2, outer = 6) d = [1, 1, 1, 2, 2, 2, 2, 4, 3, 3, 3, 6] - @test plot(b, d, group = (c, a), layout = (1, 3)) isa Plots.Plot + @test plot(b, d, group = (c, a), layout = (1, 3)) isa PlotsBase.Plot end @testset "skipissing" begin - @test plot(skipmissing(1:5)) isa Plots.Plot + @test plot(skipmissing(1:5)) isa PlotsBase.Plot end with(:gr) do @@ -252,19 +257,19 @@ with(:gr) do end @testset "recipes" begin - @test Plots.seriestype_supported(:path) ≡ :native + @test PlotsBase.seriestype_supported(:path) ≡ :native - @test plot([1, 2, 5], seriestype = :linearfit) isa Plots.Plot - @test plot([1, 2, 5], seriestype = :scatterpath) isa Plots.Plot - @test plot(1:2, 1:2, 1:2, seriestype = :scatter3d) isa Plots.Plot + @test plot([1, 2, 5], seriestype = :linearfit) isa PlotsBase.Plot + @test plot([1, 2, 5], seriestype = :scatterpath) isa PlotsBase.Plot + @test plot(1:2, 1:2, 1:2, seriestype = :scatter3d) isa PlotsBase.Plot let pl = plot(1:2, -1:1, widen = false) - Plots.abline!([0, 3], [5, -5]) + PlotsBase.abline!([0, 3], [5, -5]) @test xlims(pl) == (+1, +2) @test ylims(pl) == (-1, +1) end - @test Plots.findnz([0 1; 2 0]) == ([2, 1], [1, 2], [2, 1]) + @test PlotsBase.findnz([0 1; 2 0]) == ([2, 1], [1, 2], [2, 1]) end @testset "mesh3d" begin @@ -312,7 +317,7 @@ with(:gr) do end @testset "fillstyle" begin - @test histogram(rand(10); fillstyle = :/) isa Plots.Plot + @test histogram(rand(10); fillstyle = :/) isa PlotsBase.Plot end @testset "showable" begin @@ -324,11 +329,12 @@ with(:gr) do end @testset "legends" begin - @test plot([0:1 reverse(0:1)]; labels = ["a" "b"], leg = (0.5, 0.5)) isa Plots.Plot + @test plot([0:1 reverse(0:1)]; labels = ["a" "b"], leg = (0.5, 0.5)) isa + PlotsBase.Plot @test plot([0:1 reverse(0:1)]; labels = ["a" "b"], leg = (0.5, :outer)) isa - Plots.Plot + PlotsBase.Plot @test plot([0:1 reverse(0:1)]; labels = ["a" "b"], leg = (0.5, :inner)) isa - Plots.Plot + PlotsBase.Plot @test_logs (:warn, r"n° of legend_column.*") png( plot(1:2, legend_columns = 10), tempname(), diff --git a/test/test_output.jl b/PlotsBase/test/test_output.jl similarity index 59% rename from test/test_output.jl rename to PlotsBase/test/test_output.jl index 75cacad06..b929e9845 100644 --- a/test/test_output.jl +++ b/PlotsBase/test/test_output.jl @@ -1,9 +1,9 @@ macro test_save(fmt) quote let pl = plot(1:2), fn = tempname(), fp = tmpname() # fp is an AbstractPath from FilePathsBase.jl - getfield(Plots, $fmt)(pl, fn) - getfield(Plots, $fmt)(fn) - getfield(Plots, $fmt)(fp) + getfield(PlotsBase, $fmt)(pl, fn) + getfield(PlotsBase, $fmt)(fn) + getfield(PlotsBase, $fmt)(fp) fn_ext = string(fn, '.', $fmt) fp_ext = string(fp, '.', $fmt) @@ -22,16 +22,16 @@ macro test_save(fmt) end let pl = plot(1:2), io = PipeBuffer() - getfield(Plots, $fmt)(pl, io) - getfield(Plots, $fmt)(io) + getfield(PlotsBase, $fmt)(pl, io) + getfield(PlotsBase, $fmt)(io) @test length(io.data) > 10 end end |> esc end with(:gr) do - @test Plots.default_output_format(plot()) == "png" - @test Plots.addExtension("foo", "bar") == "foo.bar" + @test PlotsBase.default_output_format(plot()) == "png" + @test PlotsBase.addExtension("foo", "bar") == "foo.bar" @test_save :png @test_save :pdf @@ -41,27 +41,28 @@ end with(:unicodeplots) do @test_save :txt - get_font_face = - Base.get_extension(Plots, :PlotsUnicodePlotsExt).UnicodePlots.get_font_face - if get_font_face() ≢ nothing + UnicodePlots = Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlots + if UnicodePlots.get_font_face() ≢ nothing @test_save :png end end -with(:plotlyjs) do - @test_save :html - @test_save :json - @test_save :pdf - @test_save :png - @test_save :svg - # @test_save :eps -end +if Sys.isunix() + with(:plotlyjs) do + @test_save :html + @test_save :json + @test_save :pdf + @test_save :png + @test_save :svg + # @test_save :eps + end -with(:plotly) do - @test_save :pdf - @test_save :png - @test_save :svg - @test_save :html + with(:plotly) do + @test_save :pdf + @test_save :png + @test_save :svg + @test_save :html + end end if Sys.islinux() && Sys.which("pdflatex") ≢ nothing @@ -71,16 +72,15 @@ if Sys.islinux() && Sys.which("pdflatex") ≢ nothing @test_save :pdf end - # with(:pythonplot) do - # @test_save :pdf - # @test_save :png - # @test_save :svg - # @test_save :eps - # @test_save :ps - # end + with(:pythonplot) do + @test_save :pdf + @test_save :png + @test_save :svg + @test_save :eps + @test_save :ps + end end -#= with(:gaston) do @test_save :png @test_save :pdf @@ -88,26 +88,18 @@ with(:gaston) do @test_save :svg end -with(:inspectdr) do - @test_save :png - @test_save :pdf - @test_save :eps - @test_save :svg -end -=# - @testset "html" begin with(:gr) do io = PipeBuffer() pl = plot(1:2) pl.attr[:html_output_format] = :auto - Plots._show(io, MIME("text/html"), pl) + PlotsBase._show(io, MIME("text/html"), pl) pl.attr[:html_output_format] = :png - Plots._show(io, MIME("text/html"), pl) + PlotsBase._show(io, MIME("text/html"), pl) pl.attr[:html_output_format] = :svg - Plots._show(io, MIME("text/html"), pl) + PlotsBase._show(io, MIME("text/html"), pl) pl.attr[:html_output_format] = :txt - Plots._show(io, MIME("text/html"), pl) + PlotsBase._show(io, MIME("text/html"), pl) end end diff --git a/test/test_pgfplotsx.jl b/PlotsBase/test/test_pgfplotsx.jl similarity index 86% rename from test/test_pgfplotsx.jl rename to PlotsBase/test/test_pgfplotsx.jl index 8eb602e6e..81dde6ea1 100644 --- a/test/test_pgfplotsx.jl +++ b/PlotsBase/test/test_pgfplotsx.jl @@ -1,5 +1,5 @@ -using Test, Plots, Unitful, LaTeXStrings -import PGFPlotsX +using Test, PlotsBase, Unitful, LaTeXStrings +const PGFPlotsX = Base.get_extension(PlotsBase, :PGFPlotsXExt).PGFPlotsX function create_plot(args...; kwargs...) pl = plot(args...; kwargs...) @@ -12,8 +12,8 @@ function create_plot!(args...; kwargs...) end function get_pgf_axes(pl) - Plots._update_plot_object(pl) - Plots.get_backend_module(:PGFPlotsX)[1].pgfx_axes(pl.o) + PlotsBase._update_plot_object(pl) + PlotsBase.get_backend_module(:PGFPlotsX)[1].pgfx_axes(pl.o) end with(:pgfplotsx) do @@ -23,8 +23,8 @@ with(:pgfplotsx) do @test pl.series_list[1].plotattributes[:quiver] === nothing @test count(x -> x isa PGFPlotsX.Plot, axis.contents) == 1 @test !haskey(axis.contents[1].options.dict, "fill") - @test occursin("documentclass", Plots.pgfx_preamble(pl)) - @test occursin("documentclass", Plots.pgfx_preamble()) + @test occursin("documentclass", PlotsBase.pgfx_preamble(pl)) + @test occursin("documentclass", PlotsBase.pgfx_preamble()) @testset "Legends" begin pl = plot(rand(5, 2), lab = ["1" ""], arrow = true) @@ -52,7 +52,7 @@ with(:pgfplotsx) do y, z, zcolor = reverse(z), - m = (10, 0.8, :blues, Plots.stroke(0)), + m = (10, 0.8, :blues, PlotsBase.stroke(0)), leg = false, cbar = true, w = 5, @@ -78,7 +78,7 @@ with(:pgfplotsx) do pl = scatter!( y, zcolor = abs.(y .- 0.5), - m = (:hot, 0.8, Plots.stroke(1, :green)), + m = (:hot, 0.8, PlotsBase.stroke(1, :green)), ms = 10 * abs.(y .- 0.5) .+ 4, lab = ["grad", "", "ient"], ) @@ -107,9 +107,12 @@ with(:pgfplotsx) do end @testset "Marker types" begin - markers = filter((m -> begin - m in Plots.supported_markers() - end), Plots.Commons._shape_keys) + markers = filter( + (m -> begin + m in PlotsBase.supported_markers() + end), + PlotsBase.Commons._shape_keys, + ) markers = reshape(markers, 1, length(markers)) n = length(markers) x = (range(0, stop = 10, length = n + 2))[2:(end - 1)] @@ -122,22 +125,22 @@ with(:pgfplotsx) do bg = :linen, xlim = (0, 10), ylim = (0, 10), - ) isa Plots.Plot + ) isa PlotsBase.Plot end @testset "Layout" begin @test plot( - Plots.fakedata(100, 10), + PlotsBase.fakedata(100, 10), layout = 4, palette = [:grays :blues :hot :rainbow], bg_inside = [:orange :pink :darkblue :black], - ) isa Plots.Plot + ) isa PlotsBase.Plot end @testset "Polar plots" begin Θ = range(0, stop = 1.5π, length = 100) r = abs.(0.1 * randn(100) + sin.(3Θ)) - @test plot(Θ, r, proj = :polar, m = 2) isa Plots.Plot + @test plot(Θ, r, proj = :polar, m = 2) isa PlotsBase.Plot end @testset "Drawing shapes" begin @@ -173,11 +176,11 @@ with(:pgfplotsx) do xlim = (0, 1), ylim = (0, 1), leg = false, - ) isa Plots.Plot + ) isa PlotsBase.Plot end @testset "Histogram 2D" begin - @test histogram2d(randn(10_000), randn(10_000), nbins = 20) isa Plots.Plot + @test histogram2d(randn(10_000), randn(10_000), nbins = 20) isa PlotsBase.Plot end @testset "Heatmap-like" begin @@ -191,7 +194,7 @@ with(:pgfplotsx) do @test axis["colormap name"] == "plots1" end - @test wireframe(xs, ys, z, aspect_ratio = 1) isa Plots.Plot + @test wireframe(xs, ys, z, aspect_ratio = 1) isa PlotsBase.Plot # TODO: clims are wrong end @@ -205,8 +208,8 @@ with(:pgfplotsx) do p2 = contour(x, y, Z) p1 = contour(x, y, f, fill = true) p3 = contour3d(x, y, Z) - @test plot(p1, p2) isa Plots.Plot - @test_nowarn Plots._update_plot_object(p3) + @test plot(p1, p2) isa PlotsBase.Plot + @test_nowarn PlotsBase._update_plot_object(p3) # TODO: colorbar for filled contours end @@ -217,7 +220,7 @@ with(:pgfplotsx) do y = t .* sin.(θ) p1 = plot(x, y, line_z = t, linewidth = 3, legend = false) p2 = scatter(x, y, marker_z = (x, y) -> x + y, color = :bwr, legend = false) - @test plot(p1, p2) isa Plots.Plot + @test plot(p1, p2) isa PlotsBase.Plot end @testset "Framestyles" begin @@ -253,7 +256,7 @@ with(:pgfplotsx) do u = ones(length(x)) v = cos.(x) pl = plot(x, y, quiver = (u, v), arrow = true) - @test pl isa Plots.Plot + @test pl isa PlotsBase.Plot # TODO: could adjust limits to fit arrows if too long, but how ? # mktempdir() do path # @test_nowarn savefig(pl, path*"arrow.pdf") @@ -262,7 +265,7 @@ with(:pgfplotsx) do @testset "Annotations" begin y = rand(10) - ann = (3, y[3], Plots.text("this is \\#3", :left)) + ann = (3, y[3], PlotsBase.text("this is \\#3", :left)) pl = plot(y, annotations = ann, leg = false) axis_content = first(get_pgf_axes(pl)).contents nodes = filter(x -> !isa(x, PGFPlotsX.Plot), axis_content) @@ -276,8 +279,8 @@ with(:pgfplotsx) do end end annotate!([ - (5, y[5], Plots.text("this is \\#5", 16, :red, :center)), - (10, y[10], Plots.text("this is \\#10", :right, 20, "courier")), + (5, y[5], PlotsBase.text("this is \\#5", 16, :red, :center)), + (10, y[10], PlotsBase.text("this is \\#10", :right, 20, "courier")), ]) axis_content = first(get_pgf_axes(pl)).contents nodes = filter(x -> !isa(x, PGFPlotsX.Plot), axis_content) @@ -300,7 +303,7 @@ with(:pgfplotsx) do "map", "to", "series", - Plots.text("data", :green), + PlotsBase.text("data", :green), ], ) axis_content = first(get_pgf_axes(pl)).contents @@ -430,22 +433,22 @@ with(:pgfplotsx) do end @testset "Latexify - LaTeXStrings" begin - @test Plots.pgfx_sanitize_string("A string, with 2 punctuation chars.") == + @test PlotsBase.pgfx_sanitize_string("A string, with 2 punctuation chars.") == "A string, with 2 punctuation chars." - @test Plots.pgfx_sanitize_string("Interpolação polinomial") == + @test PlotsBase.pgfx_sanitize_string("Interpolação polinomial") == raw"Interpola$\textnormal{\c{c}}$$\textnormal{\~{a}}$o polinomial" - @test Plots.pgfx_sanitize_string("∫∞ ∂x") == raw"$\int$$\infty$ $\partial$x" + @test PlotsBase.pgfx_sanitize_string("∫∞ ∂x") == raw"$\int$$\infty$ $\partial$x" # special LaTeX characters - @test Plots.pgfx_sanitize_string("this is #3").s == raw"this is \#3" - @test Plots.pgfx_sanitize_string("10% increase").s == raw"10\% increase" - @test Plots.pgfx_sanitize_string("underscores _a_").s == raw"underscores \_a\_" - @test Plots.pgfx_sanitize_string("plot 1 & 2 & 3").s == raw"plot 1 \& 2 \& 3" - @test Plots.pgfx_sanitize_string("GDP in \$").s == raw"GDP in \$" - @test Plots.pgfx_sanitize_string("curly { test }").s == raw"curly \{ test \}" - - @test Plots.pgfx_sanitize_string(L"this is #5").s == raw"$this is \#5$" - @test Plots.pgfx_sanitize_string(L"10% increase").s == raw"$10\% increase$" + @test PlotsBase.pgfx_sanitize_string("this is #3").s == raw"this is \#3" + @test PlotsBase.pgfx_sanitize_string("10% increase").s == raw"10\% increase" + @test PlotsBase.pgfx_sanitize_string("underscores _a_").s == raw"underscores \_a\_" + @test PlotsBase.pgfx_sanitize_string("plot 1 & 2 & 3").s == raw"plot 1 \& 2 \& 3" + @test PlotsBase.pgfx_sanitize_string("GDP in \$").s == raw"GDP in \$" + @test PlotsBase.pgfx_sanitize_string("curly { test }").s == raw"curly \{ test \}" + + @test PlotsBase.pgfx_sanitize_string(L"this is #5").s == raw"$this is \#5$" + @test PlotsBase.pgfx_sanitize_string(L"10% increase").s == raw"$10\% increase$" end @testset "Setting correct plot titles" begin @@ -457,10 +460,10 @@ with(:pgfplotsx) do if Sys.islinux() && Sys.which("pdflatex") ≢ nothing @testset "Issues - actually compile `.tex`" begin - # Plots.jl/issues/4308 + # PlotsBase.jl/issues/4308 fn = tempname() * ".pdf" pl = plot((1:10) .^ 2, (1:10) .^ 2, xscale = :log10) - Plots.pdf(pl, fn) + PlotsBase.pdf(pl, fn) @test isfile(fn) end end diff --git a/test/test_plotly.jl b/PlotsBase/test/test_plotly.jl similarity index 66% rename from test/test_plotly.jl rename to PlotsBase/test/test_plotly.jl index dedc72e36..ee5fc6c75 100644 --- a/test/test_plotly.jl +++ b/PlotsBase/test/test_plotly.jl @@ -1,12 +1,12 @@ -using Plots, Test -with(:plotly) do +using PlotsBase, Test +Sys.isunix() && with(:plotly) do @testset "Basic" begin - @test backend() == Plots.PlotlyBackend() + @test backend() == PlotsBase.PlotlyBackend() pl = plot(rand(10)) - @test pl isa Plots.Plot - @test_nowarn Plots.plotly_series(plot()) - @test !haskey(Plots.plotly_series(pl)[1], :zmax) + @test pl isa PlotsBase.Plot + @test_nowarn PlotsBase.plotly_series(plot()) + @test !haskey(PlotsBase.plotly_series(pl)[1], :zmax) end @testset "Contours" begin @@ -16,12 +16,12 @@ with(:plotly) do @testset "Contour numbers" begin @testset "Default" begin - @test Plots.plotly_series(contour(x, y, z))[1][:ncontours] == - Plots._series_defaults[:levels] + 2 + @test PlotsBase.plotly_series(contour(x, y, z))[1][:ncontours] == + PlotsBase._series_defaults[:levels] + 2 end @testset "Specified number" begin cont = contour(x, y, z, levels = 10) - @test Plots.plotly_series(cont)[1][:ncontours] == 12 + @test PlotsBase.plotly_series(cont)[1][:ncontours] == 12 end end @@ -30,9 +30,9 @@ with(:plotly) do levels = -1:0.5:1 pl = contour(x, y, z, levels = levels) @test pl[1][1].plotattributes[:levels] == levels - @test Plots.plotly_series(pl)[1][:contours][:start] == first(levels) - @test Plots.plotly_series(pl)[1][:contours][:end] == last(levels) - @test Plots.plotly_series(pl)[1][:contours][:size] == step(levels) + @test PlotsBase.plotly_series(pl)[1][:contours][:start] == first(levels) + @test PlotsBase.plotly_series(pl)[1][:contours][:end] == last(levels) + @test PlotsBase.plotly_series(pl)[1][:contours][:size] == step(levels) end @testset "Set of contours" begin @@ -49,7 +49,7 @@ with(:plotly) do approximate number of contours with the keyword `levels`. Setting levels to -1.0:0.5:1.0 """, - ) Plots.plotly_series(pl) + ) PlotsBase.plotly_series(pl) @test series_dict[1][:contours][:start] == first(levels_range) @test series_dict[1][:contours][:end] == last(levels_range) @test series_dict[1][:contours][:size] == step(levels_range) @@ -59,8 +59,8 @@ with(:plotly) do @testset "Extra kwargs" begin pl = plot(1:5, test = "me") - @test Plots.plotly_series(pl)[1][:test] == "me" + @test PlotsBase.plotly_series(pl)[1][:test] == "me" pl = plot(1:5, test = "me", extra_kwargs = :plot) - @test Plots.plotly_layout(pl)[:test] == "me" + @test PlotsBase.plotly_layout(pl)[:test] == "me" end end diff --git a/test/test_quality.jl b/PlotsBase/test/test_quality.jl similarity index 83% rename from test/test_quality.jl rename to PlotsBase/test/test_quality.jl index bc5599ed6..aba35594e 100644 --- a/test/test_quality.jl +++ b/PlotsBase/test/test_quality.jl @@ -3,7 +3,7 @@ # TODO: fix :Contour, :Latexify and :LaTeXStrings stale imports in Plots 2.0 # :PyCall and :Conda stale deps show up when running CI Aqua.test_all( - Plots; + PlotsBase; stale_deps = (; ignore = [ :GR, @@ -19,5 +19,5 @@ deps_compat = false, # FIXME: fails `CondaPkg` piracies = false, ) - Aqua.test_ambiguities(Plots; exclude = [RecipesBase.apply_recipe]) # FIXME: remaining ambiguities + Aqua.test_ambiguities(PlotsBase; exclude = [RecipesBase.apply_recipe]) # FIXME: remaining ambiguities end diff --git a/test/test_recipes.jl b/PlotsBase/test/test_recipes.jl similarity index 78% rename from test/test_recipes.jl rename to PlotsBase/test/test_recipes.jl index b4ac1e368..86cd0e007 100644 --- a/test/test_recipes.jl +++ b/PlotsBase/test/test_recipes.jl @@ -29,20 +29,20 @@ end @testset "vline, vspan" begin vl = vline([1], widen = false) - @test Plots.xlims(vl) == (1, 2) - @test Plots.ylims(vl) == (1, 2) + @test PlotsBase.xlims(vl) == (1, 2) + @test PlotsBase.ylims(vl) == (1, 2) vl = vline([1], xlims = (0, 2), widen = false) - @test Plots.xlims(vl) == (0, 2) + @test PlotsBase.xlims(vl) == (0, 2) vl = vline([1], ylims = (-3, 5), widen = false) - @test Plots.ylims(vl) == (-3, 5) + @test PlotsBase.ylims(vl) == (-3, 5) vsp = vspan([1, 3], widen = false) - @test Plots.xlims(vsp) == (1, 3) - @test Plots.ylims(vsp) == (0, 1) # TODO: might be problematic on log-scales + @test PlotsBase.xlims(vsp) == (1, 3) + @test PlotsBase.ylims(vsp) == (0, 1) # TODO: might be problematic on log-scales vsp = vspan([1, 3], xlims = (-2, 5), widen = false) - @test Plots.xlims(vsp) == (-2, 5) + @test PlotsBase.xlims(vsp) == (-2, 5) vsp = vspan([1, 3], ylims = (-2, 5), widen = false) - @test Plots.ylims(vsp) == (-2, 5) + @test PlotsBase.ylims(vsp) == (-2, 5) end @testset "steps offset" begin @@ -75,16 +75,16 @@ end # NOTE: the following test seems to trigger these deprecated warnings: # WARNING: importing deprecated binding Colors.RGB1 into PlotUtils. -# WARNING: importing deprecated binding Colors.RGB1 into Plots. +# WARNING: importing deprecated binding Colors.RGB1 into PlotsBase. @testset "framestyle axes" begin pl = plot(-1:1, -1:1, -1:1) sp = pl.subplots[1] - defaultret = Plots.axis_drawing_info_3d(sp, :x) + defaultret = PlotsBase.axis_drawing_info_3d(sp, :x) for letter in (:x, :y, :z) for framestyle in [:box :semi :origin :zerolines :grid :none] prevha = UInt64(0) push!(sp.attr, :framestyle => framestyle) - ret = Plots.axis_drawing_info_3d(sp, letter) + ret = PlotsBase.axis_drawing_info_3d(sp, letter) ha = hash(string(ret)) @test ha != prevha prevha = ha @@ -95,11 +95,11 @@ end @testset "coverage" begin # TODO: that should cover all seriestypes without the need to have the extension loaded # currently uses plotly seriestypes only - @test :surface in Plots.all_seriestypes() - unicode_instance = Plots._backend_instance(:unicodeplots) - @test Plots.seriestype_supported(unicode_instance, :surface) === :native - @test Plots.seriestype_supported(unicode_instance, :hspan) === :recipe - @test Plots.seriestype_supported(Plots.NoBackend(), :line) === :native + @test :surface in PlotsBase.all_seriestypes() + unicode_instance = PlotsBase._backend_instance(:unicodeplots) + @test PlotsBase.seriestype_supported(unicode_instance, :surface) === :native + @test PlotsBase.seriestype_supported(unicode_instance, :hspan) === :recipe + @test PlotsBase.seriestype_supported(PlotsBase.NoBackend(), :line) === :native end with(:gr) do @@ -107,7 +107,7 @@ with(:gr) do x = y = 1:10 yerror = fill(1, length(y)) xerror = fill(0.2, length(x)) - p = Plots.xerror(x, y; xerror, linestyle = :solid) + p = PlotsBase.xerror(x, y; xerror, linestyle = :solid) plot!(p, x, y; linestyle = :dash) yerror!(p, x, y; yerror, linestyle = :dot) @test length(p.series_list) == 3 @@ -117,8 +117,8 @@ with(:gr) do end @testset "parametric" begin - @test plot(sin, sin, cos, 0, 2π) isa Plots.Plot - @test plot(sin, sin, cos, collect((-2π):(π / 4):(2π))) isa Plots.Plot + @test plot(sin, sin, cos, 0, 2π) isa PlotsBase.Plot + @test plot(sin, sin, cos, collect((-2π):(π / 4):(2π))) isa PlotsBase.Plot end @testset "dict" begin diff --git a/test/test_shorthands.jl b/PlotsBase/test/test_shorthands.jl similarity index 98% rename from test/test_shorthands.jl rename to PlotsBase/test/test_shorthands.jl index 8b3846583..fb6a96703 100644 --- a/test/test_shorthands.jl +++ b/PlotsBase/test/test_shorthands.jl @@ -97,7 +97,7 @@ end pl = plot3d([1, 2], [1, 2], [1, 2]) plot3d!(pl, [3, 4], [3, 4], [3, 4]) - @test Plots.series_list(pl[1])[1][:seriestype] === :path3d + @test PlotsBase.series_list(pl[1])[1][:seriestype] === :path3d end @testset "Set Ticks" begin diff --git a/test/test_unitful.jl b/PlotsBase/test/test_unitful.jl similarity index 84% rename from test/test_unitful.jl rename to PlotsBase/test/test_unitful.jl index efcde6e2d..69f3a2d40 100644 --- a/test/test_unitful.jl +++ b/PlotsBase/test/test_unitful.jl @@ -1,4 +1,4 @@ -using Plots, Test +using PlotsBase, Test using Unitful using Unitful: m, cm, s, DimensionError # Some helper functions to access the subplot labels and the series inside each test plot @@ -12,7 +12,7 @@ zseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[: testfile = tempname() * ".png" macro isplot(ex) # @isplot macro to streamline tests - :(@test $(esc(ex)) isa Plots.Plot) + :(@test $(esc(ex)) isa PlotsBase.Plot) end @testset "heatmap" begin @@ -151,30 +151,30 @@ end x, y = randn(3), randn(3) @testset "plot(f, x) / plot(x, f)" begin f(x) = x^2 - @test plot(f, x * m) isa Plots.Plot - @test plot(x * m, f) isa Plots.Plot + @test plot(f, x * m) isa PlotsBase.Plot + @test plot(x * m, f) isa PlotsBase.Plot g(x) = x * m # If the unit comes from the function only then it throws @test_throws DimensionError plot(x, g) @test_throws DimensionError plot(g, x) end @testset "plot(x, y, f)" begin f(x, y) = x * y - @test plot(x * m, y * s, f) isa Plots.Plot - @test plot(x * m, y, f) isa Plots.Plot - @test plot(x, y * s, f) isa Plots.Plot + @test plot(x * m, y * s, f) isa PlotsBase.Plot + @test plot(x * m, y, f) isa PlotsBase.Plot + @test plot(x, y * s, f) isa PlotsBase.Plot g(x, y) = x * y * m # If the unit comes from the function only then it throws @test_throws DimensionError plot(x, y, g) end @testset "plot(f, u)" begin f(x) = x^2 pl = plot(x * m, f.(x * m)) - @test plot!(pl, f, m) isa Plots.Plot + @test plot!(pl, f, m) isa PlotsBase.Plot @test_throws DimensionError plot!(pl, f, s) pl = plot(f, m) @test xguide(pl) == string(m) @test yguide(pl) == string(m^2) g(x) = exp(x / (3m)) - @test plot(g, u"m") isa Plots.Plot + @test plot(g, u"m") isa PlotsBase.Plot end end @@ -190,47 +190,48 @@ end end @testset "One array" begin - @test plot(x * m) isa Plots.Plot - @test plot(x * m, ylabel = "x") isa Plots.Plot - @test plot(x * m, ylims = (-1, 1)) isa Plots.Plot - @test plot(x * m, ylims = (-1, 1) .* m) isa Plots.Plot - @test plot(x * m, yunit = u"km") isa Plots.Plot - @test plot(x * m, xticks = (1:3) * m) isa Plots.Plot + @test plot(x * m) isa PlotsBase.Plot + @test plot(x * m, ylabel = "x") isa PlotsBase.Plot + @test plot(x * m, ylims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, ylims = (-1, 1) .* m) isa PlotsBase.Plot + @test plot(x * m, yunit = u"km") isa PlotsBase.Plot + @test plot(x * m, xticks = (1:3) * m) isa PlotsBase.Plot end @testset "Two arrays" begin - @test plot(x * m, y * s) isa Plots.Plot - @test plot(x * m, y * s, xlabel = "x") isa Plots.Plot - @test plot(x * m, y * s, xlims = (-1, 1)) isa Plots.Plot - @test plot(x * m, y * s, xlims = (-1, 1) .* m) isa Plots.Plot - @test plot(x * m, y * s, xunit = u"km") isa Plots.Plot - @test plot(x * m, y * s, ylabel = "y") isa Plots.Plot - @test plot(x * m, y * s, ylims = (-1, 1)) isa Plots.Plot - @test plot(x * m, y * s, ylims = (-1, 1) .* s) isa Plots.Plot - @test plot(x * m, y * s, yunit = u"ks") isa Plots.Plot - @test plot(x * m, y * s, yticks = (1:3) * s) isa Plots.Plot - @test scatter(x * m, y * s) isa Plots.Plot + @test plot(x * m, y * s) isa PlotsBase.Plot + @test plot(x * m, y * s, xlabel = "x") isa PlotsBase.Plot + @test plot(x * m, y * s, xlims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, y * s, xlims = (-1, 1) .* m) isa PlotsBase.Plot + @test plot(x * m, y * s, xunit = u"km") isa PlotsBase.Plot + @test plot(x * m, y * s, ylabel = "y") isa PlotsBase.Plot + @test plot(x * m, y * s, ylims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, y * s, ylims = (-1, 1) .* s) isa PlotsBase.Plot + @test plot(x * m, y * s, yunit = u"ks") isa PlotsBase.Plot + @test plot(x * m, y * s, yticks = (1:3) * s) isa PlotsBase.Plot + @test scatter(x * m, y * s) isa PlotsBase.Plot if dtype ≠ Symbol("Vectors of vectors") - @test scatter(x * m, y * s, zcolor = z * (m / s)) isa Plots.Plot + @test scatter(x * m, y * s, zcolor = z * (m / s)) isa PlotsBase.Plot end end @testset "Three arrays" begin - @test plot(x * m, y * s, z * (m / s)) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), xlabel = "x") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), xlims = (-1, 1)) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), xlims = (-1, 1) .* m) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), xunit = u"km") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), ylabel = "y") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), ylims = (-1, 1)) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), ylims = (-1, 1) .* s) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), yunit = u"ks") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), zlabel = "z") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), zlims = (-1, 1)) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), zlims = (-1, 1) .* (m / s)) isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), zunit = u"km/hr") isa Plots.Plot - @test plot(x * m, y * s, z * (m / s), zticks = (1:2) * m / s) isa Plots.Plot - @test scatter(x * m, y * s, z * (m / s)) isa Plots.Plot + @test plot(x * m, y * s, z * (m / s)) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), xlabel = "x") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), xlims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), xlims = (-1, 1) .* m) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), xunit = u"km") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), ylabel = "y") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), ylims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), ylims = (-1, 1) .* s) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), yunit = u"ks") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), zlabel = "z") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), zlims = (-1, 1)) isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), zlims = (-1, 1) .* (m / s)) isa + PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), zunit = u"km/hr") isa PlotsBase.Plot + @test plot(x * m, y * s, z * (m / s), zticks = (1:2) * m / s) isa PlotsBase.Plot + @test scatter(x * m, y * s, z * (m / s)) isa PlotsBase.Plot end @testset "Unitful/unitless combinations" begin @@ -239,13 +240,13 @@ end @testset "plot($(mystr(xs)), $(mystr(ys)))" for xs in [x, x * m], ys in [y, y * s] - @test plot(xs, ys) isa Plots.Plot + @test plot(xs, ys) isa PlotsBase.Plot end @testset "plot($(mystr(xs)), $(mystr(ys)), $(mystr(zs)))" for xs in [x, x * m], ys in [y, y * s], zs in [z, z * (m / s)] - @test plot(xs, ys, zs) isa Plots.Plot + @test plot(xs, ys, zs) isa PlotsBase.Plot end end end @@ -254,9 +255,9 @@ end Iterators.product(fill([1, u"m", u"s"], 2)...), ) x, y = rand(10) * us[1], rand(10) * us[2] - @test scatter(x, y) isa Plots.Plot - @test scatter(x, y, markersize = x) isa Plots.Plot - @test scatter(x, y, line_z = x) isa Plots.Plot + @test scatter(x, y) isa PlotsBase.Plot + @test scatter(x, y, markersize = x) isa PlotsBase.Plot + @test scatter(x, y, line_z = x) isa PlotsBase.Plot end @testset "contour(x::$(us[1]), y::$(us[2]))" for us in collect( @@ -264,13 +265,13 @@ end ) x, y = (1:0.01:2) * us[1], (1:0.02:2) * us[2] z = x' ./ y - @test contour(x, y, z) isa Plots.Plot - @test contourf(x, y, z) isa Plots.Plot + @test contour(x, y, z) isa PlotsBase.Plot + @test contourf(x, y, z) isa PlotsBase.Plot end @testset "ProtectedString" begin y = rand(10) * u"m" - @test plot(y, label = P"meters") isa Plots.Plot + @test plot(y, label = P"meters") isa PlotsBase.Plot end end @@ -316,7 +317,7 @@ end y = rand(10) * u"s" ey = rand(10) * u"ms" pl = plot(x, y, xerr = ex, yerr = ey) - @test pl isa Plots.Plot + @test pl isa PlotsBase.Plot @test xguide(pl) == "mm" @test yguide(pl) == "s" end @@ -326,7 +327,7 @@ end y = rand(10) * u"s" ribbon = rand(10) * u"ms" pl = plot(x, y, ribbon = ribbon) - @test pl isa Plots.Plot + @test pl isa PlotsBase.Plot @test xguide(pl) == "mm" @test yguide(pl) == "s" end @@ -336,7 +337,7 @@ end y = rand(10) * u"s" fillrange = rand(10) * u"ms" pl = plot(x, y, fillrange = fillrange) - @test pl isa Plots.Plot + @test pl isa PlotsBase.Plot @test xguide(pl) == "mm" @test yguide(pl) == "s" end @@ -389,13 +390,13 @@ end x = (1:3)u"dBV" y = (1:3)u"V" pl = plot(u, x) - @test pl isa Plots.Plot + @test pl isa PlotsBase.Plot @test xguide(pl) == "B" @test yguide(pl) == "dBV" - @test plot!(pl, v, y) isa Plots.Plot + @test plot!(pl, v, y) isa PlotsBase.Plot pl = plot(v, y) - @test pl isa Plots.Plot - @test plot!(pl, u, x) isa Plots.Plot + @test pl isa PlotsBase.Plot + @test plot!(pl, u, x) isa PlotsBase.Plot end if Sys.islinux() && Sys.which("pdflatex") ≢ nothing diff --git a/test/test_utils.jl b/PlotsBase/test/test_utils.jl similarity index 51% rename from test/test_utils.jl rename to PlotsBase/test/test_utils.jl index e093b7b98..e3d9ab01b 100644 --- a/test/test_utils.jl +++ b/PlotsBase/test/test_utils.jl @@ -12,69 +12,69 @@ [(missing, missing, missing), ("a", "b", "c")], ) for z in zipped - @test isequal(collect(zip(Plots.unzip(z)...)), z) - @test isequal(collect(zip(Plots.unzip(GeometryBasics.Point.(z))...)), z) + @test isequal(collect(zip(PlotsBase.unzip(z)...)), z) + @test isequal(collect(zip(PlotsBase.unzip(GeometryBasics.Point.(z))...)), z) end - op1 = Plots.Colorbars.process_clims((1.0, 2.0)) - op2 = Plots.Colorbars.process_clims((1, 2.0)) + op1 = PlotsBase.Colorbars.process_clims((1.0, 2.0)) + op2 = PlotsBase.Colorbars.process_clims((1, 2.0)) data = randn(100, 100) @test op1(data) == op2(data) - @test Plots.Colorbars.process_clims(nothing) == - Plots.Colorbars.process_clims(missing) == - Plots.Colorbars.process_clims(:auto) + @test PlotsBase.Colorbars.process_clims(nothing) == + PlotsBase.Colorbars.process_clims(missing) == + PlotsBase.Colorbars.process_clims(:auto) @test (==)( - Plots.texmath2unicode( + PlotsBase.texmath2unicode( raw"Equation $y = \alpha \cdot x + \beta$ and eqn $y = \sin(x)^2$", ), raw"Equation y = α ⋅ x + β and eqn y = sin(x)²", ) - @test Plots.isvector([1, 2]) - @test !Plots.isvector(nothing) - @test Plots.ismatrix([1 2; 3 4]) - @test !Plots.ismatrix(nothing) - @test Plots.isscalar(1.0) - @test !Plots.isscalar(nothing) - @test Plots.anynan(1, 3, (1, NaN, 3)) - @test Plots.allnan(1, 2, (NaN, NaN, 1)) - @test Plots.makevec([]) isa AbstractVector - @test Plots.makevec(1) isa AbstractVector - @test Plots.maketuple(1) == (1, 1) - @test Plots.maketuple((1, 1)) == (1, 1) - @test Plots.ok(1, 2) - @test !Plots.ok(1, 2, NaN) - @test Plots.ok((1, 2, 3)) - @test !Plots.ok((1, 2, NaN)) - @test Plots.nansplit([1, 2, NaN, 3, 4]) == [[1.0, 2.0], [3.0, 4.0]] - @test Plots.nanvcat([1, NaN]) |> length == 4 - - @test Plots.PlotMeasures.inch2px(1) isa AbstractFloat - @test Plots.PlotMeasures.px2inch(1) isa AbstractFloat - @test Plots.PlotMeasures.inch2mm(1) isa AbstractFloat - @test Plots.PlotMeasures.mm2inch(1) isa AbstractFloat - @test Plots.PlotMeasures.px2mm(1) isa AbstractFloat - @test Plots.PlotMeasures.mm2px(1) isa AbstractFloat + @test PlotsBase.isvector([1, 2]) + @test !PlotsBase.isvector(nothing) + @test PlotsBase.ismatrix([1 2; 3 4]) + @test !PlotsBase.ismatrix(nothing) + @test PlotsBase.isscalar(1.0) + @test !PlotsBase.isscalar(nothing) + @test PlotsBase.anynan(1, 3, (1, NaN, 3)) + @test PlotsBase.allnan(1, 2, (NaN, NaN, 1)) + @test PlotsBase.makevec([]) isa AbstractVector + @test PlotsBase.makevec(1) isa AbstractVector + @test PlotsBase.maketuple(1) == (1, 1) + @test PlotsBase.maketuple((1, 1)) == (1, 1) + @test PlotsBase.ok(1, 2) + @test !PlotsBase.ok(1, 2, NaN) + @test PlotsBase.ok((1, 2, 3)) + @test !PlotsBase.ok((1, 2, NaN)) + @test PlotsBase.nansplit([1, 2, NaN, 3, 4]) == [[1.0, 2.0], [3.0, 4.0]] + @test PlotsBase.nanvcat([1, NaN]) |> length == 4 + + @test PlotsBase.PlotMeasures.inch2px(1) isa AbstractFloat + @test PlotsBase.PlotMeasures.px2inch(1) isa AbstractFloat + @test PlotsBase.PlotMeasures.inch2mm(1) isa AbstractFloat + @test PlotsBase.PlotMeasures.mm2inch(1) isa AbstractFloat + @test PlotsBase.PlotMeasures.px2mm(1) isa AbstractFloat + @test PlotsBase.PlotMeasures.mm2px(1) isa AbstractFloat pl = plot() @test xlims() isa Tuple @test ylims() isa Tuple @test zlims() isa Tuple - @test_throws MethodError Plots.inline() - @test_throws MethodError Plots._do_plot_show(plot(), :inline) + @test_throws MethodError PlotsBase.inline() + @test_throws MethodError PlotsBase._do_plot_show(plot(), :inline) - @test plot(-1:10, xscale = :log10) isa Plots.Plot + @test plot(-1:10, xscale = :log10) isa PlotsBase.Plot ###################### - Plots.Commons.debug!(true) + PlotsBase.Commons.debug!(true) io = PipeBuffer() - Plots.Commons.debugshow(io, nothing) - Plots.Commons.debugshow(io, [1]) + PlotsBase.Commons.debugshow(io, nothing) + PlotsBase.Commons.debugshow(io, [1]) pl = plot(1:2) - Plots.Commons.dumpdict(devnull, first(pl.series_list).plotattributes) + PlotsBase.Commons.dumpdict(devnull, first(pl.series_list).plotattributes) show(devnull, pl[1][:xaxis]) # bounding boxes @@ -82,7 +82,7 @@ show(devnull, plot(1:2)) end - Plots.Commons.debug!(false) + PlotsBase.Commons.debug!(false) ###################### let pl = plot(1) @@ -102,51 +102,51 @@ push!(pl, 1:2, 2:3, 3:4) pl = plot([1, 2, 3], [4, 5, 6]) - @test Plots.PlotsPlots.xmin(pl) == 1 - @test Plots.PlotsPlots.xmax(pl) == 3 - @test Plots.Commons.ignorenan_extrema(pl) == (1, 3) + @test PlotsBase.PlotsPlots.xmin(pl) == 1 + @test PlotsBase.PlotsPlots.xmax(pl) == 3 + @test PlotsBase.Commons.ignorenan_extrema(pl) == (1, 3) - @test Plots.Commons.get_attr_symbol(:x, "lims") === :xlims - @test Plots.Commons.get_attr_symbol(:x, :lims) === :xlims + @test PlotsBase.Commons.get_attr_symbol(:x, "lims") === :xlims + @test PlotsBase.Commons.get_attr_symbol(:x, :lims) === :xlims - @test contains(Plots._document_argument(:bar_position), "bar_position") + @test contains(PlotsBase._document_argument(:bar_position), "bar_position") - @test Plots.limsType((1, 1)) === :limits - @test Plots.limsType(:undefined) === :invalid - @test Plots.limsType(:auto) === :auto - @test Plots.limsType(NaN) === :invalid + @test PlotsBase.limsType((1, 1)) === :limits + @test PlotsBase.limsType(:undefined) === :invalid + @test PlotsBase.limsType(:auto) === :auto + @test PlotsBase.limsType(NaN) === :invalid - @test Plots.ticks_type([1, 2]) === :ticks - @test Plots.ticks_type(["1", "2"]) === :labels - @test Plots.ticks_type(([1, 2], ["1", "2"])) === :ticks_and_labels - @test Plots.ticks_type(((1, 2), ("1", "2"))) === :ticks_and_labels - @test Plots.ticks_type(:undefined) === :invalid + @test PlotsBase.ticks_type([1, 2]) === :ticks + @test PlotsBase.ticks_type(["1", "2"]) === :labels + @test PlotsBase.ticks_type(([1, 2], ["1", "2"])) === :ticks_and_labels + @test PlotsBase.ticks_type(((1, 2), ("1", "2"))) === :ticks_and_labels + @test PlotsBase.ticks_type(:undefined) === :invalid pl = plot(1:2, 1:2, 1:2, proj_type = :ortho) - @test Plots.isortho(first(pl.subplots)) + @test PlotsBase.isortho(first(pl.subplots)) pl = plot(1:2, 1:2, 1:2, proj_type = :persp) - @test Plots.ispersp(first(pl.subplots)) + @test PlotsBase.ispersp(first(pl.subplots)) let pl = plot(1:2) series = first(pl.series_list) label = "fancy label" - Plots.PlotsSeries.attr!(series; label) + PlotsBase.PlotsSeries.attr!(series; label) @test series[:label] == label - @test Plots.PlotsSeries.attr(series, :label) == label + @test PlotsBase.PlotsSeries.attr(series, :label) == label label = "another label" - Plots.PlotsSeries.attr!(series, label, :label) - @test Plots.PlotsSeries.attr(series, :label) == label + PlotsBase.PlotsSeries.attr!(series, label, :label) + @test PlotsBase.PlotsSeries.attr(series, :label) == label sp = first(pl.subplots) title = "fancy title" - Plots.Subplots.attr!(sp; title) + PlotsBase.Subplots.attr!(sp; title) @test sp[:title] == title end end @testset "NaN-separated Segments" begin - segments(args...) = collect(Plots.PlotsSeries.iter_segments(args...)) + segments(args...) = collect(PlotsBase.PlotsSeries.iter_segments(args...)) nan10 = fill(NaN, 10) @test segments(11:20) == [1:10] @@ -175,12 +175,12 @@ end j = [1, 2, 3, 2] k = [2, 3, 1, 3] - X, Y, Z = Plots.mesh3d_triangles(x, y, z, (i, j, k)) + X, Y, Z = PlotsBase.mesh3d_triangles(x, y, z, (i, j, k)) @test length(X) == length(Y) == length(Z) == 4length(i) cns = [(1, 2, 3), (1, 3, 2), (1, 4, 2), (2, 3, 4)] - X, Y, Z = Plots.mesh3d_triangles(x, y, z, cns) + X, Y, Z = PlotsBase.mesh3d_triangles(x, y, z, cns) @test length(X) == length(Y) == length(Z) == 4length(i) end @@ -195,45 +195,45 @@ end pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(-3)) pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(+3)) pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft x = 0:0.01:2 pl = plot(x, -x, label = "linear") pl = plot!(x, -x .^ 2, label = "quadratic") pl = plot!(x, -x .^ 3, label = "cubic") - @test Plots._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(-3)) pl = plot(x, -x, label = "linear") pl = plot!(x, -x .^ 2, label = "quadratic") pl = plot!(x, -x .^ 3, label = "cubic") - @test Plots._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft x = [0, 1, 0, 1] y = [0, 0, 1, 1] pl = scatter(x, y, xlims = [0.0, 1.3], ylims = [0.0, 1.3], label = "test") - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(x, y, xlims = [-0.3, 1.0], ylims = [-0.3, 1.0], label = "test") - @test Plots._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft pl = scatter(x, y, xlims = [0.0, 1.3], ylims = [-0.3, 1.0], label = "test") - @test Plots._guess_best_legend_position(:best, pl) === :bottomright + @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomright pl = scatter(x, y, xlims = [-0.3, 1.0], ylims = [0.0, 1.3], label = "test") - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft y1 = [ 0.6640202072697099, @@ -250,48 +250,48 @@ end y2 = [0.40089741940615464, 0.6687326060649715, 0.6844117863127116] pl = plot(1:10, y1) pl = plot!(1:3, y2, xlims = (0, 10), ylims = (0, 1)) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright # test empty plot pl = plot([]) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright # test that we didn't overlap other placements - @test Plots._guess_best_legend_position(:bottomleft, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:bottomleft, pl) === :bottomleft # test singleton pl = plot(1:1) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright # test cycling indexes x = 0.0:0.1:1 y = [1, 2, 3] pl = scatter(x, y) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright # Test step plot with variable limits x = 0:0.001:1 y = vcat([0.0 for _ in 1:100], [1.0 for _ in 101:200], [0.5 for _ in 201:1001]) pl = scatter(x, y) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(x, y, xlims = [0, 0.25]) - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft pl = scatter(x, y, xlims = [0.1, 0.25]) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(x, y, xlims = [0.18, 0.25]) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(x, y, ylims = [-1, 0.75]) - @test Plots._guess_best_legend_position(:best, pl) === :bottomright + @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomright pl = scatter(x, y, ylims = [0.25, 0.75]) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(-x, y, ylims = [0.25, 0.75]) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright pl = scatter(-x, y) - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft pl = scatter(-x, -y) - @test Plots._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft pl = scatter(x, -y) - @test Plots._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) === :topright end @testset "dispatch" begin @@ -299,7 +299,7 @@ end pl = heatmap(rand(10, 10); xscale = :log10, yscale = :log10) @test show(devnull, pl) isa Nothing - pl = plot(Plots.Shape([(1, 1), (2, 1), (2, 2), (1, 2)]); xscale = :log10) + pl = plot(PlotsBase.Shape([(1, 1), (2, 1), (2, 2), (1, 2)]); xscale = :log10) @test show(devnull, pl) isa Nothing end end diff --git a/Project.toml b/Project.toml index 9cef1f37e..430295e24 100644 --- a/Project.toml +++ b/Project.toml @@ -1,138 +1,29 @@ name = "Plots" -author = ["Tom Breloff (@tbreloff)"] uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -version = "2.0.0-dev" +author = ["Tom Breloff (@tbreloff)"] +version = "1.41.0" [deps] -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -JLFzf = "1019f520-868f-41f5-a6de-eb00f4b6a39c" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" -REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" -RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" -LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -Scratch = "6c6a2e73-6563-6170-7368-637461726353" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Preferences = "21216c6a-2e73-6563-6e65-726566657250" -FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" -Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1" -PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" -Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" - -[extras] -PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Gaston = "4b11ee91-296f-5714-9832-002c20994614" -GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" -VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" -PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" -StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" -HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" -LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" -PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" -RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" -TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" -FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" -UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" -PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" -InspectDR = "d0351b0e-4b05-5898-87b3-e2a8edfddd1d" -SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c" -Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" [compat] -NaNMath = "0.3, 1" -Showoff = "0.3.1, 1" GR = "0.69.5 - 0.73" -Gaston = "1" -FixedPointNumbers = "0.6 - 0.8" -JLFzf = "0.1" +Pkg = "1" +PlotsBase = "1.41" PrecompileTools = "1" -PGFPlotsX = "1" -Unzip = "0.1 - 0.2" -PythonPlot = "1 - 1.0.2" -UnitfulLatexify = "1" -RecipesPipeline = "1" -LaTeXStrings = "1" -PlotUtils = "1" -JSON = "0.21, 1" -StatsBase = "0.33, 0.34" -HDF5 = "0.16" -RelocatableFolders = "0.3, 1" -Scratch = "1" -Latexify = "0.14 - 0.15, 0.16" Preferences = "1" -FFMPEG = "0.2 - 0.4" -Measures = "0.3" -julia = "1.9" -RecipesBase = "1.3.1" -UnicodeFun = "0.4" -UnicodePlots = "3.4" -PlotThemes = "2, 3" -Contour = "0.5 - 0.6" -PlotlyJS = "0.18" -PlotlyKaleido = "2.2.2" Reexport = "0.2, 1" +julia = "1.6" -[weakdeps] -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" -IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" -ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" -InspectDR = "d0351b0e-4b05-5898-87b3-e2a8edfddd1d" -Gaston = "4b11ee91-296f-5714-9832-002c20994614" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" -PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" -PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" -PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" +[extras] PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" [targets] -test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "Gtk", "GR", "Images", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] - -[extensions] -FileIOExt = "FileIO" -UnitfulExt = "Unitful" -GeometryBasicsExt = "GeometryBasics" -IJuliaExt = "IJulia" -ImageInTerminalExt = "ImageInTerminal" -PlotsGRExt = "GR" -PlotsUnicodePlotsExt = "UnicodePlots" -PlotsPGFPlotsXExt = "PGFPlotsX" -PlotsPythonPlotExt = "PythonPlot" -PlotsPlotlyJSExt = "PlotlyJS" -PlotsPlotlyKaleidoExt = "PlotlyKaleido" -PlotsInspectDRExt = "InspectDR" -PlotsGastonExt = "Gaston" +test = ["PythonPlot", "Test", "UnicodePlots"] diff --git a/ext/ImageInTerminalExt.jl b/ext/ImageInTerminalExt.jl deleted file mode 100644 index 5010c9b1a..000000000 --- a/ext/ImageInTerminalExt.jl +++ /dev/null @@ -1,31 +0,0 @@ -module ImageInTerminalExt - -import Plots -Plots.@ext_imp_use :import ImageInTerminal - -if ImageInTerminal.ENCODER_BACKEND[] == :Sixel - get!(ENV, "GKSwstype", "nul") # disable `gr` output, we display in the terminal instead - for be in ( - Plots.GRBackend, - Plots.PythonPlotBackend, - # Plots.UnicodePlotsBackend, # better and faster as MIME("text/plain") in terminal - Plots.PGFPlotsXBackend, - Plots.PlotlyJSBackend, - Plots.PlotlyBackend, - Plots.GastonBackend, - Plots.InspectDRBackend, - ) - @eval function Base.display(::Plots.PlotsDisplay, plt::Plots.Plot{$be}) - Plots.prepare_output(plt) - buf = PipeBuffer() - show(buf, MIME("image/png"), plt) - display( - ImageInTerminal.TerminalGraphicDisplay(stdout), - MIME("image/png"), - read(buf), - ) - end - end -end - -end # module diff --git a/ext/PlotsGRExt/PlotsGRExt.jl b/ext/PlotsGRExt/PlotsGRExt.jl deleted file mode 100644 index f1a4c9f72..000000000 --- a/ext/PlotsGRExt/PlotsGRExt.jl +++ /dev/null @@ -1,54 +0,0 @@ -module PlotsGRExt - -using GR: GR -using Plots: Plots -# TODO: eliminate this list -using Plots: - bbox, - left, - right, - bottom, - top, - plotarea, - axis_drawing_info, - axis_drawing_info_3d, - _guess_best_legend_position, - labelfunc_tex, - _cycle, - isortho, - isautop, - heatmap_edges, - is_uniformly_spaced, - DPI, - shape_data, - is_2tuple, - is3d, - straightline_data, - convert_to_polar - -using RecipesPipeline: RecipesPipeline -using NaNMath: NaNMath -using Plots.Arrows -using Plots.Axes -using Plots.Annotations -using Plots.Colorbars -using Plots.Colorbars: cbar_gradient, cbar_fill, cbar_lines -using Plots.Colors -using Plots.Commons -using Plots.Fonts -using Plots.Fonts: Font, PlotText -using Plots.PlotMeasures -using Plots.PlotsPlots -using Plots.PlotsSeries -using Plots.Subplots -using Plots.Shapes -using Plots.Shapes: Shape -using Plots.Ticks - -# These are overriden by GR -import Plots: labelfunc, _update_min_padding!, _show, _display, closeall - -include("initialization.jl") -include("gr.jl") - -end # module diff --git a/ext/PlotsGRExt/initialization.jl b/ext/PlotsGRExt/initialization.jl deleted file mode 100644 index 603497da4..000000000 --- a/ext/PlotsGRExt/initialization.jl +++ /dev/null @@ -1,200 +0,0 @@ -import Plots: backend_name, backend_package_name, is_marker_supported - -# unrolling the old # init_backend macro by hand case by case -const package_str = "GR" -const str = "gr" -const sym = :gr - -struct GRBackend <: Plots.AbstractBackend end - -get_concrete_backend() = GRBackend # opposite to abstract - -function __init__() - @info "Initializing GR backend in Plots; run `gr()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[GRBackend] = sym - - push!(Plots._initialized_backends, sym) -end -# Make GR know to Plots -backend_name(::GRBackend) = sym -backend_package_name(::GRBackend) = backend_package_name(sym) - -const _gr_attrs = Plots.merge_with_base_supported([ - :annotations, - :annotationrotation, - :annotationhalign, - :annotationfontsize, - :annotationfontfamily, - :annotationcolor, - :annotationvalign, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :legend_foreground_color, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :fillstyle, - :bins, - :layout, - :title, - :window_title, - :guide, - :widen, - :lims, - :ticks, - :scale, - :flip, - :titlefontfamily, - :titlefontsize, - :titlefonthalign, - :titlefontvalign, - :titlefontrotation, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_halign, - :legend_font_valign, - :legend_font_rotation, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfonthalign, - :tickfontvalign, - :tickfontrotation, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefonthalign, - :guidefontvalign, - :guidefontrotation, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_titlefont, - :colorbar_titlefontsize, - :colorbar_titlefontrotation, - :colorbar_titlefontcolor, - :colorbar_entry, - :colorbar_scale, - :clims, - :fill, - :fill_z, - :fontfamily, - :fontfamily_subplot, - :line_z, - :marker_z, - :legend_column, - :legend_font, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_rotation, - :legend_title_font_pointsize, - :legend_title_font_valigm, - :levels, - :line, - :ribbon, - :quiver, - :overwrite_figure, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontfamily, - :plot_titlefontrotation, - :plot_titlefontsize, - :plot_titlelocation, - :plot_titlevspan, - :polar, - :aspect_ratio, - :normalize, - :weights, - :inset_subplots, - :bar_width, - :arrow, - :framestyle, - :tick_direction, - :camera, - :contour_labels, - :connections, - :axis, - :thickness_scaling, - :minorgrid, - :minorgridalpha, - :minorgridlinewidth, - :minorgridstyle, - :minorticks, - :mirror, - :rotation, - :showaxis, - :tickfonthalign, - :formatter, - :mirror, - :guidefont, -]) -const _gr_seriestypes = [ - :path, - :scatter, - :straightline, - :heatmap, - :image, - :contour, - :path3d, - :scatter3d, - :surface, - :wireframe, - :mesh3d, - :volume, - :shape, -] -const _gr_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _gr_markers = vcat(Commons._all_markers, :pixel) -const _gr_scales = [:identity, :ln, :log2, :log10] - -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_gr_", s, "s") - eval(quote - Plots.$f1(::GRBackend, $s::Symbol) = $s in $v - Plots.$f2(::GRBackend) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- - -is_marker_supported(::GRBackend, shape::Shape) = true diff --git a/ext/PlotsGastonExt/PlotsGastonExt.jl b/ext/PlotsGastonExt/PlotsGastonExt.jl deleted file mode 100644 index d1a135804..000000000 --- a/ext/PlotsGastonExt/PlotsGastonExt.jl +++ /dev/null @@ -1,16 +0,0 @@ -module PlotsGastonExt - -using Gaston -using Plots: Plots, mesh3d_triangles -import Plots: _show, _display -using Plots.Commons -using Plots.PlotsPlots -using Plots.Subplots -using Plots.PlotsSeries -using Plots.Fonts -using Plots.PlotUtils: alphacolor, hex - -include("initialization.jl") -include("gaston.jl") - -end # module diff --git a/ext/PlotsGastonExt/initialization.jl b/ext/PlotsGastonExt/initialization.jl deleted file mode 100644 index 999dea35e..000000000 --- a/ext/PlotsGastonExt/initialization.jl +++ /dev/null @@ -1,145 +0,0 @@ -# unrolling the old # init_backend macro by hand case by case -# this is not a macro for the backend maintainers and explicit control - -const package_str = "Gaston" -const str = lowercase(package_str) -const sym = Symbol(str) - -struct GastonBackend <: Plots.AbstractBackend end -const T = GastonBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @info "Initializing $package_str backend in Plots; run `$str()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[T] = sym - - push!(Plots._initialized_backends, sym) - - # Additional setup required by the backend: - -end - -Plots.backend_name(::T) = sym -Plots.backend_package_name(::T) = Plots.backend_package_name(sym) - -const _gaston_attrs = Plots.merge_with_base_supported([ - :annotations, - # :background_color_legend, - # :background_color_inside, - # :background_color_outside, - # :foreground_color_legend, - # :foreground_color_grid, :foreground_color_axis, - # :foreground_color_text, :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - # :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :markerstrokestyle, - # :fillrange, :fillcolor, :fillalpha, - # :bins, - # :bar_width, :bar_edges, - :title, - :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :tickfont, - :guidefont, - :legendfont, - :grid, - :legend, - # :colorbar, :colorbar_title, - # :fill_z, :line_z, :marker_z, :levels, - # :ribbon, - :quiver, - :arrow, - # :orientation, :overwrite_figure, - :polar, - # :normalize, :weights, :contours, - :aspect_ratio, - :tick_direction, - # :framestyle, - # :camera, - # :contour_labels, - :connections, -]) - -const _gaston_seriestypes = [ - :path, - :path3d, - :scatter, - :steppre, - :stepmid, - :steppost, - :ysticks, - :xsticks, - :contour, - :shape, - :straightline, - :scatter3d, - :contour3d, - :wireframe, - :heatmap, - :surface, - :mesh3d, - :image, -] - -const _gaston_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] - -const _gaston_markers = [ - :none, - :auto, - :pixel, - :cross, - :xcross, - :+, - :x, - :star5, - :rect, - :circle, - :utriangle, - :dtriangle, - :diamond, - :pentagon, - # :hline, - # :vline, -] - -const _gaston_scales = [:identity, :ln, :log2, :log10] - -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - eval(quote - Plots.$f1(::T, $s::Symbol) = $s in $v - Plots.$f2(::T) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- diff --git a/ext/PlotsHDF5Ext/hdf5.jl b/ext/PlotsHDF5Ext/hdf5.jl deleted file mode 100644 index e2fd571a2..000000000 --- a/ext/PlotsHDF5Ext/hdf5.jl +++ /dev/null @@ -1,529 +0,0 @@ -#= - -# HDF5 Plots: Save/replay plots to/from HDF5 - -# Usage -Write to .hdf5 file using: - p = plot(...) - Plots.hdf5plot_write(p, "plotsave.hdf5") - -Read from .hdf5 file using: - pyplot() # Must first select backend - pread = Plots.hdf5plot_read("plotsave.hdf5") - display(pread) - -# TODO - 1. Support more features. - - GridLayout known not to be working. - 2. Improve error handling. - - Will likely crash if file format is off. - 3. Save data in a folder parallel to "plot". - - Will make it easier for users to locate data. - - Use HDF5 reference to link data? - 4. Develop an actual versioned file format. - - Should have some form of backward compatibility. - - Should be reliable for archival purposes. - 5. Fix construction of plot object with hdf5plot_read. - - Layout doesn't seem to get transferred well (ex: `Plots._examples[40]`). - - Not building object correctly when backends do not natively support - a certain feature (ex: :steppre) - - No support for CategoricalArrays.* structures. But they appear to be - brought into `Plots._examples[25,30]` through DataFrames.jl - so we can't - really reference them in this code. -=# - -""" - _hdf5_implementation - -Create module (namespace) for implementing HDF5 "plots". -(Avoid name collisions, while keeping names short) -""" -module _hdf5_implementation # Tools required to implements HDF5 "plots" - -import Dates - -# Plots.jl imports HDF5 to main: -import ..HDF5 -import ..HDF5: Group, Dataset - -import ..Colors, ..Colorant -import ..PlotUtils.ColorSchemes.ColorScheme - -import ..HDF5Backend, .._current_plots_version -import ..HDF5PLOT_MAP_STR2TELEM, ..HDF5PLOT_MAP_TELEM2STR -import ..HDF5Plot_PlotRef, ..HDF5PLOT_PLOTREF -import ..BoundingBox, ..Extrema, ..Length -import ..RecipesPipeline.datetimeformatter -import ..PlotUtils.ColorPalette, - ..PlotUtils.CategoricalColorGradient, ..PlotUtils.ContinuousColorGradient -import ..Surface, ..Shape, ..Arrow -import ..GridLayout, ..RootLayout -import ..Font, ..PlotText, ..SeriesAnnotations -import ..Axis, ..Subplot, ..Plot -import ..AKW, ..KW, ..DefaultsDict -import .._axis_defaults -import ..plot, ..plot! - -# Types that already have built-in HDF5 support (just write out natively): -const HDF5_SupportedTypes = Union{Number,String} - -# Dispatch types: -struct CplxTuple end # Identifies a "complex" tuple structure (not merely numbers) - -# HDF5 reader will auto-detect type correctly: -struct HDF5_AutoDetect end # See HDF5_SupportedTypes - -if length(HDF5PLOT_MAP_TELEM2STR) < 1 - # Possible element types of high-level data types: - # (Used to add type information as an HDF5 string attribute) - # (Also used to dispatch appropriate read function through _read_typed()) - _telem2str = Dict{String,Type}( - "NOTHING" => Nothing, - "SYMBOL" => Symbol, - "RGBA" => Colorant, # Write out any Colorant to an #RRGGBBAA string - "TUPLE" => Tuple, - "CTUPLE" => CplxTuple, - "EXTREMA" => Extrema, - "LENGTH" => Length, - "ARRAY" => Array, # Array{Any} (because Array{T<:Union{Number, String}} natively supported by HDF5) - - # Sub-structure types: - "T_DATETIMEFORMATTER" => typeof(datetimeformatter), - - # Sub-structure types: - "DEFAULTSDICT" => DefaultsDict, - "FONT" => Font, - "BOUNDINGBOX" => BoundingBox, - "GRIDLAYOUT" => GridLayout, - "ROOTLAYOUT" => RootLayout, - "SERIESANNOTATIONS" => SeriesAnnotations, - "PLOTTEXT" => PlotText, - "SHAPE" => Shape, - "ARROW" => Arrow, - "COLORSCHEME" => ColorScheme, - "COLORPALETTE" => ColorPalette, - "CONT_COLORGRADIENT" => ContinuousColorGradient, - "CAT_COLORGRADIENT" => CategoricalColorGradient, - "AXIS" => Axis, - "SURFACE" => Surface, - "SUBPLOT" => Subplot, - ) - merge!(HDF5PLOT_MAP_STR2TELEM, _telem2str) # Faster to create than push!()?? - merge!( - HDF5PLOT_MAP_TELEM2STR, - Dict{Type,String}(v => k for (k, v) in HDF5PLOT_MAP_STR2TELEM), - ) -end - -# Helper functions - -h5plotpath(plotname::String) = "plots/$plotname" - -_hdf5_merge!(dest::AKW, src::AKW) = - for (k, v) in src - if isa(v, Axis) - _hdf5_merge!(dest[k].plotattributes, v.plotattributes) - else - dest[k] = v - end - end - -# _type_for_map returns the type to use with HDF5PLOT_MAP_TELEM2STR[], in case it is not concrete: -_type_for_map(::Type{T}) where {T} = T # Catch-all -_type_for_map(::Type{T}) where {T<:BoundingBox} = BoundingBox -_type_for_map(::Type{T}) where {T<:ColorScheme} = ColorScheme -_type_for_map(::Type{T}) where {T<:Surface} = Surface - -# Read/write things like type name in attributes -_write_datatype_attrs(ds::Union{Group,Dataset}, ::Type{T}) where {T} = - HDF5.attributes(ds)["TYPE"] = HDF5PLOT_MAP_TELEM2STR[T] - -function _read_datatype_attrs(ds::Union{Group,Dataset}) - Base.haskey(HDF5.attributes(ds), "TYPE") || return HDF5_AutoDetect - HDF5PLOT_MAP_STR2TELEM[HDF5.read(HDF5.attributes(ds)["TYPE"])] -end - -# Type parameter attributes: -_write_typeparam_attrs(ds::Dataset, v::Length{T}) where {T} = - HDF5.attributes(ds)["TYPEPARAM"] = string(T) # Need to add units for Length - -_read_typeparam_attrs(ds::Dataset) = HDF5.read(HDF5.attributes(ds)["TYPEPARAM"]) - -_write_length_attrs(grp::Group, v::Vector) = HDF5.attributes(grp)["LENGTH"] = length(v) -_read_length_attrs(::Type{Vector}, grp::Group) = HDF5.read(HDF5.attributes(grp)["LENGTH"]) - -_write_size_attrs(grp::Group, v::Array) = HDF5.attributes(grp)["SIZE"] = [size(v)...] - -_read_size_attrs(::Type{Array}, grp::Group) = - tuple(HDF5.read(HDF5.attributes(grp)["SIZE"])...) - -# _write_typed(): Simple (leaf) datatypes. (Labels with type name.) - -set_value!(grp::Group, name::String, v) = (grp[name] = v; grp[name]) - -# Default behaviour: Assumes value is supported by HDF5 format -_write_typed(grp::Group, name::String, v::HDF5_SupportedTypes) = - (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr - -_write_typed(grp::Group, name::String, v::Nothing) = - _write_datatype_attrs(set_value!(grp, name, "nothing"), Nothing) # Redundancy check/easier to read HDF5 file - -_write_typed(grp::Group, name::String, v::Symbol) = - _write_datatype_attrs(set_value!(grp, name, string(v)), Symbol) - -_write_typed(grp::Group, name::String, v::Colorant) = - _write_datatype_attrs(set_value!(grp, name, "#" * Colors.hex(v, :RRGGBBAA)), Colorant) - -_write_typed(grp::Group, name::String, v::Extrema) = - _write_datatype_attrs(set_value!(grp, name, [v.emin, v.emax]), Extrema) # More compact than writing struct - -function _write_typed(grp::Group, name::String, v::Length) - grp[name] = v.value - _write_datatype_attrs(grp[name], Length) - _write_typeparam_attrs(grp[name], v) -end - -_write_typed(grp::Group, name::String, v::typeof(datetimeformatter)) = - _write_datatype_attrs(set_value!(grp, name, string(v)), typeof(datetimeformatter)) # Just write something that helps reader - -_write_typed(grp::Group, name::String, v::Array{T}) where {T<:Number} = - (set_value!(grp, name, v); nothing) # No need to _write_datatype_attr - -_write_typed(grp::Group, name::String, v::AbstractRange) = - _write_typed(grp, name, collect(v)) # For now - -# Helper functions for writing complex data structures - -# Write an array using HDF5 hierarchy (when not using simple numeric eltype): -function _write_harray(grp::Group, name::String, v::Array) - sgrp = HDF5.create_group(grp, name) - lidx = LinearIndices(size(v)) - - for iter in eachindex(v) - coord = lidx[iter] - elem = v[iter] - idxstr = join(coord, "_") - _write_typed(sgrp, "v$idxstr", elem) - end - - _write_size_attrs(sgrp, v) -end - -# Write Dict without tagging with type: -_write(grp::Group, name::String, d::AbstractDict) = - let sgrp = HDF5.create_group(grp, name) - for (k, v) in d - kstr = string(k) - _write_typed(sgrp, kstr, v) - end - end - -# Write out arbitrary `struct`s: -_writestructgeneric(grp::Group, obj::T) where {T} = - for fname in fieldnames(T) - v = getfield(obj, fname) - _write_typed(grp, String(fname), v) - end - -# _write_typed(): More complex structures. (Labels with type name.) - -# Catch-all (default behaviour for `struct`s): -function _write_typed(grp::Group, name::String, v::T) where {T} - # NOTE: need "name" parameter so that call signature is same with built-ins - MT = _type_for_map(T) - try # Check to see if type is supported - typestr = HDF5PLOT_MAP_TELEM2STR[MT] - catch - @warn "HDF5Plots does not yet support structs of type `$MT`\n\n$grp" - return - end - - # If attribute is supported and no writer is defined, then this should work: - objgrp = HDF5.create_group(grp, name) - _write_datatype_attrs(objgrp, MT) - _writestructgeneric(objgrp, v) -end - -function _write_typed(grp::Group, name::String, v::Array{T}) where {T} - _write_harray(grp, name, v) - _write_datatype_attrs(grp[name], Array) # Any -end - -function _write_typed(grp::Group, name::String, v::Tuple, ::Type{ELT}) where {ELT<:Number} # Basic Tuple - _write_typed(grp, name, [v...]) - _write_datatype_attrs(grp[name], Tuple) -end -function _write_typed(grp::Group, name::String, v::Tuple, ::Type) # CplxTuple - _write_harray(grp, name, [v...]) - _write_datatype_attrs(grp[name], CplxTuple) -end -_write_typed(grp::Group, name::String, v::Tuple) = _write_typed(grp, name, v, eltype(v)) - -_write_typed(grp::Group, name::String, v::Dict) = nothing - -function _write_typed(grp::Group, name::String, d::DefaultsDict) # Typically for plot attributes - _write(grp, name, d) - _write_datatype_attrs(grp[name], DefaultsDict) -end - -function _write_typed(grp::Group, name::String, v::Axis) - sgrp = HDF5.create_group(grp, name) - # Ignore: sps::Vector{Subplot} - _write_typed(sgrp, "plotattributes", v.plotattributes) - _write_datatype_attrs(sgrp, Axis) -end - -function _write_typed(grp::Group, name::String, v::Subplot) - # Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. - sgrp = HDF5.create_group(grp, name) - _write_typed(sgrp, "index", v[:subplot_index]) - _write_datatype_attrs(sgrp, Subplot) - return -end - -_write_typed(grp::Group, name::String, v::Plot) = nothing # Don't write plot references - -# _write(): Write out more complex structures -# NOTE: No need to write out type information (inferred from hierarchy) - -function _write(grp::Group, sp::Subplot{HDF5Backend}) - _write_typed(grp, "attr", sp.attr) - - listgrp = HDF5.create_group(grp, "series_list") - _write_length_attrs(listgrp, sp.series_list) - for (i, series) in enumerate(sp.series_list) - # Just write .plotattributes part: - _write(listgrp, "$i", series.plotattributes) - end -end - -function _write(grp::Group, plt::Plot{HDF5Backend}) - _write_typed(grp, "attr", plt.attr) - - listgrp = HDF5.create_group(grp, "subplots") - _write_length_attrs(listgrp, plt.subplots) - for (i, sp) in enumerate(plt.subplots) - sgrp = HDF5.create_group(listgrp, "$i") - _write(sgrp, sp) - end -end - -function hdf5plot_write( - plt::Plot{HDF5Backend}, - path::AbstractString; - name::String = "_unnamed", -) - HDF5.h5open(path, "w") do file - HDF5.write_dataset(file, "VERSION_INFO", string(_current_plots_version)) - grp = HDF5.create_group(file, h5plotpath(name)) - _write(grp, plt) - end -end - -# _read(): Read data, but not type information. - -# Types with built-in HDF5 support: -_read(::Type{HDF5_AutoDetect}, ds::Dataset) = HDF5.read(ds) - -function _read(::Type{Nothing}, ds::Dataset) - nstr = "nothing" - v = HDF5.read(ds) - nstr == v || throw( - Meta.ParseError("_read(::Nothing, ::Group): Read $v != $nstr:\n$(HDF5.name(ds))"), - ) - return -end -_read(::Type{Symbol}, ds::Dataset) = Symbol(HDF5.read(ds)) -_read(::Type{Colorant}, ds::Dataset) = parse(Colorant, HDF5.read(ds)) -_read(::Type{Tuple}, ds::Dataset) = tuple(HDF5.read(ds)...) -_read(::Type{Extrema}, ds::Dataset) = - let v = HDF5.read(ds) - Extrema(v[1], v[2]) - end -function _read(::Type{Length}, ds::Dataset) - TUNIT = Symbol(_read_typeparam_attrs(ds)) - v = HDF5.read(ds) - Length{TUNIT,typeof(v)}(v) -end -_read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter - -# Helper functions for reading in complex data structures - -# When type is unknown, _read_typed() figures it out: -function _read_typed(grp::Group, name::String) - ds = grp[name] - _read(_read_datatype_attrs(ds), ds) -end - -# _readstructgeneric: Needs object values to be written out with _write_typed(): -function _readstructgeneric(::Type{T}, grp::Group) where {T} - vlist = Array{Any}(nothing, fieldcount(T)) - for (i, fname) in enumerate(fieldnames(T)) - vlist[i] = _read_typed(grp, String(fname)) - end - T(vlist...) -end - -# Read KW from group: -function _read(::Type{KW}, grp::Group) - d = KW() - gkeys = keys(grp) - for k in gkeys - try - v = _read_typed(grp, k) - d[Symbol(k)] = v - catch e - @warn "Could not read field $k" e grp - end - end - d -end - -# _read(): More complex structures. - -# Catch-all (default behaviour for `struct`s): -_read(T::Type, grp::Group) = _readstructgeneric(T, grp) - -function _read(::Type{Array}, grp::Group) # Array{Any} - sz = _read_size_attrs(Array, grp) - tuple(0) == sz && return [] - result = Array{Any}(undef, sz) - lidx = LinearIndices(sz) - - for iter in eachindex(result) - coord = lidx[iter] - idxstr = join(coord, "_") - result[iter] = _read_typed(grp, "v$idxstr") - end - - # Hack: Implicitly make Julia detect element type. - # (Should probably write it explicitly to file) - result = [elem for elem in result] # Potentially make more specific - reshape(result, sz) -end - -_read(::Type{CplxTuple}, grp::Group) = tuple(_read(Array, grp)...) - -function _read(::Type{GridLayout}, grp::Group) - # parent = _read_typed(grp, "parent") # Can't use generic reader - parent = RootLayout() # TODO: support parent??? - minpad = _read_typed(grp, "minpad") - bbox = _read_typed(grp, "bbox") - grid = _read_typed(grp, "grid") - widths = _read_typed(grp, "widths") - heights = _read_typed(grp, "heights") - attr = KW() # TODO support attr: _read_typed(grp, "attr") - - GridLayout(parent, minpad, bbox, grid, widths, heights, attr) -end -# Defaults depends on context. So: user must constructs with defaults, then read. -function _read(::Type{DefaultsDict}, grp::Group) - # User should set DefaultsDict.defaults to one of: - # _plot_defaults, _subplot_defaults, _axis_defaults, _series_defaults - path = HDF5.name(ds) - @warn "Cannot yet read DefaultsDict using _read_typed():\n $path\nCannot fully reconstruct plot." -end - -# 1st arg appears to be ref to subplots. Seems to work without it. -_read(::Type{Axis}, grp::Group) = - Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), _axis_defaults)) - -# Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index. -_read(::Type{Subplot}, grp::Group) = - HDF5PLOT_PLOTREF.ref.subplots[_read_typed(grp, "index")] - -# _read(): Main plot structures - -function _read(grp::Group, sp::Subplot) - listgrp = HDF5.open_group(grp, "series_list") - nseries = _read_length_attrs(Vector, listgrp) - - for i in 1:nseries - sgrp = HDF5.open_group(listgrp, "$i") - seriesinfo = _read(KW, sgrp) - - plot!(sp, seriesinfo[:x], seriesinfo[:y]) # Add data & create data structures - _hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo) - end - - # Perform after adding series... otherwise values get overwritten: - agrp = HDF5.open_group(grp, "attr") - _hdf5_merge!(sp.attr, _read(KW, agrp)) - - return sp -end - -function _read_plot(grp::Group) - listgrp = HDF5.open_group(grp, "subplots") - n = _read_length_attrs(Vector, listgrp) - - # Construct new plot, +allocate subplots: - plt = plot(layout = n) - HDF5PLOT_PLOTREF.ref = plt # Used when reading "layout" - - agrp = HDF5.open_group(grp, "attr") - _hdf5_merge!(plt.attr, _read(KW, agrp)) - - for (i, sp) in enumerate(plt.subplots) - sgrp = HDF5.open_group(listgrp, "$i") - _read(sgrp, sp) - end - - plt -end - -hdf5plot_read(path::AbstractString; name::String = "_unnamed") = - HDF5.h5open(path, "r") do file - grp = HDF5.open_group(file, h5plotpath("_unnamed")) - return _read_plot(grp) - end - -end # module _hdf5_implementation - -# Implement Plots.jl backend interface for HDF5Backend - -is_marker_supported(::HDF5Backend, shape::Shape) = true - -# Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{HDF5Backend}) end - -# Set up the subplot within the backend object. -function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end - -# Add one series to the underlying backend object. -# Called once per series -# NOTE: Seems to be called when user calls plot()... even if backend -# plot, sp.o has not yet been constructed... -function _series_added(plt::Plot{HDF5Backend}, series::Series) end - -# When series data is added/changed, this callback can do dynamic updates to the backend object. -# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here. -function _series_updated(plt::Plot{HDF5Backend}, series::Series) end - -# called just before updating layout bounding boxes... in case you need to prep -# for the calcs -function _before_layout_calcs(plt::Plot{HDF5Backend}) end - -# Set the (left, top, right, bottom) minimum padding around the plot area -# to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{HDF5Backend}) end - -# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations]) -function _update_plot_object(plt::Plot{HDF5Backend}) end - -# ---------------------------------------------------------------- - -# Display/show the plot (open a GUI window, or browser page, for example). -function _display(plt::Plot{HDF5Backend}) - msg = "HDF5 interface does not support `display()` function." - msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." - @warn msg - return -end - -# Interface actually required to use HDF5Backend - -hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString) = - _hdf5_implementation.hdf5plot_write(plt, path) -hdf5plot_write(path::AbstractString) = _hdf5_implementation.hdf5plot_write(current(), path) -hdf5plot_read(path::AbstractString) = _hdf5_implementation.hdf5plot_read(path) diff --git a/ext/PlotsInspectDR/PlotsInspectDR.jl b/ext/PlotsInspectDR/PlotsInspectDR.jl deleted file mode 100644 index 8b1378917..000000000 --- a/ext/PlotsInspectDR/PlotsInspectDR.jl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ext/PlotsInspectDR/inspectdr.jl b/ext/PlotsInspectDR/inspectdr.jl deleted file mode 100644 index b80b71952..000000000 --- a/ext/PlotsInspectDR/inspectdr.jl +++ /dev/null @@ -1,543 +0,0 @@ - -# https://github.com/ma-laforge/InspectDR.jl - -#=TODO: - Tweak scale factor for width & other sizes - -Not supported by InspectDR: - :foreground_color_grid - :foreground_color_border - :polar, - -Add in functionality to Plots.jl: - :aspect_ratio, -=# - -should_warn_on_unsupported(::InspectDRBackend) = false - -is_marker_supported(::InspectDRBackend, shape::Shape) = true - -#Do we avoid Map to avoid possible pre-comile issues? -function _inspectdr_mapglyph(s::Symbol) - s === :rect && return :square - s -end - -function _inspectdr_mapglyph(s::Shape) - x, y = coords(s) - InspectDR.GlyphPolyline(x, y) -end - -# py_marker(markers::AVec) = map(py_marker, markers) -function _inspectdr_mapglyph(markers::AVec) - @warn "Vectors of markers are currently unsupported in InspectDR." - _inspectdr_mapglyph(markers[1]) -end - -_inspectdr_mapglyphsize(v::Real) = v -function _inspectdr_mapglyphsize(v::Vector) - @warn "Vectors of marker sizes are currently unsupported in InspectDR." - _inspectdr_mapglyphsize(v[1]) -end - -_inspectdr_mapcolor(v::Colorant) = v -function _inspectdr_mapcolor(g::PlotUtils.ColorGradient) - @warn "Color gradients are currently unsupported in InspectDR." - # Pick middle color: - _inspectdr_mapcolor(g.colors[div(1 + end, 2)]) -end -function _inspectdr_mapcolor(v::AVec) - @warn "Vectors of colors are currently unsupported in InspectDR." - # Pick middle color: - _inspectdr_mapcolor(v[div(1 + end, 2)]) -end - -# Hack: suggested point size does not seem adequate relative to plot size, for some reason. -_inspectdr_mapptsize(v) = 1.5 * v - -_inspectdr_add_annotations(plot, sp::Subplot, x, y, val) = nothing # What kind of annotation is this? - -#plot::InspectDR.Plot2D -function _inspectdr_add_annotations(plot, sp::Subplot, x, y, val::PlotText) - vmap = Dict{Symbol,Symbol}(:top => :t, :bottom => :b) # :vcenter - hmap = Dict{Symbol,Symbol}(:left => :l, :right => :r) # :hcenter - align = Symbol(get(vmap, val.font.valign, :c), get(hmap, val.font.halign, :c)) - fnt = InspectDR.Font( - val.font.family, - val.font.pointsize, - color = _inspectdr_mapcolor(val.font.color), - ) - ann = InspectDR.atext( - texmath2unicode(val.str), - x = x, - y = y, - font = fnt, - angle = -val.font.rotation, # minus for consistency with other backends - align = align, - ) - InspectDR.add(plot, ann) - nothing -end - -# placement relative to figure -function _inspectdr_add_annotations( - plot, - sp::Subplot, - pos::Union{Tuple,Symbol}, - val::PlotText, -) - x, y, val = locate_annotation(sp, pos, val) - _inspectdr_add_annotations(plot, sp, x, y, val) -end - -# --------------------------------------------------------------------------- - -function _inspectdr_getaxisticks(ticks, gridlines, xfrm) - TickCustom = InspectDR.TickCustom - _xfrm(coord) = InspectDR.axis2aloc(Float64(coord), xfrm.spec) #Ensure Float64 - in case - - ttype = ticks_type(ticks) - if ticks === :native - # keep current - elseif ttype === :ticks_and_labels - pos = ticks[1] - labels = ticks[2] - nticks = length(ticks[1]) - newticks = TickCustom[TickCustom(_xfrm(pos[i]), labels[i]) for i in 1:nticks] - gridlines = InspectDR.GridLinesCustom(gridlines) - gridlines.major = newticks - gridlines.minor = [] - gridlines.displayminor = false - elseif ttype === :ticks - nticks = length(ticks) - gridlines.major = Float64[_xfrm(t) for t in ticks] - gridlines.minor = [] - gridlines.displayminor = false - elseif isnothing(ticks) - gridlines.major = [] - gridlines.minor = [] - else # Assume ticks === :native - # keep current - end - - gridlines # keep current -end - -function _inspectdr_setticks(sp::Subplot, plot, strip, xaxis, yaxis) - _get_ticks(axis) = axis[:ticks] === :native ? :native : get_ticks(sp, axis) - - xticks = _get_ticks(xaxis) - yticks = _get_ticks(yaxis) - - (xticks === :native && yticks === :native) && return # Don't "eval" tick values - - # TODO: Allow InspectDR to independently "eval" x or y ticks - ext = InspectDR.getextents_aloc(plot, 1) - grid = InspectDR._eval(strip.grid, plot.xscale, strip.yscale, ext) - grid.xlines = - _inspectdr_getaxisticks(xticks, grid.xlines, InspectDR.InputXfrm1D(plot.xscale)) - grid.ylines = - _inspectdr_getaxisticks(yticks, grid.ylines, InspectDR.InputXfrm1D(strip.yscale)) - strip.grid = grid -end - -# --------------------------------------------------------------------------- - -function _inspectdr_getscale(s::Symbol, yaxis::Bool) - #TODO: Support :asinh, :sqrt - kwargs = yaxis ? (:tgtmajor => 8, :tgtminor => 2) : () #More grid lines on y-axis - if :log2 == s - InspectDR.AxisScale(:log2; kwargs...) - elseif :log10 == s - InspectDR.AxisScale(:log10; kwargs...) - elseif :ln == s - InspectDR.AxisScale(:ln; kwargs...) - else #identity - InspectDR.AxisScale(:lin; kwargs...) - end -end - -# --------------------------------------------------------------------------- - -#Glyph used when plotting "Shape"s: -INSPECTDR_GLYPH_SHAPE = - InspectDR.GlyphPolyline(2 * InspectDR.GLYPH_SQUARE.x, InspectDR.GLYPH_SQUARE.y) - -mutable struct InspecDRPlotRef - mplot::Union{Nothing,InspectDR.Multiplot} - gui::Union{Nothing,InspectDR.GtkPlot} -end - -_inspectdr_getmplot(::Any) = nothing -_inspectdr_getmplot(r::InspecDRPlotRef) = r.mplot - -_inspectdr_getgui(::Any) = nothing -_inspectdr_getgui(gplot::InspectDR.GtkPlot) = (gplot.destroyed ? nothing : gplot) -_inspectdr_getgui(r::InspecDRPlotRef) = _inspectdr_getgui(r.gui) -push!(_initialized_backends, :inspectdr) - -# --------------------------------------------------------------------------- - -# Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{InspectDRBackend}) - mplot = _inspectdr_getmplot(plt.o) - gplot = _inspectdr_getgui(plt.o) - - # :overwrite_figure: want to reuse current figure - if plt[:overwrite_figure] && mplot !== nothing - mplot.subplots = [] # Reset - if gplot !== nothing # Ensure still references current plot - gplot.src = mplot - end - else # want new one: - mplot = InspectDR.Multiplot() - gplot = nothing # Will be created later - end - - # break link with old subplots - foreach(sp -> sp.o = nothing, plt.subplots) - - InspecDRPlotRef(mplot, gplot) -end - -# --------------------------------------------------------------------------- - -# Set up the subplot within the backend object. -function _initialize_subplot(plt::Plot{InspectDRBackend}, sp::Subplot{InspectDRBackend}) - plot = sp.o - # Don't do anything without a "subplot" object: Will process later. - plot === nothing && return - plot.data = [] - plot.userannot = [] #Clear old markers/text annotation/polyline "annotation" - plot -end - -# --------------------------------------------------------------------------- - -# Add one series to the underlying backend object. -# Called once per series -# NOTE: Seems to be called when user calls plot()... even if backend -# plot, sp.o has not yet been constructed... -function _series_added(plt::Plot{InspectDRBackend}, series::Series) - st = series[:seriestype] - sp = series[:subplot] - - # Don't do anything without a "subplot" object: Will process later. - (plot = sp.o) === nothing && return - - clims = get_clims(sp, series) - - _vectorize(v) = isa(v, Vector) ? v : collect(v) #InspectDR only supports vectors - x, y = if st === :straightline - straightline_data(series) - else - _vectorize(series[:x]), _vectorize(series[:y]) - end - - # No support for polar grid... but can still perform polar transformation: - if ispolar(sp) - Θ = x - r = y - x = r .* cos.(Θ) - y = r .* sin.(Θ) - end - - # doesn't handle mismatched x/y - wrap data (pyplot behaviour): - nx, ny = map(length, (x, y)) - if nx < ny - series[:x] = Float64[x[mod1(i, nx)] for i in 1:ny] - elseif ny > nx - series[:y] = Float64[y[mod1(i, ny)] for i in 1:nx] - end - - #= TODO: Eventually support - series[:fillcolor] #I think this is fill under line - zorder = series[:series_plotindex] - - For st in :shape: - zorder = series[:series_plotindex], - =# - - if st in (:shape,) - x, y = shape_data(series) - nmax = 0 - for (i, rng) in enumerate(iter_segments(x, y)) - nmax = i - if length(rng) > 1 - linewidth = series[:linewidth] - c = plot_color(get_linecolor(series), get_linealpha(series)) - linecolor = _inspectdr_mapcolor(_cycle(c, i)) - c = plot_color(get_fillcolor(series), get_fillalpha(series)) - fillcolor = _inspectdr_mapcolor(_cycle(c, i)) - line = InspectDR.line(style = :solid, width = linewidth, color = linecolor) - apline = InspectDR.PolylineAnnotation( - x[rng], - y[rng], - line = line, - fillcolor = fillcolor, - ) - InspectDR.add(plot, apline) - end - end - - i = (nmax >= 2 ? div(nmax, 2) : nmax) #Must pick one set of colors for legend - if i > 1 #Add dummy waveform for legend entry: - linewidth = series[:linewidth] - c = plot_color(get_linecolor(series), get_linealpha(series)) - linecolor = _inspectdr_mapcolor(_cycle(c, i)) - c = plot_color(get_fillcolor(series), get_fillalpha(series)) - fillcolor = _inspectdr_mapcolor(_cycle(c, i)) - wfrm = InspectDR.add(plot, Float64[], Float64[], id = series[:label]) - wfrm.line = InspectDR.line( - style = :none, - width = linewidth, #linewidth affects glyph - ) - wfrm.glyph = InspectDR.glyph( - shape = INSPECTDR_GLYPH_SHAPE, - size = 8, - color = linecolor, - fillcolor = fillcolor, - ) - end - elseif st in (:path, :scatter, :straightline) #, :steppre, :stepmid, :steppost) - # NOTE: In Plots.jl, :scatter plots have 0-linewidths (I think). - linewidth = series[:linewidth] - # More efficient & allows some support for markerstrokewidth: - _style = (0 == linewidth ? :none : series[:linestyle]) - wfrm = InspectDR.add(plot, x, y, id = series[:label]) - wfrm.line = InspectDR.line( - style = _style, - width = series[:linewidth], - color = plot_color(get_linecolor(series), get_linealpha(series)), - ) - # InspectDR does not control markerstrokewidth independently. - if _style === :none - # Use this property only if no line is displayed: - wfrm.line.width = series[:markerstrokewidth] - end - wfrm.glyph = InspectDR.glyph( - shape = _inspectdr_mapglyph(series[:markershape]), - size = _inspectdr_mapglyphsize(series[:markersize]), - color = _inspectdr_mapcolor( - plot_color(get_markerstrokecolor(series), get_markerstrokealpha(series)), - ), - fillcolor = _inspectdr_mapcolor( - plot_color(get_markercolor(series, clims), get_markeralpha(series)), - ), - ) - end - - # this is all we need to add the series_annotations text - anns = series[:series_annotations] - for (xi, yi, str, fnt) in EachAnn(anns, x, y) - _inspectdr_add_annotations(plot, sp, xi, yi, PlotText(str, fnt)) - end -end - -# --------------------------------------------------------------------------- - -# When series data is added/changed, this callback can do dynamic updates to the backend object. -# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here. -_series_updated(plt::Plot{InspectDRBackend}, series::Series) = nothing - -# --------------------------------------------------------------------------- - -function _inspectdr_setupsubplot(sp::Subplot{InspectDRBackend}) - plot = sp.o - strip = plot.strips[1] #Only 1 strip supported with Plots.jl - - xaxis = sp[:xaxis] - yaxis = sp[:yaxis] - xgrid_show = xaxis[:grid] - ygrid_show = yaxis[:grid] - - strip.grid = InspectDR.GridRect( - vmajor = xgrid_show, # vminor=xgrid_show, - hmajor = ygrid_show, # hminor=ygrid_show, - ) - - plot.xscale = _inspectdr_getscale(xaxis[:scale], false) - strip.yscale = _inspectdr_getscale(yaxis[:scale], true) - xmin, xmax = axis_limits(sp, :x) - ymin, ymax = axis_limits(sp, :y) - if ispolar(sp) - #Plots.jl appears to give (xmin,xmax) ≜ (Θmin,Θmax) & (ymin,ymax) ≜ (rmin,rmax) - rmax = NaNMath.max(abs(ymin), abs(ymax)) - xmin, xmax = -rmax, rmax - ymin, ymax = -rmax, rmax - end - plot.xext_full = InspectDR.PExtents1D(xmin, xmax) - strip.yext_full = InspectDR.PExtents1D(ymin, ymax) - #Set current extents = full extents (needed for _eval(strip.grid,...)) - plot.xext = plot.xext_full - strip.yext = strip.yext_full - _inspectdr_setticks(sp, plot, strip, xaxis, yaxis) - - a = plot.annotation - a.title = texmath2unicode(sp[:title]) - a.xlabel = texmath2unicode(xaxis[:guide]) - a.ylabels = [texmath2unicode(yaxis[:guide])] - - #Modify base layout of new object: - l = plot.layout.defaults = deepcopy(InspectDR.defaults.plotlayout) - #IMPORTANT: Must deepcopy to ensure we don't change layouts of other plots. - #Works because plot uses defaults (not user-overwritten `layout.values`) - l.frame_canvas.fillcolor = _inspectdr_mapcolor(sp[:background_color_subplot]) - l.frame_data.fillcolor = _inspectdr_mapcolor(sp[:background_color_inside]) - l.frame_data.line.color = _inspectdr_mapcolor(xaxis[:foreground_color_axis]) - l.font_title = InspectDR.Font( - sp[:titlefontfamily], - _inspectdr_mapptsize(sp[:titlefontsize]), - color = _inspectdr_mapcolor(sp[:titlefontcolor]), - ) - #Cannot independently control fonts of axes with InspectDR: - l.font_axislabel = InspectDR.Font( - xaxis[:guidefontfamily], - _inspectdr_mapptsize(xaxis[:guidefontsize]), - color = _inspectdr_mapcolor(xaxis[:guidefontcolor]), - ) - l.font_ticklabel = InspectDR.Font( - xaxis[:tickfontfamily], - _inspectdr_mapptsize(xaxis[:tickfontsize]), - color = _inspectdr_mapcolor(xaxis[:tickfontcolor]), - ) - l.enable_legend = (sp[:legend_position] !== :none) - #l.halloc_legend = 150 #TODO: compute??? - l.font_legend = InspectDR.Font( - sp[:legend_font_family], - _inspectdr_mapptsize(sp[:legend_font_pointsize]), - color = _inspectdr_mapcolor(sp[:legend_font_color]), - ) - l.frame_legend.fillcolor = _inspectdr_mapcolor(sp[:legend_background_color]) - #_round!() ensures values use integer spacings (looks better on screen): - InspectDR._round!(InspectDR.autofit2font!(l, legend_width = 10.0)) #10 "em"s wide -end - -# called just before updating layout bounding boxes... in case you need to prep -# for the calcs -function _before_layout_calcs(plt::Plot{InspectDRBackend}) - (mplot = _inspectdr_getmplot(plt.o)) === nothing && return - - mplot.title = plt[:plot_title] - if isempty(mplot.title) - # Don't use window_title... probably not what you want. - # mplot.title = plt[:window_title] - end - - mplot.layout[:frame].fillcolor = _inspectdr_mapcolor(plt[:background_color_outside]) - mplot.layout[:frame] = mplot.layout[:frame] #register changes - resize!(mplot.subplots, length(plt.subplots)) - nsubplots = length(plt.subplots) - for (i, sp) in enumerate(plt.subplots) - isassigned(mplot.subplots, i) || (mplot.subplots[i] = InspectDR.Plot2D()) - sp.o = mplot.subplots[i] - plot = sp.o - _initialize_subplot(plt, sp) - _inspectdr_setupsubplot(sp) - - # add the annotations - for ann in sp[:annotations] - _inspectdr_add_annotations(plot, sp, ann...) - end - end - - # Do not yet support absolute plot positioning. - # Just try to make things look more-or less ok: - mplot.layout[:ncolumns] = if nsubplots <= 1 - 1 - elseif nsubplots <= 4 - 2 - elseif nsubplots <= 6 - 3 - elseif nsubplots <= 12 - 4 - else - 5 - end - - foreach(series -> _series_added(plt, series), plt.series_list) - nothing -end - -# ---------------------------------------------------------------- - -# Set the (left, top, right, bottom) minimum padding around the plot area -# to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{InspectDRBackend}) - plot = sp.o - isa(plot, InspectDR.Plot2D) || return sp.minpad - # Computing plotbounds with 0-BoundingBox returns required padding: - bb = InspectDR.plotbounds(plot.layout.values, InspectDR.BoundingBox(0, 0, 0, 0)) - # NOTE: plotbounds always pads for titles, legends, etc. even if not in use. - # TODO: possibly zero-out items not in use?? - - # add in the user-specified margin to InspectDR padding: - leftpad = abs(bb.xmin) * px + sp[:left_margin] - toppad = abs(bb.ymin) * px + sp[:top_margin] - rightpad = abs(bb.xmax) * px + sp[:right_margin] - bottompad = abs(bb.ymax) * px + sp[:bottom_margin] - sp.minpad = (leftpad, toppad, rightpad, bottompad) -end - -# ---------------------------------------------------------------- - -# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations]) -function _update_plot_object(plt::Plot{InspectDRBackend}) - (mplot = _inspectdr_getmplot(plt.o)) === nothing && return - mplot.bblist = InspectDR.BoundingBox[] - - for (i, sp) in enumerate(plt.subplots) - figw, figh = sp.plt[:size] - pcts = bbox_to_pcts(sp.bbox, figw * px, figh * px) - _left, _bottom, _width, _height = pcts - ymax = 1.0 - _bottom - ymin = ymax - _height - bb = InspectDR.BoundingBox(_left, _left + _width, ymin, ymax) - push!(mplot.bblist, bb) - end - - (gplot = _inspectdr_getgui(plt.o)) === nothing && return - - gplot.src = mplot #Ensure still references current plot - InspectDR.refresh(gplot) - nothing -end - -# ---------------------------------------------------------------- - -_inspectdr_show(io::IO, mime::MIME, ::Nothing, w, h) = - throw(ErrorException("Cannot show(::IO, ...) plot - not yet generated")) -_inspectdr_show(io::IO, mime::MIME, mplot, w, h) = - InspectDR._show(io, mime, mplot, Float64(w), Float64(h)) - -function _show(io::IO, mime::MIME{Symbol("image/png")}, plt::Plot{InspectDRBackend}) - dpi = plt[:dpi] # TODO: support - _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o), plt[:size]...) -end -for (mime, fmt) in ( - "image/svg+xml" => "svg", - "application/eps" => "eps", - "image/eps" => "eps", - # "application/postscript" => "ps", # TODO: support once Cairo supports PSSurface - "application/pdf" => "pdf", -) - @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{InspectDRBackend}) - _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o), plt[:size]...) - end -end - -# ---------------------------------------------------------------- - -# Display/show the plot (open a GUI window, or browser page, for example). -function _display(plt::Plot{InspectDRBackend}) - (mplot = _inspectdr_getmplot(plt.o)) === nothing && return - - if (gplot = _inspectdr_getgui(plt.o)) === nothing - gplot = display(InspectDR.GtkDisplay(), mplot) - else - # redundant... Plots.jl will call _update_plot_object: - # InspectDR.refresh(gplot) - end - plt.o = InspecDRPlotRef(mplot, gplot) - gplot -end diff --git a/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl b/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl deleted file mode 100644 index db64fb1c0..000000000 --- a/ext/PlotsPGFPlotsXExt/PlotsPGFPlotsXExt.jl +++ /dev/null @@ -1,60 +0,0 @@ -module PlotsPGFPlotsXExt - -using PGFPlotsX: PGFPlotsX -using LaTeXStrings: LaTeXString -using UUIDs: uuid4 -using Latexify: Latexify -using Contour: Contour # TODO: this could become its own extensionoo -using PlotUtils: PlotUtils, ColorGradient, color_list -using Printf: @sprintf - -using Plots: Plots, straightline_data, shape_data -# TODO: eliminate this list -using Plots: - bbox, - left, - right, - bottom, - width, - height, - labelfunc_tex, - top, - plotarea, - axis_drawing_info, - _guess_best_legend_position, - prepare_output, - current -using Plots: GridLayout -using RecipesPipeline: RecipesPipeline -using Plots.Arrows -using Plots.Axes -using Plots.Axes: has_ticks -using Plots.Annotations -using Plots.Colorbars -using Plots.Colors -using Plots.Commons -using Plots.Fonts -using Plots.Fonts: Font, PlotText -using Plots.PlotMeasures -using Plots.PlotsPlots -using Plots.PlotsSeries -using Plots.Subplots -using Plots.Surfaces -using Plots.Shapes -using Plots.Shapes: Shape -using Plots.Ticks - -import Plots: - _display, - _show, - _update_min_padding!, - labelfunc, - _create_backend_figure, - _series_added, - _update_plot_object, - pgfx_sanitize_string - -include("initialization.jl") -include("pgfplotsx.jl") - -end # module diff --git a/ext/PlotsPGFPlotsXExt/initialization.jl b/ext/PlotsPGFPlotsXExt/initialization.jl deleted file mode 100644 index 749a5c20c..000000000 --- a/ext/PlotsPGFPlotsXExt/initialization.jl +++ /dev/null @@ -1,218 +0,0 @@ -# unrolling the old # init_backend macro by hand case by case -# this is not a macro for the backend maintainers and explicit control - -const package_str = "PGFPlotsX" -const str = "pgfplotsx" -const sym = :pgfplotsx - -struct PGFPlotsXBackend <: Plots.AbstractBackend end -const T = PGFPlotsXBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @info "Initializing $package_str backend in Plots; run `$str()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[T] = sym - - push!(Plots._initialized_backends, sym) - - # Additional setup required by the backend: - -end - -Plots.backend_name(::T) = sym -Plots.backend_package_name(::T) = Plots.backend_package_name(sym) - -const _pgfplotsx_attrs = Plots.merge_with_base_supported([ - :annotations, - :annotationrotation, - :annotationhalign, - :annotationfontsize, - :annotationfontfamily, - :annotationcolor, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :legend_foreground_color, - :foreground_color_grid, - :foreground_color_axis, - :foreground_color_text, - :foreground_color_border, - :label, - :seriescolor, - :seriesalpha, - :line, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :bins, - :layout, - :title, - :window_title, - :guide, - :widen, - :lims, - :ticks, - :scale, - :flip, - :titlefontfamily, - :titlefontsize, - :titlefonthalign, - :titlefontvalign, - :titlefontrotation, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_halign, - :legend_font_valign, - :legend_font_rotation, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfonthalign, - :tickfontvalign, - :tickfontrotation, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefonthalign, - :guidefontvalign, - :guidefontrotation, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_titlefontsize, - :colorbar_titlefontcolor, - :colorbar_titlefontrotation, - :colorbar_entry, - :fill, - :fill_z, - :line_z, - :marker_z, - :levels, - :legend_column, - :legend_title, - :legend_title_font_color, - :legend_title_font_pointsize, - :ribbon, - :quiver, - :orientation, - :overwrite_figure, - :polar, - :plot_title, - :plot_titlefontcolor, - :plot_titlefontrotation, - :plot_titlefontsize, - :plot_titlevspan, - :aspect_ratio, - :normalize, - :weights, - :inset_subplots, - :bar_width, - :arrow, - :framestyle, - :tick_direction, - :thickness_scaling, - :camera, - :contour_labels, - :connections, - :thickness_scaling, - :axis, - :draw_arrow, - :minorgrid, - :minorgridalpha, - :minorgridlinewidth, - :minorgridstyle, - :minorticks, - :mirror, - :rotation, - :showaxis, - :tickfontrotation, - :draw_arrow, -]) -const _pgfplotsx_seriestypes = [ - :path, - :scatter, - :straightline, - :path3d, - :scatter3d, - :surface, - :wireframe, - :heatmap, - :mesh3d, - :contour, - :contour3d, - :quiver, - :shape, - :steppre, - :stepmid, - :steppost, - :ysticks, - :xsticks, -] -const _pgfplotsx_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _pgfplotsx_markers = [ - :none, - :auto, - :circle, - :rect, - :diamond, - :utriangle, - :dtriangle, - :ltriangle, - :rtriangle, - :cross, - :xcross, - :x, - :+, - :star5, - :star6, - :pentagon, - :hline, - :vline, -] -const _pgfplotsx_scales = [:identity, :ln, :log2, :log10] -Plots.is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true - -# additional constants -const _pgfplotsx_series_ids = KW() - -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - eval(quote - Plots.$f1(::T, $s::Symbol) = $s in $v - Plots.$f2(::T) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- diff --git a/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl b/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl deleted file mode 100644 index 8abde206f..000000000 --- a/ext/PlotsPlotlyJSExt/PlotsPlotlyJSExt.jl +++ /dev/null @@ -1,12 +0,0 @@ -module PlotsPlotlyJSExt - -using PlotlyJS: PlotlyJS -using Plots.Commons -using Plots.Plotly -using Plots.PlotsPlots -import Plots: _show, _display, closeall, current, isplotnull - -include("initialization.jl") -include("plotlyjs.jl") - -end # module diff --git a/ext/PlotsPlotlyJSExt/initialization.jl b/ext/PlotsPlotlyJSExt/initialization.jl deleted file mode 100644 index 34d8077f4..000000000 --- a/ext/PlotsPlotlyJSExt/initialization.jl +++ /dev/null @@ -1,53 +0,0 @@ -# unrolling the old # init_backend macro by hand case by case -# this is not a macro for the backend maintainers and explicit control - -const package_str = "PlotlyJS" -const str = lowercase(package_str) -const sym = Symbol(str) - -struct PlotlyJSBackend <: Plots.AbstractBackend end -const T = PlotlyJSBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @info "Initializing $package_str backend in Plots; run `$str()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[T] = sym - - push!(Plots._initialized_backends, sym) - - # Additional setup required by the backend: - -end - -Plots.backend_name(::T) = sym -Plots.backend_package_name(::T) = Plots.backend_package_name(sym) - -const _plotlyjs_attrs = Plots.Plotly._plotly_attrs -const _plotlyjs_seriestypes = Plots.Plotly._plotly_seriestypes -const _plotlyjs_styles = Plots.Plotly._plotly_styles -const _plotlyjs_markers = Plots.Plotly._plotly_markers -const _plotlyjs_scales = Plots.Plotly._plotly_scales - -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - eval(quote - Plots.$f1(::T, $s::Symbol) = $s in $v - Plots.$f2(::T) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- diff --git a/ext/PlotsPlotlyJSExt/plotlyjs.jl b/ext/PlotsPlotlyJSExt/plotlyjs.jl deleted file mode 100644 index 0ae9b83df..000000000 --- a/ext/PlotsPlotlyJSExt/plotlyjs.jl +++ /dev/null @@ -1,52 +0,0 @@ -# https://github.com/JuliaPlots/PlotlyJS.jl - -# ------------------------------------------------------------------------------ -function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) - plt[:overwrite_figure] && closeall() - plt.o = PlotlyJS.plot() - traces = PlotlyJS.GenericTrace[] - for series_dict in plotly_series(plt) - plotly_type = pop!(series_dict, :type) - series_dict[:transpose] = false - push!(traces, PlotlyJS.GenericTrace(plotly_type; series_dict...)) - end - PlotlyJS.addtraces!(plt.o, traces...) - layout = plotly_layout(plt) - w, h = plt[:size] - PlotlyJS.relayout!(plt.o, layout, width = w, height = h) - plt.o -end - -# ------------------------------------------------------------------------------ - -for (mime, fmt) in ( - "application/pdf" => "pdf", - "image/png" => "png", - "image/svg+xml" => "svg", - "image/eps" => "eps", -) - @eval _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyJSBackend}) = - PlotlyJS.savefig(io, plotlyjs_syncplot(plt), format = $fmt) -end - -# Use the Plotly implementation for json and html: -_show(io::IO, mime::MIME"application/vnd.plotly.v1+json", plt::Plot{PlotlyJSBackend}) = - plotly_show_js(io, plt) - -html_head(plt::Plot{PlotlyJSBackend}) = plotly_html_head(plt) -html_body(plt::Plot{PlotlyJSBackend}) = plotly_html_body(plt) - -_show(io::IO, ::MIME"text/html", plt::Plot{PlotlyJSBackend}) = - write(io, embeddable_html(plt)) - -_display(plt::Plot{PlotlyJSBackend}) = display(plotlyjs_syncplot(plt)) - -PlotlyJS.WebIO.render(plt::Plot{PlotlyJSBackend}) = - PlotlyJS.WebIO.render(plotlyjs_syncplot(plt)) - -closeall(::PlotlyJSBackend) = - if !isplotnull() && isa(current().o, PlotlyJS.SyncPlot) - close(current().o) - end - -Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyJSBackend}) = true diff --git a/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl b/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl deleted file mode 100644 index 13cf65244..000000000 --- a/ext/PlotsPythonPlotExt/PlotsPythonPlotExt.jl +++ /dev/null @@ -1,84 +0,0 @@ -module PlotsPythonPlotExt - -import Plots: - _before_layout_calcs, - _create_backend_figure, - _display, - _show, - _update_min_padding!, - _update_plot_object, - closeall, - is_marker_supported, - labelfunc - -using NaNMath: NaNMath -using Plots.Annotations -using Plots.Arrows -using Plots.Axes -using Plots.Colorbars -using Plots.Colorbars: cbar_fill, cbar_gradient, cbar_lines -using Plots.Colors -using Plots.Commons -using Plots.Commons: _all_markers, _3dTypes, single_color -using Plots.Fonts -using Plots.Fonts: Font, PlotText -using Plots.PlotMeasures -using Plots.PlotMeasures: px2inch -using Plots.PlotUtils: PlotUtils, ColorGradient, plot_color, color_list, cgrad -using Plots.PlotsPlots -using Plots.PlotsSeries -using Plots.Shapes -using Plots.Shapes: Shape -using Plots.Subplots -using Plots.Ticks -using Plots.Ticks: no_minor_intervals -using Plots: - DPI, - Plots, - Surface, - _cycle, - _guess_best_legend_position, - axis_drawing_info, - axis_drawing_info_3d, - bbox, - bottom, - convert_to_polar, - heatmap_edges, - is3d, - is_2tuple, - is_uniformly_spaced, - isautop, - isortho, - labelfunc_tex, - mesh3d_triangles, - left, - merge_with_base_supported, - plotarea, - right, - shape_data, - straightline_data, - top, - isscalar, - isvector, - supported_scales, - ticks_type, - legend_angle, - legend_anchor_index, - legend_pos_from_angle, - width, - ispositive, - height, - bbox_to_pcts -using PythonPlot: PythonPlot - -const PythonCall = PythonPlot.PythonCall -const mpl_toolkits = PythonCall.pynew() # PythonCall.pyimport("mpl_toolkits") -const mpl = PythonPlot.matplotlib -const numpy = PythonCall.pynew() # PythonCall.pyimport("numpy") - -using RecipesPipeline: RecipesPipeline - -include("initialization.jl") -include("pythonplot.jl") - -end # module diff --git a/ext/PlotsPythonPlotExt/initialization.jl b/ext/PlotsPythonPlotExt/initialization.jl deleted file mode 100644 index 29af0c990..000000000 --- a/ext/PlotsPythonPlotExt/initialization.jl +++ /dev/null @@ -1,192 +0,0 @@ -import Plots: backend_name, backend_package_name, is_marker_supported - -# unrolling the old # init_backend macro by hand case by case -const package_str = "PythonPlot" -const str = "pythonplot" -const sym = :pythonplot - -struct PythonPlotBackend <: Plots.AbstractBackend end -const T = PythonPlotBackend - -get_concrete_backend() = T - -function __init__() - @info "Initializing $package_str backend in Plots; run `$str()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[T] = sym - - push!(Plots._initialized_backends, sym) - - if PythonPlot.version < v"3.4" - @warn """You are using Matplotlib $(PythonPlot.version), which is no longer - officially supported by the Plots community. To ensure smooth Plots.jl - integration update your Matplotlib library to a version ≥ 3.4.0 - """ - end - - # PythonCall.pycopy!(mpl, PythonCall.pyimport("matplotlib")) - PythonCall.pycopy!(mpl_toolkits, PythonCall.pyimport("mpl_toolkits")) - PythonCall.pycopy!(numpy, PythonCall.pyimport("numpy")) - # PythonCall.pyimport("mpl_toolkits.axes_grid1") - numpy.seterr(invalid = "ignore") - PythonPlot.ioff() # we don't want every command to update the figure -end -# Make pythonplot known to Plots -backend_name(::T) = sym -backend_package_name(::T) = backend_package_name(sym) - -const _pythonplot_attrs = merge_with_base_supported([ - :annotations, - :legend_background_color, - :background_color_inside, - :background_color_outside, - :foreground_color_grid, - :legend_foreground_color, - :foreground_color_title, - :foreground_color_axis, - :foreground_color_border, - :foreground_color_guide, - :foreground_color_text, - :label, - :linecolor, - :linestyle, - :linewidth, - :linealpha, - :markershape, - :markercolor, - :markersize, - :markeralpha, - :markerstrokewidth, - :markerstrokecolor, - :markerstrokealpha, - :fillrange, - :fillcolor, - :fillalpha, - :fillstyle, - :bins, - :bar_width, - :bar_edges, - :bar_position, - :title, - :titlelocation, - :titlefont, - :window_title, - :guide, - :guide_position, - :widen, - :lims, - :ticks, - :scale, - :flip, - :rotation, - :titlefontfamily, - :titlefontsize, - :titlefontcolor, - :legend_font_family, - :legend_font_pointsize, - :legend_font_color, - :tickfontfamily, - :tickfontsize, - :tickfontcolor, - :guidefontfamily, - :guidefontsize, - :guidefontcolor, - :grid, - :gridalpha, - :gridstyle, - :gridlinewidth, - :legend_position, - :legend_title, - :colorbar, - :colorbar_title, - :colorbar_entry, - :colorbar_ticks, - :colorbar_tickfontfamily, - :colorbar_tickfontsize, - :colorbar_tickfonthalign, - :colorbar_tickfontvalign, - :colorbar_tickfontrotation, - :colorbar_tickfontcolor, - :colorbar_titlefontcolor, - :colorbar_titlefontsize, - :colorbar_scale, - :marker_z, - :line, - :line_z, - :fill, - :fill_z, - :fontfamily, - :fontfamily_subplot, - :legend_column, - :legend_font, - :legend_title, - :legend_title_font_color, - :legend_title_font_family, - :legend_title_font_pointsize, - :levels, - :ribbon, - :quiver, - :arrow, - :orientation, - :overwrite_figure, - :polar, - :normalize, - :weights, - :contours, - :aspect_ratio, - :clims, - :inset_subplots, - :dpi, - :stride, - :framestyle, - :tick_direction, - :camera, - :contour_labels, - :connections, -]) - -const _pythonplot_seriestypes = [ - :path, - :steppre, - :stepmid, - :steppost, - :shape, - :straightline, - :scatter, - :hexbin, - :heatmap, - :image, - :contour, - :contour3d, - :path3d, - :scatter3d, - :mesh3d, - :surface, - :wireframe, -] - -const _pythonplot_styles = [:auto, :solid, :dash, :dot, :dashdot] -const _pythonplot_markers = vcat(_all_markers, :pixel) -const _pythonplot_scales = [:identity, :ln, :log2, :log10] - -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - eval(quote - Plots.$f1(::T, $s::Symbol) = $s in $v - Plots.$f2(::T) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- diff --git a/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl b/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl deleted file mode 100644 index 6d251d60e..000000000 --- a/ext/PlotsUnicodePlotsExt/PlotsUnicodePlotsExt.jl +++ /dev/null @@ -1,41 +0,0 @@ -module PlotsUnicodePlotsExt - -using UnicodePlots -using Plots: Plots, isijulia, texmath2unicode, straightline_data, shape_data -# TODO: eliminate this list -using Plots: - bbox, - left, - right, - bottom, - top, - plotarea, - axis_drawing_info, - mesh3d_triangles, - _guess_best_legend_position, - prepare_output -using Plots: GridLayout -using RecipesPipeline: RecipesPipeline -using Plots.Arrows -using Plots.Axes -using Plots.Axes: has_ticks -using Plots.Annotations -using Plots.Colorbars -using Plots.Colors -using Plots.Commons -using Plots.Fonts -using Plots.Fonts: Font, PlotText -using Plots.PlotMeasures -using Plots.PlotsPlots -using Plots.PlotsSeries -using Plots.Subplots -using Plots.Shapes -using Plots.Shapes: Shape -using Plots.Ticks - -import Plots: _before_layout_calcs, _display, _show - -include("initialization.jl") -include("unicodeplots.jl") - -end # module diff --git a/ext/PlotsUnicodePlotsExt/initialization.jl b/ext/PlotsUnicodePlotsExt/initialization.jl deleted file mode 100644 index 4752da423..000000000 --- a/ext/PlotsUnicodePlotsExt/initialization.jl +++ /dev/null @@ -1,118 +0,0 @@ -# unrolling the old # init_backend macro by hand case by case - -const package_str = "UnicodePlots" -const str = "unicodeplots" -const sym = :unicodeplots - -struct UnicodePlotsBackend <: Plots.AbstractBackend end -const T = UnicodePlotsBackend - -get_concrete_backend() = UnicodePlotsBackend # opposite to abstract - -function __init__() - @info "Initializing $package_str backend in Plots; run `$str()` to activate it." - Plots._backendType[sym] = get_concrete_backend() - Plots._backendSymbol[T] = sym - - push!(Plots._initialized_backends, sym) -end -# Make unicodeplots know to Plots -Plots.backend_name(::UnicodePlotsBackend) = sym -Plots.backend_package_name(::UnicodePlotsBackend) = Plots.backend_package_name(sym) - -const _unicodeplots_attrs = Plots.merge_with_base_supported([ - :annotations, - :bins, - :guide, - :widen, - :grid, - :label, - :layout, - :legend, - :legend_title_font_color, - :lims, - :line, - :linealpha, - :linecolor, - :linestyle, - :markershape, - :plot_title, - :quiver, - :arrow, - :seriesalpha, - :seriescolor, - :scale, - :flip, - :title, - # :marker_z, - :line_z, -]) -const _unicodeplots_seriestypes = [ - :path, - :path3d, - :scatter, - :scatter3d, - :straightline, - # :bar, - :shape, - :histogram2d, - :heatmap, - :contour, - # :contour3d, - :image, - :spy, - :surface, - :wireframe, - :mesh3d, -] -const _unicodeplots_styles = [:auto, :solid] -const _unicodeplots_markers = [ - :none, - :auto, - :pixel, - # vvvvvvvvvv shapes - :circle, - :rect, - :star5, - :diamond, - :hexagon, - :cross, - :xcross, - :utriangle, - :dtriangle, - :rtriangle, - :ltriangle, - :pentagon, - # :heptagon, - # :octagon, - :star4, - :star6, - # :star7, - :star8, - :vline, - :hline, - :+, - :x, -] -const _unicodeplots_scales = [:identity, :ln, :log2, :log10] -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - eval(quote - Plots.$f1(::UnicodePlotsBackend, $s::Symbol) = $s in $v - Plots.$f2(::UnicodePlotsBackend) = sort(collect($v)) - end) -end - -## results in: -# Plots.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# Plots.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# Plots.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- diff --git a/src/Plots.jl b/src/Plots.jl index fc5ebd9dc..3d9c6e9f6 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -1,205 +1,121 @@ module Plots - -if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel")) - @eval Base.Experimental.@optlevel 1 -end -if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_methods")) - @eval Base.Experimental.@max_methods 1 +using PrecompileTools +using Preferences +using Reexport +using Pkg +@reexport using PlotsBase + +function __init__() + ccall(:jl_generating_output, Cint, ()) == 1 && return + load_default_backend() end -using Pkg, Dates, Printf, Statistics, Base64, LinearAlgebra, SparseArrays, Random -using PrecompileTools, Preferences, Reexport, RelocatableFolders -using Base.Meta -@reexport using RecipesBase -@reexport using PlotThemes -@reexport using PlotUtils - -import RecipesBase: plot, plot!, animate, is_explicit, grid -import RecipesPipeline -import RecipesPipeline: - inverse_scale_func, - datetimeformatter, - AbstractSurface, - group_as_matrix, # for StatsPlots - dateformatter, - timeformatter, - needs_3d_axes, - DefaultsDict, - explicitkeys, - scale_func, - is_surface, - Formatted, - reset_kw!, - SliceIt, - pop_kw!, - Volume, - is3d -import UnicodeFun -import StatsBase -import Downloads -import Showoff -import Unzip -import JLFzf -import JSON - -#! format: off -export - grid, - bbox, - plotarea, - KW, - - theme, - protect, - plot, - plot!, - attr!, - - current, - default, - with, - twinx, - twiny, - - pie, - pie!, - plot3d, - plot3d!, - - title!, - annotate!, - - xlims, - ylims, - zlims, - - savefig, - png, - gui, - inline, - closeall, - - backend, - backends, - backend_name, - backend_object, - - text, - font, - stroke, - brush, - OHLC, - arrow, - Shape, - cgrad, +# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: +# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" +# ==> this must always be done during precompilation, otherwise +# the cache will not invalidate when preferences change +const PLOTS_DEFAULT_BACKEND = lowercase(load_preference(Plots, "default_backend", "gr")) + +function load_default_backend() + # environment variable preempts the `Preferences` based mechanism + PlotsBase.CURRENT_BACKEND.sym = + get(ENV, "PLOTS_DEFAULT_BACKEND", PLOTS_DEFAULT_BACKEND) |> lowercase |> Symbol + if (pkg_name = PlotsBase.backend_package_name()) ≡ :GR + @eval import GR + end + Base.invokelatest(PlotsBase.backend, PlotsBase.CURRENT_BACKEND.sym) +end - frame, - gif, - mov, - mp4, - webm, - animate, - @animate, - @gif, - @P_str, - Animation, +function set_default_backend!( + backend::Union{Nothing,AbstractString,Symbol} = nothing; + force = true, + kw..., +) + if backend ≡ nothing + delete_preferences!(Plots, "default_backend"; force, kw...) + else + # NOTE: `_check_installed` already throws a warning + if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing + set_preferences!(Plots, "default_backend" => value; force, kw...) + end + end + nothing +end - test_examples, - coords, +function diagnostics(io::IO = stdout) + origin = if has_preference(Plots, "default_backend") + "`Preferences`" + elseif haskey(ENV, "PLOTS_DEFAULT_BACKEND") + "environment variable" + else + "fallback" + end + if (be = backend_name()) ≡ :none + @info "no `Plots` backends currently initialized" + else + be_name = string(PlotsBase.backend_package_name(be)) + @info "selected `Plots` backend: $be_name, from $origin" + Pkg.status( + ["Plots", "PlotsBase", "RecipesBase", "RecipesPipeline", be_name]; + mode = Pkg.PKGMODE_MANIFEST, + io, + ) + end + nothing +end - translate, - translate!, - rotate, - rotate!, - center, - plotattr, - scalefontsizes, - resetfontsizes -#! format: on -using Measures: Measures -include("PlotMeasures.jl") -using .PlotMeasures -using .PlotMeasures: Length, AbsoluteLength, Measure -import .PlotMeasures: width, height -# --------------------------------------------------------- -macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) - Expr( - :module, - true, - mod, - Expr( - :block, - Expr( - :import, - Expr( - :(:), - Expr(:., :., :., parent), - (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., - ), - ), - Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...), - ), - ) |> esc +# COV_EXCL_START +@setup_workload begin + load_default_backend() + @debug PlotsBase.backend_package_name() + n = length(PlotsBase._examples) + imports = sizehint!(Expr[], n) + examples = sizehint!(Expr[], 10n) + scratch_dir = mktempdir() + for i in setdiff( + 1:n, + PlotsBase._backend_skips[backend_name()], + PlotsBase._animation_examples, + ) + PlotsBase._examples[i].external && continue + (imp = PlotsBase._examples[i].imports) ≡ nothing || + push!(imports, PlotsBase.replace_module(imp)) + func = gensym(string(i)) + push!( + examples, + quote + $func() = begin # evaluate each example in a local scope + $(PlotsBase._examples[i].exprs) + $i == 1 || return # only for one example + fn = joinpath(scratch_dir, tempname()) + pl = current() + show(devnull, pl) + # FIXME: pgfplotsx requires bug + backend_name() ≡ :pgfplotsx && return + if backend_name() ≡ :unicodeplots + savefig(pl, "$fn.txt") + return + end + showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") + showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") + if showable(MIME"image/svg+xml"(), pl) + show(IOBuffer(), MIME"image/svg+xml"(), pl) + end + nothing + end + $func() + end, + ) + end + withenv("GKSwstype" => "nul") do + @compile_workload begin + load_default_backend() + eval.(imports) + eval.(examples) + end + end + PlotsBase.CURRENT_PLOT.nullableplot = nothing end -using NaNMath: NaNMath -include("Commons/Commons.jl") -using .Commons -using .Commons.Frontend -# --------------------------------------------------------- -include("Fonts.jl") -@reexport using .Fonts -using .Fonts: Font, PlotText -include("Ticks.jl") -using .Ticks -include("Series.jl") -using .PlotsSeries -include("Subplots.jl") -using .Subplots -import .Subplots: plotarea, plotarea!, leftpad, toppad, bottompad, rightpad -include("Axes.jl") -using .Axes -include("Surfaces.jl") -include("Colorbars.jl") -using .Colorbars -include("PlotsPlots.jl") -using .PlotsPlots -include("layouts.jl") -# --------------------------------------------------------- -include("utils.jl") -using .Surfaces -include("axes_utils.jl") -include("legend.jl") -include("Shapes.jl") -using .Shapes -using .Shapes: Shape, _shapes, rotate! -include("Annotations.jl") -using .Annotations -using .Annotations: SeriesAnnotations, process_annotation -include("Arrows.jl") -using .Arrows -include("Strokes.jl") -using .Strokes -using .Strokes: Stroke, Brush -include("BezierCurves.jl") -using .BezierCurves -include("themes.jl") -include("plot.jl") -include("pipeline.jl") -include("arg_desc.jl") -include("recipes.jl") -include("animation.jl") -include("examples.jl") -include("plotattr.jl") -include("backends/nobackend.jl") -include("abstract_backend.jl") -include("alignment.jl") -const CURRENT_BACKEND = CurrentBackend(:none, NoBackend()) -include("output.jl") -include("shorthands.jl") -include("backends/web.jl") -include("backends/plotly.jl") -using .Plotly -include("init.jl") -include("users.jl") +# COV_EXCL_STOP end diff --git a/src/init.jl b/src/init.jl deleted file mode 100644 index 4372982b9..000000000 --- a/src/init.jl +++ /dev/null @@ -1,111 +0,0 @@ -using Scratch -using REPL - -const _plotly_local_file_path = Ref{Union{Nothing,String}}(nothing) -# use fixed version of Plotly instead of the latest one for stable dependency -# see github.com/JuliaPlots/Plots.jl/pull/2779 -const _plotly_min_js_filename = "plotly-2.6.3.min.js" - -const _use_local_dependencies = Ref(false) -const _use_local_plotlyjs = Ref(false) - -_plots_defaults() = - if isdefined(Main, :PLOTS_DEFAULTS) - copy(Dict{Symbol,Any}(Main.PLOTS_DEFAULTS)) - else - Dict{Symbol,Any}() - end - -function _plots_theme_defaults() - user_defaults = _plots_defaults() - theme(pop!(user_defaults, :theme, :default); user_defaults...) -end - -function _plots_plotly_defaults() - if bool_env("PLOTS_HOST_DEPENDENCY_LOCAL", "false") - _plotly_local_file_path[] = - fn = joinpath(@get_scratch!("plotly"), _plotly_min_js_filename) - isfile(fn) || - Downloads.download("https://cdn.plot.ly/$(_plotly_min_js_filename)", fn) - _use_local_plotlyjs[] = true - end - _use_local_dependencies[] = _use_local_plotlyjs[] -end - -function __init__() - _plots_theme_defaults() - _plots_plotly_defaults() - - insert!( - Base.Multimedia.displays, - findlast( - x -> x isa Base.TextDisplay || x isa REPL.REPLDisplay, - Base.Multimedia.displays, - ) + 1, - PlotsDisplay(), - ) - - i -> - begin - while PlotsDisplay() in Base.Multimedia.displays - popdisplay(PlotsDisplay()) - end - insert!( - Base.Multimedia.displays, - findlast(x -> x isa REPL.REPLDisplay, Base.Multimedia.displays) + 1, - PlotsDisplay(), - ) - end |> atreplinit - - nothing -end - -################################################################## - -# COV_EXCL_START -# TODO: revise and re-enable before release -# @setup_workload begin -# @debug backend_package_name() -# n = length(_examples) -# imports = sizehint!(Expr[], n) -# examples = sizehint!(Expr[], 10n) -# for i in setdiff(1:n, _backend_skips[backend_name()], _animation_examples) -# _examples[i].external && continue -# (imp = _examples[i].imports) === nothing || push!(imports, imp) -# func = gensym(string(i)) -# push!( -# examples, -# quote -# $func() = begin # evaluate each example in a local scope -# $(_examples[i].exprs) -# $i == 1 || return # only for one example -# fn = tempname() -# pl = current() -# show(devnull, pl) -# # FIXME: pgfplotsx requires bug -# backend_name() === :pgfplotsx && return -# if backend_name() === :unicodeplots -# savefig(pl, "$fn.txt") -# return -# end -# showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") -# showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") -# if showable(MIME"image/svg+xml"(), pl) -# show(IOBuffer(), MIME"image/svg+xml"(), pl) -# end -# nothing -# end -# $func() -# end, -# ) -# end -# withenv("GKSwstype" => "nul") do -# @compile_workload begin -# load_default_backend() -# eval.(imports) -# eval.(examples) -# end -# end -# CURRENT_PLOT.nullableplot = nothing -# end -# # COV_EXCL_STOP diff --git a/test/preferences.jl b/test/preferences.jl new file mode 100644 index 000000000..6f5101ffb --- /dev/null +++ b/test/preferences.jl @@ -0,0 +1,75 @@ + +@testset "Preferences" begin + Plots.set_default_backend!() # start with empty preferences + + withenv("PLOTS_DEFAULT_BACKEND" => "invalid") do + @test_logs (:error, r"Unsupported backend.*") Plots.load_default_backend() + end + @test_logs (:error, r"Unsupported backend.*") backend(:invalid) + + @test Plots.load_default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() + + withenv("PLOTS_DEFAULT_BACKEND" => "unicodeplots") do + @test_logs (:info, r".*environment variable") Plots.diagnostics(devnull) + @test Plots.load_default_backend() == + Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() + end + + @test Plots.load_default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() + @test Plots.PlotsBase.backend_package_name() ≡ :GR + @test Plots.backend_name() ≡ :gr + + @test_logs (:info, r".*fallback") Plots.diagnostics(devnull) + + @test Plots.PlotsBase.merge_with_base_supported([:annotations, :guide]) isa Set + @test Plots.PlotsBase.CurrentBackend(:gr).sym ≡ :gr + + @test_logs (:warn, r".*is not compatible with") Plots.set_default_backend!(:invalid) + + @testset "persistent backend" begin + # this test mimics a restart, which is needed after a preferences change + Plots.set_default_backend!(:unicodeplots) + script = tempname() + write( + script, + """ + using Pkg, Test; io = (devnull, stdout)[1] # toggle for debugging + Pkg.activate(; temp = true, io) + Pkg.develop(; path = "$(escape_string(pkgdir(Plots)))", io) + Pkg.add("UnicodePlots"; io) # checked by Plots + import UnicodePlots + using Plots + res = @testset "Preferences UnicodePlots" begin + @test_logs (:info, r".*Preferences") Plots.diagnostics(io) + @test backend() == Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() + end + exit(res.n_passed == 2 ? 0 : 123) + """, + ) + @test success(run(```$(Base.julia_cmd()) $script```)) + end + + is_pkgeval() || for pkg in TEST_PACKAGES + be = Symbol(lowercase(pkg)) + (Sys.isapple() && be ≡ :gaston) && continue # FIXME: hangs + (Sys.iswindows() && be ≡ :plotlyjs && is_ci()) && continue # FIXME: OutOfMemory + @test_logs Plots.set_default_backend!(be) # test the absence of warnings + rm.(Base.find_all_in_cache_path(Base.module_keys[Plots])) # make sure the compiled cache is removed + script = tempname() + write( + script, + """ + import $pkg + using Test, Plots + $be() + res = @testset "Persistent backend $pkg" begin + @test Plots.backend_name() ≡ :$be + end + exit(res.n_passed == 1 ? 0 : 123) + """, + ) + @test success(run(```$(Base.julia_cmd()) $script```)) # test default precompilation + end + + Plots.set_default_backend!() # clear `Preferences` key +end diff --git a/test/runtests.jl b/test/runtests.jl index cd1cedb6f..054a40117 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,93 +1,29 @@ -import Unitful: m, s, cm, DimensionError -import Plots: PLOTS_SEED, Plot, with -import SentinelArrays: ChainedVector -import GeometryBasics -import OffsetArrays -import FreeType # for `unicodeplots` -import LibGit2 -import Aqua -import JSON +const TEST_PACKAGES = + strip.(split(get(ENV, "PLOTS_TEST_PACKAGES", "GR,UnicodePlots,PythonPlot"), ",")) +using PlotsBase + +# initialize all backends +for pkg in TEST_PACKAGES + @eval import $(Symbol(pkg)) # trigger extension + getproperty(PlotsBase, Symbol(lowercase(pkg)))() +end +gr() -using VisualRegressionTests -using RecipesPipeline -using FilePathsBase -using LaTeXStrings using Preferences -using RecipesBase -using TestImages -using Unitful -using FileIO using Plots -using Dates using Test -# NOTE: don't use `plotly` (test hang, not surprised), test only the backends used in the docs -const TEST_BACKENDS = let - var = get(ENV, "PLOTS_TEST_BACKENDS", nothing) - if var !== nothing - Symbol.(lowercase.(strip.(split(var, ",")))) - else - [ - :gr, - :unicodeplots, - # :pythonplot, # currently segfaults - :pgfplotsx, - :plotlyjs, - # :gaston, # currently doesn't precompile (on julia v1.10) - # :inspectdr # currently doesn't precompile - ] - end -end +is_auto() = Plots.PlotsBase.bool_env("VISUAL_REGRESSION_TESTS_AUTO") +is_pkgeval() = Plots.PlotsBase.bool_env("JULIA_PKGEVAL") +is_ci() = Plots.PlotsBase.bool_env("CI") -# initial load - required for `should_warn_on_unsupported` +# get `Preferences` set backend, if any +const PREVIOUS_DEFAULT_BACKEND = load_preference(Plots, "default_backend") -import GR -import UnicodePlots -import PythonPlot -import PGFPlotsX -import PlotlyJS -# import Gaston -# initialize all backends -for be in TEST_BACKENDS - getproperty(Plots, be)() -end -gr() - -is_auto() = Plots.bool_env("VISUAL_REGRESSION_TESTS_AUTO", "false") -is_pkgeval() = Plots.bool_env("JULIA_PKGEVAL", "false") -is_ci() = Plots.bool_env("CI", "false") - -if !is_ci() - @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 -end +include("preferences.jl") -for name in ( - # "quality", # Persistent tasks cannot resolve versions - "misc", - "utils", - "args", - "defaults", - "dates", - "axes", - "layouts", - "contours", - "components", - "shorthands", - "recipes", - # "unitful", # many fail - # "hdf5plots", - "pgfplotsx", - "plotly", - # "animations", # some failing - # "output", # some plotly failing - "backends", -) - @testset "$name" begin - if is_auto() || is_pkgeval() - # skip the majority of tests if we only want to update reference images or under `PkgEval` (timeout limit) - name != "backends" && continue - end - gr() # reset to default backend (safer) - include("test_$name.jl") - end +if PREVIOUS_DEFAULT_BACKEND === nothing + delete_preferences!(Plots, "default_backend") # restore the absence of a preference +else + Plots.set_default_backend!(PREVIOUS_DEFAULT_BACKEND) # reset to previous state end From d7aa8bc02dfa35034df22c1143cf1a0cb6aeb346 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 14:42:20 +0200 Subject: [PATCH 03/85] fix tests - restore preferences - cleanup (#4914) * rework extension `__init__` mechanism * checkpoint plotly * fix `pythonplot` * format * more `pythonplot` fixes * checkpoint replace * syntax * fix `plotlyjs` * restructure modules * stable `GR` * checkpoint broken `references` gr * broken viewport * fix `dev` * update * no project * remove `--project` * update * fix `plotarea` * fixes * stable `PlotsBase` * cleanup * format * restrcit gaston to `linux` * restrcit `PlolyJS` test * update format * update action * update `Aqua` * cleanup - fix `Plots` tests * move preferences back to `PlotsBase` * simplifications * format * change versions * rework ci * format * test downstream * update ci * move ci scripts * fix * fix * update * debug ci * fix * update versions * update * update * update * update * update * update * update * update * restore CI * update workflow * add `PlotsBase` precompile statements * update tests * update preference - simplifications * update test * update --- .github/workflows/ci.yml | 106 ++---- .github/workflows/format_check.yml | 2 +- PlotsBase/Project.toml | 8 +- PlotsBase/ext/GRExt.jl | 250 +++++-------- PlotsBase/ext/GastonExt.jl | 188 ++++------ PlotsBase/ext/HDF5Ext.jl | 131 +++---- PlotsBase/ext/IJuliaExt.jl | 37 +- PlotsBase/ext/PGFPlotsXExt.jl | 183 ++++----- PlotsBase/ext/PlotlyJSExt.jl | 58 +-- PlotsBase/ext/PlotlyKaleidoExt.jl | 2 +- PlotsBase/ext/PythonPlotExt.jl | 413 +++++++++------------ PlotsBase/ext/UnicodePlotsExt.jl | 70 +--- PlotsBase/ext/UnitfulExt.jl | 24 +- PlotsBase/src/Annotations.jl | 40 +- PlotsBase/src/Arrows.jl | 8 +- PlotsBase/src/Axes.jl | 95 ++--- PlotsBase/src/BezierCurves.jl | 6 +- PlotsBase/src/Colorbars.jl | 58 +-- PlotsBase/src/Commons/Commons.jl | 134 +++++-- PlotsBase/src/Commons/aliases.jl | 2 +- PlotsBase/src/Commons/attrs.jl | 104 +++--- PlotsBase/src/Commons/layouts.jl | 167 +++++++++ PlotsBase/src/Commons/measures.jl | 75 ++++ PlotsBase/src/Commons/postprocess_attrs.jl | 19 +- PlotsBase/src/{Series.jl => DataSeries.jl} | 79 ++-- PlotsBase/src/Fonts.jl | 36 +- PlotsBase/src/PlotMeasures.jl | 40 -- PlotsBase/src/{PlotsPlots.jl => Plots.jl} | 93 ++--- PlotsBase/src/PlotsBase.jl | 101 +++-- PlotsBase/src/Shapes.jl | 29 +- PlotsBase/src/Strokes.jl | 14 +- PlotsBase/src/Subplots.jl | 125 +++---- PlotsBase/src/Surfaces.jl | 16 +- PlotsBase/src/Ticks.jl | 33 +- PlotsBase/src/abstract_backend.jl | 205 ---------- PlotsBase/src/alignment.jl | 4 +- PlotsBase/src/animation.jl | 4 +- PlotsBase/src/axes_utils.jl | 80 ++-- PlotsBase/src/backends.jl | 259 +++++++++++++ PlotsBase/src/backends/nobackend.jl | 15 - PlotsBase/src/examples.jl | 38 +- PlotsBase/src/layouts.jl | 252 +------------ PlotsBase/src/output.jl | 27 +- PlotsBase/src/pipeline.jl | 50 ++- PlotsBase/src/plot.jl | 7 +- PlotsBase/src/{backends => }/plotly.jl | 161 ++++---- PlotsBase/src/preferences.jl | 49 +++ PlotsBase/src/recipes.jl | 106 +++--- PlotsBase/src/utils.jl | 148 ++++---- PlotsBase/src/{backends => }/web.jl | 4 +- PlotsBase/test/runtests.jl | 49 ++- PlotsBase/test/test_args.jl | 8 +- PlotsBase/test/test_axes.jl | 42 ++- PlotsBase/test/test_backends.jl | 170 +-------- PlotsBase/test/test_components.jl | 8 +- PlotsBase/test/test_contours.jl | 8 +- PlotsBase/test/test_defaults.jl | 28 +- PlotsBase/test/test_layouts.jl | 28 +- PlotsBase/test/test_misc.jl | 4 +- PlotsBase/test/test_output.jl | 2 +- PlotsBase/test/test_pgfplotsx.jl | 18 +- PlotsBase/test/test_preferences.jl | 96 +++++ PlotsBase/test/test_quality.jl | 13 +- PlotsBase/test/test_recipes.jl | 18 +- PlotsBase/test/test_reference.jl | 147 ++++++++ PlotsBase/test/test_shorthands.jl | 2 +- PlotsBase/test/test_utils.jl | 98 ++--- Project.toml | 14 +- RecipesBase/src/RecipesBase.jl | 14 +- RecipesPipeline/src/user_recipe.jl | 16 +- ci/downstream.jl | 78 ++++ ci/matplotlib.jl | 25 ++ src/Plots.jl | 177 ++++----- test/preferences.jl | 75 ---- test/runtests.jl | 33 +- 75 files changed, 2613 insertions(+), 2713 deletions(-) create mode 100644 PlotsBase/src/Commons/layouts.jl create mode 100644 PlotsBase/src/Commons/measures.jl rename PlotsBase/src/{Series.jl => DataSeries.jl} (92%) delete mode 100644 PlotsBase/src/PlotMeasures.jl rename PlotsBase/src/{PlotsPlots.jl => Plots.jl} (81%) delete mode 100644 PlotsBase/src/abstract_backend.jl create mode 100644 PlotsBase/src/backends.jl delete mode 100644 PlotsBase/src/backends/nobackend.jl rename PlotsBase/src/{backends => }/plotly.jl (92%) create mode 100644 PlotsBase/src/preferences.jl rename PlotsBase/src/{backends => }/web.jl (94%) create mode 100644 PlotsBase/test/test_preferences.jl create mode 100644 PlotsBase/test/test_reference.jl create mode 100644 ci/downstream.jl create mode 100644 ci/matplotlib.jl delete mode 100644 test/preferences.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dae99f4a..2a996b021 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,10 @@ jobs: ci: if: "!contains(github.event.head_commit.message, '[skip ci]')" env: - GKS_ENCODING: "utf8" - GKSwstype: "nul" - JULIA_CONDAPKG_BACKEND: "MicroMamba" - MPLBACKEND: "agg" + JULIA_CONDAPKG_BACKEND: MicroMamba + MPLBACKEND: agg + GKS_ENCODING: utf8 + GKSwstype: nul name: Julia ${{ matrix.version }} - ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} runs-on: ${{ matrix.os }} @@ -28,35 +28,18 @@ jobs: fail-fast: false matrix: version: - - '1.6' # LTS (minimal declared julia compat in `Project.toml`) - - '1' # latest stable + - '1' # latest stable + - '1.9' # minimal declared julia compat in `Project.toml` experimental: - false os: [ubuntu-latest, windows-latest, macos-latest] arch: [x64] include: - - os: ubuntu-latest - experimental: false - prefix: xvfb-run # julia-actions/julia-runtest/blob/master/README.md - - os: ubuntu-latest - experimental: false - prefix: xvfb-run - version: '1.7' # only test intermediate release on `ubuntu` to spare resources - - os: ubuntu-latest - experimental: false - prefix: xvfb-run - version: '1.8' # only test intermediate release on `ubuntu` to spare resources - - os: ubuntu-latest - experimental: false - prefix: xvfb-run - version: '1.9' # only test intermediate release on `ubuntu` to spare resources - os: ubuntu-latest experimental: true - prefix: xvfb-run - version: '~1.11.0-0' # upcoming julia version, next `rc` + version: '~1.11.0-0' # upcoming julia version (`alpha`, `beta` or `rc`) - os: ubuntu-latest experimental: true - prefix: xvfb-run version: 'nightly' steps: @@ -66,7 +49,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get -y update - sudo apt-get -y install gnuplot poppler-utils texlive-{latex-base,latex-extra,luatex} g++ + sudo apt-get -y install g++ gnuplot poppler-utils texlive-{latex-base,latex-extra,luatex} sudo fc-cache -vr - name: Set LD_PRELOAD @@ -76,69 +59,42 @@ jobs: - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.version }} + - uses: julia-actions/cache@v1 - - uses: julia-actions/julia-buildpkg@latest - - name: Run upstream RecipesBase, RecipesPipeline tests - shell: julia --project=@. --color=yes {0} + - name: Develop RecipesBase, RecipesPipeline, PlotsBase, Plots + env: + JULIA_PKG_PRECOMPILE_AUTO: 0 + shell: julia --color=yes {0} run: | using Pkg - foreach(("RecipesBase", "RecipesPipeline")) do name - Pkg.develop(path=name); Pkg.test(name; coverage=true) - end + foreach(path -> Pkg.develop(; path), ("./RecipesBase", "./RecipesPipeline", "./PlotsBase", ".")) - name: Install conda based matplotlib - shell: julia --project=@. --color=yes {0} - run: | - using Pkg; Pkg.add("CondaPkg") - using CondaPkg; CondaPkg.resolve() - libgcc = if Sys.islinux() - # see discourse.julialang.org/t/glibcxx-version-not-found/82209/8 - # julia 1.8.3 is built with libstdc++.so.6.0.29, so we must restrict to this version (gcc 11.3.0, not gcc 12.2.0) - # see gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html - specs = Dict( - v"3.4.29" => ">=11.1,<12.1", - v"3.4.30" => ">=12.1,<13.1", - v"3.4.31" => ">=13.1,<14.1", - v"3.4.32" => ">=14.1,<15.1", - v"3.4.33" => ">=15.1,<16.1", - # ... keep this up-to-date with gcc 16 - )[Base.BinaryPlatforms.detect_libstdcxx_version()] - ("libgcc-ng$specs", "libstdcxx-ng$specs") - else - () - end - CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) - CondaPkg.status() - - - name: Run upstream PlotsBase tests - shell: julia --project=@. --color=yes {0} - run: | - using Pkg - foreach(("PlotsBase",)) do name - Pkg.develop(path=name); Pkg.test(name; coverage=true) - end + env: + JULIA_PKG_PRECOMPILE_AUTO: 0 + run: julia --color=yes ci/matplotlib.jl - - uses: julia-actions/julia-runtest@latest + - name: Test RecipesBase, RecipesPipeline, PlotsBase, Plots timeout-minutes: 60 - with: - prefix: ${{ matrix.prefix }} # for `xvfb-run` - - - name: Run downstream tests - if: startsWith(matrix.os, 'ubuntu') - shell: xvfb-run julia --project=@. --color=yes {0} run: | - using Pkg - foreach(("StatsPlots", "GraphRecipes")) do name - Pkg.activate(tempdir()) - foreach(path -> Pkg.develop(; path), ("RecipesBase", "RecipesPipeline", ".")) - Pkg.add(name); Pkg.test(name; coverage=true) - end + cmd=(julia --color=yes) + if [ "$RUNNER_OS" == "Linux" ]; then + cmd=(xvfb-run ${cmd[@]}) + fi + echo ${cmd[@]} + ${cmd[@]} -e ' + using Pkg + foreach(name -> Pkg.test(name; coverage=true), ("RecipesBase", "RecipesPipeline", "PlotsBase", "Plots")) + ' + - name: Test downstream packages + if: startsWith(matrix.os, 'ubuntu') + run: xvfb-run julia --color=yes ci/downstream.jl - uses: julia-actions/julia-processcoverage@latest if: startsWith(matrix.os, 'ubuntu') with: - directories: RecipesBase/src,RecipesPipeline/src,src + directories: RecipesBase/src,RecipesPipeline/src,PlotsBase/src,src - uses: codecov/codecov-action@v4 if: startsWith(matrix.os, 'ubuntu') with: diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 951358a7d..afb15383b 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -27,7 +27,7 @@ jobs: - name: Format Julia files run: | using JuliaFormatter - format(["RecipesBase", "RecipesPipeline", "src", "test", "ext"]) + format(["RecipesBase", "RecipesPipeline", "PlotsBase", "src", "test"]) shell: julia --color=yes --compile=min -O0 {0} - name: suggester / JuliaFormatter uses: reviewdog/action-suggester@v1 diff --git a/PlotsBase/Project.toml b/PlotsBase/Project.toml index 192ef1cf0..f2df17c9a 100644 --- a/PlotsBase/Project.toml +++ b/PlotsBase/Project.toml @@ -1,6 +1,6 @@ name = "PlotsBase" uuid = "c52230a3-c5da-43a3-9e85-260fcdfdc737" -version = "1.41.0" +version = "0.1" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -19,6 +19,8 @@ NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +Preferences = "21216c6a-2e73-6563-6e65-726566657250" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -87,6 +89,8 @@ PlotThemes = "2, 3" PlotUtils = "1" PlotlyJS = "0.18" PlotlyKaleido = "2.2.2" +PrecompileTools = "1" +Preferences = "1" Printf = "1" PythonPlot = "1" Random = "1" @@ -105,7 +109,7 @@ UnicodePlots = "3" UnitfulLatexify = "1" Unzip = "0.1 - 0.2" UUIDs = "1" -julia = "1.6" +julia = "1.9" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/PlotsBase/ext/GRExt.jl b/PlotsBase/ext/GRExt.jl index 5bc1e7f67..58211a305 100644 --- a/PlotsBase/ext/GRExt.jl +++ b/PlotsBase/ext/GRExt.jl @@ -5,41 +5,22 @@ import RecipesPipeline import NaNMath import GR -import PlotsBase.Colorbars: cbar_gradient, cbar_fill, cbar_lines - -using PlotsBase.PlotMeasures using PlotsBase.Annotations -using PlotsBase.PlotsSeries -using PlotsBase.PlotsPlots +using PlotsBase.DataSeries using PlotsBase.Colorbars using PlotsBase.Subplots using PlotsBase.Commons using PlotsBase.Arrows using PlotsBase.Shapes using PlotsBase.Colors +using PlotsBase.Plots using PlotsBase.Fonts using PlotsBase.Fonts using PlotsBase.Ticks using PlotsBase.Axes -const package_str = "GR" -const str = lowercase(package_str) -const sym = Symbol(str) - struct GRBackend <: PlotsBase.AbstractBackend end - -get_concrete_backend() = GRBackend # opposite to abstract - -function __init__() - @debug "Initializing GR backend in PlotsBase; run `gr()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[GRBackend] = sym - - push!(PlotsBase._initialized_backends, sym) -end -# Make GR know to Plots -PlotsBase.backend_name(::GRBackend) = sym -PlotsBase.backend_package_name(::GRBackend) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static GRBackend gr const _gr_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -196,28 +177,6 @@ const _gr_styles = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] const _gr_markers = vcat(Commons._all_markers, :pixel) const _gr_scales = [:identity, :ln, :log2, :log10] -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::GRBackend, $s::Symbol) = $s in $v - PlotsBase.$f2(::GRBackend) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- - PlotsBase.is_marker_supported(::GRBackend, shape::Shape) = true # https://github.com/jheinen/GR.jl - significant contributions by @jheinen @@ -473,7 +432,7 @@ end gr_inqtext(x, y, s) = gr_inqtext(x, y, string(s)) gr_inqtext(x, y, s::AbstractString) = if (occursin('\\', s) || occursin("10^{", s)) && - match(r".*\$[^\$]+?\$.*", String(s)) === nothing + match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.inqtextext(x, y, s) else GR.inqtext(x, y, s) @@ -482,7 +441,7 @@ gr_inqtext(x, y, s::AbstractString) = gr_text(x, y, s) = gr_text(x, y, string(s)) gr_text(x, y, s::AbstractString) = if (occursin('\\', s) || occursin("10^{", s)) && - match(r".*\$[^\$]+?\$.*", String(s)) === nothing + match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.textext(x, y, s) else GR.text(x, y, s) @@ -583,7 +542,7 @@ gr_nominal_size(s) = minimum(get_size(s)) / 500 # draw ONE Shape function gr_draw_marker(series, xi, yi, zi, clims, i, msize, strokewidth, shape::Shape) # convert to ndc coords (percentages of window) ... - xi, yi = if zi === nothing + xi, yi = if zi ≡ nothing GR.wctondc(xi, yi) else gr_w3tondc(xi, yi, zi) @@ -617,7 +576,7 @@ function gr_draw_marker(series, xi, yi, zi, clims, i, msize, strokewidth, shape: gr_set_transparency(get_markeralpha(series, i)) GR.setmarkertype(gr_markertypes[shape]) GR.setmarkersize(0.3msize / gr_nominal_size(series)) - if zi === nothing + if zi ≡ nothing GR.polymarker([xi], [yi]) else GR.polymarker3d([xi], [yi], [zi]) @@ -674,10 +633,10 @@ end function gr_viewport_from_bbox(sp::Subplot{GRBackend}, bb::BoundingBox, w, h, vp_canvas) viewport = GRViewport( - vp_canvas.xmax * (PlotsBase.left(bb) / w), - vp_canvas.xmax * (PlotsBase.right(bb) / w), - vp_canvas.ymax * (1 - PlotsBase.bottom(bb) / h), - vp_canvas.ymax * (1 - PlotsBase.top(bb) / h), + vp_canvas.xmax * (left(bb) / w), + vp_canvas.xmax * (right(bb) / w), + vp_canvas.ymax * (1 - bottom(bb) / h), + vp_canvas.ymax * (1 - top(bb) / h), ) hascolorbar(sp) && (viewport.xmax -= 0.1(1 + 0.5gr_is3d(sp))) viewport @@ -714,11 +673,12 @@ struct GRColorbar end function gr_update_colorbar!(cbar::GRColorbar, series::Series) - (style = colorbar_style(series)) === nothing && return + (style = colorbar_style(series)) ≡ nothing && return list = - style == cbar_gradient ? cbar.gradients : - style == cbar_fill ? cbar.fills : - style == cbar_lines ? cbar.lines : error("Unknown colorbar style: $style.") + style == Colorbars.cbar_gradient ? cbar.gradients : + style == Colorbars.cbar_fill ? cbar.fills : + style == Colorbars.cbar_lines ? cbar.lines : + error("Unknown colorbar style: $style.") push!(list, series) end @@ -844,18 +804,18 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport) end position(symb) = - if symb === :top || symb === :right + if symb ≡ :top || symb ≡ :right 0.95 - elseif symb === :left || symb === :bottom + elseif symb ≡ :left || symb ≡ :bottom 0.05 else 0.5 end alignment(symb) = - if symb === :top || symb === :right + if symb ≡ :top || symb ≡ :right :right - elseif symb === :left || symb === :bottom + elseif symb ≡ :left || symb ≡ :bottom :left else :center @@ -873,7 +833,7 @@ function gr_set_gradient(c) end gr_set_gradient(series::Series) = - (color = get_colorgradient(series)) !== nothing && gr_set_gradient(color) + (color = get_colorgradient(series)) ≢ nothing && gr_set_gradient(color) # this is our new display func... set up the viewport_canvas, compute bounding boxes, and display each subplot function gr_display(plt::Plot, dpi_factor = 1) @@ -973,12 +933,6 @@ function gr_get_ticks_size(ticks, rot) w, h end -function labelfunc(scale::Symbol, backend::GRBackend) - texfunc = PlotsBase.labelfunc_tex(scale) - # replace dash with \minus (U+2212) - label -> replace(texfunc(label), "-" => "−") -end - function gr_axis_height(sp, axis) GR.savestate() ticks = get_ticks(sp, axis, update = false) @@ -1011,6 +965,12 @@ function gr_axis_width(sp, axis) w end +function PlotsBase.labelfunc(scale::Symbol, backend::GRBackend) + texfunc = PlotsBase.labelfunc_tex(scale) + # replace dash with \minus (U+2212) + label -> replace(texfunc(label), "-" => "−") +end + function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) dpi = sp.plt[:thickness_scaling] width, height = sp_size = get_size(sp) @@ -1081,14 +1041,14 @@ function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) if (guide = zaxis[:guide]) != "" gr_set_font(guidefont(zaxis), sp) l = last(gr_text_size(guide)) - padding[mirrored(zaxis, :right) ? :right : :left][] += 1mm + height * l * px # NOTE: why `height` here ? + padding[mirrored(zaxis, :right) ? :right : :left][] += 1mm + height * l * px # NOTE: why `height` here ? end else # Add margin for x/y ticks & labels for (ax, tc, (a, b)) in ((xaxis, xticks, (:top, :bottom)), (yaxis, yticks, (:right, :left))) if !isempty(first(tc)) - isy = ax[:letter] === :y + isy = ax[:letter] ≡ :y gr_set_tickfont(sp, ax) ts = gr_get_ticks_size(tc, ax[:rotation]) l = 0.01 + (isy ? first(ts) : last(ts)) @@ -1123,7 +1083,7 @@ remap(x, lo, hi) = (x - lo) / (hi - lo) get_z_normalized(z, clims...) = isnan(z) ? 256 / 255 : remap(clamp(z, clims...), clims...) function gr_clims(sp, args...) - sp[:clims] === :auto || return get_clims(sp) + sp[:clims] ≡ :auto || return get_clims(sp) lo, hi = get_clims(sp, args...) if lo == hi if lo == 0 @@ -1152,8 +1112,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport) PlotsBase._update_min_padding!(sp) # the viewports for this subplot and the whole plot - vp_sp = gr_viewport_from_bbox(sp, PlotsBase.bbox(sp), w, h, vp_canvas) - vp_plt = gr_viewport_from_bbox(sp, PlotsBase.plotarea(sp), w, h, vp_canvas) + vp_sp = gr_viewport_from_bbox(sp, bbox(sp), w, h, vp_canvas) + vp_plt = gr_viewport_from_bbox(sp, plotarea(sp), w, h, vp_canvas) # update plot viewport leg = gr_get_legend_geometry(vp_plt, sp) @@ -1228,7 +1188,6 @@ function gr_add_legend(sp, leg, viewport_area) legend_rows, legend_cols = leg.column_layout if leg.w > 0 || leg.h > 0 xpos, ypos = gr_legend_pos(sp, leg, viewport_area) # position between the legend line and text (see ref(1)) - #@show vertical leg.w leg.h leg.pad leg.span leg.entries (legend_rows, legend_cols) (xpos, ypos) leg.dx leg.dy leg.textw leg.texth GR.setfillintstyle(GR.INTSTYLE_SOLID) gr_set_fillcolor(sp[:legend_background_color]) # ymax @@ -1241,7 +1200,7 @@ function gr_add_legend(sp, leg, viewport_area) GR.fillrect(xs..., ys...) # allocating white space for actual legend width here gr_set_line(1, :solid, sp[:legend_foreground_color], sp) GR.drawrect(xs..., ys...) # drawing actual legend width here - if (ttl = sp[:legend_title]) !== nothing + if (ttl = sp[:legend_title]) ≢ nothing shift = legend_rows > 1 ? 0.5(legend_cols - 1) * leg.dx : 0 # shifting title to center if multi-column gr_set_font(legendtitlefont(sp), sp) _debug[] && gr_legend_bbox(xpos, ypos, leg) @@ -1275,10 +1234,7 @@ function gr_add_legend(sp, leg, viewport_area) gr_set_line(clamped_lw, ls, lc, sp) # see github.com/JuliaPlots/Plots.jl/issues/3003 _debug[] && gr_legend_bbox(xpos, ypos, leg) - if ( - (st === :shape || series[:fillrange] !== nothing) && - series[:ribbon] === nothing - ) + if ((st ≡ :shape || series[:fillrange] ≢ nothing) && series[:ribbon] ≡ nothing) (fc = get_fillcolor(series, clims)) |> gr_set_fill gr_set_fillstyle(get_fillstyle(series, 0)) l, r = xpos + lft, xpos + rgt @@ -1294,18 +1250,18 @@ function gr_add_legend(sp, leg, viewport_area) gr_polyline(x, y, GR.fillarea) gr_set_transparency(lc, la) gr_set_line(clamped_lw, ls, lc, sp) - st === :shape && gr_polyline(x, y) + st ≡ :shape && gr_polyline(x, y) end max_markersize = Inf if st in (:path, :straightline, :path3d) max_markersize = leg.base_markersize gr_set_transparency(lc, la) - filled = series[:fillrange] !== nothing && series[:ribbon] === nothing + filled = series[:fillrange] ≢ nothing && series[:ribbon] ≡ nothing GR.polyline(xpos .+ [lft, rgt], ypos .+ (filled ? [top, top] : [0, 0])) end - if (msh = series[:markershape]) !== :none + if (msh = series[:markershape]) ≢ :none msz = max(first(series[:markersize]), 0) msw = max(first(series[:markerstrokewidth]), 0) mfac = 0.8 * lfps / (msz + 0.5 * msw + 1e-20) @@ -1340,7 +1296,7 @@ function gr_add_legend(sp, leg, viewport_area) end mirrored(ax::Axis, sym::Symbol) = - ax[:guide_position] === sym || (ax[:guide_position] === :auto && ax[:mirror]) + ax[:guide_position] ≡ sym || (ax[:guide_position] ≡ :auto && ax[:mirror]) function gr_legend_pos(sp::Subplot, leg, vp) xaxis, yaxis = sp[:xaxis], sp[:yaxis] @@ -1349,7 +1305,7 @@ function gr_legend_pos(sp::Subplot, leg, vp) if (lp = sp[:legend_position]) isa Real return gr_legend_pos(lp, leg, vp) elseif lp isa Tuple{<:Real,Symbol} - axisclearance = if lp[2] === :outer + axisclearance = if lp[2] ≡ :outer [ !ymirror * gr_axis_width(sp, yaxis), ymirror * gr_axis_width(sp, yaxis), @@ -1382,13 +1338,13 @@ function gr_legend_pos(sp::Subplot, leg, vp) vp.xmin + 0.5width(vp) - 0.5leg.w + leg.xoffset end ypos = if occursin("bottom", leg_str) - vp.ymin + if lp === :outerbottom + vp.ymin + if lp ≡ :outerbottom -leg.yoffset - leg.dy - !xmirror * gr_axis_height(sp, xaxis) else leg.yoffset + leg.h end elseif occursin("top", leg_str) # default / best - vp.ymax + if lp === :outertop + vp.ymax + if lp ≡ :outertop leg.yoffset + leg.h + xmirror * gr_axis_height(sp, xaxis) else -leg.yoffset - leg.dy @@ -1416,7 +1372,7 @@ function gr_legend_pos(theta::Real, leg, vp; axisclearance = nothing) ymin = vp.ymin - leg.yoffset - leg.dy - axisclearance[3] ymax = vp.ymax + leg.yoffset + leg.h + axisclearance[4] end - legend_pos_from_angle(theta, xmin, xcenter(vp), xmax, ymin, ycenter(vp), ymax) + PlotsBase.legend_pos_from_angle(theta, xmin, xcenter(vp), xmax, ymin, ycenter(vp), ymax) end const gr_legend_marker_to_line_factor = Ref(2.0) @@ -1426,13 +1382,13 @@ function gr_get_legend_geometry(vp, sp) textw = texth = 0.0 has_title = false nseries = 0 - if sp[:legend_position] !== :none + if sp[:legend_position] ≢ :none GR.savestate() GR.selntran(0) GR.setcharup(0, 1) GR.setscale(0) ttl = sp[:legend_title] - if (has_title = ttl !== nothing) + if (has_title = ttl ≢ nothing) gr_set_font(legendtitlefont(sp), sp) (l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(ttl))) texth = t - b @@ -1523,7 +1479,7 @@ function gr_update_viewport_legend!(vp, sp, leg) xaxis, yaxis = sp[:xaxis], sp[:yaxis] xmirror = mirrored(xaxis, :top) ymirror = mirrored(yaxis, :right) - leg_str = if (lp = sp[:legend_position]) isa Tuple{<:Real,Symbol} && lp[2] === :outer + leg_str = if (lp = sp[:legend_position]) isa Tuple{<:Real,Symbol} && lp[2] ≡ :outer x, y = gr_legend_pos(sp, leg, vp) # dry run, to figure out horz = x < vp.xmin ? "left" : (x > vp.xmax ? "right" : "") vert = y < vp.ymin ? "bot" : (y > vp.ymax ? "top" : "") @@ -1544,7 +1500,7 @@ function gr_update_viewport_legend!(vp, sp, leg) vp.ymin += yoff + !xmirror * gr_axis_height(sp, xaxis) end end - if lp === :inline + if lp ≡ :inline if yaxis[:mirror] vp.xmin += leg.textw else @@ -1555,8 +1511,8 @@ function gr_update_viewport_legend!(vp, sp, leg) end gr_update_viewport_ratio!(vp, sp) = - if (ratio = get_aspect_ratio(sp)) !== :none - ratio === :equal && (ratio = 1) + if (ratio = get_aspect_ratio(sp)) ≢ :none + ratio ≡ :equal && (ratio = 1) x_min, x_max, y_min, y_max = gr_xy_axislims(sp) viewport_ratio = width(vp) / height(vp) window_ratio = (x_max - x_min) / (y_max - y_min) / ratio @@ -1635,7 +1591,7 @@ function gr_draw_axes(sp, vp) # rmin, rmax = GR.adjustrange(ignorenan_minimum(r), ignorenan_maximum(r)) rmin, rmax = axis_limits(sp, :y) gr_polaraxes(rmin, rmax, sp) - elseif sp[:framestyle] !== :none + elseif sp[:framestyle] ≢ :none foreach(letter -> gr_draw_axis(sp, letter, vp), (:x, :y)) end GR.settransparency(1.0) @@ -1713,7 +1669,7 @@ gr_draw_spine(sp, axis, segments, func = gr_polyline) = gr_draw_border(sp, axis, segments, func = gr_polyline) = if sp[:framestyle] in (:box, :semi) - intensity = sp[:framestyle] === :semi ? 0.5 : 1 + intensity = sp[:framestyle] ≡ :semi ? 0.5 : 1 GR.setclip(0) gr_set_line(intensity, :solid, axis[:foreground_color_border], sp) gr_set_transparency(axis[:foreground_color_border], intensity) @@ -1727,7 +1683,7 @@ gr_draw_ticks(sp, axis, segments, func = gr_polyline) = gr_set_line(1, :solid, axis[:foreground_color_grid], sp) gr_set_transparency( axis[:foreground_color_grid], - axis[:tick_direction] === :out ? axis[:gridalpha] : 0, + axis[:tick_direction] ≡ :out ? axis[:gridalpha] : 0, ) else gr_set_line(1, :solid, axis[:foreground_color_axis], sp) @@ -1744,14 +1700,14 @@ function gr_label_ticks(sp, letter, ticks) _, (oamin, oamax) = map(l -> axis_limits(sp, l), letters) gr_set_tickfont(sp, letter) - out_factor = ifelse(ax[:tick_direction] === :out, 1.5, 1) + out_factor = ifelse(ax[:tick_direction] ≡ :out, 1.5, 1) - isy = letter === :y + isy = letter ≡ :y x_offset = isy ? -0.015out_factor : 0 y_offset = isy ? 0 : -0.008out_factor rot = ax[:rotation] % 360 - ov = sp[:framestyle] === :origin ? 0 : xor(oax[:flip], ax[:mirror]) ? oamax : oamin + ov = sp[:framestyle] ≡ :origin ? 0 : xor(oax[:flip], ax[:mirror]) ? oamax : oamin sgn = ax[:mirror] ? -1 : 1 sgn2 = iseven(Int(floor(rot / 90))) ? -1 : 1 sgn3 = if isy @@ -1786,12 +1742,12 @@ function gr_label_ticks_3d(sp, letter, ticks) ax = sp[get_attr_symbol(letter, :axis)] ax[:showaxis] || return - isy, isz = letter .=== (:y, :z) + isy, isz = letter .≡ (:y, :z) n0, n1 = isy ? (namax, namin) : (namin, namax) gr_set_tickfont(sp, letter) - nt = sp[:framestyle] === :origin ? 0 : ax[:mirror] ? n1 : n0 - ft = sp[:framestyle] === :origin ? 0 : ax[:mirror] ? famax : famin + nt = sp[:framestyle] ≡ :origin ? 0 : ax[:mirror] ? n1 : n0 + ft = sp[:framestyle] ≡ :origin ? 0 : ax[:mirror] ? famax : famin rot = mod(ax[:rotation], 360) sgn = ax[:mirror] ? -1 : 1 @@ -1801,7 +1757,7 @@ function gr_label_ticks_3d(sp, letter, ticks) axisθ = isz ? 270 : mod(gr_get_3d_axis_angle(cvs, nt, ft, letter), 360) # issue: doesn't work with 1 tick axisϕ = mod(axisθ - 90, 360) - out_factor = ifelse(ax[:tick_direction] === :out, 1.5, 1) + out_factor = ifelse(ax[:tick_direction] ≡ :out, 1.5, 1) axis_offset = 0.012out_factor y_offset, x_offset = axis_offset .* sincosd(axisϕ) @@ -1849,26 +1805,24 @@ gr_label_axis(sp, letter, vp) = GR.savestate() guide_position = ax[:guide_position] rotation = float(ax[:guidefontrotation]) # github.com/JuliaPlots/Plots.jl/issues/3089 - if letter === :x + if letter ≡ :x # default rotation = 0. should yield GR.setcharup(0, 1) i.e. 90° xpos = xposition(vp, position(ax[:guidefonthalign])) halign = alignment(ax[:guidefonthalign]) - ypos, valign = - if guide_position === :top || (guide_position === :auto && mirror) - vp.ymax + 0.015 + (mirror ? gr_axis_height(sp, ax) : 0.015), :top - else - vp.ymin - 0.015 - (mirror ? 0.015 : gr_axis_height(sp, ax)), :bottom - end + ypos, valign = if guide_position ≡ :top || (guide_position ≡ :auto && mirror) + vp.ymax + 0.015 + (mirror ? gr_axis_height(sp, ax) : 0.015), :top + else + vp.ymin - 0.015 - (mirror ? 0.015 : gr_axis_height(sp, ax)), :bottom + end else rotation += 90 # default rotation = 0. should yield GR.setcharup(-1, 0) i.e. 180° ypos = yposition(vp, position(ax[:guidefontvalign])) halign = alignment(ax[:guidefontvalign]) - xpos, valign = - if guide_position === :right || (guide_position === :auto && mirror) - vp.xmax + 0.03 + mirror * gr_axis_width(sp, ax), :bottom - else - vp.xmin - 0.03 - !mirror * gr_axis_width(sp, ax), :top - end + xpos, valign = if guide_position ≡ :right || (guide_position ≡ :auto && mirror) + vp.xmax + 0.03 + mirror * gr_axis_width(sp, ax), :bottom + else + vp.xmin - 0.03 - !mirror * gr_axis_width(sp, ax), :top + end end gr_set_font(guidefont(ax), sp; rotation, halign, valign) gr_text(xpos, ypos, ax[:guide]) @@ -1879,7 +1833,7 @@ gr_label_axis_3d(sp, letter) = if (ax = sp[get_attr_symbol(letter, :axis)])[:guide] != "" letters = axes_letters(sp, letter) (amin, amax), (namin, namax), (famin, famax) = map(l -> axis_limits(sp, l), letters) - n0, n1 = letter === :y ? (namax, namin) : (namin, namax) + n0, n1 = letter ≡ :y ? (namax, namin) : (namin, namax) GR.savestate() gr_set_font( @@ -1896,13 +1850,13 @@ gr_label_axis_3d(sp, letter) = x, y = gr_w3tondc(sort_3d_axes(ag, ng, fg, letter)...) if letter in (:x, :y) h = gr_axis_height(sp, ax) - x_offset = letter === :x ? -h : h + x_offset = letter ≡ :x ? -h : h y_offset = -h else x_offset = -0.03 - gr_axis_width(sp, ax) y_offset = 0 end - letter === :z && GR.setcharup(-1, 0) + letter ≡ :z && GR.setcharup(-1, 0) sgn = ax[:mirror] ? -1 : 1 gr_text(x + sgn * x_offset, y + sgn * y_offset, ax[:guide]) GR.restorestate() @@ -1911,11 +1865,11 @@ gr_label_axis_3d(sp, letter) = gr_add_title(sp, vp_plt, vp_sp) = if (title = sp[:title]) != "" GR.savestate() - xpos, ypos, halign, valign = if (loc = sp[:titlelocation]) === :left + xpos, ypos, halign, valign = if (loc = sp[:titlelocation]) ≡ :left vp_plt.xmin, vp_sp.ymax, :left, :top - elseif loc === :center + elseif loc ≡ :center xcenter(vp_plt), vp_sp.ymax, :center, :top - elseif loc === :right + elseif loc ≡ :right vp_plt.xmax, vp_sp.ymax, :right, :top else xposition(vp_plt, loc[1]), @@ -1941,9 +1895,9 @@ function gr_add_series(sp, series) frng = series[:fillrange] # recompute data - if ispolar(sp) && z === nothing + if ispolar(sp) && z ≡ nothing extrema_r = gr_y_axislims(sp) - if frng !== nothing + if frng ≢ nothing _, frng = PlotsBase.convert_to_polar(x, frng, extrema_r) end x, y = PlotsBase.convert_to_polar(x, y, extrema_r) @@ -1958,34 +1912,34 @@ function gr_add_series(sp, series) # draw the series clims = gr_clims(sp, series) if (st = series[:seriestype]) in (:path, :scatter, :straightline) - if st === :straightline + if st ≡ :straightline x, y = PlotsBase.straightline_data(series) end gr_draw_segments(series, x, y, nothing, frng, clims) - if series[:markershape] !== :none + if series[:markershape] ≢ :none gr_draw_markers(series, x, y, nothing, clims) end - elseif st === :shape + elseif st ≡ :shape gr_draw_shapes(series, clims) elseif st in (:path3d, :scatter3d) gr_draw_segments(series, x, y, z, nothing, clims) - if st === :scatter3d || series[:markershape] !== :none + if st ≡ :scatter3d || series[:markershape] ≢ :none gr_draw_markers(series, x, y, z, clims) end - elseif st === :contour + elseif st ≡ :contour gr_draw_contour(series, x, y, z, clims) elseif st in (:surface, :wireframe, :mesh3d) GR.setwindow(-1, 1, -1, 1) gr_draw_surface(series, x, y, z, clims) - elseif st === :volume + elseif st ≡ :volume sp[:legend_position] = :none GR.gr3.clear() - elseif st === :heatmap + elseif st ≡ :heatmap # `z` is already transposed, so we need to reverse before passing its size. x, y = PlotsBase.heatmap_edges(x, xscale, y, yscale, reverse(size(z)), ispolar(series)) gr_draw_heatmap(series, x, y, z, clims) - elseif st === :image + elseif st ≡ :image gr_draw_image(series, x, y, z, clims) end @@ -1995,7 +1949,7 @@ function gr_add_series(sp, series) gr_text(GR.wctondc(xi, yi)..., str) end - if sp[:legend_position] === :inline && should_add_to_legend(series) + if sp[:legend_position] ≡ :inline && should_add_to_legend(series) gr_set_textcolor(plot_color(sp[:legend_font_color])) offset, halign, valign = if sp[:yaxis][:mirror] _, i = sp[:xaxis][:flip] ? findmax(x) : findmin(x) @@ -2013,10 +1967,10 @@ function gr_add_series(sp, series) end function gr_draw_segments(series, x, y, z, fillrange, clims) - (x === nothing || length(x) ≤ 1) && return - if fillrange !== nothing # prepare fill-in + (x ≡ nothing || length(x) ≤ 1) && return + if fillrange ≢ nothing # prepare fill-in GR.setfillintstyle(GR.INTSTYLE_SOLID) - fr_from, fr_to = PlotsBase.is_2tuple(fillrange) ? fillrange : (y, fillrange) + fr_from, fr_to = is_2tuple(fillrange) ? fillrange : (y, fillrange) end # draw the line(s) @@ -2024,9 +1978,9 @@ function gr_draw_segments(series, x, y, z, fillrange, clims) for segment in series_segments(series, st; check = true) i, rng = segment.attr_index, segment.range isempty(rng) && continue - is3d = st === :path3d && z !== nothing - is2d = st === :path || st === :straightline - if is2d && fillrange !== nothing + is3d = st ≡ :path3d && z ≢ nothing + is2d = st ≡ :path || st ≡ :straightline + if is2d && fillrange ≢ nothing (fc = get_fillcolor(series, clims, i)) |> gr_set_fillcolor gr_set_fillstyle(get_fillstyle(series, i)) fx = _cycle(x, vcat(rng, reverse(rng))) @@ -2061,7 +2015,7 @@ function gr_draw_markers( ) isempty(x) && return GR.setfillintstyle(GR.INTSTYLE_SOLID) - (shapes = series[:markershape]) === :none && return + (shapes = series[:markershape]) ≡ :none && return for segment in series_segments(series, :scatter) rng = intersect(eachindex(IndexLinear(), x), segment.range) isempty(rng) && continue @@ -2119,7 +2073,7 @@ function gr_draw_contour(series, x, y, z, clims) gr_set_line(get_linewidth(series), get_linestyle(series), get_linecolor(series), series) gr_set_transparency(get_fillalpha(series)) h = gr_contour_levels(series, clims) - if series[:fillrange] !== nothing + if series[:fillrange] ≢ nothing GR.contourf(x, y, h, z, Int(series[:contour_labels] == true)) else black = plot_color(:black) @@ -2131,7 +2085,7 @@ end function gr_draw_surface(series, x, y, z, clims) e_kwargs = series[:extra_kwargs] - if (st = series[:seriestype]) === :surface + if (st = series[:seriestype]) ≡ :surface if ndims(x) == ndims(y) == ndims(z) == 2 GR.gr3.surface(x', y', z, GR.OPTION_3D_MESH) else @@ -2150,10 +2104,10 @@ function gr_draw_surface(series, x, y, z, clims) GR.gr3.surface(x, y, z, d_opt) end end - elseif st === :wireframe + elseif st ≡ :wireframe GR.setfillcolorind(0) GR.surface(x, y, z, get(e_kwargs, :display_option, GR.OPTION_FILLED_MESH)) - elseif st === :mesh3d + elseif st ≡ :mesh3d if series[:connections] isa AbstractVector{<:AbstractVector{Int}} # Combination of any polygon types cns = map(cns -> [length(cns), cns...], series[:connections]) @@ -2221,7 +2175,7 @@ function gr_draw_heatmap(series, x, y, z, clims) # pdf output, and also supports alpha values. # Note that drawimage draws uniformly spaced data correctly # even on log scales, where it is visually non-uniform. - _z, colors = if (scale = sp[:colorbar_scale]) === :identity + _z, colors = if (scale = sp[:colorbar_scale]) ≡ :identity z, plot_color.(get(fillgrad, z, clims), series[:fillalpha]) elseif scale ∈ _log_scales z_log, z_normalized = gr_z_normalized_log_scaled(scale, z, clims) @@ -2235,7 +2189,7 @@ function gr_draw_heatmap(series, x, y, z, clims) if something(series[:fillalpha], 1) < 1 @warn "GR: transparency not supported in non-uniform heatmaps. Alpha values ignored." end - _z, z_normalized = if (scale = sp[:colorbar_scale]) === :identity + _z, z_normalized = if (scale = sp[:colorbar_scale]) ≡ :identity z, get_z_normalized.(z, clims...) elseif scale ∈ _log_scales gr_z_normalized_log_scaled(scale, z, clims) @@ -2274,7 +2228,7 @@ for (mime, fmt) in ( "image/svg+xml" => "svg", ) @eval function PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) - dpi_factor = $fmt == "png" ? plt[:dpi] / PlotsBase.DPI : 1 + dpi_factor = $fmt == "png" ? plt[:dpi] / DPI : 1 filepath = tempname() * "." * $fmt # workaround windows bug github.com/JuliaLang/julia/issues/46989 touch(filepath) @@ -2293,7 +2247,7 @@ for (mime, fmt) in ( end function PlotsBase._display(plt::Plot{GRBackend}) - if plt[:display_type] === :inline + if plt[:display_type] ≡ :inline filepath = tempname() * ".pdf" GR.emergencyclosegks() withenv( @@ -2319,4 +2273,4 @@ end PlotsBase.closeall(::GRBackend) = GR.emergencyclosegks() -end # module +end # module diff --git a/PlotsBase/ext/GastonExt.jl b/PlotsBase/ext/GastonExt.jl index 27fb87a50..980d3c9b4 100644 --- a/PlotsBase/ext/GastonExt.jl +++ b/PlotsBase/ext/GastonExt.jl @@ -1,39 +1,24 @@ module GastonExt import RecipesPipeline -import PlotsBase: PlotsBase, ticks_type import PlotUtils +import PlotsBase import Gaston -using PlotsBase.PlotMeasures -using PlotsBase.PlotsSeries -using PlotsBase.PlotsPlots +using PlotsBase.Annotations +using PlotsBase.DataSeries using PlotsBase.Colorbars +using PlotsBase.Surfaces using PlotsBase.Subplots using PlotsBase.Commons +using PlotsBase.Colors +using PlotsBase.Plots using PlotsBase.Ticks using PlotsBase.Fonts using PlotsBase.Axes -const package_str = "Gaston" -const str = lowercase(package_str) -const sym = Symbol(str) - struct GastonBackend <: PlotsBase.AbstractBackend end -const T = GastonBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) -end - -PlotsBase.backend_name(::T) = sym -PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static GastonBackend gaston const _gaston_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -133,28 +118,6 @@ const _gaston_markers = [ const _gaston_scales = [:identity, :ln, :log2, :log10] -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::T, $s::Symbol) = $s in $v - PlotsBase.$f2(::T) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- - # https://github.com/mbaz/Gaston. PlotsBase.should_warn_on_unsupported(::GastonBackend) = false @@ -181,7 +144,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{GastonBackend}) foreach(series -> gaston_add_series(plt, series), plt.series_list) for sp in plt.subplots - sp === nothing && continue + sp ≡ nothing && continue for ann in sp[:annotations] x, y, val = locate_annotation(sp, ann...) sp.o.axesconf *= "; set label '$(val.str)' at $x,$y $(gaston_font(val.font))" @@ -218,14 +181,14 @@ for (mime, term) in ( @eval function PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend}) term = String($term) tmpfile = tempname() * ".$term" - if plt.o !== nothing + if plt.o ≢ nothing ret = Gaston.save(; saveopts = gaston_saveopts(plt), handle = plt.o.handle, output = tmpfile, term, ) - if ret === nothing || ret + if ret ≡ nothing || ret while !isfile(tmpfile) end # avoid race condition with read in next line write(io, read(tmpfile)) @@ -246,7 +209,7 @@ function gaston_saveopts(plt::Plot{GastonBackend}) saveopts = ["size " * join(plt[:size], ',')] # scale all plot elements to match PlotsBase.jl DPI standard - scaling = plt[:dpi] / PlotsBase.DPI + scaling = plt[:dpi] / DPI push!( saveopts, @@ -269,7 +232,7 @@ function gaston_get_subplots(n, plt_subplots, layout) nr, nc = size(layout) sps = Array{Any}(nothing, nr, nc) for r in 1:nr, c in 1:nc # NOTE: col major - sps[r, c] = if (l = layout[r, c]) isa PlotsBase.GridLayout + sps[r, c] = if (l = layout[r, c]) isa GridLayout n, sub = gaston_get_subplots(n, plt_subplots, l) size(sub) == (1, 1) ? only(sub) : sub else @@ -287,7 +250,7 @@ end function gaston_init_subplots(plt, sps) sz = nr, nc = size(sps) for c in 1:nc, r in 1:nr # NOTE: row major - if (sp = sps[r, c]) isa Subplot || sp === nothing + if (sp = sps[r, c]) isa Subplot || sp ≡ nothing gaston_init_subplot(plt, sp) else gaston_init_subplots(plt, sp) @@ -301,7 +264,7 @@ function gaston_init_subplot( plt::Plot{GastonBackend}, sp::Union{Nothing,Subplot{GastonBackend}}, ) - obj = if sp === nothing + obj = if sp ≡ nothing sp else dims = @@ -328,7 +291,7 @@ function gaston_multiplot_pos_size(layout, parent_xy_wh) # width and height (pct) are multiplicative (parent) w = layout.widths[c].value * parent_xy_wh[3] h = layout.heights[r].value * parent_xy_wh[4] - if isa(l, PlotsBase.EmptyLayout) + if isa(l, EmptyLayout) dat[r, c] = (c - 1) * w, (r - 1) * h, w, h, nothing else # previous position (origin) @@ -336,9 +299,9 @@ function gaston_multiplot_pos_size(layout, parent_xy_wh) prev_c = c > 1 ? dat[r, c - 1] : nothing prev_r isa Array && (prev_r = prev_r[end, end]) prev_c isa Array && (prev_c = prev_c[end, end]) - x = prev_c !== nothing ? prev_c[1] + prev_c[3] : parent_xy_wh[1] - y = prev_r !== nothing ? prev_r[2] + prev_r[4] : parent_xy_wh[2] - dat[r, c] = if l isa PlotsBase.GridLayout + x = prev_c ≢ nothing ? prev_c[1] + prev_c[3] : parent_xy_wh[1] + y = prev_r ≢ nothing ? prev_r[2] + prev_r[4] : parent_xy_wh[2] + dat[r, c] = if l isa GridLayout sub = gaston_multiplot_pos_size(l, (x, y, w, h)) size(sub) == (1, 1) ? only(sub) : sub else @@ -356,11 +319,10 @@ function gaston_multiplot_pos_size!(dat) gaston_multiplot_pos_size!(xy_wh_sp) elseif xy_wh_sp isa Tuple x, y, w, h, sp = xy_wh_sp - sp === nothing && continue - sp.o === nothing && continue + sp ≡ nothing && continue + sp.o ≡ nothing && continue # gnuplot screen coordinates: bottom left at 0,0 and top right at 1,1 gx, gy = x, 1 - y - h - # @show gx, gy w, h sp.o.axesconf = "set origin $gx, $gy; set size $w, $h; " * sp.o.axesconf end end @@ -369,11 +331,11 @@ end function gaston_add_series(plt::Plot{GastonBackend}, series::Series) sp = series[:subplot] - (gsp = sp.o) === nothing && return + (gsp = sp.o) ≡ nothing && return x, y, z = series[:x], series[:y], series[:z] st = series[:seriestype] curves = Gaston.Curve[] - if gsp.dims == 2 && z === nothing + 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])) @@ -385,7 +347,7 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) supp = nothing # supplementary column if z isa Surface z = z.surf - if st === :image + if st ≡ :image z = reverse(Float32.(Gray.(z)), dims = 1) # flip y axis nr, nc = size(z) if (ly = length(y)) == 2 && ly != nr @@ -398,9 +360,9 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) length(x) == size(z, 2) + 1 && (x = (x[1:(end - 1)] + x[2:end]) / 2) length(y) == size(z, 1) + 1 && (y = (y[1:(end - 1)] + y[2:end]) / 2) end - if st === :mesh3d + if st ≡ :mesh3d x, y, z = PlotsBase.mesh3d_triangles(x, y, z, series[:connections]) - elseif st === :surface + elseif st ≡ :surface if ndims(x) == ndims(y) == ndims(z) == 1 # must reinterpret 1D data for `pm3d` (points are ordered) x, y = unique(x), unique(y) @@ -461,31 +423,31 @@ function gaston_seriesconf!( fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i)) fs = gaston_fillstyle(get_fillstyle(series, i)) lc, dt, lw = gaston_lc_ls_lw(series, clims, i) - curveconf *= if fr !== nothing # filled curves, but not filled curves with markers + curveconf *= if fr ≢ nothing # filled curves, but not filled curves with markers "w filledcurves fc $fc fs $fs border lc $lc lw $lw dt $dt,'' w lines lc $lc lw $lw dt $dt" - elseif series[:markershape] === :none # simplepath + elseif series[:markershape] ≡ :none # simplepath "w lines lc $lc dt $dt lw $lw" else pt, ps, mc = gaston_mk_ms_mc(series, clims, i) "w lp lc $mc dt $dt lw $lw pt $pt ps $ps" end - elseif st === :shape + elseif st ≡ :shape fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i)) fs = gaston_fillstyle(get_fillstyle(series, i)) lc, = gaston_lc_ls_lw(series, clims, i) curveconf *= "w filledcurves fc $fc fs $fs border lc $lc" elseif st ∈ (:steppre, :stepmid, :steppost) - step = if st === :steppre + step = if st ≡ :steppre "fsteps" - elseif st === :stepmid + elseif st ≡ :stepmid "histeps" - elseif st === :steppost + elseif st ≡ :steppost "steps" end curveconf *= "w $step" lc, dt, lw = gaston_lc_ls_lw(series, clims, i) push!(extra_curves, "w points lc $lc dt $dt lw $lw notitle") - elseif st === :image + elseif st ≡ :image gsp.axesconf *= gaston_palette_conf(series) curveconf *= "w image pixels" elseif st ∈ (:contour, :contour3d) @@ -496,7 +458,7 @@ function gaston_seriesconf!( push!(extra_curves, "w labels notitle") end levels = collect(contour_levels(series, clims)) - if st === :contour # 2D + if st ≡ :contour # 2D gsp.axesconf *= if filled "; set view map; set palette maxcolors $(length(levels))" else @@ -507,11 +469,11 @@ function gaston_seriesconf!( elseif st ∈ (:surface, :heatmap) curveconf *= "w pm3d" gsp.axesconf *= gaston_palette_conf(series) - st === :heatmap && (gsp.axesconf *= "; set view map") + st ≡ :heatmap && (gsp.axesconf *= "; set view map") elseif st ∈ (:wireframe, :mesh3d) lc, dt, lw = gaston_lc_ls_lw(series, clims, i) curveconf *= "w lines lc $lc dt $dt lw $lw" - elseif st === :quiver + elseif st ≡ :quiver curveconf *= "w vectors filled" else @warn "PlotsBase(Gaston): $st is not implemented yet" @@ -563,7 +525,7 @@ function gaston_parse_axes_attrs( fs = sp[:framestyle] for letter in (:x, :y, :z) - (letter === :z && dims == 2) && continue + (letter ≡ :z && dims == 2) && continue axis = sp[get_attr_symbol(letter, :axis)] # NOTE: there is no `z2tics` concept in gnuplot (only 2D) @@ -576,7 +538,7 @@ function gaston_parse_axes_attrs( # guide labels guide_font = guidefont(axis) - if letter === :y && dims == 2 + if letter ≡ :y && dims == 2 # vertical by default (consistency witht other backends) guide_font = font(guide_font; rotation = guide_font.rotation + 90) end @@ -585,19 +547,19 @@ function gaston_parse_axes_attrs( "set $(letter)$(I)label '$(axis[:guide])' $(gaston_font(guide_font))", ) - logscale, base = if (scale = axis[:scale]) === :identity + logscale, base = if (scale = axis[:scale]) ≡ :identity "nologscale", "" - elseif scale === :log10 + elseif scale ≡ :log10 "logscale", "10" - elseif scale === :log2 + elseif scale ≡ :log2 "logscale", "2" - elseif scale === :ln + elseif scale ≡ :ln "logscale", "e" end push!(axesconf, "set $logscale $letter $base") # handle ticks - if axis[:showaxis] && fs !== :none + if axis[:showaxis] && fs ≢ :none if polar push!(axesconf, "set size square; unset $(letter)tics") else @@ -607,7 +569,7 @@ function gaston_parse_axes_attrs( ) # major tick locations - if axis[:ticks] !== :native + if axis[:ticks] ≢ :native if axis[:flip] hi, lo = axis_limits(sp, letter) else @@ -626,7 +588,7 @@ function gaston_parse_axes_attrs( ticks = get_ticks(sp, axis) gaston_set_ticks!(axesconf, ticks, letter, I, "", "") - if axis[:minorticks] !== :native && !no_minor_intervals(axis) + if axis[:minorticks] ≢ :native && !no_minor_intervals(axis) minor_ticks = get_minor_ticks(sp, axis, ticks) gaston_set_ticks!(axesconf, minor_ticks, letter, I, "m", "add") end @@ -636,7 +598,7 @@ function gaston_parse_axes_attrs( if fs in (:zerolines, :origin) push!(axesconf, "set $(letter)zeroaxis") end - if !axis[:showaxis] || fs === :none + if !axis[:showaxis] || fs ≡ :none push!(axesconf, "set tics scale 0", "set format x \"\"", "set format y \"\"") end @@ -646,14 +608,14 @@ function gaston_parse_axes_attrs( push!(axesconf, "set grid " * (polar ? "polar" : "m$(letter)tics")) end - if (ratio = get_aspect_ratio(sp)) !== :none + if (ratio = get_aspect_ratio(sp)) ≢ :none if dims == 2 - ratio === :equal && (ratio = -1) + ratio ≡ :equal && (ratio = -1) push!(axesconf, "set size ratio $ratio") else # ratio and square have no effect on 3D plots, # but do affect 3D projections created using set view map - if ratio === :equal + if ratio ≡ :equal push!(axesconf, "set view equal xyz") end end @@ -673,11 +635,11 @@ function gaston_parse_axes_attrs( left = gp_borders[:bottom_left_back] top = gp_borders[:bottom_right_front] right = gp_borders[:bottom_right_back] - if fs === :box + if fs ≡ :box bottom + left + top + right - elseif fs === :semi + elseif fs ≡ :semi bottom + left - elseif fs === :axes + elseif fs ≡ :axes (sp[:xaxis][:mirror] ? top : bottom) + (sp[:yaxis][:mirror] ? right : left) else 0 @@ -714,9 +676,9 @@ function gaston_parse_axes_attrs( tmin, tmax = axis_limits(sp, :x, false, false) rmin, rmax = axis_limits(sp, :y, false, false) rticks = get_ticks(sp, :y) - gaston_ticks = if (ttype = ticks_type(rticks)) === :ticks + gaston_ticks = if (ttype = PlotsBase.ticks_type(rticks)) ≡ :ticks string.(rticks) - elseif ttype === :ticks_and_labels + elseif ttype ≡ :ticks_and_labels ["'$l' $t" for (t, l) in zip(rticks...)] end push!( @@ -746,19 +708,19 @@ function gaston_fix_ticks_overflow(ticks::AbstractVector) end function gaston_set_ticks!(axesconf, ticks, letter, I, maj_min, add) - ticks === :auto && return + ticks ≡ :auto && return if ticks ∈ (:none, nothing, false) push!(axesconf, "unset $(maj_min)$(letter)tics") return end - gaston_ticks = if (ttype = ticks_type(ticks)) === :ticks + gaston_ticks = if (ttype = PlotsBase.ticks_type(ticks)) ≡ :ticks tics = gaston_fix_ticks_overflow(ticks) if maj_min == "m" map(t -> "'' $t 1", tics) # see gnuplot manual 'Mxtics' else map(string, tics) end - elseif ttype === :ticks_and_labels + elseif ttype ≡ :ticks_and_labels tics = gaston_fix_ticks_overflow(first(ticks)) labs = last(ticks) map(i -> "'$(gaston_enclose_tick_string(labs[i]))' $(tics[i])", eachindex(tics)) @@ -766,7 +728,7 @@ function gaston_set_ticks!(axesconf, ticks, letter, I, maj_min, add) @error "Gaston: invalid input for $(maj_min)$(letter)ticks: $ticks ($ttype)" nothing end - if gaston_ticks !== nothing + if gaston_ticks ≢ nothing push!(axesconf, "set $(letter)$(I)tics $add (" * join(gaston_ticks, ", ") * ")") end nothing @@ -794,7 +756,7 @@ function gaston_set_legend!(axesconf, sp, any_label) pos *= sp[:legend_column] == 1 ? "vertical" : "horizontal" push!(axesconf, "set key $pos box lw 1 opaque noautotitle") push!(axesconf, "set key $(gaston_font(legendfont(sp), rot=false, align=false))") - if sp[:legend_title] !== nothing + if sp[:legend_title] ≢ nothing # NOTE: cannot use legendtitlefont(sp) as it will override legendfont push!(axesconf, "set key title '$(sp[:legend_title])'") end @@ -814,7 +776,7 @@ gaston_valign(k) = (top = :top, vcenter = :center, bottom = :bottom)[k] # from the gnuplot docs: # - an alpha value of 0 represents a fully opaque color; i.e., "#00RRGGBB" is the same as "#RRGGBB". # - an alpha value of 255 (FF) represents full transparency -gaston_alpha(alpha) = alpha === nothing ? 0 : alpha +gaston_alpha(alpha) = alpha ≡ nothing ? 0 : alpha gaston_lc_ls_lw(series::Series, clims, i::Int) = ( gaston_color(get_linecolor(series, clims, i), get_linealpha(series, i)), @@ -847,17 +809,17 @@ gaston_palette_conf(series) = function gaston_marker(marker, alpha) # NOTE: :rtriangle, :ltriangle, :hexagon, :heptagon, :octagon seems unsupported by gnuplot filled = gaston_alpha(alpha) != 1 - marker === :none && return -1 - marker === :pixel && return 0 + marker ≡ :none && return -1 + marker ≡ :pixel && return 0 marker ∈ (:+, :cross) && return 1 marker ∈ (:x, :xcross) && return 2 - marker === :star5 && return 3 - marker === :rect && return filled ? 5 : 4 - marker === :circle && return filled ? 7 : 6 - marker === :utriangle && return filled ? 9 : 8 - marker === :dtriangle && return filled ? 11 : 10 - marker === :diamond && return filled ? 13 : 12 - marker === :pentagon && return filled ? 15 : 14 + marker ≡ :star5 && return 3 + marker ≡ :rect && return filled ? 5 : 4 + marker ≡ :circle && return filled ? 7 : 6 + marker ≡ :utriangle && return filled ? 9 : 8 + marker ≡ :dtriangle && return filled ? 11 : 10 + marker ≡ :diamond && return filled ? 13 : 12 + marker ≡ :pentagon && return filled ? 15 : 14 # @debug "PlotsBase(Gaston): unsupported marker $marker" 1 end @@ -869,18 +831,18 @@ function gaston_color(col, alpha = 0) end function gaston_linestyle(style) - style === :solid && return 1 - style === :dash && return 2 - style === :dot && return 3 - style === :dashdot && return 4 - style === :dashdotdot && return 5 + style ≡ :solid && return 1 + style ≡ :dash && return 2 + style ≡ :dot && return 3 + style ≡ :dashdot && return 4 + style ≡ :dashdotdot && return 5 1 end function gaston_enclose_tick_string(tick_string) - findfirst('^', tick_string) === nothing && return tick_string + findfirst('^', tick_string) ≡ nothing && return tick_string base, power = split(tick_string, '^') "$base^{$power}" end -end # module +end # module diff --git a/PlotsBase/ext/HDF5Ext.jl b/PlotsBase/ext/HDF5Ext.jl index 1aeaf7783..cbd39498c 100644 --- a/PlotsBase/ext/HDF5Ext.jl +++ b/PlotsBase/ext/HDF5Ext.jl @@ -2,47 +2,29 @@ module HDF5Ext import HDF5: HDF5, Group, Dataset +import RecipesPipeline: RecipesPipeline, Surface, DefaultsDict, datetimeformatter import PlotUtils: PlotUtils, Colors import PlotUtils.ColorSchemes: ColorScheme import PlotUtils.Colors: Colorant -import RecipesPipeline -import RecipesPipeline.datetimeformatter -import PlotUtils.ColorPalette, - PlotUtils.CategoricalColorGradient, PlotUtils.ContinuousColorGradient -import PlotsBase: - PlotsBase, Surface, Arrow, GridLayout, RootLayout, Font, PlotText, SeriesAnnotations -import PlotsBase: BoundingBox, Length, Plot, DefaultsDict, plot, plot! +import PlotsBase -using PlotsBase.PlotsSeries +using PlotsBase.Annotations +using PlotsBase.DataSeries using PlotsBase.Subplots using PlotsBase.Commons +using PlotsBase.Arrows using PlotsBase.Shapes +using PlotsBase.Plots +using PlotsBase.Fonts using PlotsBase.Axes import Dates -const package_str = "HDF5" -const str = lowercase(package_str) -const sym = Symbol(str) - struct HDF5Backend <: PlotsBase.AbstractBackend end -const T = HDF5Backend - -get_concrete_backend() = T # opposite to abstract +PlotsBase.@extension_static HDF5Backend hdf5 -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) -end - -PlotsBase.backend_name(::T) = sym -PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) - -const _hdf5_attr = PlotsBase.merge_with_base_supported([ +const _hdf5_attrs = PlotsBase.merge_with_base_supported([ :annotations, :legend_background_color, :background_color_inside, @@ -109,7 +91,7 @@ const _hdf5_attr = PlotsBase.merge_with_base_supported([ :dpi, :colorbar_title, ]) -const _hdf5_seriestype = [ +const _hdf5_seriestypes = [ :path, :steppre, :stepmid, @@ -127,29 +109,9 @@ const _hdf5_seriestype = [ :surface, :wireframe, ] -const _hdf5_style = [:auto, :solid, :dash, :dot, :dashdot] -const _hdf5_marker = vcat(PlotsBase.Commons._all_markers, :pixel) -const _hdf5_scale = [:identity, :ln, :log2, :log10] - -#= -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::HDF5Backend, $s::Symbol) = $s in $v - PlotsBase.$f2(::HDF5Backend) = sort(collect($v)) - end |> eval -end -=# - -## results in: -# PlotsBase.is_attr_supported(::HDF5Backend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::HDF5Backend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::HDF5Backend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- +const _hdf5_styles = [:auto, :solid, :dash, :dot, :dashdot] +const _hdf5_markers = vcat(Commons._all_markers, :pixel) +const _hdf5_scales = [:identity, :ln, :log2, :log10] # Additional constants # Dict has problems using "Types" as keys. Initialize in "_initialize_backend": @@ -199,9 +161,9 @@ if length(HDF5PLOT_MAP_TELEM2STR) < 1 "SHAPE" => Shape, "ARROW" => Arrow, "COLORSCHEME" => ColorScheme, - "COLORPALETTE" => ColorPalette, - "CONT_COLORGRADIENT" => ContinuousColorGradient, - "CAT_COLORGRADIENT" => CategoricalColorGradient, + "COLORPALETTE" => PlotUtils.ColorPalette, + "CONT_COLORGRADIENT" => PlotUtils.ContinuousColorGradient, + "CAT_COLORGRADIENT" => PlotUtils.CategoricalColorGradient, "AXIS" => Axis, "SURFACE" => Surface, "SUBPLOT" => Subplot, @@ -215,7 +177,7 @@ end # Helper functions -h5plotpath(plotname::String) = "plots/$plotname" +h5plotpath(name::String) = "plots/$name" _hdf5_merge!(dest::AKW, src::AKW) = for (k, v) in src @@ -406,18 +368,6 @@ function _write(grp::Group, plt::Plot{HDF5Backend}) end end -function hdf5plot_write( - plt::Plot{HDF5Backend}, - path::AbstractString; - name::String = "_unnamed", -) - HDF5.h5open(path, "w") do file - HDF5.write_dataset(file, "VERSION_INFO", string(PlotsBase._current_plots_version)) - grp = HDF5.create_group(file, h5plotpath(name)) - _write(grp, plt) - end -end - # _read(): Read data, but not type information. # Types with built-in HDF5 support: @@ -540,7 +490,7 @@ function _read(grp::Group, sp::Subplot) sgrp = HDF5.open_group(listgrp, "$i") seriesinfo = _read(KW, sgrp) - plot!(sp, seriesinfo[:x], seriesinfo[:y]) # Add data & create data structures + PlotsBase.plot!(sp, seriesinfo[:x], seriesinfo[:y]) # Add data & create data structures _hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo) end @@ -556,7 +506,7 @@ function _read_plot(grp::Group) n = _read_length_attrs(Vector, listgrp) # Construct new plot, +allocate subplots: - plt = plot(layout = n) + plt = PlotsBase.plot(layout = n) HDF5PLOT_PLOTREF.ref = plt # Used when reading "layout" agrp = HDF5.open_group(grp, "attr") @@ -570,47 +520,41 @@ function _read_plot(grp::Group) plt end -hdf5plot_read(path::AbstractString; name::String = "_unnamed") = - HDF5.h5open(path, "r") do file - grp = HDF5.open_group(file, h5plotpath("_unnamed")) - return _read_plot(grp) - end - # Implement PlotsBase.jl backend interface for HDF5Backend -is_marker_supported(::HDF5Backend, shape::Shape) = true +PlotsBase.is_marker_supported(::HDF5Backend, shape::Shape) = true # Create the window/figure for this backend. -function _create_backend_figure(plt::Plot{HDF5Backend}) end +function PlotsBase._create_backend_figure(plt::Plot{HDF5Backend}) end # Set up the subplot within the backend object. -function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end +function PlotsBase._initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end # Add one series to the underlying backend object. # Called once per series # NOTE: Seems to be called when user calls plot()... even if backend # plot, sp.o has not yet been constructed... -function _series_added(plt::Plot{HDF5Backend}, series::Series) end +function PlotsBase._series_added(plt::Plot{HDF5Backend}, series::Series) end # When series data is added/changed, this callback can do dynamic updates to the backend object. # note: if the backend rebuilds the plot from scratch on display, then you might not do anything here. -function _series_updated(plt::Plot{HDF5Backend}, series::Series) end +function PlotsBase._series_updated(plt::Plot{HDF5Backend}, series::Series) end # called just before updating layout bounding boxes... in case you need to prep # for the calcs -function _before_layout_calcs(plt::Plot{HDF5Backend}) end +function PlotsBase._before_layout_calcs(plt::Plot{HDF5Backend}) end # Set the (left, top, right, bottom) minimum padding around the plot area # to fit ticks, tick labels, guides, colorbars, etc. -function _update_min_padding!(sp::Subplot{HDF5Backend}) end +function PlotsBase._update_min_padding!(sp::Subplot{HDF5Backend}) end # Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations]) -function _update_plot_object(plt::Plot{HDF5Backend}) end +function PlotsBase._update_plot_object(plt::Plot{HDF5Backend}) end # ---------------------------------------------------------------- # Display/show the plot (open a GUI window, or browser page, for example). -function _display(plt::Plot{HDF5Backend}) +function PlotsBase._display(plt::Plot{HDF5Backend}) msg = "HDF5 interface does not support `display()` function." msg *= "\nUse `PlotsBase.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." @warn msg @@ -618,7 +562,22 @@ function _display(plt::Plot{HDF5Backend}) end # Interface actually required to use HDF5Backend +PlotsBase.hdf5plot_write(path::AbstractString; kw...) = + PlotsBase.hdf5plot_write(current(), path; kw...) -hdf5plot_write(path::AbstractString) = hdf5plot_write(current(), path) +PlotsBase.hdf5plot_write( + plt::Plot{HDF5Backend}, + path::AbstractString; + name::String = "_unnamed", +) = + HDF5.h5open(path, "w") do file + HDF5.write_dataset(file, "VERSION_INFO", string(PlotsBase._version)) + _write(HDF5.create_group(file, h5plotpath(name)), plt) + end + +PlotsBase.hdf5plot_read(path::AbstractString; name::String = "_unnamed") = + HDF5.h5open(path, "r") do file + return _read_plot(HDF5.open_group(file, h5plotpath("_unnamed"))) + end -end # module +end # module diff --git a/PlotsBase/ext/IJuliaExt.jl b/PlotsBase/ext/IJuliaExt.jl index b94020260..47f71b049 100644 --- a/PlotsBase/ext/IJuliaExt.jl +++ b/PlotsBase/ext/IJuliaExt.jl @@ -10,54 +10,33 @@ const IJulia = function _init_ijulia_plotting() # IJulia is more stable with local file PlotsBase._use_local_plotlyjs[] = - PlotsBase._plotly_local_file_path[] === nothing ? false : + PlotsBase._plotly_local_file_path[] ≡ nothing ? false : isfile(PlotsBase._plotly_local_file_path[]) ENV["MPLBACKEND"] = "Agg" end -""" -Add extra jupyter mimetypes to display_dict based on the plot backed. - -The default is nothing, except for plotly based backends, where it -adds data for `application/vnd.plotly.v1+json` that is used in -frontends like jupyterlab and nteract. -""" -_ijulia__extra_mime_info!(plt::Plot, out::Dict) = out - -function _ijulia__extra_mime_info!(plt::Plot{PlotsBase.PlotlyJSBackend}, out::Dict) - out["application/vnd.plotly.v1+json"] = - Dict(:data => PlotsBase.plotly_series(plt), :layout => PlotsBase.plotly_layout(plt)) - out -end - -function _ijulia__extra_mime_info!(plt::Plot{PlotsBase.PlotlyBackend}, out::Dict) - out["application/vnd.plotly.v1+json"] = - Dict(:data => PlotsBase.plotly_series(plt), :layout => PlotsBase.plotly_layout(plt)) - out -end - function _ijulia_display_dict(plt::Plot) output_type = Symbol(plt.attr[:html_output_format]) - if output_type === :auto + if output_type ≡ :auto output_type = get(PlotsBase._best_html_output_type, PlotsBase.backend_name(plt.backend), :svg) end out = Dict() - if output_type === :txt + if output_type ≡ :txt mime = "text/plain" out[mime] = sprint(show, MIME(mime), plt) - elseif output_type === :png + elseif output_type ≡ :png mime = "image/png" out[mime] = base64encode(show, MIME(mime), plt) - elseif output_type === :svg + elseif output_type ≡ :svg mime = "image/svg+xml" out[mime] = sprint(show, MIME(mime), plt) - elseif output_type === :html + elseif output_type ≡ :html mime = "text/html" out[mime] = sprint(show, MIME(mime), plt) - _ijulia__extra_mime_info!(plt, out) - elseif output_type === :pdf + PlotsBase._ijulia__extra_mime_info!(plt, out) + elseif output_type ≡ :pdf mime = "application/pdf" out[mime] = base64encode(show, MIME(mime), plt) else diff --git a/PlotsBase/ext/PGFPlotsXExt.jl b/PlotsBase/ext/PGFPlotsXExt.jl index e72abe26f..a9789befc 100644 --- a/PlotsBase/ext/PGFPlotsXExt.jl +++ b/PlotsBase/ext/PGFPlotsXExt.jl @@ -1,19 +1,18 @@ module PGFPlotsXExt import PlotsBase: PlotsBase, pgfx_sanitize_string -import PlotUtils: PlotUtils, ColorGradient import LaTeXStrings: LaTeXString import Printf: @sprintf import UUIDs: uuid4 + import RecipesPipeline +import PlotUtils import PGFPlotsX import Latexify import Contour -using PlotsBase.PlotMeasures using PlotsBase.Annotations -using PlotsBase.PlotsSeries -using PlotsBase.PlotsPlots +using PlotsBase.DataSeries using PlotsBase.Colorbars using PlotsBase.Subplots using PlotsBase.Surfaces @@ -21,29 +20,13 @@ using PlotsBase.Commons using PlotsBase.Colors using PlotsBase.Shapes using PlotsBase.Arrows +using PlotsBase.Plots using PlotsBase.Fonts using PlotsBase.Ticks using PlotsBase.Axes -const package_str = "PGFPlotsX" -const str = lowercase(package_str) -const sym = Symbol(str) - struct PGFPlotsXBackend <: PlotsBase.AbstractBackend end -const T = PGFPlotsXBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) -end - -PlotsBase.backend_name(::T) = sym -PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static PGFPlotsXBackend pgfplotsx const _pgfplotsx_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -216,28 +199,6 @@ PlotsBase.is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true # additional constants const _pgfplotsx_series_ids = KW() -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::T, $s::Symbol) = $s in $v - PlotsBase.$f2(::T) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- - const Options = PGFPlotsX.Options const Table = PGFPlotsX.Table @@ -313,7 +274,7 @@ surface_to_vecs(x::AVec, y::AVec, z::AVec) = x, y, z Base.push!(pgfx_plot::PGFPlotsXPlot, item) = push!(pgfx_plot.the_plot, item) pgfx_split_extra_kw(extra) = - (get(extra, :add, nothing), filter(x -> first(x) !== :add, extra)) + (get(extra, :add, nothing), filter(x -> first(x) ≢ :add, extra)) curly(obj) = "{$(string(obj))}" @@ -332,8 +293,8 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) # extract extra kwargs extra_plot, extra_plot_opt = pgfx_split_extra_kw(plt[:extra_plot_kwargs]) the_plot = PGFPlotsX.TikzPicture(Options(extra_plot_opt...)) - extra_plot !== nothing && push!(the_plot, wraptuple(extra_plot)...) - bgc = plt.attr[if plt.attr[:background_color_outside] === :match + extra_plot ≢ nothing && push!(the_plot, wraptuple(extra_plot)...) + bgc = plt.attr[if plt.attr[:background_color_outside] ≡ :match :background_color else :background_color_outside @@ -353,9 +314,9 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) end for sp in plt.subplots - bb2 = PlotsBase.bbox(sp) + bb2 = bbox(sp) dx, dy = bb2.x0 - sp_w, sp_h = PlotsBase.width(bb2), PlotsBase.height(bb2) + sp_w, sp_h = width(bb2), height(bb2) if sp[:subplot_index] == plt[:plot_titleindex] x = dx + sp_w / 2 - 10mm # FIXME: get rid of magic constant y = dy + sp_h / 2 @@ -404,7 +365,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) sp_w > 0mm && push!(axis_opt, "width" => string(sp_w - (rpad + lpad))) sp_h > 0mm && push!(axis_opt, "height" => string(sp_h - (tpad + bpad))) for letter in (:x, :y, :z) - if letter !== :z || RecipesPipeline.is3d(sp) + if letter ≢ :z || RecipesPipeline.is3d(sp) pgfx_axis!(axis_opt, sp, letter) end end @@ -437,7 +398,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) if hascolorbar(sp) formatter = latex_formatter(sp[:colorbar_formatter]) cticks = curly(join(get_colorbar_ticks(sp; formatter = formatter)[1], ',')) - letter = sp[:colorbar] === :top ? :x : :y + letter = sp[:colorbar] ≡ :top ? :x : :y colorbar_style = push!( Options("$(letter)label" => sp[:colorbar_title]), @@ -446,7 +407,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) "$(letter)ticklabel style" => pgfx_get_colorbar_ticklabel_style(sp), ) - if sp[:colorbar] === :top + if sp[:colorbar] ≡ :top push!( colorbar_style, "at" => "(0.5, 1.05)", @@ -472,15 +433,12 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) push!(axis_opt, "colorbar" => "false") end if RecipesPipeline.is3d(sp) - if (ar = sp[:aspect_ratio]) !== :auto - push!( - axis_opt, - "unit vector ratio" => ar === :equal ? 1 : join(ar, ' '), - ) + if (ar = sp[:aspect_ratio]) ≢ :auto + push!(axis_opt, "unit vector ratio" => ar ≡ :equal ? 1 : join(ar, ' ')) end push!(axis_opt, "view" => tuple(sp[:camera])) end - axisf = if sp[:projection] === :polar + axisf = if sp[:projection] ≡ :polar # push!(axis_opt, "xmin" => 90) # push!(axis_opt, "xmax" => 450) PGFPlotsX.PolarAxis @@ -489,8 +447,8 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) end extra_sp, extra_sp_opt = pgfx_split_extra_kw(sp[:extra_kwargs]) axis = axisf(merge(axis_opt, Options(extra_sp_opt...))) - extra_sp !== nothing && push!(axis, wraptuple(extra_sp)...) - if sp[:legend_title] !== nothing + extra_sp ≢ nothing && push!(axis, wraptuple(extra_sp)...) + if sp[:legend_title] ≢ nothing legtfont = legendtitlefont(sp) leg_opt = Options( "font" => pgfx_font(legtfont.pointsize, pgfx_thickness_scaling(sp)), @@ -521,15 +479,15 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) if ( RecipesPipeline.is3d(series) || st in (:heatmap, :contour) || - (st === :quiver && opt[:z] !== nothing) + (st ≡ :quiver && opt[:z] ≢ nothing) ) PGFPlotsX.Plot3 else PGFPlotsX.Plot end if ( - series[:fillrange] !== nothing && - series[:ribbon] === nothing && + series[:fillrange] ≢ nothing && + series[:ribbon] ≡ nothing && !isfilledcontour(series) ) push!(series_opt, "area legend" => nothing) @@ -539,7 +497,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) axis.contents[end] isa PGFPlotsX.LegendEntry ? axis.contents[end - 1] : axis.contents[end] merge!(last_plot.options, Options(extra_series_opt...)) - if extra_series !== nothing + if extra_series ≢ nothing push!(axis.contents[end], wraptuple(extra_series)...) end # add series annotations @@ -589,7 +547,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o for (k, segment) in enumerate(segments) i, rng = segment.attr_index, segment.range segment_opt = pgfx_linestyle(opt, i) - if opt[:markershape] !== :none + if opt[:markershape] ≢ :none if (marker = _cycle(opt[:markershape], i)) isa Shape scale_factor = 0.00125 msize = opt[:markersize] * scale_factor @@ -610,10 +568,10 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o segment_opt = merge(segment_opt, pgfx_marker(opt, i)) end # add fillrange - if (sf = opt[:fillrange]) !== nothing && !isfilledcontour(series) + if (sf = opt[:fillrange]) ≢ nothing && !isfilledcontour(series) if sf isa Number || sf isa AVec pgfx_fillrange_series!(axis, series, series_func, i, _cycle(sf, rng), rng) - elseif sf isa Tuple && series[:ribbon] !== nothing + elseif sf isa Tuple && series[:ribbon] ≢ nothing for sfi in sf pgfx_fillrange_series!( axis, @@ -627,7 +585,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o end if ( i == 1 && - series[:subplot][:legend_position] !== :none && + series[:subplot][:legend_position] ≢ :none && pgfx_should_add_to_legend(series) ) pgfx_filllegend!(series_opt, opt) @@ -648,11 +606,11 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o isempty(opt[:label]) && push!(arrow_opt, "forget plot" => nothing) rx, ry = opt[:x][rng], opt[:y][rng] nx, ny = length(rx), length(ry) - x_arrow, y_arrow, x_path, y_path = if arrow.side === :head + x_arrow, y_arrow, x_path, y_path = if arrow.side ≡ :head rx[(nx - 1):nx], ry[(ny - 1):ny], rx[1:(nx - 1)], ry[1:(ny - 1)] - elseif arrow.side === :tail + elseif arrow.side ≡ :tail rx[2:-1:1], ry[2:-1:1], rx[2:nx], ry[2:ny] - elseif arrow.side === :both + elseif arrow.side ≡ :both rx[[2, 1, nx - 1, nx]], ry[[2, 1, ny - 1, ny]], rx[2:(nx - 1)], ry[2:(ny - 1)] end coords = Table([ @@ -670,7 +628,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o end push!(axis, series_func(merge(series_opt, segment_opt), coordinates)) # fill between functions - if sf isa Tuple && series[:ribbon] === nothing + if sf isa Tuple && series[:ribbon] ≡ nothing sf1, sf2 = sf @assert sf1 == series_index "First index of the tuple has to match the current series index." push!( @@ -814,7 +772,7 @@ function pgfx_add_series!(::Val{:contour3d}, axis, series_opt, series, series_fu end function pgfx_add_series!(::Val{:quiver}, axis, series_opt, series, series_func, opt) - if (quiver = opt[:quiver]) !== nothing + if (quiver = opt[:quiver]) ≢ nothing push!( series_opt, "quiver" => Options( @@ -824,7 +782,7 @@ function pgfx_add_series!(::Val{:quiver}, axis, series_opt, series, series_func, ), ) x, y, z = opt[:x], opt[:y], opt[:z] - table = if z !== nothing + table = if z ≢ nothing push!(series_opt["quiver"], "w" => "\\thisrow{w}") pgfx_axis!(axis.options, series[:subplot], :z) [:x => x, :y => y, :z => z, :u => quiver[1], :v => quiver[2], :w => quiver[3]] @@ -867,7 +825,7 @@ function pgfx_add_series!(::Val{:xsticks}, axis, series_opt, args...) end function pgfx_add_legend!(axis, series, opt, i = 1) - if series[:subplot][:legend_position] !== :none + if series[:subplot][:legend_position] ≢ :none leg_entry = if (lab = opt[:label]) isa AVec get(lab, i, "") elseif lab isa AbstractString @@ -893,9 +851,9 @@ pgfx_series_arguments(series, opt) = surface_to_vecs(opt[:x], opt[:y], opt[:z]) elseif RecipesPipeline.is3d(st) opt[:x], opt[:y], opt[:z] - elseif st === :straightline + elseif st ≡ :straightline PlotsBase.straightline_data(series) - elseif st === :shape + elseif st ≡ :shape PlotsBase.shape_data(series) elseif ispolar(series) theta, r = opt[:x], opt[:y] @@ -998,12 +956,13 @@ function pgfx_get_legend_pos(v::Tuple{<:Real,Symbol}) "north west" "north" "north east" ] I = legend_anchor_index(s) - rect, anchor = if v[2] === :inner + rect, anchor = if v[2] ≡ :inner (0.07, 0.5, 1.0, 0.07, 0.52, 1.0), anchors[I, I] else (-0.15, 0.5, 1.05, -0.15, 0.52, 1.1), anchors[4 - I, 4 - I] end - return "at" => string(legend_pos_from_angle(v[1], rect...)), "anchor" => anchor + return "at" => string(PlotsBase.legend_pos_from_angle(v[1], rect...)), + "anchor" => anchor end function pgfx_get_legend_style(sp) @@ -1060,9 +1019,9 @@ function pgfx_get_ticklabel_style(sp, axis) ) # aligning rotated tick labels to ticks if RecipesPipeline.is3d(sp) - if axis === sp[:xaxis] + if axis ≡ sp[:xaxis] push!(opt, "anchor" => axis[:rotation] < 60 ? "north east" : "east") - elseif axis === sp[:yaxis] + elseif axis ≡ sp[:yaxis] push!(opt, "anchor" => axis[:rotation] < 45 ? "north west" : "north east") else push!( @@ -1074,7 +1033,7 @@ function pgfx_get_ticklabel_style(sp, axis) end else if mod(axis[:rotation], 90) > 0 # 0 and ±90 already look good with the default anchor - push!(opt, "anchor" => axis === sp[:xaxis] ? "north east" : "south east") + push!(opt, "anchor" => axis ≡ sp[:xaxis] ? "north east" : "south east") end end return opt @@ -1104,11 +1063,11 @@ pgfx_arrow(::Nothing) = "every arrow/.append style={-}" function pgfx_arrow(arr::Arrow, side = arr.side) components = "" arrow_head = "{Stealth[length = $(arr.headlength)pt, width = $(arr.headwidth)pt" - arr.style === :open && (arrow_head *= ", open") + arr.style ≡ :open && (arrow_head *= ", open") arrow_head *= "]}" - (side === :both || side === :tail) && (components *= arrow_head) + (side ≡ :both || side ≡ :tail) && (components *= arrow_head) components *= "-" - (side === :both || side === :head) && (components *= arrow_head) + (side ≡ :both || side ≡ :head) && (components *= arrow_head) return "every arrow/.append style={$components}" end @@ -1124,7 +1083,7 @@ end pgfx_colormap(cl::PlotUtils.AbstractColorList) = pgfx_colormap(PlotUtils.color_list(cl)) pgfx_colormap(v::Vector{<:Colorant}) = join(map(c -> @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)), v), '\n') -pgfx_colormap(cg::ColorGradient) = join( +pgfx_colormap(cg::PlotUtils.ColorGradient) = join( map(1:length(cg)) do i @sprintf( "rgb(%.8f)=(%.8f,%.8f,%.8f)", @@ -1141,7 +1100,7 @@ pgfx_framestyle(style::Symbol) = if style in (:box, :axes, :origin, :zerolines, :grid, :none) style else - default_style = style === :semi ? :box : :axes + default_style = style ≡ :semi ? :box : :axes @warn "Framestyle :$style is not (yet) supported by the PGFPlotsX backend. :$default_style was chosen instead." default_style end @@ -1151,7 +1110,7 @@ pgfx_thickness_scaling(sp::Subplot) = pgfx_thickness_scaling(sp.plt) pgfx_thickness_scaling(series) = pgfx_thickness_scaling(series[:subplot]) function pgfx_fillstyle(plotattributes, i = 1) - if (a = get_fillalpha(plotattributes, i)) === nothing + if (a = get_fillalpha(plotattributes, i)) ≡ nothing a = alpha(single_color(get_fillcolor(plotattributes, i))) end Options("fill" => get_fillcolor(plotattributes, i), "fill opacity" => a) @@ -1167,7 +1126,7 @@ function pgfx_linestyle(linewidth::Real, color, α = 1, linestyle = :solid) ) end -pgfx_legend_col(s::Symbol) = s === :horizontal ? -1 : 1 +pgfx_legend_col(s::Symbol) = s ≡ :horizontal ? -1 : 1 pgfx_legend_col(n) = n function pgfx_linestyle(plotattributes, i = 1) @@ -1233,11 +1192,11 @@ function pgfx_marker(plotattributes, i = 1) pgfx_thickness_scaling(plotattributes) * 0.75 * _cycle(plotattributes[:markerstrokewidth], i), - "rotate" => if shape === :dtriangle + "rotate" => if shape ≡ :dtriangle 180 - elseif shape === :rtriangle + elseif shape ≡ :rtriangle 270 - elseif shape === :ltriangle + elseif shape ≡ :ltriangle 90 else 0 @@ -1290,7 +1249,7 @@ function pgfx_fillrange_series!(axis, series, series_func, i, fillrange, rng) opt[:x][rng], opt[:y][rng], opt[:z][rng] elseif ispolar(series) rad2deg.(opt[:x][rng]), opt[:y][rng] - elseif series[:seriestype] === :straightline + elseif series[:seriestype] ≡ :straightline PlotsBase.straightline_data(series) else opt[:x][rng], opt[:y][rng] @@ -1335,7 +1294,7 @@ function pgfx_sanitize_plot!(plt) end for subplot in plt.subplots for (key, value) in subplot.attr - if key === :annotations && subplot.attr[:annotations] !== nothing + if key ≡ :annotations && subplot.attr[:annotations] ≢ nothing old_ann = subplot.attr[key] for i in eachindex(old_ann) # [1:end-1] is a tuple of coordinates, [end] - text @@ -1355,8 +1314,8 @@ function pgfx_sanitize_plot!(plt) end for series in plt.series_list for (key, value) in series.plotattributes - if key === :series_annotations && - series.plotattributes[:series_annotations] !== nothing + if key ≡ :series_annotations && + series.plotattributes[:series_annotations] ≢ nothing old_ann = series.plotattributes[key].strs for i in eachindex(old_ann) series.plotattributes[key].strs[i] = pgfx_sanitize_string(old_ann[i]) @@ -1413,9 +1372,9 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) framestyle = pgfx_framestyle(sp[:framestyle] == false ? :none : sp[:framestyle]) # axis label position - labelpos = if letter === :x + labelpos = if letter ≡ :x pgfx_get_xguide_pos(axis[:guide_position]) - elseif letter === :y + elseif letter ≡ :y pgfx_get_yguide_pos(axis[:guide_position]) else "" @@ -1443,38 +1402,38 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) scale = axis[:scale] if (is_log_scale = scale in (:ln, :log2, :log10)) push!(opt, "$(letter)mode" => "log") - scale === :ln || push!(opt, "log basis $letter" => "$(scale === :log2 ? 2 : 10)") + scale ≡ :ln || push!(opt, "log basis $letter" => "$(scale ≡ :log2 ? 2 : 10)") end # ticks on or off - if axis[:ticks] in (nothing, false, :none) || framestyle === :none + if axis[:ticks] in (nothing, false, :none) || framestyle ≡ :none push!(opt, "$(letter)majorticks" => "false") elseif framestyle in (:grid, :zerolines) push!(opt, "$letter tick style" => Options("draw" => "none")) end # grid on or off - push!(opt, "$(letter)majorgrids" => string(axis[:grid] && framestyle !== :none)) + push!(opt, "$(letter)majorgrids" => string(axis[:grid] && framestyle ≢ :none)) # limits - lims = if ispolar(sp) && letter === :x + lims = if ispolar(sp) && letter ≡ :x rad2deg.(axis_limits(sp, :x)) else axis_limits(sp, letter) end push!(opt, "$(letter)min" => lims[1], "$(letter)max" => lims[2]) - if axis[:ticks] ∉ (nothing, false, :none, :native) && framestyle !== :none + if axis[:ticks] ∉ (nothing, false, :none, :native) && framestyle ≢ :none vals, labs = ticks = get_ticks(sp, axis, formatter = latex_formatter(axis[:formatter])) # pgfplot ignores ticks with angles below `90` when `xmin = 90`, so shift values - tick_values = if ispolar(sp) && letter === :x + tick_values = if ispolar(sp) && letter ≡ :x vcat(rad2deg.(vals[3:end]), 360, 405) else vals end tick_labels = if axis[:showaxis] - if is_log_scale && axis[:ticks] === :auto + if is_log_scale && axis[:ticks] ≡ :auto labels = wrap_power_labels(labs) if (lab = first(labels)) isa LaTeXString || pgfx_is_inline_math(lab) join(labels, ',') @@ -1482,7 +1441,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) "\\(" * join(labels, "\\),\\(") * "\\)" end else - labels = if ispolar(sp) && letter === :x + labels = if ispolar(sp) && letter ≡ :x vcat(labs[3:end], "0", "45") else labs @@ -1496,7 +1455,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) opt, "$(letter)ticklabels" => curly(tick_labels), "$(letter)tick" => curly(join(tick_values, ',')), - if (tick_dir = axis[:tick_direction]) === :none || axis[:showaxis] === false + if (tick_dir = axis[:tick_direction]) ≡ :none || axis[:showaxis] ≡ false "$(letter)tick style" => "draw=none" else "$(letter)tick align" => "$(tick_dir)side" @@ -1516,8 +1475,8 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) # Hence, we hack around with extra ticks. # Unfortunately this conflicts with `:zerolines` framestyle hack. # So minor ticks are not working with `:zerolines`. - if (minor_ticks = get_minor_ticks(sp, axis, ticks)) !== nothing - if ispolar(sp) && letter === :x + if (minor_ticks = get_minor_ticks(sp, axis, ticks)) ≢ nothing + if ispolar(sp) && letter ≡ :x minor_ticks = vcat(rad2deg.(minor_ticks[3:end]), 360, 405) end push!( @@ -1547,7 +1506,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) push!( opt, # the * after line disables the arrow at the axis "axis $letter line$(axis[:draw_arrow] ? "" : "*")" => - (axis[:mirror] ? "right" : framestyle === :axes ? "left" : "middle"), + (axis[:mirror] ? "right" : framestyle ≡ :axes ? "left" : "middle"), ) end @@ -1556,7 +1515,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter) push!(opt, "$(letter)ticklabel pos" => (axis[:mirror] ? "right" : "left")) end - if framestyle === :zerolines + if framestyle ≡ :zerolines gs = pgfx_linestyle(pgfx_thickness_scaling(sp), axis[:foreground_color_border], 1) push!( opt, @@ -1636,4 +1595,4 @@ function PlotsBase._display(plt::Plot{PGFPlotsXBackend}) display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot) end -end # module +end # module diff --git a/PlotsBase/ext/PlotlyJSExt.jl b/PlotsBase/ext/PlotlyJSExt.jl index 27f06a4c1..3b3acea36 100644 --- a/PlotsBase/ext/PlotlyJSExt.jl +++ b/PlotsBase/ext/PlotlyJSExt.jl @@ -1,33 +1,14 @@ module PlotlyJSExt -import PlotsBase: PlotsBase, Plot, isijulia -using PlotsBase.PlotsPlots +import PlotsBase: PlotsBase, Plot using PlotsBase.Commons using PlotsBase.Plotly +using PlotsBase.Plots import PlotlyJS: PlotlyJS, WebIO -# unrolling the old # init_backend macro by hand case by case -# this is not a macro for the backend maintainers and explicit control -const package_str = "PlotlyJS" -const str = lowercase(package_str) -const sym = Symbol(str) - struct PlotlyJSBackend <: PlotsBase.AbstractBackend end -const T = PlotlyJSBackend - -get_concrete_backend() = T # opposite to abstract - -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) -end - -PlotsBase.backend_name(::T) = sym -PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static PlotlyJSBackend plotlyjs const _plotlyjs_attrs = PlotsBase.Plotly._plotly_attrs const _plotlyjs_seriestypes = PlotsBase.Plotly._plotly_seriestypes @@ -35,31 +16,6 @@ const _plotlyjs_styles = PlotsBase.Plotly._plotly_styles const _plotlyjs_markers = PlotsBase.Plotly._plotly_markers const _plotlyjs_scales = PlotsBase.Plotly._plotly_scales -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::T, $s::Symbol) = $s in $v - PlotsBase.$f2(::T) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- -# https://github.com/JuliaPlots/PlotlyJS.jl - -# ------------------------------------------------------------------------------ - function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) plt[:overwrite_figure] && PlotsBase.closeall() plt.o = PlotlyJS.plot() @@ -112,4 +68,10 @@ PlotsBase.closeall(::PlotlyJSBackend) = Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyJSBackend}) = true -end # module +function PlotsBase._ijulia__extra_mime_info!(plt::Plot{PlotlyJSBackend}, out::Dict) + out["application/vnd.plotly.v1+json"] = + Dict(:data => plotly_series(plt), :layout => plotly_layout(plt)) + out +end + +end # module diff --git a/PlotsBase/ext/PlotlyKaleidoExt.jl b/PlotsBase/ext/PlotlyKaleidoExt.jl index bff882427..1a86add86 100644 --- a/PlotsBase/ext/PlotlyKaleidoExt.jl +++ b/PlotsBase/ext/PlotlyKaleidoExt.jl @@ -27,4 +27,4 @@ for (mime, fmt) in ( ) end -end # module +end # module diff --git a/PlotsBase/ext/PythonPlotExt.jl b/PlotsBase/ext/PythonPlotExt.jl index 5b37e0bd6..90a5298b2 100644 --- a/PlotsBase/ext/PythonPlotExt.jl +++ b/PlotsBase/ext/PythonPlotExt.jl @@ -4,57 +4,65 @@ import RecipesPipeline import PythonPlot import NaNMath +const PythonCall = PythonPlot.PythonCall +const pyisnone = + isdefined(PythonCall, :pyisnone) ? PythonCall.pyisnone : PythonCall.Core.pyisnone + +const mpl_toolkits = PythonCall.pynew() +const numpy = PythonCall.pynew() +const mpl = PythonCall.pynew() + +using PlotUtils + import PlotsBase -import PlotsBase.PlotUtils: PlotUtils, ColorGradient, plot_color, color_list, cgrad -import PlotsBase.Commons: Commons, single_color -using PlotsBase.PlotMeasures using PlotsBase.Annotations -using PlotsBase.PlotsSeries -using PlotsBase.PlotsPlots +using PlotsBase.DataSeries using PlotsBase.Colorbars +using PlotsBase.Surfaces using PlotsBase.Subplots using PlotsBase.Commons using PlotsBase.Colors using PlotsBase.Arrows using PlotsBase.Shapes +using PlotsBase.Plots using PlotsBase.Fonts using PlotsBase.Ticks using PlotsBase.Axes -const package_str = "PythonPlot" -const str = lowercase(package_str) -const sym = Symbol(str) - struct PythonPlotBackend <: PlotsBase.AbstractBackend end -const T = PythonPlotBackend - -get_concrete_backend() = T - -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) +function PlotsBase.extension_init(::PythonPlotBackend) if PythonPlot.version < v"3.4" @warn """You are using Matplotlib $(PythonPlot.version), which is no longer officially supported by the Plots community. To ensure smooth PlotsBase.jl integration update your Matplotlib library to a version ≥ 3.4.0 """ end - PythonCall.pycopy!(mpl, PythonCall.pyimport("matplotlib")) PythonCall.pycopy!(mpl_toolkits, PythonCall.pyimport("mpl_toolkits")) PythonCall.pycopy!(numpy, PythonCall.pyimport("numpy")) PythonCall.pyimport("mpl_toolkits.axes_grid1") numpy.seterr(invalid = "ignore") - PythonPlot.ioff() # we don't want every command to update the figure + PythonPlot.ioff() # we don't want every command to update the figure + + # WARNING: matplotlib uses a reverse convention: `labeltop` instead of `toplabel` + for keyword in (:linthresh, :base, :label) + Commons.new_attr_dict!(keyword) + for letter in (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) + Commons.set_attr_symbol!(keyword, string(letter)) + end + end + + # problem: github.com/tbreloff/Plots.jl/issues/308 + # solution: hack from @stevengj: github.com/JuliaPy/PyPlot.jl/pull/223#issuecomment-229747768 + let otherdisplays = + splice!(Base.Multimedia.displays, 2:length(Base.Multimedia.displays)) + append!(Base.Multimedia.displays, otherdisplays) + end end -PlotsBase.backend_name(::T) = sym -PlotsBase.backend_package_name(::T) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static PythonPlotBackend pythonplot const _pythonplot_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -190,59 +198,12 @@ const _pythonplot_styles = [:auto, :solid, :dash, :dot, :dashdot] const _pythonplot_markers = vcat(Commons._all_markers, :pixel) const _pythonplot_scales = [:identity, :ln, :log2, :log10] -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::T, $s::Symbol) = $s in $v - PlotsBase.$f2(::T) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- - # github.com/stevengj/PythonPlot.jl -const PythonCall = PythonPlot.PythonCall -const mpl = PythonCall.pynew() # PythonCall.pyimport("matplotlib") -const mpl_toolkits = PythonCall.pynew() # PythonCall.pyimport("mpl_toolkits") -const numpy = PythonCall.pynew() # PythonCall.pyimport("numpy") - -const pyisnone = if isdefined(PythonCall, :pyisnone) - PythonCall.pyisnone -else - PythonCall.Core.pyisnone -end - PlotsBase.is_marker_supported(::PythonPlotBackend, shape::Shape) = true -# problem: github.com/tbreloff/Plots.jl/issues/308 -# solution: hack from @stevengj: github.com/JuliaPy/PyPlot.jl/pull/223#issuecomment-229747768 -let otherdisplays = splice!(Base.Multimedia.displays, 2:length(Base.Multimedia.displays)) - append!(Base.Multimedia.displays, otherdisplays) -end - -for k in (:linthresh, :base, :label) - # add PythonPlot specific symbols to cache - Commons._attrsymbolcache[k] = Dict{Symbol,Symbol}() - for letter in (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) - Commons._attrsymbolcache[k][letter] = Symbol(k, letter) - end -end - _py_handle_surface(v) = v -_py_handle_surface(z::PlotsBase.Surface) = z.surf +_py_handle_surface(z::Surface) = z.surf _py_color(s) = _py_color(parse(Colorant, string(s))) _py_color(c::Colorant) = [red(c), green(c), blue(c), alpha(c)] # NOTE: returning a tuple fails `PythonPlot` @@ -274,11 +235,11 @@ _py_shading(c, z) = mpl.colors.LightSource(270, 45).shade( # get the style (solid, dashed, etc) function _py_linestyle(seriestype::Symbol, linestyle::Symbol) - seriestype === :none && return " " - linestyle === :solid && return "-" - linestyle === :dash && return "--" - linestyle === :dot && return ":" - linestyle === :dashdot && return "-." + seriestype ≡ :none && return " " + linestyle ≡ :solid && return "-" + linestyle ≡ :dash && return "--" + linestyle ≡ :dot && return ":" + linestyle ≡ :dashdot && return "-." @warn "Unknown linestyle $linestyle" "-" end @@ -297,23 +258,24 @@ end # get the marker shape function _py_marker(marker::Symbol) - marker === :none && return " " - marker === :circle && return "o" - marker === :rect && return "s" - marker === :diamond && return "D" - marker === :utriangle && return "^" - marker === :dtriangle && return "v" - marker === :+ && return "+" - marker === :x && return "x" - marker === :star5 && return "*" - marker === :pentagon && return "p" - marker === :hexagon && return "h" - marker === :octagon && return "8" - marker === :pixel && return "," - marker === :hline && return "_" - marker === :vline && return "|" - haskey(_shapes, marker) && return _py_marker(_shapes[marker]) - + marker ≡ :none && return " " + marker ≡ :circle && return "o" + marker ≡ :rect && return "s" + marker ≡ :diamond && return "D" + marker ≡ :utriangle && return "^" + marker ≡ :dtriangle && return "v" + marker ≡ :+ && return "+" + marker ≡ :x && return "x" + marker ≡ :star5 && return "*" + marker ≡ :pentagon && return "p" + marker ≡ :hexagon && return "h" + marker ≡ :octagon && return "8" + marker ≡ :pixel && return "," + marker ≡ :hline && return "_" + marker ≡ :vline && return "|" + let _shapes = Shapes._shapes + haskey(_shapes, marker) && return _py_marker(_shapes[marker]) + end @warn "Unknown marker $marker" "o" end @@ -331,16 +293,16 @@ function _py_marker(marker::AbstractString) end function _py_stepstyle(seriestype::Symbol) - seriestype === :steppost && return "steps-post" - seriestype === :stepmid && return "steps-mid" - seriestype === :steppre && return "steps-pre" + seriestype ≡ :steppost && return "steps-post" + seriestype ≡ :stepmid && return "steps-mid" + seriestype ≡ :steppre && return "steps-pre" "default" end function _py_fillstepstyle(seriestype::Symbol) - seriestype === :steppost && return "post" - seriestype === :stepmid && return "mid" - seriestype === :steppre && return "pre" + seriestype ≡ :steppost && return "post" + seriestype ≡ :stepmid && return "mid" + seriestype ≡ :steppre && return "pre" nothing end @@ -367,17 +329,17 @@ get_locator_and_formatter(vals::AVec) = mpl.ticker.FixedLocator(eachindex(vals)), mpl.ticker.FixedFormatter(vals) labelfunc(scale::Symbol, backend::PythonPlotBackend) = - PythonPlot.LaTeXStrings.latexstring ∘ PlotsBase.labelfunc_tex(scale) + PythonPlot.LaTeXStrings.latexstring ∘ labelfunc_tex(scale) _py_mask_nans(z) = PythonPlot.pycall(numpy.ma.masked_invalid, z) # --------------------------------------------------------------------------- function fix_xy_lengths!(plt::Plot{PythonPlotBackend}, series::Series) - if (x = series[:x]) !== nothing + if (x = series[:x]) ≢ nothing y = series[:y] nx, ny = length(x), length(y) - if !(get(series.plotattributes, :z, nothing) isa PlotsBase.Surface || nx == ny) + if !(get(series.plotattributes, :z, nothing) isa Surface || nx == ny) if nx < ny series[:x] = map(i -> Float64(x[mod1(i, nx)]), 1:ny) else @@ -426,11 +388,11 @@ _py_renderer(fig) = _py_canvas(fig).get_renderer() _py_drawfig(fig) = fig.draw(_py_renderer(fig)) # `get_points` returns a numpy array in the form [x0 y0; x1 y1] coords (origin is bottom-left (0, 0)!) -_py_extents(obj) = PythonCall.PyArray(obj.get_window_extent().get_points()) +_py_extents(obj) = PythonPlot.PyArray(obj.get_window_extent().get_points()) # see cjdoris.github.io/PythonCall.jl/stable/conversion-to-julia/#py2jl-conversion -to_vec(x) = PythonCall.pyconvert(Vector, x) -to_str(x) = PythonCall.pyconvert(String, x) +to_vec(x) = PythonPlot.pyconvert(Vector, x) +to_str(x) = PythonPlot.pyconvert(String, x) # compute a bounding box (with origin top-left), however PythonPlot gives coords with origin bottom-left function _py_bbox(obj) @@ -438,12 +400,12 @@ function _py_bbox(obj) fl, fr, fb, ft = bb = _py_extents(obj.get_figure()) l, r, b, t = ex = _py_extents(obj) # @show obj bb ex - x0, y0, width, height = l * px, (ft - t) * px, (r - l) * px, (t - b) * px - # @show width height - PlotsBase.BoundingBox(x0, y0, width, height) + x0, y0, w, h = l * px, (ft - t) * px, (r - l) * px, (t - b) * px + # @show w h + BoundingBox(x0, y0, w, h) end -_py_bbox(::Nothing) = PlotsBase.BoundingBox(0mm, 0mm) +_py_bbox(::Nothing) = BoundingBox(0mm, 0mm) # get the bounding box of the union of the objects function _py_bbox(v::AVec) @@ -493,7 +455,7 @@ _py_thickness_scale(plt::Plot{PythonPlotBackend}, ptsz) = ptsz * plt[:thickness_ # Create the window/figure for this backend. function PlotsBase._create_backend_figure(plt::Plot{PythonPlotBackend}) - w, h = map(s -> PlotMeasures.px2inch(s * plt[:dpi] / PlotsBase.DPI), plt[:size]) + w, h = map(s -> Commons.px2inch(s * plt[:dpi] / DPI), plt[:size]) # reuse the current figure? plt[:overwrite_figure] ? PythonPlot.gcf() : PythonPlot.figure() end @@ -541,9 +503,9 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # ax = getAxis(plt, series) x, y, z = (_py_handle_surface(series[letter]) for letter in (:x, :y, :z)) - if st === :straightline + if st ≡ :straightline x, y = PlotsBase.straightline_data(series) - elseif st === :shape + elseif st ≡ :shape x, y = PlotsBase.shape_data(series) end @@ -562,7 +524,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) vmin, vmax = clims = get_clims(sp, series) # Dict to store extra kwargs - extrakw = if st === :wireframe || st === :hexbin + extrakw = if st ≡ :wireframe || st ≡ :hexbin # vmin, vmax cause an error for wireframe plot # We are not supporting clims for hexbin as calculation of bins is not trivial KW() @@ -576,8 +538,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # pass in an integer value as an arg, but a levels list as a keyword arg levels = series[:levels] - levelargs = PlotsBase.isscalar(levels) ? levels : () - PlotsBase.isvector(levels) && (extrakw[:levels] = levels) + levelargs = isscalar(levels) ? levels : () + isvector(levels) && (extrakw[:levels] = levels) # add custom frame shapes to markershape? series_annotations_shapes!(series, :xy) @@ -612,7 +574,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) ) |> push_h end - if (a = series[:arrow]) !== nothing && !RecipesPipeline.is3d(st) # TODO: handle 3d later + if (a = series[:arrow]) ≢ nothing && !RecipesPipeline.is3d(st) # TODO: handle 3d later if typeof(a) != Arrow @warn "Unexpected type for arrow: $(typeof(a))" else @@ -639,18 +601,16 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end # add markers ? - if series[:markershape] !== :none && st ∈ _py_marker_series + if series[:markershape] ≢ :none && st ∈ _py_marker_series for segment in series_segments(series, :scatter) i, rng = segment.attr_index, segment.range - args = if st === :bar - x[rng], y[rng] - end + args = x[rng], y[rng] RecipesPipeline.is3d(sp) && (args = (args..., z[rng])) ax.scatter( args...; zorder = zorder + 0.5, - marker = _py_marker(PlotsBase._cycle(series[:markershape], i)), - s = _py_thickness_scale(plt, PlotsBase._cycle(series[:markersize], i)) .^ 2, + marker = _py_marker(_cycle(series[:markershape], i)), + s = _py_thickness_scale(plt, _cycle(series[:markersize], i)) .^ 2, facecolors = _py_color( get_markercolor(series, i, cbar_scale), get_markeralpha(series, i), @@ -666,7 +626,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end end - if st === :shape + if st ≡ :shape for segment in series_segments(series) i, rng = segment.attr_index, segment.range if length(rng) > 1 @@ -713,7 +673,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end end end - elseif st === :image + elseif st ≡ :image x, y = series[:x], series[:y] xmin, xmax = ignorenan_extrema(x) ymin, ymax = ignorenan_extrema(y) @@ -729,7 +689,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) else z # hopefully it's in a data format that will "just work" with imshow end - aspect = if get_aspect_ratio(sp) === :equal + aspect = if get_aspect_ratio(sp) ≡ :equal "equal" else "auto" @@ -743,7 +703,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) zorder, aspect, ) |> push_h - elseif st === :heatmap + elseif st ≡ :heatmap x, y = PlotsBase.heatmap_edges(x, xaxis[:scale], y, yaxis[:scale], size(z)) expand_extrema!(xaxis, x) expand_extrema!(yaxis, y) @@ -758,7 +718,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) label, extrakw..., ) |> push_h - elseif st === :mesh3d + elseif st ≡ :mesh3d cns = series[:connections] polygons = if cns isa AbstractVector{<:AbstractVector{<:Integer}} # Combination of any polygon types @@ -798,7 +758,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) ) |> ax.add_collection3d |> push_h - elseif st === :hexbin + elseif st ≡ :hexbin sekw = series[:extra_kwargs] extrakw[:mincnt] = get(sekw, :mincnt, nothing) extrakw[:edgecolors] = get(sekw, :edgecolors, edgecolor) @@ -806,7 +766,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) x, y; C = series[:weights], - gridsize = series[:bins] === :auto ? 100 : series[:bins], # 100 is the default value + gridsize = series[:bins] ≡ :auto ? 100 : series[:bins], # 100 is the default value cmap = _py_fillcolormap(series), # applies to the pcolorfast object linewidths, zorder, @@ -815,7 +775,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) extrakw..., ) |> push_h elseif st ∈ (:contour, :contour3d) - if st === :contour3d + if st ≡ :contour3d extrakw[:extend3d] = true if !ismatrix(x) || !ismatrix(y) x, y = repeat(x', length(y), 1), repeat(y, 1, length(x)) @@ -838,10 +798,10 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) extrakw..., ) ) |> push_h - series[:contour_labels] === true && ax.clabel(handle, handle.levels) + series[:contour_labels] ≡ true && ax.clabel(handle, handle.levels) # contour fills - series[:fillrange] !== nothing && + series[:fillrange] ≢ nothing && ax.contourf( x, y, @@ -857,8 +817,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) if !ismatrix(x) || !ismatrix(y) x, y = repeat(x', length(y), 1), repeat(y, 1, length(x)) end - if st === :surface - if series[:fill_z] !== nothing + if st ≡ :surface + if series[:fill_z] ≢ nothing # the surface colors are different than z-value extrakw[:facecolors] = _py_shading(series[:fillcolor], _py_handle_surface(series[:fill_z])) @@ -918,15 +878,15 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # handleSmooth(plt, ax, series, series[:smooth]) # handle area filling - if (fillrange = series[:fillrange]) !== nothing && st !== :contour + if (fillrange = series[:fillrange]) ≢ nothing && st ≢ :contour for segment in series_segments(series) i, rng = segment.attr_index, segment.range f, dim1, dim2 = :fill_between, x[rng], y[rng] n = length(dim1) args = if typeof(fillrange) <: Union{Real,AVec} - dim1, PlotsBase._cycle(fillrange, rng), dim2 - elseif PlotsBase.is_2tuple(fillrange) - dim1, PlotsBase._cycle(fillrange[1], rng), PlotsBase._cycle(fillrange[2], rng) + dim1, _cycle(fillrange, rng), dim2 + elseif is_2tuple(fillrange) + dim1, _cycle(fillrange[1], rng), _cycle(fillrange[2], rng) end la = get_linealpha(series, i) @@ -965,9 +925,9 @@ _py_set_lims(ax, sp::Subplot, axis::Axis) = end function _py_set_ticks(sp, ax, ticks, letter) - ticks === :auto && return + ticks ≡ :auto && return axis = getproperty(ax, get_attr_symbol(letter, :axis)) - if ticks === :none || ticks === nothing || ticks == false + if ticks ≡ :none || ticks ≡ nothing || ticks == false kw = KW() for dir in (:top, :bottom, :left, :right) kw[dir] = kw[get_attr_symbol(:label, dir)] = false @@ -976,9 +936,9 @@ function _py_set_ticks(sp, ax, ticks, letter) return end - tick_values, tick_labels = if (ttype = PlotsBase.ticks_type(ticks)) === :ticks + tick_values, tick_labels = if (ttype = ticks_type(ticks)) ≡ :ticks ticks, [] - elseif ttype === :ticks_and_labels + elseif ttype ≡ :ticks_and_labels ticks else error("Invalid input for $(letter)ticks: $ticks") @@ -1004,12 +964,12 @@ end function _py_set_scale(ax, sp::Subplot, scale::Symbol, letter::Symbol) scale ∈ PlotsBase.supported_scales() || return @warn "Unhandled scale value in PythonPlot: $scale" - scl, kw = if scale === :identity + scl, kw = if scale ≡ :identity "linear", KW() else "symlog", KW( - get_attr_symbol(:base, Symbol()) => _log_scale_bases[scale], + get_attr_symbol(:base, Symbol()) => Commons._log_scale_bases[scale], get_attr_symbol(:linthresh, Symbol()) => NaNMath.max( 1e-16, _py_compute_axis_minval(sp, sp[get_attr_symbol(letter, :axis)]), @@ -1029,8 +989,8 @@ _py_set_spine_color(spines::Dict, color) = function _py_set_axis_colors(sp, ax, a::Axis) _py_set_spine_color(ax.spines, _py_color(a[:foreground_color_border])) - axissym = get_attr_symbol(a[:letter], :axis) - if hasproperty(ax, axissym) + axis_sym = get_attr_symbol(a[:letter], :axis) + if hasproperty(ax, axis_sym) tickcolor = sp[:framestyle] ∈ (:zerolines, :grid) ? _py_color(plot_color(a[:foreground_color_grid], a[:gridalpha])) : @@ -1041,7 +1001,7 @@ function _py_set_axis_colors(sp, ax, a::Axis) colors = tickcolor, labelcolor = _py_color(a[:tickfontcolor]), ) - getproperty(ax, axissym).label.set_color(_py_color(a[:guidefontcolor])) + getproperty(ax, axis_sym).label.set_color(_py_color(a[:guidefontcolor])) end end @@ -1054,7 +1014,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) w, h = plt[:size] fig = plt.o fig.clear() - fig.set_size_inches(w / PlotsBase.DPI, h / PlotsBase.DPI, forward = true) + fig.set_size_inches(w / DPI, h / DPI, forward = true) fig.set_facecolor(_py_color(plt[:background_color_outside])) fig.set_dpi(plt[:dpi]) @@ -1069,7 +1029,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) # update subplots for sp in plt.subplots - (ax = sp.o) === nothing && continue + (ax = sp.o) ≡ nothing && continue xaxis, yaxis = sp[:xaxis], sp[:yaxis] # add the annotations @@ -1104,7 +1064,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) kw = KW() handle = if !isempty(sp[:zaxis][:discrete_values]) && - cbar_series[:seriestype] === :heatmap + cbar_series[:seriestype] ≡ :heatmap kw[:ticks], kw[:format] = get_locator_and_formatter(sp[:zaxis][:discrete_values]) # kw[:values] = eachindex(sp[:zaxis][:discrete_values]) @@ -1112,20 +1072,20 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) kw[:boundaries] = vcat(0, kw[:values] + 0.5) cbar_series[:serieshandle][end] elseif any( - cbar_series[attr] !== nothing for attr in (:line_z, :fill_z, :marker_z) + cbar_series[attr] ≢ nothing for attr in (:line_z, :fill_z, :marker_z) ) cmin, cmax = get_clims(sp) - norm = if cbar_scale === :identity + norm = if cbar_scale ≡ :identity mpl.colors.Normalize(vmin = cmin, vmax = cmax) else mpl.colors.LogNorm(vmin = cmin, vmax = cmax) end cmap = nothing for func in (_py_linecolormap, _py_fillcolormap, _py_markercolormap) - (cmap = func(cbar_series)) === nothing || break + (cmap = func(cbar_series)) ≡ nothing || break end c_map = mpl.cm.ScalarMappable(; cmap, norm) - c_map.set_array(PythonCall.pylist([])) + c_map.set_array(PythonPlot.pylist([])) c_map else cbar_series[:serieshandle][end] @@ -1140,22 +1100,22 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) else # divider approach works only with 2d plots divider = mpl_toolkits.axes_grid1.make_axes_locatable(ax) - pos, pad, orientation = if cb_sym === :left + pos, pad, orientation = if cb_sym ≡ :left cb_sym, "5%", "vertical" - elseif cb_sym === :top + elseif cb_sym ≡ :top cb_sym, "2.5%", "horizontal" - elseif cb_sym === :bottom + elseif cb_sym ≡ :bottom cb_sym, "5%", "horizontal" else # :right or :best :right, "2.5%", "vertical" end # Reasonable value works most of the usecases cax = divider.append_axes(string(pos); size = "5%", label, pad) - if cb_sym === :left + if cb_sym ≡ :left cax.yaxis.set_ticks_position("left") - elseif cb_sym === :right + elseif cb_sym ≡ :right cax.yaxis.set_ticks_position("right") - elseif cb_sym === :top + elseif cb_sym ≡ :top cax.xaxis.set_ticks_position("top") else # :bottom or :best cax.xaxis.set_ticks_position("bottom") @@ -1172,7 +1132,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) ) # cbar.formatter.set_useOffset(false) # this for some reason does not work, must be a pyplot bug, instead this is a workaround: - cbar_scale === :identity && cbar.formatter.set_powerlimits((-Inf, Inf)) + cbar_scale ≡ :identity && cbar.formatter.set_powerlimits((-Inf, Inf)) cbar.update_ticks() ticks = get_colorbar_ticks(sp) @@ -1182,8 +1142,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) yaxis, cbar.ax.yaxis, :y # colorbar inherits from y axis end _py_set_scale(cbar.ax, sp, sp[:colorbar_scale], ticks_letter) - sp[:colorbar_ticks] === :native || - _py_set_ticks(sp, cbar.ax, ticks, ticks_letter) + sp[:colorbar_ticks] ≡ :native || _py_set_ticks(sp, cbar.ax, ticks, ticks_letter) for lab in cbar_axis.get_ticklabels() lab.set_fontsize(_py_thickness_scale(plt, sp[:colorbar_tickfontsize])) @@ -1197,9 +1156,9 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) # Adjust thickness of the cbar ticks intensity = 0.5 cbar_axis.set_tick_params( - direction = axis[:tick_direction] === :out ? "out" : "in", + direction = axis[:tick_direction] ≡ :out ? "out" : "in", width = _py_thickness_scale(plt, intensity), - length = axis[:tick_direction] === :none ? 0 : + length = axis[:tick_direction] ≡ :none ? 0 : 5_py_thickness_scale(plt, intensity), ) @@ -1217,7 +1176,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) end # Then set visible some of them - if framestyle === :semi + if framestyle ≡ :semi intensity = 0.5 pyspine = getproperty(ax.spines, yaxis[:mirror] ? "left" : "right") @@ -1227,7 +1186,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) pyspine = getproperty(ax.spines, xaxis[:mirror] ? "bottom" : "top") pyspine.set_linewidth(_py_thickness_scale(plt, intensity)) pyspine.set_alpha(intensity) - elseif framestyle === :box + elseif framestyle ≡ :box ax.tick_params(top = true) # Add ticks too ax.tick_params(right = true) # Add ticks too elseif framestyle ∈ (:axes, :origin) @@ -1235,13 +1194,13 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) (xaxis[:mirror] ? "bottom" : "top", yaxis[:mirror] ? "left" : "right") getproperty(ax.spines, loc).set_visible(false) end - if framestyle === :origin + if framestyle ≡ :origin ax.spines.bottom.set_position("zero") ax.spines.left.set_position("zero") end elseif framestyle ∈ (:grid, :none, :zerolines) _py_hide_spines(ax) - if framestyle === :zerolines + if framestyle ≡ :zerolines ax.axhline( y = 0, color = _py_color(xaxis[:foreground_color_axis]), @@ -1257,37 +1216,37 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) if xaxis[:mirror] ax.xaxis.set_label_position("top") # the guides - framestyle === :box || ax.xaxis.tick_top() + framestyle ≡ :box || ax.xaxis.tick_top() end if yaxis[:mirror] ax.yaxis.set_label_position("right") # the guides - framestyle === :box || ax.yaxis.tick_right() + framestyle ≡ :box || ax.yaxis.tick_right() end end # axis attributes for letter in (:x, :y, :z) - axissym = get_attr_symbol(letter, :axis) - hasproperty(ax, axissym) || continue - axis = sp[axissym] - pyaxis = getproperty(ax, axissym) + axis_sym = get_attr_symbol(letter, :axis) + hasproperty(ax, axis_sym) || continue + axis = sp[axis_sym] + pyaxis = getproperty(ax, axis_sym) - if axis[:guide_position] !== :auto && letter !== :z + if axis[:guide_position] ≢ :auto && letter ≢ :z pyaxis.set_label_position(string(axis[:guide_position])) end _py_set_scale(ax, sp, axis) _py_set_lims(ax, sp, axis) - (ispolar(sp) && letter === :y) && ax.set_rlabel_position(90) - ticks = framestyle === :none ? nothing : get_ticks(sp, axis) + (ispolar(sp) && letter ≡ :y) && ax.set_rlabel_position(90) + ticks = framestyle ≡ :none ? nothing : get_ticks(sp, axis) - has_major_ticks = ticks !== :none && ticks !== nothing && ticks !== false - has_major_ticks &= if (ttype = PlotsBase.ticks_type(ticks)) === :ticks + has_major_ticks = ticks ≢ :none && ticks ≢ nothing && ticks ≢ false + has_major_ticks &= if (ttype = ticks_type(ticks)) ≡ :ticks length(ticks) > 0 - elseif ttype === :ticks_and_labels + elseif ttype ≡ :ticks_and_labels tcs, labs = ticks - if framestyle === :origin + if framestyle ≡ :origin # don't show the 0 tick label for the origin framestyle labs[tcs .== 0] .= "" end @@ -1300,7 +1259,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) intensity = 0.5 # this value corresponds to scaling of other grid elements length_factor = 6 # arbitrary factor (closest to mpl examples) - if axis[:ticks] === :native # it is easier to reset than to account for this + if axis[:ticks] ≡ :native # it is easier to reset than to account for this _py_set_lims(ax, sp, axis) pyaxis.set_major_locator(mpl.ticker.AutoLocator()) pyaxis.set_major_formatter(mpl.ticker.ScalarFormatter()) @@ -1322,9 +1281,9 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) _py_set_ticks(sp, ax, ticks, letter) pyaxis.set_tick_params( - direction = axis[:tick_direction] === :out ? "out" : "in", + direction = axis[:tick_direction] ≡ :out ? "out" : "in", width = _py_thickness_scale(plt, intensity), - length = axis[:tick_direction] === :none ? 0 : + length = axis[:tick_direction] ≡ :none ? 0 : length_factor * _py_thickness_scale(plt, intensity), ) else @@ -1341,7 +1300,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) RecipesPipeline.is3d(sp) && pyaxis.set_rotate_label(false) axis[:flip] && getproperty(ax, Symbol(:invert_, letter, :axis))() - axis[:guidefontrotation] + if letter === :y && !RecipesPipeline.is3d(sp) + axis[:guidefontrotation] + if letter ≡ :y && !RecipesPipeline.is3d(sp) 90 else 0 @@ -1364,18 +1323,18 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) if !no_minor_intervals(axis) && has_major_ticks ax.minorticks_on() n_minor_intervals = num_minor_intervals(axis) - if (scale = axis[:scale]) === :identity + if (scale = axis[:scale]) ≡ :identity mpl.ticker.AutoMinorLocator(n_minor_intervals) else mpl.ticker.LogLocator( - base = _log_scale_bases[scale], + base = Commons._log_scale_bases[scale], subs = 1:n_minor_intervals, ) end |> pyaxis.set_minor_locator pyaxis.set_tick_params( which = "minor", - direction = axis[:tick_direction] === :out ? "out" : "in", - length = axis[:tick_direction] === :none ? 0 : + direction = axis[:tick_direction] ≡ :out ? "out" : "in", + length = axis[:tick_direction] ≡ :none ? 0 : 0.5length_factor * _py_thickness_scale(plt, intensity), ) end @@ -1412,11 +1371,11 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) end # aspect ratio - if (ratio = get_aspect_ratio(sp)) !== :none + if (ratio = get_aspect_ratio(sp)) ≢ :none if RecipesPipeline.is3d(sp) - if ratio === :auto + if ratio ≡ :auto nothing - elseif ratio === :equal + elseif ratio ≡ :equal ax.set_box_aspect((1, 1, 1)) else ax.set_box_aspect(ratio) @@ -1452,18 +1411,17 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) end expand_padding!(padding, bb, plotbb) = - if PlotsBase.ispositive(PlotsBase.width(bb)) && - PlotsBase.ispositive(PlotsBase.height(bb)) - padding[1] = max(padding[1], PlotsBase.left(plotbb) - PlotsBase.left(bb)) - padding[2] = max(padding[2], PlotsBase.top(plotbb) - PlotsBase.top(bb)) - padding[3] = max(padding[3], PlotsBase.right(bb) - PlotsBase.right(plotbb)) - padding[4] = max(padding[4], PlotsBase.bottom(bb) - PlotsBase.bottom(plotbb)) + if ispositive(width(bb)) && ispositive(height(bb)) + padding[1] = max(padding[1], left(plotbb) - left(bb)) + padding[2] = max(padding[2], top(plotbb) - top(bb)) + padding[3] = max(padding[3], right(bb) - right(plotbb)) + padding[4] = max(padding[4], bottom(bb) - bottom(plotbb)) end -# Set the (left, top, right, bottom) minimum padding around the plot area +# set the (left, top, right, bottom) minimum padding around the plot area # to fit ticks, tick labels, guides, colorbars, etc. function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) - (ax = sp.o) === nothing && return sp.minpad + (ax = sp.o) ≡ nothing && return sp.minpad plotbb = _py_bbox(ax) # TODO: this should initialize to the margin from sp.attr @@ -1500,7 +1458,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) # add ∈ the user-specified margin padding .+= [sp[:left_margin], sp[:top_margin], sp[:right_margin], sp[:bottom_margin]] - sp.minpad = Tuple((PlotsBase.DPI / sp.plt[:dpi]) .* padding) + sp.minpad = Tuple((DPI / sp.plt[:dpi]) .* padding) end # ----------------------------------------------------------------- @@ -1512,8 +1470,8 @@ _py_add_annotations(sp::Subplot{PythonPlotBackend}, x, y, val::PlotText) = sp.o. val.str, xy = (x, y), size = _py_thickness_scale(sp.plt, val.font.pointsize), - horizontalalignment = val.font.halign === :hcenter ? "center" : string(val.font.halign), - verticalalignment = val.font.valign === :vcenter ? "center" : string(val.font.valign), + horizontalalignment = val.font.halign ≡ :hcenter ? "center" : string(val.font.halign), + verticalalignment = val.font.valign ≡ :vcenter ? "center" : string(val.font.valign), color = _py_color(val.font.color), rotation = val.font.rotation, family = val.font.family, @@ -1527,8 +1485,8 @@ _py_add_annotations(sp::Subplot{PythonPlotBackend}, x, y, z, val::PlotText) = sp z, val.str; size = _py_thickness_scale(sp.plt, val.font.pointsize), - horizontalalignment = val.font.halign === :hcenter ? "center" : string(val.font.halign), - verticalalignment = val.font.valign === :vcenter ? "center" : string(val.font.valign), + horizontalalignment = val.font.halign ≡ :hcenter ? "center" : string(val.font.halign), + verticalalignment = val.font.valign ≡ :vcenter ? "center" : string(val.font.valign), color = _py_color(val.font.color), rotation = val.font.rotation, family = val.font.family, @@ -1540,16 +1498,12 @@ _py_add_annotations(sp::Subplot{PythonPlotBackend}, x, y, z, val::PlotText) = sp _py_legend_pos(pos::Tuple{S,T}) where {S<:Real,T<:Real} = "lower left" function _py_legend_pos(pos::Tuple{<:Real,Symbol}) - s, c = sincosd(pos[1]) .* (pos[2] === :outer ? -1 : 1) + s, c = sincosd(pos[1]) .* (pos[2] ≡ :outer ? -1 : 1) yanchors = "lower", "center", "upper" xanchors = "left", "center", "right" - join( - [ - yanchors[PlotsBase.legend_anchor_index(s)], - xanchors[PlotsBase.legend_anchor_index(c)], - ], - ' ', - ) + let lac = PlotsBase.legend_anchor_index + join([yanchors[lac(s)], xanchors[lac(c)]], ' ') + end end # legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax) @@ -1558,7 +1512,7 @@ _py_legend_bbox(pos::Tuple{<:Real,Symbol}) = _py_legend_bbox(pos) = pos function _py_add_legend(plt::Plot, sp::Subplot, ax) - (leg = sp[:legend_position]) === :none && return + (leg = sp[:legend_position]) ≡ :none && return # gotta do this to ensure both axes are included labels, handles = [], [] @@ -1570,7 +1524,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) clims = get_clims(sp, series) nseries += 1 # add a line/marker and a label - if series[:seriestype] === :shape || series[:fillrange] !== nothing + if series[:seriestype] ≡ :shape || series[:fillrange] ≢ nothing lc = get_linecolor(series, clims) fc = get_fillcolor(series, clims) la = get_linealpha(series) @@ -1620,7 +1574,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) solid_joinstyle = "miter", dash_capstyle = "butt", dash_joinstyle = "miter", - marker = _py_marker(PlotsBase._cycle(series[:markershape], 1)), + marker = _py_marker(_cycle(series[:markershape], 1)), markersize = _py_thickness_scale(plt, 0.8sp[:legend_font_pointsize]), markeredgecolor = _py_color( single_color(get_markerstrokecolor(series)), @@ -1671,7 +1625,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) ) leg.get_frame().set_linewidth(_py_thickness_scale(plt, 1)) leg.set_zorder(1_000) - if sp[:legend_title] !== nothing + if sp[:legend_title] ≢ nothing leg.set_title(string(sp[:legend_title])) PythonPlot.setp( leg.get_title(), @@ -1698,27 +1652,24 @@ end # position the subplot in the backend. function PlotsBase._update_plot_object(plt::Plot{PythonPlotBackend}) for sp in plt.subplots - (ax = sp.o) === nothing && return + (ax = sp.o) ≡ nothing && return figw, figh = sp.plt[:size] .* px # ax.set_position signature: `[left, bottom, width, height]` - PlotsBase.bbox_to_pcts(sp.plotarea, figw, figh) |> ax.set_position + bbox_to_pcts(sp.plotarea, figw, figh) |> ax.set_position if haskey(sp.attr, :cbar_ax) && RecipesPipeline.is3d(sp) # 2D plots are completely handled by axis dividers bb = sp.attr[:cbar_bbox] # this is the bounding box of just the colors of the colorbar (not labels) pad = 2mm - cb_bbox = PlotsBase.BoundingBox( - PlotsBase.right(sp.bbox) - 2PlotsBase.width(bb) - 2pad, # x0 - PlotsBase.top(sp.bbox) + pad, # y0 - PlotsBase.width(bb), # width - PlotsBase.height(sp.bbox) - 2pad, # height + cb_bbox = BoundingBox( + right(sp.bbox) - 2width(bb) - 2pad, # x0 + top(sp.bbox) + pad, # y0 + width(bb), # width + height(sp.bbox) - 2pad, # height ) - get( - sp[:extra_kwargs], - "3d_colorbar_axis", - PlotsBase.bbox_to_pcts(cb_bbox, figw, figh), - ) |> sp.attr[:cbar_ax].set_position + get(sp[:extra_kwargs], "3d_colorbar_axis", bbox_to_pcts(cb_bbox, figw, figh)) |> + sp.attr[:cbar_ax].set_position end end PythonPlot.draw() @@ -1757,4 +1708,4 @@ end PlotsBase.closeall(::PythonPlotBackend) = PythonPlot.close("all") -end # module +end # module diff --git a/PlotsBase/ext/UnicodePlotsExt.jl b/PlotsBase/ext/UnicodePlotsExt.jl index b517e2176..9d306cc4e 100644 --- a/PlotsBase/ext/UnicodePlotsExt.jl +++ b/PlotsBase/ext/UnicodePlotsExt.jl @@ -4,38 +4,21 @@ import PlotsBase: PlotsBase, texmath2unicode import RecipesPipeline import UnicodePlots -using PlotsBase.PlotMeasures -using PlotsBase.PlotsSeries using PlotsBase.Annotations -using PlotsBase.PlotsPlots +using PlotsBase.DataSeries using PlotsBase.Colorbars using PlotsBase.Subplots using PlotsBase.Commons using PlotsBase.Shapes using PlotsBase.Arrows using PlotsBase.Colors +using PlotsBase.Plots using PlotsBase.Fonts using PlotsBase.Ticks using PlotsBase.Axes -const package_str = "UnicodePlots" -const str = lowercase(package_str) -const sym = Symbol(str) - struct UnicodePlotsBackend <: PlotsBase.AbstractBackend end -const T = UnicodePlotsBackend - -get_concrete_backend() = UnicodePlotsBackend # opposite to abstract - -function __init__() - @debug "Initializing $package_str backend in PlotsBase; run `$str()` to activate it." - PlotsBase._backendType[sym] = get_concrete_backend() - PlotsBase._backendSymbol[T] = sym - - push!(PlotsBase._initialized_backends, sym) -end -PlotsBase.backend_name(::UnicodePlotsBackend) = sym -PlotsBase.backend_package_name(::UnicodePlotsBackend) = PlotsBase.backend_package_name(sym) +PlotsBase.@extension_static UnicodePlotsBackend unicodeplots const _unicodeplots_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -112,27 +95,6 @@ const _unicodeplots_markers = [ :x, ] const _unicodeplots_scales = [:identity, :ln, :log2, :log10] -# ----------------------------------------------------------------------------- -# Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods -# defined in abstract_backend.jl - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_$(str)_", s, "s") - quote - PlotsBase.$f1(::UnicodePlotsBackend, $s::Symbol) = $s in $v - PlotsBase.$f2(::UnicodePlotsBackend) = sort(collect($v)) - end |> eval -end - -## results in: -# PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool -# ... -# PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} -# ... -# PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} -# ----------------------------------------------------------------------------- # https://github.com/JuliaPlots/UnicodePlots.jl @@ -148,7 +110,7 @@ const _canvas_map = ( PlotsBase.should_warn_on_unsupported(::UnicodePlotsBackend) = false -function PlotsBase._before_layout_calcs(plt::PlotsBase.Plot{UnicodePlotsBackend}) +function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend}) plt.o = UnicodePlots.Plot[] up_width = UnicodePlots.DEFAULT_WIDTH[] up_height = UnicodePlots.DEFAULT_HEIGHT[] @@ -412,11 +374,7 @@ end # ------------------------------------------------------------------------------------------ -function PlotsBase._show( - io::IO, - ::MIME"image/png", - plt::PlotsBase.Plot{UnicodePlotsBackend}, -) +function PlotsBase._show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) applicable(UnicodePlots.save_image, io) || "PlotsBase(UnicodePlots): saving to `.png` requires `import FreeType, FileIO`" |> ArgumentError |> @@ -428,7 +386,7 @@ function PlotsBase._show( imgs = [] sps = 0 for r in 1:nr, c in 1:nc - if (l = plt.layout[r, c]) isa PlotsBase.GridLayout && size(l) != (1, 1) + if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) unsupported_layout_error() else img = UnicodePlots.png_image(plt.o[sps += 1]; pixelsize = 32) @@ -461,16 +419,12 @@ function PlotsBase._show( nothing end -Base.show(plt::PlotsBase.Plot{UnicodePlotsBackend}) = show(stdout, plt) -Base.show(io::IO, plt::PlotsBase.Plot{UnicodePlotsBackend}) = +Base.show(plt::Plot{UnicodePlotsBackend}) = show(stdout, plt) +Base.show(io::IO, plt::Plot{UnicodePlotsBackend}) = PlotsBase._show(io, MIME("text/plain"), plt) # NOTE: _show(...) must be kept for Base.showable (src/output.jl) -function PlotsBase._show( - io::IO, - ::MIME"text/plain", - plt::PlotsBase.Plot{UnicodePlotsBackend}, -) +function PlotsBase._show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) PlotsBase.prepare_output(plt) nr, nc = size(plt.layout) if nr == 1 && nc == 1 # fast path @@ -490,7 +444,7 @@ function PlotsBase._show( for r in 1:nr lmax = 0 for c in 1:nc - if (l = plt.layout[r, c]) isa PlotsBase.GridLayout && size(l) != (1, 1) + if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) unsupported_layout_error() else if get(l.attr, :blank, false) @@ -531,9 +485,9 @@ function PlotsBase._show( end # we only support MIME"text/plain", hence display(...) falls back to plain-text on stdout -function PlotsBase._display(plt::PlotsBase.Plot{UnicodePlotsBackend}) +function PlotsBase._display(plt::Plot{UnicodePlotsBackend}) show(stdout, plt) println(stdout) end -end # module +end # module diff --git a/PlotsBase/ext/UnitfulExt.jl b/PlotsBase/ext/UnitfulExt.jl index ede2e5713..e4e42636e 100644 --- a/PlotsBase/ext/UnitfulExt.jl +++ b/PlotsBase/ext/UnitfulExt.jl @@ -20,7 +20,8 @@ import Unitful: import PlotsBase: PlotsBase, @recipe, PlotText, Subplot, AVec, AMat, Axis import RecipesBase import LaTeXStrings: LaTeXString -import Latexify: latexify +import Latexify + using UnitfulLatexify const MissingOrQuantity = Union{Missing,<:Quantity,<:LogScaled} @@ -32,7 +33,7 @@ Main recipe @recipe function f(::Type{T}, x::T) where {T<:AbstractArray{<:MissingOrQuantity}} # COV_EXCL_LINE axisletter = plotattributes[:letter] # x, y, or z clims_types = (:contour, :contourf, :heatmap, :surface) - if axisletter === :z && get(plotattributes, :seriestype, :nothing) ∈ clims_types + if axisletter ≡ :z && get(plotattributes, :seriestype, :nothing) ∈ clims_types u = get(plotattributes, :zunit, _unit(eltype(x))) ustripattribute!(plotattributes, :clims, u) append_unit_if_needed!(plotattributes, :colorbar_title, u) @@ -59,7 +60,7 @@ function fixaxis!(attr, x, axisletter) # fix the attributes: labels, lims, ticks, marker/line stuff, etc. append_unit_if_needed!(attr, axislabel, u) ustripattribute!(attr, err, u) - if axisletter === :y + if axisletter ≡ :y ustripattribute!(attr, :ribbon, u) ustripattribute!(attr, :fillrange, u) end @@ -145,7 +146,7 @@ function fixaspectratio!(attr, u, axisletter) # Keep the default behavior (let PlotsBase figure it out) return end - if aspect_ratio === :equal + if aspect_ratio ≡ :equal aspect_ratio = 1 end #======================================================================================= @@ -158,9 +159,9 @@ function fixaspectratio!(attr, u, axisletter) made, and the default aspect ratio fixing of PlotsBase throws a `DimensionError` as it tries to compare `0 < 1u"m"`. =======================================================================================# - if axisletter === :y + if axisletter ≡ :y attr[:aspect_ratio] = aspect_ratio * u - elseif axisletter === :x + elseif axisletter ≡ :x attr[:aspect_ratio] = aspect_ratio / u end nothing @@ -231,20 +232,20 @@ append_unit_if_needed!(attr, key, u) = append_unit_if_needed!(attr, key, label::ProtectedString, u) = nothing append_unit_if_needed!(attr, key, label::UnitfulString, u) = nothing function append_unit_if_needed!(attr, key, label::Nothing, u) - attr[key] = if attr[:plot_object].backend == PlotsBase._backend_instance(:pgfplotsx) - UnitfulString(LaTeXString(latexify(u)), u) + attr[key] = if attr[:plot_object].backend == PlotsBase.backend_instance(:pgfplotsx) + UnitfulString(LaTeXString(Latexify.latexify(u)), u) else UnitfulString(string(u), u) end end function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString} isempty(label) && return attr[key] = UnitfulString(label, u) - if attr[:plot_object].backend == PlotsBase._backend_instance(:pgfplotsx) + if attr[:plot_object].backend == PlotsBase.backend_instance(:pgfplotsx) attr[key] = UnitfulString( LaTeXString( format_unit_label( label, - latexify(u), + Latexify.latexify(u), get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round), ), ), @@ -351,8 +352,7 @@ function _unit(x) unit(x) end -function PlotsBase.pgfx_sanitize_string(s::UnitfulString) +PlotsBase.pgfx_sanitize_string(s::UnitfulString) = UnitfulString(PlotsBase.pgfx_sanitize_string(s.content), s.unit) -end end # module diff --git a/PlotsBase/src/Annotations.jl b/PlotsBase/src/Annotations.jl index 2c8c8a1c7..e73a095c9 100644 --- a/PlotsBase/src/Annotations.jl +++ b/PlotsBase/src/Annotations.jl @@ -1,14 +1,8 @@ # internal module module Annotations -using ..PlotsBase.Commons -using ..PlotsBase.Dates -using ..PlotsBase.Fonts: Font, PlotText, text, font -using ..PlotsBase.Shapes: Shape, _shapes -using ..PlotsBase.PlotMeasures: pct -using ..PlotsBase: Series, Subplot, TimeType, Length -using ..PlotsBase: is_2tuple, is3d, discrete_value! -export EachAnn, +export SeriesAnnotations, + EachAnn, series_annotations, series_annotations_shapes!, process_annotation, @@ -16,6 +10,13 @@ export EachAnn, annotations, assign_annotation_coord! +import ..PlotsBase: Series, Subplot, TimeType, is3d, discrete_value! + +using ..Commons +using ..Shapes +using ..Dates +using ..Fonts + mutable struct SeriesAnnotations strs::AVec # the labels/names font::Font @@ -69,8 +70,8 @@ function series_annotations(strs::AVec, args...) shp = arg elseif isa(arg, Font) fnt = arg - elseif isa(arg, Symbol) && haskey(_shapes, arg) - shp = _shapes[arg] + elseif isa(arg, Symbol) && haskey(Shapes._shapes, arg) + shp = Shapes._shapes[arg] elseif isa(arg, Number) scalefactor = arg, arg elseif is_2tuple(arg) @@ -87,7 +88,7 @@ end function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) anns = series[:series_annotations] - if anns !== nothing && anns.baseshape !== nothing + if anns ≢ nothing && anns.baseshape ≢ nothing # we use baseshape to overwrite the markershape attribute # with a list of custom shapes for each msw, msh = anns.scalefactor @@ -127,7 +128,7 @@ mutable struct EachAnn end function Base.iterate(ea::EachAnn, i = 1) - (ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y)) && return + (ea.anns ≡ nothing || isempty(ea.anns.strs) || i > length(ea.y)) && return tmp = _cycle(ea.anns.strs, i) str, fnt = if isa(tmp, PlotText) @@ -156,8 +157,7 @@ _annotationfont(sp::Subplot) = font(; _annotation(sp::Subplot, font, lab, pos...; alphabet = "abcdefghijklmnopqrstuvwxyz") = ( pos..., - lab === :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : - _text_label(lab, font), + lab ≡ :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : _text_label(lab, font), ) assign_annotation_coord!(axis, x) = discrete_value!(axis, x)[1] @@ -205,11 +205,11 @@ process_annotation(sp::Subplot, ann) = function _relative_position(xmin, xmax, pos::Length{:pct}, scale::Symbol) # !TODO Add more scales in the future (asinh, sqrt) ? - if scale === :log || scale === :ln + if scale ≡ :log || scale ≡ :ln exp(log(xmin) + pos.value * log(xmax / xmin)) - elseif scale === :log10 + elseif scale ≡ :log10 exp10(log10(xmin) + pos.value * log10(xmax / xmin)) - elseif scale === :log2 + elseif scale ≡ :log2 exp2(log2(xmin) + pos.value * log2(xmax / xmin)) else # :identity (linear scale) xmin + pos.value * (xmax - xmin) @@ -251,4 +251,8 @@ locate_annotation(sp::Subplot, x, y, z, label::PlotText) = (x, y, z, label) locate_annotation(sp::Subplot, pos::Symbol, label::PlotText) = locate_annotation(sp, position_multiplier[pos], label) -end # Annotations +end # module + +# ------------------------------------------------------------------- + +using .Annotations diff --git a/PlotsBase/src/Arrows.jl b/PlotsBase/src/Arrows.jl index 8d058d3c2..4bc470480 100644 --- a/PlotsBase/src/Arrows.jl +++ b/PlotsBase/src/Arrows.jl @@ -1,8 +1,9 @@ module Arrows -using ..PlotsBase.Commons export Arrow, arrow, add_arrows +using ..PlotsBase.Commons + # style is :open or :closed (for now) struct Arrow style::Symbol @@ -59,4 +60,7 @@ function add_arrows(func::Function, x::AVec, y::AVec) end end end -end # Arrows + +end # module + +using .Arrows diff --git a/PlotsBase/src/Axes.jl b/PlotsBase/src/Axes.jl index 4e604fcba..ea099aeeb 100644 --- a/PlotsBase/src/Axes.jl +++ b/PlotsBase/src/Axes.jl @@ -1,13 +1,15 @@ module Axes export Axis, Extrema, tickfont, guidefont, widen_factor, scale_inverse_scale_func -export sort_3d_axes, axes_letters, process_axis_arg!, has_ticks -import PlotsBase: get_ticks -using PlotsBase: PlotsBase, RecipesPipeline, Subplot, DefaultsDict, TimeType -using PlotsBase.Commons: _axis_defaults_byletter, _all_axis_attrs, dumpdict -using PlotsBase.Commons -using PlotsBase.Ticks -using PlotsBase.Fonts +export sort_3d_axes, axes_letters, process_axis_arg!, has_ticks, get_axis + +import ..PlotsBase +import ..PlotsBase: Subplot, DefaultsDict, TimeType, attr! + +using ..RecipesPipeline +using ..Commons +using ..Ticks +using ..Fonts const default_widen_factor = Ref(1.06) const _widen_seriestypes = ( @@ -49,7 +51,7 @@ function Axis(sp::Subplot, letter::Symbol, args...; kw...) :show => true, # show or hide the axis? (useful for linked subplots) ) - attr = DefaultsDict(explicit, _axis_defaults_byletter[letter]) + attr = DefaultsDict(explicit, Commons._axis_defaults_byletter[letter]) # update the defaults attr!(Axis([sp], attr), args...; kw...) @@ -57,9 +59,9 @@ end # properly retrieve from axis.attr, passing `:match` to the correct key Base.getindex(axis::Axis, k::Symbol) = - if (v = axis.plotattributes[k]) === :match - if haskey(Commons.Commons._match_map2, k) - axis.sps[1][Commons.Commons._match_map2[k]] + if (v = axis.plotattributes[k]) ≡ :match + if haskey(Commons._match_map2, k) + axis.sps[1][Commons._match_map2[k]] else axis[Commons._match_map[k]] end @@ -77,9 +79,9 @@ end Extrema() = Extrema(Inf, -Inf) # ------------------------------------------------------------------------- sort_3d_axes(x, y, z, letter) = - if letter === :x + if letter ≡ :x x, y, z - elseif letter === :y + elseif letter ≡ :y y, x, z else z, y, x @@ -89,13 +91,13 @@ axes_letters(sp, letter) = if RecipesPipeline.is3d(sp) sort_3d_axes(:x, :y, :z, letter) else - letter === :x ? (:x, :y) : (:y, :x) + letter ≡ :x ? (:x, :y) : (:y, :x) end scale_inverse_scale_func(scale::Symbol) = ( RecipesPipeline.scale_func(scale), RecipesPipeline.inverse_scale_func(scale), - scale === :identity, + scale ≡ :identity, ) function get_axis(sp::Subplot, letter::Symbol) axissym = get_attr_symbol(letter, :axis) @@ -116,22 +118,22 @@ function Commons.axis_limits( ex = axis[:extrema] amin, amax = ex.emin, ex.emax lims = process_limits(axis[:lims], axis) - lims === nothing && warn_invalid_limits(axis[:lims], letter) + lims ≡ nothing && warn_invalid_limits(axis[:lims], letter) if (has_user_lims = lims isa Tuple) lmin, lmax = lims if lmin isa Number && isfinite(lmin) amin = lmin elseif lmin isa Symbol - lmin === :auto || @warn "Invalid min $(letter)limit" lmin + lmin ≡ :auto || @warn "Invalid min $(letter)limit" lmin end if lmax isa Number && isfinite(lmax) amax = lmax elseif lmax isa Symbol - lmax === :auto || @warn "Invalid max $(letter)limit" lmax + lmax ≡ :auto || @warn "Invalid max $(letter)limit" lmax end end - if lims === :symmetric + if lims ≡ :symmetric amax = max(abs(amin), abs(amax)) amin = -amax end @@ -142,15 +144,15 @@ function Commons.axis_limits( amin, amax = zero(amin), one(amax) end if ispolar(axis.sps[1]) - if axis[:letter] === :x + if axis[:letter] ≡ :x amin, amax = 0, 2π - elseif lims === :auto + elseif lims ≡ :auto # widen max radius so ticks dont overlap with theta axis amin, amax = 0, amax + 0.1abs(amax - amin) end - elseif lims_factor !== nothing + elseif lims_factor ≢ nothing amin, amax = scale_lims(amin, amax, lims_factor, axis[:scale]) - elseif lims === :round + elseif lims ≡ :round amin, amax = round_limits(amin, amax, axis[:scale]) end @@ -159,14 +161,14 @@ function Commons.axis_limits( !has_user_lims && consider_aspect && letter in (:x, :y) && - !(aspect_ratio === :none || RecipesPipeline.is3d(:sp)) + !(aspect_ratio ≡ :none || RecipesPipeline.is3d(:sp)) ) aspect_ratio = aspect_ratio isa Number ? aspect_ratio : 1 area = PlotsBase.plotarea(sp) plot_ratio = PlotsBase.height(area) / PlotsBase.width(area) dist = amax - amin - factor = if letter === :x + factor = if letter ≡ :x ydist, = axis_limits(sp, :y, widen_factor(sp[:yaxis]), false) |> collect |> diff axis_ratio = aspect_ratio * ydist / dist axis_ratio / plot_ratio @@ -195,12 +197,12 @@ function widen_factor(axis::Axis; factor = default_widen_factor[]) elseif widen isa Number return widen else - widen === :auto || @warn "Invalid value specified for `widen`: $widen" + widen ≡ :auto || @warn "Invalid value specified for `widen`: $widen" end # automatic behavior: widen if limits aren't specified and series type is appropriate lims = process_limits(axis[:lims], axis) - (lims isa Tuple || lims === :round) && return + (lims isa Tuple || lims ≡ :round) && return for sp in axis.sps, series in series_list(sp) series.plotattributes[:seriestype] in _widen_seriestypes && return factor end @@ -249,13 +251,15 @@ Scale the limits of the axis specified by `letter` (one of `:x`, `:y`, `:z`) by given `factor` around the limits' middle point. If `letter` is omitted, all axes are affected. """ -function scale_lims!(sp::Subplot, letter, factor) +function Commons.scale_lims!(sp::Subplot, letter, factor) axis = get_axis(sp, letter) from, to = PlotsBase.get_sp_lims(sp, letter) axis[:lims] = scale_lims(from, to, factor, axis[:scale]) end -scale_lims!(factor::Number) = scale_lims!(PlotsBase.current(), factor) -scale_lims!(letter::Symbol, factor) = scale_lims!(PlotsBase.current(), letter, factor) +Commons.scale_lims!(factor::Number) = scale_lims!(PlotsBase.current(), factor) +Commons.scale_lims!(letter::Symbol, factor) = + scale_lims!(PlotsBase.current(), letter, factor) + #---------------------------------------------------------------------- function process_axis_arg!(plotattributes::AKW, arg, letter = "") T = typeof(arg) @@ -282,7 +286,7 @@ function process_axis_arg!(plotattributes::AKW, arg, letter = "") elseif T <: AVec plotattributes[get_attr_symbol(letter, :ticks)] = arg - elseif arg === nothing + elseif arg ≡ nothing plotattributes[get_attr_symbol(letter, :ticks)] = [] elseif T <: Bool || arg in Commons._all_showaxis_attrs @@ -303,10 +307,10 @@ function process_axis_arg!(plotattributes::AKW, arg, letter = "") end end -has_ticks(axis::Axis) = get(axis, :ticks, nothing) |> PlotsBase.Ticks._has_ticks +has_ticks(axis::Axis) = _has_ticks(get(axis, :ticks, nothing)) # update an Axis object with magic args and keywords -function attr!(axis::Axis, args...; kw...) +function PlotsBase.attr!(axis::Axis, args...; kw...) # first process args plotattributes = axis.plotattributes foreach(arg -> process_axis_arg!(plotattributes, arg), args) @@ -317,9 +321,9 @@ function attr!(axis::Axis, args...; kw...) # then override for any keywords... only those keywords that already exists in plotattributes for (k, v) in kw haskey(plotattributes, k) || continue - if k === :discrete_values + if k ≡ :discrete_values foreach(x -> discrete_value!(axis, x), v) # add these discrete values to the axis - elseif k === :lims && isa(v, NTuple{2,TimeType}) + elseif k ≡ :lims && isa(v, NTuple{2,TimeType}) plotattributes[k] = (v[1].instant.periods.value, v[2].instant.periods.value) else plotattributes[k] = v @@ -336,7 +340,7 @@ end # ------------------------------------------------------------------------- -Base.show(io::IO, axis::Axis) = dumpdict(io, axis.plotattributes, "Axis") +Base.show(io::IO, axis::Axis) = Commons.dumpdict(io, axis.plotattributes, "Axis") ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) tickfont(ax::Axis) = font(; @@ -365,7 +369,7 @@ function _update_axis( ) # build the KW of arguments from the letter version (i.e. xticks --> ticks) kw = KW() - for k in _all_axis_attrs + for k in Commons._all_axis_attrs # first get the args without the letter: `tickfont = font(10)` # note: we don't pop because we want this to apply to all axes! (delete after all have finished) if haskey(plotattributes_in, k) @@ -399,15 +403,20 @@ end """ returns (continuous_values, discrete_values) for the ticks on this axis """ -function get_ticks(sp::Subplot, axis::Axis; update = true, formatter = axis[:formatter]) +function Commons.get_ticks( + sp::Subplot, + axis::Axis; + update = true, + formatter = axis[:formatter], +) if update || !haskey(axis.plotattributes, :optimized_ticks) dvals = axis[:discrete_values] ticks = _transform_ticks(axis[:ticks], axis) axis.plotattributes[:optimized_ticks] = if ( - axis[:letter] === :x && + axis[:letter] ≡ :x && ticks isa Symbol && - ticks !== :none && + ticks ≢ :none && !isempty(dvals) && ispolar(sp) ) @@ -415,7 +424,7 @@ function get_ticks(sp::Subplot, axis::Axis; update = true, formatter = axis[:for else cvals = axis[:continuous_values] alims = axis_limits(sp, axis[:letter]) - get_ticks(ticks, cvals, dvals, alims, axis[:scale], formatter) + Commons.get_ticks(ticks, cvals, dvals, alims, axis[:scale], formatter) end end axis.plotattributes[:optimized_ticks] @@ -457,6 +466,8 @@ function PlotsBase.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} ex end +end # Axes + # ------------------------------------------------------------------------- -end # Axes +using .Axes diff --git a/PlotsBase/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl index cf9eb5119..1b7cc051e 100644 --- a/PlotsBase/src/BezierCurves.jl +++ b/PlotsBase/src/BezierCurves.jl @@ -19,4 +19,8 @@ end PlotsBase.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = map(curve, Base.range(first(range), stop = last(range), length = n)) -end +end # module + +# ------------------------------------------------------------------- + +using .BezierCurves diff --git a/PlotsBase/src/Colorbars.jl b/PlotsBase/src/Colorbars.jl index cfac48642..17a55a800 100644 --- a/PlotsBase/src/Colorbars.jl +++ b/PlotsBase/src/Colorbars.jl @@ -1,16 +1,18 @@ module Colorbars -export colorbar_style, - get_clims, update_clims, hascolorbar, get_colorbar_ticks, _update_subplot_colorbars -using PlotsBase.Commons: Commons, NaNMath, ignorenan_extrema -using PlotsBase.PlotsSeries -using PlotsBase.Subplots: Subplot, series_list -using PlotsBase.Surfaces: AbstractSurface -using PlotsBase.Ticks -using PlotsBase.Ticks: _transform_ticks -import PlotsBase.Commons.get_clims - -# These functions return an operator for use in `get_clims(::Seres, op)` +export colorbar_style, get_clims, update_clims, hascolorbar +export get_colorbar_ticks, _update_subplot_colorbars + +import ..Commons: NaNMath, ignorenan_extrema, get_clims + +using ..Subplots: Subplot, series_list +using ..Surfaces: AbstractSurface +using ..Ticks: _transform_ticks +using ..DataSeries +using ..Commons +using ..Ticks + +# these functions return an operator for use in `get_clims(::Seres, op)` process_clims(lims::Tuple{<:Number,<:Number}) = (zlims -> ifelse.(isfinite.(lims), lims, zlims)) ∘ ignorenan_extrema process_clims(s::Union{Symbol,Nothing,Missing}) = ignorenan_extrema @@ -31,13 +33,10 @@ function update_clims(sp::Subplot, op = process_clims(sp[:clims]))::Tuple{Float6 for series in series_list(sp) if series[:colorbar_entry]::Bool # Avoid calling the inner `update_clims` if at all possible; dynamic dispatch hell - if ( - series[:seriestype] ∈ Commons._z_colored_series && - series[:z] !== nothing - ) || - series[:line_z] !== nothing || - series[:marker_z] !== nothing || - series[:fill_z] !== nothing + if (series[:seriestype] ∈ Commons._z_colored_series && series[:z] ≢ nothing) || + series[:line_z] ≢ nothing || + series[:marker_z] ≢ nothing || + series[:fill_z] ≢ nothing zmin, zmax = _update_clims(zmin, zmax, update_clims(series, op)...) else zmin, zmax = _update_clims(zmin, zmax, NaN, NaN) @@ -77,16 +76,16 @@ function update_clims(series::Series, op = ignorenan_extrema)::Tuple{Float64,Flo zmin, zmax = Inf, -Inf # keeping this unrolled has higher performance - if series[:seriestype] ∈ Commons._z_colored_series && series[:z] !== nothing + if series[:seriestype] ∈ Commons._z_colored_series && series[:z] ≢ nothing zmin, zmax = update_clims(zmin, zmax, series[:z], op) end - if series[:line_z] !== nothing + if series[:line_z] ≢ nothing zmin, zmax = update_clims(zmin, zmax, series[:line_z], op) end - if series[:marker_z] !== nothing + if series[:marker_z] ≢ nothing zmin, zmax = update_clims(zmin, zmax, series[:marker_z], op) end - if series[:fill_z] !== nothing + if series[:fill_z] ≢ nothing zmin, zmax = update_clims(zmin, zmax, series[:fill_z], op) end return series[:clims_calculated] = zmin <= zmax ? (zmin, zmax) : (NaN, NaN) @@ -116,16 +115,16 @@ function colorbar_style(series::Series) elseif iscontour(series) cbar_lines elseif series[:seriestype] ∈ (:heatmap, :surface) || - any(series[z] !== nothing for z in (:marker_z, :line_z, :fill_z)) + any(series[z] ≢ nothing for z in (:marker_z, :line_z, :fill_z)) cbar_gradient else nothing end end -hascolorbar(series::Series) = colorbar_style(series) !== nothing +hascolorbar(series::Series) = colorbar_style(series) ≢ nothing hascolorbar(sp::Subplot) = - sp[:colorbar] !== :none && any(hascolorbar(s) for s in series_list(sp)) + sp[:colorbar] ≢ :none && any(hascolorbar(s) for s in series_list(sp)) function get_colorbar_ticks(sp::Subplot; update = true, formatter = sp[:colorbar_formatter]) if update || !haskey(sp.attr, :colorbar_optimized_ticks) @@ -140,7 +139,12 @@ function get_colorbar_ticks(sp::Subplot; update = true, formatter = sp[:colorbar return sp.attr[:colorbar_optimized_ticks] end -# Dynamic callback from the pipeline if needed +# dynamic callback from the pipeline if needed _update_subplot_colorbars(sp::Subplot) = update_clims(sp) _update_subplot_colorbars(sp::Subplot, series::Series) = update_clims(sp, series) -end # Colorbars + +end # module + +# ------------------------------------------------------------------- + +using .Colorbars diff --git a/PlotsBase/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl index 5d995153d..a58e53b30 100644 --- a/PlotsBase/src/Commons/Commons.jl +++ b/PlotsBase/src/Commons/Commons.jl @@ -1,10 +1,8 @@ "Things that should be common to all backends and frontend modules" module Commons -export AVec, AMat, KW, AKW, TicksArgs -export PlotsBase, PLOTS_SEED -export _haligns, _valigns, _cbar_width -# Functions +export AVec, + AMat, KW, AKW, TicksArgs, PlotsBase, PLOTS_SEED, _haligns, _valigns, _cbar_width export get_subplot, coords, ispolar, @@ -38,18 +36,35 @@ export anynan, ignorenan_maximum, ignorenan_mean, ignorenan_minimum -#exports from args.jl +export istuple, isvector, ismatrix, isscalar, is_2tuple export default, wraptuple, merge_with_base_supported -using PlotsBase: PlotsBase, Printf, NaNMath, cgrad -import PlotsBase: RecipesPipeline -using PlotsBase.Colors: Colorant, @colorant_str -using PlotsBase.ColorTypes: alpha -using PlotsBase.Measures: mm, BoundingBox -using PlotsBase.PlotUtils: PlotUtils, ColorPalette, plot_color, isdark, ColorGradient -using PlotsBase.RecipesBase -using PlotsBase: DEFAULT_LINEWIDTH -using PlotsBase: Statistics +export px, pct, plotarea, plotarea! +export width, height, leftpad, toppad, bottompad, rightpad +export origin, left, right, bottom, top, bbox, bbox! +export DEFAULT_BBOX, DEFAULT_MINPAD, DEFAULT_LINEWIDTH +export MM_PER_PX, MM_PER_INCH, DPI, PX_PER_INCH + +export GridLayout, EmptyLayout, RootLayout +export BBox, BoundingBox, mm, cm, inch, pt, w, h +export bbox_to_pcts, xy_mm_to_pcts +export Length, AbsoluteLength, Measure +export to_pixels, ispositive, get_ticks, scale_lims! + +import Measures: + Measures, Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, w, h +import PlotUtils: PlotUtils, ColorPalette, plot_color, isdark, ColorGradient +import PlotsBase: PlotsBase, RecipesPipeline, cgrad + +using ..Colors: Colorant, @colorant_str +using ..ColorTypes: alpha +using ..RecipesBase +using ..Statistics +using ..NaNMath +using ..Printf + +const width = Measures.width +const height = Measures.height const AVec = AbstractVector const AMat = AbstractMatrix @@ -57,14 +72,9 @@ const KW = Dict{Symbol,Any} const AKW = AbstractDict{Symbol,Any} const TicksArgs = Union{AVec{T},Tuple{AVec{T},AVec{S}},Symbol} where {T<:Real,S<:AbstractString} -const PLOTS_SEED = 1234 -const PX_PER_INCH = 100 -const DPI = PX_PER_INCH -const MM_PER_INCH = 25.4 -const MM_PER_PX = MM_PER_INCH / PX_PER_INCH + const _haligns = :hcenter, :left, :right const _valigns = :vcenter, :top, :bottom -const _cbar_width = 5mm const _all_scales = [:identity, :ln, :log2, :log10, :asinh, :sqrt] const _log_scales = [:ln, :log2, :log10] const _log_scale_bases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) @@ -90,14 +100,26 @@ const _segmenting_vector_attributes = ( const _segmenting_array_attributes = :line_z, :fill_z, :marker_z const _debug = Ref(false) -function get_subplot end -function get_clims end -function series_list end -function coords end -function ispolar end -function expand_extrema! end -function axis_limits end -function preprocess_attributes! end +# docs.julialang.org/en/v1/manual/methods/#Empty-generic-functions +macro generic_functions(args...) + blk = Expr(:block) + foreach(arg -> push!(blk.args, :(function $arg end)), args) + blk |> esc +end + +@generic_functions get_ticks get_subplot get_clims +@generic_functions series_list coords ispolar axis_limits +@generic_functions expand_extrema! preprocess_attributes! scale_lims! + +@generic_functions width height leftpad toppad bottompad rightpad +@generic_functions origin left right bottom top +@generic_functions plotarea plotarea! + +include("measures.jl") + +using ..RecipesBase: AbstractLayout +include("layouts.jl") + # --------------------------------------------------------------- wraptuple(x::Tuple) = x wraptuple(x) = (x,) @@ -109,12 +131,12 @@ all_lineLtypes(arg) = true_or_all_true(a -> get(Commons._typeAliases, a, a) in Commons._all_seriestypes, arg) all_styles(arg) = true_or_all_true(a -> get(Commons._styleAliases, a, a) in Commons._all_styles, arg) -all_shapes(arg) = (true_or_all_true( +all_shapes(arg) = true_or_all_true( a -> get(Commons._marker_aliases, a, a) in Commons._all_markers || a isa PlotsBase.Shape, arg, -)) +) all_alphas(arg) = true_or_all_true( a -> (typeof(a) <: Real && a > 0 && a < 1) || ( @@ -131,17 +153,30 @@ include("attrs.jl") function _override_seriestype_check(plotattributes::AKW, st::Symbol) # do we want to override the series type? if !RecipesPipeline.is3d(st) && st ∉ (:contour, :contour3d, :quiver) - if (z = plotattributes[:z]) !== nothing && + if (z = plotattributes[:z]) ≢ nothing && size(plotattributes[:x]) == size(plotattributes[:y]) == size(z) - st = st === :scatter ? :scatter3d : :path3d + st = st ≡ :scatter ? :scatter3d : :path3d plotattributes[:seriestype] = st end end st end -"These should only be needed in frontend modules" -PlotsBase.@ScopeModule( +macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) + import_ex = Expr( + :import, + Expr( + :(:), + Expr(:., :., :., parent), + (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., + ), + ) + export_ex = Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...) + Expr(:module, true, mod, Expr(:block, import_ex, export_ex)) |> esc +end + +"these should only be needed in frontend modules" +@ScopeModule( Frontend, Commons, _subplot_defaults, @@ -157,7 +192,7 @@ PlotsBase.@ScopeModule( function fg_color(plotattributes::AKW) fg = get(plotattributes, :foreground_color, :auto) - if fg === :auto + if fg ≡ :auto bg = plot_color(get(plotattributes, :background_color, :white)) fg = alpha(bg) > 0 && isdark(bg) ? colorant"white" : colorant"black" else @@ -165,15 +200,36 @@ function fg_color(plotattributes::AKW) end end function color_or_nothing!(plotattributes, k::Symbol) - plotattributes[k] = (v = plotattributes[k]) === :match ? v : plot_color(v) + plotattributes[k] = (v = plotattributes[k]) ≡ :match ? v : plot_color(v) nothing end +istuple(::Tuple) = true +istuple(::Any) = false +isvector(::AVec) = true +isvector(::Any) = false +ismatrix(::AMat) = true +ismatrix(::Any) = false +isscalar(::Real) = true +isscalar(::Any) = false + +is_2tuple(v) = typeof(v) <: Tuple && length(v) == 2 + # cache joined symbols so they can be looked up instead of constructed each time const _attrsymbolcache = Dict{Symbol,Dict{Symbol,Symbol}}() -get_attr_symbol(letter::Symbol, keyword::String) = get_attr_symbol(letter, Symbol(keyword)) get_attr_symbol(letter::Symbol, keyword::Symbol) = _attrsymbolcache[letter][keyword] +get_attr_symbol(letter::Symbol, keyword::String) = get_attr_symbol(letter, Symbol(keyword)) + +new_attr_dict!(letter::Symbol)::Dict{Symbol,Symbol} = + get!(() -> Dict{Symbol,Symbol}(), _attrsymbolcache, letter) + +# NOTE: using `keyword::String` allows to disambiguate argument order +set_attr_symbol!(letter::Symbol, keyword::String) = + let letter_keyword = Symbol(letter, keyword) + _attrsymbolcache[letter][Symbol(keyword)] = letter_keyword + end + # ------------------------------------------------------------------------------------ _cycle(v::AVec, idx::Int) = v[mod(idx, axes(v, 1))] _cycle(v::AMat, idx::Int) = size(v, 1) == 1 ? v[end, mod(idx, axes(v, 2))] : v[:, mod(idx, axes(v, 2))] @@ -260,10 +316,10 @@ reverse_if(x, cond) = cond ? reverse(x) : x function get_aspect_ratio(sp) ar = sp[:aspect_ratio] check_aspect_ratio(ar) - if ar === :auto + if ar ≡ :auto ar = :none for series in series_list(sp) - if series[:seriestype] === :image + if series[:seriestype] ≡ :image ar = :equal end end diff --git a/PlotsBase/src/Commons/aliases.jl b/PlotsBase/src/Commons/aliases.jl index 1a8ed86a7..f7a887e75 100644 --- a/PlotsBase/src/Commons/aliases.jl +++ b/PlotsBase/src/Commons/aliases.jl @@ -9,7 +9,7 @@ function aliases_and_autopick( options::AVec, plotIndex::Int, ) - if plotattributes[sym] === :auto + if plotattributes[sym] ≡ :auto plotattributes[sym] = autopick_ignore_none_auto(options, plotIndex) elseif haskey(aliases, plotattributes[sym]) plotattributes[sym] = aliases[plotattributes[sym]] diff --git a/PlotsBase/src/Commons/attrs.jl b/PlotsBase/src/Commons/attrs.jl index 5f453a477..6e9ad60c4 100644 --- a/PlotsBase/src/Commons/attrs.jl +++ b/PlotsBase/src/Commons/attrs.jl @@ -5,7 +5,7 @@ const _keyAliases = Dict{Symbol,Symbol}() function add_aliases(sym::Symbol, aliases::Symbol...) for alias in aliases - (haskey(_keyAliases, alias) || alias === sym) && return + (haskey(_keyAliases, alias) || alias ≡ sym) && return _keyAliases[alias] = sym end nothing @@ -695,11 +695,11 @@ function default(k::Symbol) haskey(defaults, k) && return defaults[k] end haskey(_axis_defaults, k) && return _axis_defaults[k] - if (axis_k = parse_axis_kw(k)) !== nothing + if (axis_k = parse_axis_kw(k)) ≢ nothing letter, key = axis_k return _axis_defaults_byletter[letter][key] end - k === :letter && return k # for type recipe processing + k ≡ :letter && return k # for type recipe processing missing end @@ -715,7 +715,7 @@ function default(k::Symbol, v) _axis_defaults[k] = v return v end - if (axis_k = parse_axis_kw(k)) !== nothing + if (axis_k = parse_axis_kw(k)) ≢ nothing letter, key = axis_k _axis_defaults_byletter[letter][key] = v return v @@ -746,7 +746,7 @@ end # if arg is a valid color value, then set plotattributes[csym] and return true function handle_colors!(plotattributes::AKW, arg, csym::Symbol) try - plotattributes[csym] = if arg === :auto + plotattributes[csym] = if arg ≡ :auto :auto else plot_color(arg) @@ -767,22 +767,18 @@ function process_line_attr(plotattributes::AKW, arg) plotattributes[:linestyle] = arg elseif typeof(arg) <: PlotsBase.Stroke - arg.width === nothing || (plotattributes[:linewidth] = arg.width) - arg.color === nothing || ( - plotattributes[:linecolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:linealpha] = arg.alpha) - arg.style === nothing || (plotattributes[:linestyle] = arg.style) + arg.width ≡ nothing || (plotattributes[:linewidth] = arg.width) + arg.color ≡ nothing || + (plotattributes[:linecolor] = arg.color ≡ :auto ? :auto : plot_color(arg.color)) + arg.alpha ≡ nothing || (plotattributes[:linealpha] = arg.alpha) + arg.style ≡ nothing || (plotattributes[:linestyle] = arg.style) elseif typeof(arg) <: PlotsBase.Brush - arg.size === nothing || (plotattributes[:fillrange] = arg.size) - arg.color === nothing || ( - plotattributes[:fillcolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:fillalpha] = arg.alpha) - arg.style === nothing || (plotattributes[:fillstyle] = arg.style) + arg.size ≡ nothing || (plotattributes[:fillrange] = arg.size) + arg.color ≡ nothing || + (plotattributes[:fillcolor] = 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) <: PlotsBase.Arrow || arg in (:arrow, :arrows) plotattributes[:arrow] = arg @@ -811,21 +807,21 @@ function process_marker_attr(plotattributes::AKW, arg) plotattributes[:markerstrokestyle] = arg elseif typeof(arg) <: PlotsBase.Stroke - arg.width === nothing || (plotattributes[:markerstrokewidth] = arg.width) - arg.color === nothing || ( + arg.width ≡ nothing || (plotattributes[:markerstrokewidth] = arg.width) + arg.color ≡ nothing || ( plotattributes[:markerstrokecolor] = - arg.color === :auto ? :auto : plot_color(arg.color) + arg.color ≡ :auto ? :auto : plot_color(arg.color) ) - arg.alpha === nothing || (plotattributes[:markerstrokealpha] = arg.alpha) - arg.style === nothing || (plotattributes[:markerstrokestyle] = arg.style) + arg.alpha ≡ nothing || (plotattributes[:markerstrokealpha] = arg.alpha) + arg.style ≡ nothing || (plotattributes[:markerstrokestyle] = arg.style) elseif typeof(arg) <: PlotsBase.Brush - arg.size === nothing || (plotattributes[:markersize] = arg.size) - arg.color === nothing || ( + arg.size ≡ nothing || (plotattributes[:markersize] = arg.size) + arg.color ≡ nothing || ( plotattributes[:markercolor] = - arg.color === :auto ? :auto : plot_color(arg.color) + arg.color ≡ :auto ? :auto : plot_color(arg.color) ) - arg.alpha === nothing || (plotattributes[:markeralpha] = arg.alpha) + arg.alpha ≡ nothing || (plotattributes[:markeralpha] = arg.alpha) # linealpha elseif all_alphas(arg) @@ -848,13 +844,11 @@ end function process_fill_attr(plotattributes::AKW, arg) # fr = get(plotattributes, :fillrange, 0) if typeof(arg) <: PlotsBase.Brush - arg.size === nothing || (plotattributes[:fillrange] = arg.size) - arg.color === nothing || ( - plotattributes[:fillcolor] = - arg.color === :auto ? :auto : plot_color(arg.color) - ) - arg.alpha === nothing || (plotattributes[:fillalpha] = arg.alpha) - arg.style === nothing || (plotattributes[:fillstyle] = arg.style) + arg.size ≡ nothing || (plotattributes[:fillrange] = arg.size) + arg.color ≡ nothing || + (plotattributes[:fillcolor] = 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 @@ -886,15 +880,15 @@ function process_grid_attr!(plotattributes::AKW, arg, letter) plotattributes[get_attr_symbol(letter, :gridstyle)] = arg elseif typeof(arg) <: PlotsBase.Stroke - arg.width === nothing || + arg.width ≡ nothing || (plotattributes[get_attr_symbol(letter, :gridlinewidth)] = arg.width) - arg.color === nothing || ( + arg.color ≡ nothing || ( plotattributes[get_attr_symbol(letter, :foreground_color_grid)] = arg.color in (:auto, :match) ? :match : plot_color(arg.color) ) - arg.alpha === nothing || + arg.alpha ≡ nothing || (plotattributes[get_attr_symbol(letter, :gridalpha)] = arg.alpha) - arg.style === nothing || + arg.style ≡ nothing || (plotattributes[get_attr_symbol(letter, :gridstyle)] = arg.style) # linealpha @@ -924,15 +918,15 @@ function process_minor_grid_attr!(plotattributes::AKW, arg, letter) plotattributes[get_attr_symbol(letter, :minorgrid)] = true elseif typeof(arg) <: PlotsBase.Stroke - arg.width === nothing || + arg.width ≡ nothing || (plotattributes[get_attr_symbol(letter, :minorgridlinewidth)] = arg.width) - arg.color === nothing || ( + arg.color ≡ nothing || ( plotattributes[get_attr_symbol(letter, :foreground_color_minor_grid)] = arg.color in (:auto, :match) ? :match : plot_color(arg.color) ) - arg.alpha === nothing || + arg.alpha ≡ nothing || (plotattributes[get_attr_symbol(letter, :minorgridalpha)] = arg.alpha) - arg.style === nothing || + arg.style ≡ nothing || (plotattributes[get_attr_symbol(letter, :minorgridstyle)] = arg.style) plotattributes[get_attr_symbol(letter, :minorgrid)] = true @@ -977,7 +971,7 @@ end Symbol(fontname, :valign) --> arg.valign Symbol(fontname, :rotation) --> arg.rotation Symbol(fontname, :color) --> arg.color - elseif arg === :center + elseif arg ≡ :center Symbol(fontname, :halign) --> :hcenter Symbol(fontname, :valign) --> :vcenter elseif arg ∈ _haligns @@ -1013,7 +1007,7 @@ function _add_markershape(plotattributes::AKW) # add the markershape if it needs to be added... hack to allow "m=10" to add a shape, # and still allow overriding in _apply_recipe ms = pop!(plotattributes, :markershape_to_add, :none) - if !haskey(plotattributes, :markershape) && ms !== :none + if !haskey(plotattributes, :markershape) && ms ≢ :none plotattributes[:markershape] = ms end end @@ -1046,7 +1040,7 @@ function convert_legend_value(val::Symbol) :inline, ) val - elseif val === :horizontal + elseif val ≡ :horizontal -1 else error("Invalid symbol for legend: $val") @@ -1150,7 +1144,7 @@ ensure_gradient!(plotattributes::AKW, csym::Symbol, asym::Symbol) = # get a good default linewidth... 0 for surface and heatmaps _replace_linewidth(plotattributes::AKW) = - if plotattributes[:linewidth] === :auto + if plotattributes[:linewidth] ≡ :auto plotattributes[:linewidth] = (get(plotattributes, :seriestype, :path) ∉ (:surface, :heatmap, :image)) * DEFAULT_LINEWIDTH[] @@ -1161,9 +1155,9 @@ label_to_string(label::Bool, series_plotindex) = label_to_string(label::Nothing, series_plotindex) = "" label_to_string(label::Missing, series_plotindex) = "" label_to_string(label::Symbol, series_plotindex) = - if label === :auto + if label ≡ :auto string("y", series_plotindex) - elseif label === :none + elseif label ≡ :none "" else throw(ArgumentError("unsupported symbol $(label) passed to `label`")) @@ -1192,8 +1186,8 @@ Also creates pluralized and non-underscore aliases for these keywords. """ macro add_attributes(level, expr, match_table) expr = macroexpand(__module__, expr) # to expand @static - expr isa Expr && expr.head === :struct || error("Invalid usage of @add_attributes") - if (T = expr.args[2]) isa Expr && T.head === :<: + expr isa Expr && expr.head ≡ :struct || error("Invalid usage of @add_attributes") + if (T = expr.args[2]) isa Expr && T.head ≡ :<: T = T.args[1] end @@ -1239,12 +1233,12 @@ function _splitdef!(blk, key_dict) # var continue elseif ei isa Expr - if ei.head === :(=) + if ei.head ≡ :(=) lhs = ei.args[1] if lhs isa Symbol # var = defexpr var = lhs - elseif lhs isa Expr && lhs.head === :(::) && lhs.args[1] isa Symbol + elseif lhs isa Expr && lhs.head ≡ :(::) && lhs.args[1] isa Symbol # var::T = defexpr var = lhs.args[1] type = lhs.args[2] @@ -1262,11 +1256,11 @@ function _splitdef!(blk, key_dict) defexpr = ei.args[2] # defexpr key_dict[var] = defexpr blk.args[i] = lhs - elseif ei.head === :(::) && ei.args[1] isa Symbol + elseif ei.head ≡ :(::) && ei.args[1] isa Symbol # var::Typ var = ei.args[1] key_dict[var] = defexpr - elseif ei.head === :block + elseif ei.head ≡ :block # can arise with use of @static inside type decl _kwdef!(ei, value_attrs, key_attrs) end diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl new file mode 100644 index 000000000..52cc11201 --- /dev/null +++ b/PlotsBase/src/Commons/layouts.jl @@ -0,0 +1,167 @@ +make_measure_hor(n::Number) = n * w +make_measure_hor(m::Measure) = m + +make_measure_vert(n::Number) = n * h +make_measure_vert(m::Measure) = m + +""" + bbox(x, y, w, h [,originargs...]) + bbox(layout) + +Create a bounding box for plotting +""" +function bbox(x, y, w, h, oarg1::Symbol, originargs::Symbol...) + oargs = vcat(oarg1, originargs...) + orighor = :left + origver = :top + for oarg in oargs + if oarg ≡ :center + orighor = origver = oarg + elseif oarg in (:left, :right, :hcenter) + orighor = oarg + elseif oarg in (:top, :bottom, :vcenter) + origver = oarg + else + @warn "Unused origin arg in bbox construction: $oarg" + end + end + bbox(x, y, w, h; h_anchor = orighor, v_anchor = origver) +end + +# create a new bbox +function bbox(x, y, width, height; h_anchor = :left, v_anchor = :top) + x = make_measure_hor(x) + y = make_measure_vert(y) + width = make_measure_hor(width) + height = make_measure_vert(height) + left = if h_anchor ≡ :left + x + elseif h_anchor in (:center, :hcenter) + 0.5w - 0.5width + x + else + 1w - x - width + end + top = if v_anchor ≡ :top + y + elseif v_anchor in (:center, :vcenter) + 0.5h - 0.5height + y + else + 1h - y - height + end + BoundingBox(left, top, width, height) +end + +# NOTE: (0,0) is the top-left !!! +left(bbox::BoundingBox) = bbox.x0[1] +top(bbox::BoundingBox) = bbox.x0[2] +right(bbox::BoundingBox) = left(bbox) + width(bbox) +bottom(bbox::BoundingBox) = top(bbox) + height(bbox) +origin(bbox::BoundingBox) = left(bbox) + width(bbox) / 2, top(bbox) + height(bbox) / 2 +Base.size(bbox::BoundingBox) = (width(bbox), height(bbox)) + +# ----------------------------------------------------------- +# AbstractLayout + +left(layout::AbstractLayout) = left(bbox(layout)) +top(layout::AbstractLayout) = top(bbox(layout)) +right(layout::AbstractLayout) = right(bbox(layout)) +bottom(layout::AbstractLayout) = bottom(bbox(layout)) +width(layout::AbstractLayout) = width(bbox(layout)) +height(layout::AbstractLayout) = height(bbox(layout)) + +leftpad(layout::AbstractLayout) = 0mm +toppad(layout::AbstractLayout) = 0mm +rightpad(layout::AbstractLayout) = 0mm +bottompad(layout::AbstractLayout) = 0mm + +leftpad(pad) = pad[1] +toppad(pad) = pad[2] +rightpad(pad) = pad[3] +bottompad(pad) = pad[4] + +Base.show(io::IO, layout::AbstractLayout) = print(io, "$(typeof(layout))$(size(layout))") + +# this is the available area for drawing everything in this layout... as percentages of total canvas +bbox(layout::AbstractLayout) = layout.bbox +bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb) + +# layouts are recursive, tree-like structures, and most will have a parent field +Base.parent(layout::AbstractLayout) = layout.parent +parent_bbox(layout::AbstractLayout) = bbox(parent(layout)) + +# ----------------------------------------------------------- +# RootLayout + +# this is the parent of the top-level layout +struct RootLayout <: AbstractLayout end + +Base.show(io::IO, layout::RootLayout) = Base.show_default(io, layout) +Base.parent(::RootLayout) = nothing +parent_bbox(::RootLayout) = DEFAULT_BBOX[] +bbox(::RootLayout) = DEFAULT_BBOX[] + +# ----------------------------------------------------------- +# EmptyLayout + +# contains blank space +mutable struct EmptyLayout <: AbstractLayout + parent::AbstractLayout + bbox::BoundingBox + attr::KW # store label, width, and height for initialization + # label # this is the label that the subplot will take (since we create a layout before initialization) +end +EmptyLayout(parent = RootLayout(); kw...) = EmptyLayout(parent, DEFAULT_BBOX[], KW(kw)) + +Base.size(layout::EmptyLayout) = (0, 0) +Base.length(layout::EmptyLayout) = 0 +Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing + +# ----------------------------------------------------------- +# GridLayout + +# nested, gridded layout with optional size percentages +mutable struct GridLayout <: AbstractLayout + parent::AbstractLayout + minpad::Tuple # leftpad, toppad, rightpad, bottompad + bbox::BoundingBox + grid::Matrix{AbstractLayout} # Nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion + widths::Vector{Measure} + heights::Vector{Measure} + attr::KW +end + +leftpad(layout::GridLayout) = leftpad(layout.minpad) +toppad(layout::GridLayout) = toppad(layout.minpad) +rightpad(layout::GridLayout) = rightpad(layout.minpad) +bottompad(layout::GridLayout) = bottompad(layout.minpad) + +function GridLayout( + dims...; + parent = RootLayout(), + widths = zeros(dims[2]), + heights = zeros(dims[1]), + kw..., +) + grid = Matrix{AbstractLayout}(undef, dims...) + layout = GridLayout( + parent, + DEFAULT_MINPAD[], + DEFAULT_BBOX[], + grid, + Measure[w * pct for w in widths], + Measure[h * pct for h in heights], + # convert(Vector{Float64}, widths), + # convert(Vector{Float64}, heights), + KW(kw), + ) + for i in eachindex(grid) + grid[i] = EmptyLayout(layout) + end + layout +end + +Base.size(layout::GridLayout) = size(layout.grid) +Base.length(layout::GridLayout) = length(layout.grid) +Base.getindex(layout::GridLayout, r::Int, c::Int) = layout.grid[r, c] +Base.setindex!(layout::GridLayout, v, r::Int, c::Int) = layout.grid[r, c] = v +Base.setindex!(layout::GridLayout, v, ci::CartesianIndex) = layout.grid[ci] = v diff --git a/PlotsBase/src/Commons/measures.jl b/PlotsBase/src/Commons/measures.jl new file mode 100644 index 000000000..127991391 --- /dev/null +++ b/PlotsBase/src/Commons/measures.jl @@ -0,0 +1,75 @@ + +const DEFAULT_BBOX = Ref(BoundingBox(0mm, 0mm, 0mm, 0mm)) +const DEFAULT_MINPAD = Ref((20mm, 5mm, 2mm, 10mm)) +const DEFAULT_LINEWIDTH = Ref(1) +const PLOTS_SEED = 1234 +const PX_PER_INCH = 100 +const DPI = PX_PER_INCH +const MM_PER_INCH = 25.4 +const MM_PER_PX = MM_PER_INCH / PX_PER_INCH +const _cbar_width = 5mm + +# allow pixels and percentages +const px = Measures.AbsoluteLength(0.254) +const pct = Measures.Length{:pct,Float64}(1.0) + +const BBox = Measures.Absolute2DBox + +to_pixels(m::AbsoluteLength) = m.value / px.value + +# convert x,y coordinates from absolute coords to percentages... +# returns x_pct, y_pct +function xy_mm_to_pcts(x::AbsoluteLength, y::AbsoluteLength, figw, figh, flipy = true) + xmm, ymm = x.value, y.value + if flipy + ymm = figh.value - ymm # flip y when origin in bottom-left + end + xmm / figw.value, ymm / figh.value +end + +# convert a bounding box from absolute coords to percentages... +# returns an array of percentages of figure size: [left, bottom, width, height] +function bbox_to_pcts(bb::BoundingBox, figw, figh, flipy = true) + mms = Float64[f(bb).value for f in (left, bottom, width, height)] + if flipy + mms[2] = figh.value - mms[2] # flip y when origin in bottom-left + end + mms ./ Float64[figw.value, figh.value, figw.value, figh.value] +end + +Base.show(io::IO, bbox::BoundingBox) = print( + io, + "BBox{l,t,r,b,w,h = $(left(bbox)),$(top(bbox)), $(right(bbox)),$(bottom(bbox)), $(width(bbox)),$(height(bbox))}", +) + +# Base.:*{T,N}(m1::Length{T,N}, m2::Length{T,N}) = Length{T,N}(m1.value * m2.value) +ispositive(m::Measure) = m.value > 0 + +# union together bounding boxes +function Base.:+(bb1::BoundingBox, bb2::BoundingBox) + # empty boxes don't change the union + ispositive(width(bb1)) || return bb2 + ispositive(height(bb1)) || return bb2 + ispositive(width(bb2)) || return bb1 + ispositive(height(bb2)) || return bb1 + + l = min(left(bb1), left(bb2)) + t = min(top(bb1), top(bb2)) + r = max(right(bb1), right(bb2)) + b = max(bottom(bb1), bottom(bb2)) + BoundingBox(l, t, r - l, b - t) +end + +Base.convert(::Type{<:Measure}, x::Float64) = x * pct + +Base.:*(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * m2.value) +Base.:*(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * m1.value) +Base.:/(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value / m2.value) +Base.:/(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value / m1.value) + +inch2px(inches::Real) = float(inches * PX_PER_INCH) +px2inch(px::Real) = float(px / PX_PER_INCH) +inch2mm(inches::Real) = float(inches * MM_PER_INCH) +mm2inch(mm::Real) = float(mm / MM_PER_INCH) +px2mm(px::Real) = float(px * MM_PER_PX) +mm2px(mm::Real) = float(mm / MM_PER_PX) diff --git a/PlotsBase/src/Commons/postprocess_attrs.jl b/PlotsBase/src/Commons/postprocess_attrs.jl index f6cfad022..67e48da7f 100644 --- a/PlotsBase/src/Commons/postprocess_attrs.jl +++ b/PlotsBase/src/Commons/postprocess_attrs.jl @@ -1,21 +1,18 @@ # add all pluralized forms to the _keyAliases dict -for arg in _all_attrs - add_aliases(arg, makeplural(arg)) -end +foreach(arg -> add_aliases(arg, makeplural(arg)), _all_attrs) # fill symbol cache for letter in (:x, :y, :z) - _attrsymbolcache[letter] = Dict{Symbol,Symbol}() - for k in _axis_attrs + new_attr_dict!(letter) + for keyword in _axis_attrs # populate attribute cache - lk = Symbol(letter, k) - _attrsymbolcache[letter][k] = lk - # allow the underscore version too: xguide or x_guide - add_aliases(lk, Symbol(letter, "_", k)) + letter_keyword = set_attr_symbol!(letter, string(keyword)) + # allow the underscore version too: `xguide` or `x_guide` + add_aliases(letter_keyword, Symbol(letter, "_", keyword)) end - for k in (_magic_axis_attrs..., :(_discrete_indices)) - _attrsymbolcache[letter][k] = Symbol(letter, k) + for keyword in (_magic_axis_attrs..., :(_discrete_indices)) + _attrsymbolcache[letter][keyword] = Symbol(letter, keyword) end end diff --git a/PlotsBase/src/Series.jl b/PlotsBase/src/DataSeries.jl similarity index 92% rename from PlotsBase/src/Series.jl rename to PlotsBase/src/DataSeries.jl index 411e82549..17043f0d0 100644 --- a/PlotsBase/src/Series.jl +++ b/PlotsBase/src/DataSeries.jl @@ -1,4 +1,4 @@ -module PlotsSeries +module DataSeries export Series, should_add_to_legend, @@ -20,11 +20,13 @@ export get_linestyle, get_fillalpha, get_markercolor, get_markeralpha -import PlotsBase.Commons: get_subplot, _series_defaults -using PlotsBase.Commons -using PlotsBase.Commons: get_gradient -using PlotsBase.PlotUtils: ColorGradient, plot_color -using PlotsBase: PlotsBase, DefaultsDict, RecipesPipeline, get_attr_symbol, KW + +import ..Commons: get_gradient, get_subplot, _series_defaults +import ..PlotsBase + +using ..PlotsBase: DefaultsDict, RecipesPipeline, get_attr_symbol, KW +using ..PlotUtils: ColorGradient, plot_color +using ..Commons mutable struct Series plotattributes::DefaultsDict @@ -36,23 +38,6 @@ Base.get(series::Series, k::Symbol, v) = get(series.plotattributes, k, v) Base.push!(series::Series, args...) = extend_series!(series, args...) Base.append!(series::Series, args...) = extend_series!(series, args...) -# TODO: consider removing -attr(series::Series, k::Symbol) = series.plotattributes[k] -attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) -function attr!(series::Series; kw...) - plotattributes = KW(kw) - PlotsBase.Commons.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes - if haskey(_series_defaults, k) - series[k] = v - else - @warn "unused key $k in series attr" - end - end - PlotsBase._series_updated(series[:subplot].plt, series) - series -end - should_add_to_legend(series::Series) = series.plotattributes[:primary] && series.plotattributes[:label] != "" && @@ -103,7 +88,7 @@ end function copy_series!(series, letter) plt = series[:plot_object] for s in plt.series_list, l in (:x, :y, :z) - if (s !== series || l !== letter) && s[l] === series[letter] + if (s ≢ series || l ≢ letter) && s[l] ≡ series[letter] series[letter] = copy(series[letter]) end end @@ -137,11 +122,11 @@ for comp in (:line, :fill, :marker) ) c = series[$Symbol($compcolor)] # series[:linecolor], series[:fillcolor], series[:markercolor] z = series[$Symbol($comp_z)] # series[:line_z], series[:fill_z], series[:marker_z] - if z === nothing + if z ≡ nothing isa(c, ColorGradient) ? c : plot_color(_cycle(c, i)) else grad = get_gradient(c) - if s === :identity + if s ≡ :identity get(grad, z[i], (cmin, cmax)) else base = _log_scale_bases[s] @@ -151,7 +136,7 @@ for comp in (:line, :fill, :marker) end function $get_compcolor(series, i::Integer = 1, s::Symbol = :identity) - if series[$Symbol($comp_z)] === nothing + if series[$Symbol($comp_z)] ≡ nothing $get_compcolor(series, 0, 1, i, s) else $get_compcolor(series, get_clims(series[:subplot]), i, s) @@ -182,17 +167,17 @@ function get_colorgradient(series::Series) series[:fillcolor] elseif st in (:contour, :wireframe, :contour3d) series[:linecolor] - elseif series[:marker_z] !== nothing + elseif series[:marker_z] ≢ nothing series[:markercolor] - elseif series[:line_z] !== nothing + elseif series[:line_z] ≢ nothing series[:linecolor] - elseif series[:fill_z] !== nothing + elseif series[:fill_z] ≢ nothing series[:fillcolor] end end iscontour(series::Series) = series[:seriestype] in (:contour, :contour3d) -isfilledcontour(series::Series) = iscontour(series) && series[:fillrange] !== nothing +isfilledcontour(series::Series) = iscontour(series) && series[:fillrange] ≢ nothing function contour_levels(series::Series, clims) iscontour(series) || error("Not a contour series") @@ -225,12 +210,12 @@ struct NaNSegmentsIterator end function Base.iterate(itr::NaNSegmentsIterator, nextidx::Int = itr.n1) - (i = findfirst(!PlotsBase.Commons.anynan(itr.args), nextidx:(itr.n2))) === nothing && + (i = findfirst(!PlotsBase.Commons.anynan(itr.args), nextidx:(itr.n2))) ≡ nothing && return nextval = nextidx + i - 1 j = findfirst(PlotsBase.Commons.anynan(itr.args), nextval:(itr.n2)) - nextnan = j === nothing ? itr.n2 + 1 : nextval + j - 1 + nextnan = j ≡ nothing ? itr.n2 + 1 : nextval + j - 1 nextval:(nextnan - 1), nextnan end @@ -258,7 +243,7 @@ has_attribute_segments(series::Series) = function series_segments(series::Series, seriestype::Symbol = :path; check = false) x, y, z = series[:x], series[:y], series[:z] - (x === nothing || isempty(x)) && return UnitRange{Int}[] + (x ≡ nothing || isempty(x)) && return UnitRange{Int}[] args = RecipesPipeline.is3d(series) ? (x, y, z) : (x, y) nan_segments = collect(iter_segments(args...)) @@ -280,7 +265,7 @@ function series_segments(series::Series, seriestype::Symbol = :path; check = fal segments = if has_attribute_segments(series) map(nan_segments) do r - if seriestype === :shape + if seriestype ≡ :shape warn_on_inconsistent_shape_attrs(series, x, y, z, r) (SeriesSegment(r, first(r)),) elseif seriestype in (:scatter, :scatter3d) @@ -329,4 +314,26 @@ function warn_on_inconsistent_shape_attrs(series, x, y, z, r) end end end -end # PlotsSeries + +end # module + +# ------------------------------------------------------------------- + +using .DataSeries + +# TODO: consider removing +attr(series::Series, k::Symbol) = series.plotattributes[k] +attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) +function attr!(series::Series; kw...) + plotattributes = KW(kw) + Commons.preprocess_attributes!(plotattributes) + for (k, v) in plotattributes + if haskey(_series_defaults, k) + series[k] = v + else + @warn "unused key $k in series attr" + end + end + _series_updated(series[:subplot].plt, series) + series +end diff --git a/PlotsBase/src/Fonts.jl b/PlotsBase/src/Fonts.jl index 965c15611..2434bc018 100644 --- a/PlotsBase/src/Fonts.jl +++ b/PlotsBase/src/Fonts.jl @@ -1,11 +1,12 @@ module Fonts -using PlotsBase.Colors -using PlotsBase.Commons -using PlotsBase.Commons: +using ..Colors +using ..Commons +using ..Commons: _initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes, _initial_fontsizes + # keep in mind: these will be reexported and are public API -export font, scalefontsizes, resetfontsizes, text, is_horizontal, Font, PlotText +export Font, PlotText, font, scalefontsizes, resetfontsizes, text, is_horizontal mutable struct Font family::AbstractString @@ -44,7 +45,7 @@ function font(args...; kw...) for arg in args T = typeof(arg) - @assert arg !== :match + @assert arg ≢ :match if T == Font family = arg.family @@ -53,7 +54,7 @@ function font(args...; kw...) valign = arg.valign rotation = arg.rotation color = arg.color - elseif arg === :center + elseif arg ≡ :center halign = :hcenter valign = :vcenter elseif arg ∈ _haligns @@ -78,21 +79,21 @@ function font(args...; kw...) end for sym in keys(kw) - if sym === :family + if sym ≡ :family family = string(kw[sym]) - elseif sym === :pointsize + elseif sym ≡ :pointsize pointsize = kw[sym] - elseif sym === :halign + elseif sym ≡ :halign halign = kw[sym] - halign === :center && (halign = :hcenter) + halign ≡ :center && (halign = :hcenter) @assert halign ∈ _haligns - elseif sym === :valign + elseif sym ≡ :valign valign = kw[sym] - valign === :center && (valign = :vcenter) + valign ≡ :center && (valign = :vcenter) @assert valign ∈ _valigns - elseif sym === :rotation + elseif sym ≡ :rotation rotation = kw[sym] - elseif sym === :color + elseif sym ≡ :color col = kw[sym] color = col isa Colorant ? col : parse(Colorant, col) else @@ -174,4 +175,9 @@ text(str, args...; kw...) = PlotText(string(str), font(args...; kw...)) Base.length(t::PlotText) = length(t.str) is_horizontal(t::PlotText) = abs(sind(t.font.rotation)) ≤ sind(45) -end # Fonts + +end # module + +# ------------------------------------------------------------------- + +@reexport using .Fonts diff --git a/PlotsBase/src/PlotMeasures.jl b/PlotsBase/src/PlotMeasures.jl deleted file mode 100644 index bcbc843ef..000000000 --- a/PlotsBase/src/PlotMeasures.jl +++ /dev/null @@ -1,40 +0,0 @@ -module PlotMeasures - -export PX_PER_INCH, - DPI, MM_PER_INCH, MM_PER_PX, DEFAULT_BBOX, DEFAULT_MINPAD, DEFAULT_LINEWIDTH - -import ..Measures -import ..Measures: - Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height, w, h - -const BBox = Measures.Absolute2DBox -export BBox, BoundingBox, mm, cm, inch, px, pct, pt, w, h - -# allow pixels and percentages -const px = AbsoluteLength(0.254) -const pct = Length{:pct,Float64}(1.0) - -const PX_PER_INCH = 100 -const DPI = PX_PER_INCH -const MM_PER_INCH = 25.4 -const MM_PER_PX = MM_PER_INCH / PX_PER_INCH -const _cbar_width = 5mm -const DEFAULT_BBOX = Ref(BoundingBox(0mm, 0mm, 0mm, 0mm)) -const DEFAULT_MINPAD = Ref((20mm, 5mm, 2mm, 10mm)) -const DEFAULT_LINEWIDTH = Ref(1) - -Base.convert(::Type{<:Measure}, x::Float64) = x * pct - -Base.:*(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * m2.value) -Base.:*(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * m1.value) -Base.:/(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value / m2.value) -Base.:/(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value / m1.value) - -inch2px(inches::Real) = float(inches * PX_PER_INCH) -px2inch(px::Real) = float(px / PX_PER_INCH) -inch2mm(inches::Real) = float(inches * MM_PER_INCH) -mm2inch(mm::Real) = float(mm / MM_PER_INCH) -px2mm(px::Real) = float(px * MM_PER_PX) -mm2px(mm::Real) = float(mm / MM_PER_PX) - -end diff --git a/PlotsBase/src/PlotsPlots.jl b/PlotsBase/src/Plots.jl similarity index 81% rename from PlotsBase/src/PlotsPlots.jl rename to PlotsBase/src/Plots.jl index 25a561201..7eae830ec 100644 --- a/PlotsBase/src/PlotsPlots.jl +++ b/PlotsBase/src/Plots.jl @@ -1,33 +1,29 @@ -module PlotsPlots +module Plots export Plot, PlotOrSubplot, - _update_plot_attrs, plottitlefont, ignorenan_extrema, - protect, - InputWrapper -import PlotsBase.Axes: _update_axis, scale_lims! -import PlotsBase.Commons: ignorenan_extrema, _cycle -import PlotsBase.Ticks: get_ticks -using PlotsBase: - PlotsBase, - AbstractPlot, - AbstractBackend, - DefaultsDict, - Series, - AbstractLayout, - RecipesPipeline -using PlotsBase.PlotMeasures -using PlotsBase.Colorbars: _update_subplot_colorbars -using PlotsBase.Subplots: Subplot, _update_subplot_colors, _update_margins -using PlotsBase.Axes: Axis, get_axis -using PlotsBase.PlotUtils: get_color_palette -using PlotsBase.Commons -using PlotsBase.Commons.Frontend -using PlotsBase.Fonts: font + _update_plot_attrs, + InputWrapper, + protect + +import ..RecipesBase: AbstractLayout, AbstractBackend, AbstractPlot +import ..RecipesPipeline: RecipesPipeline, DefaultsDict +import ..Subplots: Subplot, _update_subplot_colors, _update_margins +import ..Colorbars: _update_subplot_colorbars +import ..Commons: ignorenan_extrema, _cycle + +using ..PlotUtils +using ..DataSeries +using ..Commons.Frontend +using ..Commons +using ..Fonts +using ..Ticks +using ..Axes const SubplotMap = Dict{Any,Subplot} + mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} backend::T # the backend type n::Int # number of series @@ -50,7 +46,7 @@ mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} nothing, Subplot[], SubplotMap(), - PlotsBase.EmptyLayout(), + EmptyLayout(), Subplot[], false, ) @@ -62,14 +58,14 @@ mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} sp = deepcopy(osp) # FIXME: fails `PlotlyJS` ? plt.layout.grid[1, 1] = sp # reset some attributes - sp.minpad = PlotMeasures.DEFAULT_MINPAD[] - sp.bbox = PlotMeasures.DEFAULT_BBOX[] - sp.plotarea = PlotMeasures.DEFAULT_BBOX[] + sp.minpad = DEFAULT_MINPAD[] + sp.bbox = DEFAULT_BBOX[] + sp.plotarea = DEFAULT_BBOX[] sp.plt = plt # change the enclosing plot push!(plt.subplots, sp) plt end -end # Plot +end const PlotOrSubplot = Union{Plot,Subplot} # ----------------------------------------------------------- @@ -138,16 +134,16 @@ end # --------------------------------------------------------------- -"Smallest x in plot" +"smallest x in plot" xmin(plt::Plot) = ignorenan_minimum([ ignorenan_minimum(series.plotattributes[:x]) for series in plt.series_list ]) -"Largest x in plot" +"largest x in plot" xmax(plt::Plot) = ignorenan_maximum([ ignorenan_maximum(series.plotattributes[:x]) for series in plt.series_list ]) -"Extrema of x-values in plot" +"extrema of x-values in plot" ignorenan_extrema(plt::Plot) = (xmin(plt), xmax(plt)) # --------------------------------------------------------------- @@ -155,7 +151,7 @@ ignorenan_extrema(plt::Plot) = (xmin(plt), xmax(plt)) # properly retrieve from plt.attr, passing `:match` to the correct key Base.getindex(plt::Plot, k::Symbol) = - if (v = plt.attr[k]) === :match + if (v = plt.attr[k]) ≡ :match plt[Commons._match_map[k]] else v @@ -175,15 +171,15 @@ Base.ndims(plt::Plot) = 2 # clear out series list, but retain subplots Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) -PlotsBase.get_subplot(plt::Plot, sp::Subplot) = sp -PlotsBase.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] -PlotsBase.get_subplot(plt::Plot, k) = plt.spmap[k] -PlotsBase.series_list(plt::Plot) = plt.series_list +Commons.get_subplot(plt::Plot, sp::Subplot) = sp +Commons.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] +Commons.get_subplot(plt::Plot, k) = plt.spmap[k] +Commons.series_list(plt::Plot) = plt.series_list -get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) +Commons.get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) -get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x === sp, plt.subplots) -PlotsBase.RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = +get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x ≡ sp, plt.subplots) +RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = Commons.preprocess_attributes!(plotattributes) plottitlefont(p::Plot) = font(; @@ -218,7 +214,7 @@ function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol) nothing end -function PlotsBase.Axes._update_axis( +function Axes._update_axis( plt::Plot, sp::Subplot, plotattributes_in::AKW, @@ -228,14 +224,14 @@ function PlotsBase.Axes._update_axis( # get (maybe initialize) the axis axis = get_axis(sp, letter) - _update_axis(axis, plotattributes_in, letter, subplot_index) + Axes._update_axis(axis, plotattributes_in, letter, subplot_index) # convert a bool into auto or nothing if isa(axis[:ticks], Bool) axis[:ticks] = axis[:ticks] ? :auto : nothing end - PlotsBase.Axes._update_axis_colors(axis) + Axes._update_axis_colors(axis) _update_axis_links(plt, axis, letter) nothing end @@ -265,7 +261,7 @@ function _update_subplot_attrs( lims_warned = false for letter in (:x, :y, :z) - _update_axis(plt, sp, plotattributes_in, letter, subplot_index) + Axes._update_axis(plt, sp, plotattributes_in, letter, subplot_index) lk = get_attr_symbol(letter, :lims) # warn against using `Range` in x,y,z lims @@ -280,14 +276,19 @@ function _update_subplot_attrs( PlotsBase.Subplots._update_subplot_periphery(sp, anns) end -function scale_lims!(plt::Plot, letter, factor) +function Commons.scale_lims!(plt::Plot, letter, factor) foreach(sp -> scale_lims!(sp, letter, factor), plt.subplots) plt end -function scale_lims!(plt::Union{Plot,Subplot}, factor) +function Commons.scale_lims!(plt::Union{Plot,Subplot}, factor) foreach(letter -> scale_lims!(plt, letter, factor), (:x, :y, :z)) plt end Commons.get_size(plt::Plot) = get_size(plt.attr) Commons.get_thickness_scaling(plt::Plot) = get_thickness_scaling(plt.attr) -end # PlotsPlots + +end # module + +# ------------------------------------------------------------------- + +using .Plots diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl index c78fef6f6..09ad15f27 100644 --- a/PlotsBase/src/PlotsBase.jl +++ b/PlotsBase/src/PlotsBase.jl @@ -8,7 +8,7 @@ if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_m end using Pkg, Dates, Printf, Statistics, Base64, LinearAlgebra, SparseArrays, Random -using Reexport, RelocatableFolders +using PrecompileTools, Preferences, Reexport, RelocatableFolders using Base.Meta @reexport using RecipesBase @reexport using PlotThemes @@ -37,6 +37,7 @@ import RecipesPipeline: import UnicodeFun import StatsBase import Downloads +import Measures import Showoff import Unzip import JLFzf @@ -115,72 +116,37 @@ export plotattr, scalefontsizes, resetfontsizes + #! format: on -import Measures -include("PlotMeasures.jl") -using .PlotMeasures -import .PlotMeasures: Length, AbsoluteLength, Measure, width, height -# --------------------------------------------------------- -macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) - Expr( - :module, - true, - mod, - Expr( - :block, - Expr( - :import, - Expr( - :(:), - Expr(:., :., :., parent), - (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., - ), - ), - Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...), - ), - ) |> esc -end import NaNMath + +const _project = Pkg.Types.read_package(normpath(@__DIR__, "..", "Project.toml")) +const _version = _project.version +const _compat = _project.compat + include("Commons/Commons.jl") using .Commons using .Commons.Frontend -# --------------------------------------------------------- + +Commons.@generic_functions attr attr! rotate rotate! + include("Fonts.jl") -@reexport using .Fonts -using .Fonts: Font, PlotText include("Ticks.jl") -using .Ticks -include("Series.jl") -using .PlotsSeries +include("DataSeries.jl") include("Subplots.jl") -using .Subplots -import .Subplots: plotarea, plotarea!, leftpad, toppad, bottompad, rightpad include("Axes.jl") -using .Axes include("Surfaces.jl") include("Colorbars.jl") -using .Colorbars -include("PlotsPlots.jl") -using .PlotsPlots +include("Plots.jl") include("layouts.jl") -# --------------------------------------------------------- include("utils.jl") -using .Surfaces include("axes_utils.jl") include("legend.jl") include("Shapes.jl") -using .Shapes -using .Shapes: Shape, _shapes, rotate! include("Annotations.jl") -using .Annotations -using .Annotations: SeriesAnnotations, process_annotation include("Arrows.jl") -using .Arrows include("Strokes.jl") -using .Strokes -using .Strokes: Stroke, Brush include("BezierCurves.jl") -using .BezierCurves include("themes.jl") include("plot.jl") include("pipeline.jl") @@ -189,16 +155,45 @@ include("recipes.jl") include("animation.jl") include("examples.jl") include("plotattr.jl") -include("backends/nobackend.jl") -include("abstract_backend.jl") include("alignment.jl") -const CURRENT_BACKEND = CurrentBackend(:none) include("output.jl") include("shorthands.jl") -include("backends/web.jl") -include("backends/plotly.jl") -using .Plotly +include("backends.jl") +include("web.jl") +include("plotly.jl") +include("preferences.jl") include("init.jl") include("users.jl") +# COV_EXCL_START +@setup_workload begin + backend(:none) + n = length(_examples) + imports = sizehint!(Expr[], n) + examples = sizehint!(Expr[], 10n) + scratch_dir = mktempdir() + for i in setdiff(1:n, _backend_skips[backend_name()], _animation_examples) + _examples[i].external && continue + (imp = _examples[i].imports) ≡ nothing || push!(imports, imp) + func = gensym(string(i)) + push!(examples, quote + $func() = begin # evaluate each example in a local scope + $(_examples[i].exprs) + $i == 1 || return # trigger display only for one example + fn = joinpath(scratch_dir, tempname()) + show(devnull, current()) + nothing + end + $func() + end) + end + @compile_workload begin + backend(:none) + eval.(imports) + eval.(examples) + end + CURRENT_PLOT.nullableplot = nothing +end +# COV_EXCL_STOP + end diff --git a/PlotsBase/src/Shapes.jl b/PlotsBase/src/Shapes.jl index 81b412ec0..2c8b9e55e 100644 --- a/PlotsBase/src/Shapes.jl +++ b/PlotsBase/src/Shapes.jl @@ -1,7 +1,9 @@ module Shapes -using PlotsBase: PlotsBase, RecipesPipeline -using PlotsBase.Commons +import ..PlotsBase + +using ..RecipesPipeline +using ..Commons # keep in mind: these will be reexported and are public API export Shape, @@ -16,9 +18,7 @@ export Shape, scale!, scale, translate, - translate!, - rotate, - rotate! + translate! const P2 = NTuple{2,Float64} const P3 = NTuple{3,Float64} @@ -205,13 +205,20 @@ rotate_x(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = rotate_y(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = ((y - centery) * cos(θ) + (x - centerx) * sin(θ) + centery) -rotate(x::Real, y::Real, θ::Real, c) = (rotate_x(x, y, θ, c...), rotate_y(x, y, θ, c...)) +end # module + +# ------------------------------------------------------------------- + +using .Shapes + +rotate(x::Real, y::Real, θ::Real, c) = + (Shapes.rotate_x(x, y, θ, c...), Shapes.rotate_y(x, y, θ, c...)) function rotate!(shape::Shape, θ::Real, c = center(shape)) x, y = coords(shape) for i in eachindex(x) - xi = rotate_x(x[i], y[i], θ, c...) - yi = rotate_y(x[i], y[i], θ, c...) + xi = Shapes.rotate_x(x[i], y[i], θ, c...) + yi = Shapes.rotate_y(x[i], y[i], θ, c...) x[i], y[i] = xi, yi end shape @@ -220,9 +227,7 @@ end "rotate an object in space" function rotate(shape::Shape, θ::Real, c = center(shape)) x, y = coords(shape) - x_new = rotate_x.(x, y, θ, c...) - y_new = rotate_y.(x, y, θ, c...) + x_new = Shapes.rotate_x.(x, y, θ, c...) + y_new = Shapes.rotate_y.(x, y, θ, c...) Shape(x_new, y_new) end - -end # Shapes diff --git a/PlotsBase/src/Strokes.jl b/PlotsBase/src/Strokes.jl index 5fcb8a5b3..840f42a55 100644 --- a/PlotsBase/src/Strokes.jl +++ b/PlotsBase/src/Strokes.jl @@ -1,8 +1,10 @@ module Strokes -export stroke, brush, Stroke, Brush -using PlotsBase.Colors: Colorant -using PlotsBase.Commons: all_alphas, all_reals, all_styles +export Stroke, Brush, stroke, brush + +using ..Colors: Colorant +using ..Commons: all_alphas, all_reals, all_styles + struct Stroke width color @@ -77,6 +79,8 @@ function brush(args...; alpha = nothing) Brush(size, color, alpha) end -# ----------------------------------------------------------------------- +end # module + +# ------------------------------------------------------------------- -end # Strokes +using .Strokes diff --git a/PlotsBase/src/Subplots.jl b/PlotsBase/src/Subplots.jl index 3abbf6fc0..c41b4c1b2 100644 --- a/PlotsBase/src/Subplots.jl +++ b/PlotsBase/src/Subplots.jl @@ -6,29 +6,19 @@ export Subplot, legendtitlefont, titlefont, get_series_color, - needs_any_3d_axes, - plotarea, - plotarea!, - toppad, - leftpad, - bottompad, - rightpad -import PlotsBase.Ticks: get_ticks -using PlotsBase: - PlotsBase, - RecipesPipeline, - Series, - AbstractBackend, - AbstractLayout, - BoundingBox, - DefaultsDict -using PlotsBase.RecipesPipeline: RecipesPipeline, Surface, Volume -using PlotsBase.PlotUtils: get_color_palette -using PlotsBase.Commons -using PlotsBase.Commons.Frontend -using PlotsBase.Commons: convert_legend_value, like_surface -using PlotsBase.Fonts -using PlotsBase.PlotMeasures + needs_any_3d_axes +import PlotsBase + +import ..Commons: BoundingBox, convert_legend_value, like_surface +import ..RecipesPipeline: RecipesPipeline, Surface, Volume, DefaultsDict +import ..RecipesBase: AbstractLayout, AbstractBackend +import ..DataSeries: Series +import ..PlotUtils + +using ..Commons.Frontend +using ..Commons +using ..Fonts +using ..Ticks # a single subplot mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout @@ -42,7 +32,7 @@ mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout o # can store backend-specific data... like a pyplot ax plt # the enclosing Plot object (can't give it a type because of no forward declarations) - Subplot(::T; parent = PlotsBase.RootLayout()) where {T<:AbstractBackend} = new{T}( + Subplot(::T; parent = RootLayout()) where {T<:AbstractBackend} = new{T}( parent, Series[], 0, @@ -57,9 +47,9 @@ end # properly retrieve from sp.attr, passing `:match` to the correct key Base.getindex(sp::Subplot, k::Symbol) = - if (v = sp.attr[k]) === :match - if haskey(Commons.Commons._match_map2, k) - sp.plt[Commons.Commons._match_map2[k]] + if (v = sp.attr[k]) ≡ :match + if haskey(Commons._match_map2, k) + sp.plt[Commons._match_map2[k]] else sp[Commons._match_map[k]] end @@ -82,41 +72,23 @@ Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}") Return the bounding box of a subplot. """ -plotarea(sp::Subplot) = sp.plotarea -plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox) +Commons.plotarea(sp::Subplot) = sp.plotarea +Commons.plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox) Base.size(sp::Subplot) = (1, 1) Base.length(sp::Subplot) = 1 Base.getindex(sp::Subplot, r::Int, c::Int) = sp -leftpad(sp::Subplot) = sp.minpad[1] -toppad(sp::Subplot) = sp.minpad[2] -rightpad(sp::Subplot) = sp.minpad[3] -bottompad(sp::Subplot) = sp.minpad[4] - -function attr!(sp::Subplot; kw...) - plotattributes = KW(kw) - PlotsBase.Commons.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes - if haskey(_subplot_defaults, k) - sp[k] = v - else - @warn "unused key $k in subplot attr" - end - end - sp -end - -PlotsBase.series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] === sp, sp.plt.series_list) -PlotsBase.RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" +RecipesPipeline.is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d" +PlotsBase.series_list(sp::Subplot) = sp.series_list # filter(series -> series.plotattributes[:subplot] ≡ sp, sp.plt.series_list) PlotsBase.ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar" -get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) +Commons.get_ticks(sp::Subplot, s::Symbol) = get_ticks(sp, sp[get_attr_symbol(s, :axis)]) # converts a symbol or string into a Colorant or ColorGradient # and assigns a color automatically get_series_color(c, sp::Subplot, n::Int, seriestype) = - if c === :auto + if c ≡ :auto like_surface(seriestype) ? PlotsBase.cgrad() : _cycle(sp[:color_palette], n) elseif isa(c, Int) _cycle(sp[:color_palette], c) @@ -174,7 +146,7 @@ function _update_subplot_periphery(sp::Subplot, anns::AVec) # handle legend/colorbar sp.attr[:legend_position] = convert_legend_value(sp.attr[:legend_position]) sp.attr[:colorbar] = convert_legend_value(sp.attr[:colorbar]) - if sp.attr[:colorbar] === :legend + if sp.attr[:colorbar] ≡ :legend sp.attr[:colorbar] = sp.attr[:legend_position] end nothing @@ -183,7 +155,7 @@ end function _update_subplot_colors(sp::Subplot) # background colors color_or_nothing!(sp.attr, :background_color_subplot) - sp.attr[:color_palette] = get_color_palette(sp.attr[:color_palette], 30) + sp.attr[:color_palette] = PlotUtils.get_color_palette(sp.attr[:color_palette], 30) color_or_nothing!(sp.attr, :legend_background_color) color_or_nothing!(sp.attr, :background_color_inside) @@ -214,9 +186,9 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) for letter in (:x, :y, :z) data = plotattributes[letter] if ( - letter !== :z && - plotattributes[:seriestype] === :straightline && - any(series[:seriestype] !== :straightline for series in series_list(sp)) && + letter ≢ :z && + plotattributes[:seriestype] ≡ :straightline && + any(series[:seriestype] ≢ :straightline for series in series_list(sp)) && length(data) > 1 && data[1] != data[2] ) @@ -235,7 +207,7 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) data = plotattributes[letter] = Surface(Matrix{Float64}(data.surf)) end expand_extrema!(axis, data) - elseif data !== nothing + elseif data ≢ nothing # TODO: need more here... gotta track the discrete reference value # as well as any coord offset (think of boxplot shape coords... they all # correspond to the same x-value) @@ -248,10 +220,10 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) # expand for fillrange fr = plotattributes[:fillrange] - if fr === nothing && plotattributes[:seriestype] === :bar + if fr ≡ nothing && plotattributes[:seriestype] ≡ :bar fr = 0.0 end - if fr !== nothing && !RecipesPipeline.is3d(plotattributes) + if fr ≢ nothing && !RecipesPipeline.is3d(plotattributes) axis = sp.attr[:yaxis] if typeof(fr) <: Tuple foreach(x -> expand_extrema!(axis, x), fr) @@ -261,11 +233,11 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) end # expand for bar_width - if plotattributes[:seriestype] === :bar + if plotattributes[:seriestype] ≡ :bar dsym = :x data = plotattributes[dsym] - if (bw = plotattributes[:bar_width]) === nothing + if (bw = plotattributes[:bar_width]) ≡ nothing pos = filter(>(0), diff(sort(data))) plotattributes[:bar_width] = bw = Commons._bar_width * ignorenan_minimum(pos) end @@ -275,7 +247,7 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) end # expand for heatmaps - if plotattributes[:seriestype] === :heatmap + if plotattributes[:seriestype] ≡ :heatmap for letter in (:x, :y) data = plotattributes[letter] axis = sp[get_attr_symbol(letter, :axis)] @@ -290,6 +262,29 @@ function PlotsBase.expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) expand_extrema!(sp[:yaxis], (ymin, ymax)) end -Commons.get_size(sp::Subplot) = Commons.get_size(sp.plt) -Commons.get_thickness_scaling(sp::Subplot) = Commons.get_thickness_scaling(sp.plt) -end # Subplots +Commons.get_size(sp::Subplot) = get_size(sp.plt) +Commons.get_thickness_scaling(sp::Subplot) = get_thickness_scaling(sp.plt) + +end # module + +# ------------------------------------------------------------------- + +using .Subplots + +Commons.leftpad(sp::Subplot) = sp.minpad[1] +Commons.toppad(sp::Subplot) = sp.minpad[2] +Commons.rightpad(sp::Subplot) = sp.minpad[3] +Commons.bottompad(sp::Subplot) = sp.minpad[4] + +function attr!(sp::Subplot; kw...) + plotattributes = KW(kw) + PlotsBase.Commons.preprocess_attributes!(plotattributes) + for (k, v) in plotattributes + if haskey(_subplot_defaults, k) + sp[k] = v + else + @warn "unused key $k in subplot attr" + end + end + sp +end diff --git a/PlotsBase/src/Surfaces.jl b/PlotsBase/src/Surfaces.jl index 90c2c7794..12eb688be 100644 --- a/PlotsBase/src/Surfaces.jl +++ b/PlotsBase/src/Surfaces.jl @@ -2,10 +2,11 @@ module Surfaces export SurfaceFunction, Surface -import PlotsBase: PlotsBase, expand_extrema!, Commons -using PlotsBase.Axes: Axis -using RecipesPipeline: AbstractSurface, Surface -using PlotsBase.Commons +import PlotsBase: PlotsBase, expand_extrema! +using ..RecipesPipeline: AbstractSurface, Surface + +using ..Commons +using ..Axes function PlotsBase.expand_extrema!(a::Axis, surf::Surface) ex = a[:extrema] @@ -19,4 +20,9 @@ struct SurfaceFunction <: AbstractSurface end Commons.handle_surface(z::Surface) = permutedims(z.surf) -end + +end # module + +# ------------------------------------------------------------------- + +using .Surfaces diff --git a/PlotsBase/src/Ticks.jl b/PlotsBase/src/Ticks.jl index af37828f5..e5c9f2b9d 100644 --- a/PlotsBase/src/Ticks.jl +++ b/PlotsBase/src/Ticks.jl @@ -1,24 +1,31 @@ module Ticks -export get_ticks, _has_ticks, _transform_ticks, get_minor_ticks, no_minor_intervals -using PlotsBase.Commons -using PlotsBase.Dates +export _has_ticks, _transform_ticks, get_minor_ticks +export no_minor_intervals, num_minor_intervals, ticks_type + +using ..Commons +using ..Dates const DEFAULT_MINOR_INTERVALS = Ref(5) # 5 intervals -> 4 ticks +ticks_type(ticks::AVec{<:Real}) = :ticks +ticks_type(ticks::AVec{<:AbstractString}) = :labels +ticks_type(ticks::Tuple{<:Union{AVec,Tuple},<:Union{AVec,Tuple}}) = :ticks_and_labels +ticks_type(ticks) = :invalid + # get_ticks from axis symbol :x, :y, or :z -get_ticks(ticks::NTuple{2,Any}, args...) = ticks -get_ticks(::Nothing, cvals::T, args...) where {T} = T[], String[] -get_ticks(ticks::Bool, args...) = +Commons.get_ticks(ticks::NTuple{2,Any}, args...) = ticks +Commons.get_ticks(::Nothing, cvals::T, args...) where {T} = T[], String[] +Commons.get_ticks(ticks::Bool, args...) = ticks ? get_ticks(:auto, args...) : get_ticks(nothing, args...) -get_ticks(::T, args...) where {T} = +Commons.get_ticks(::T, args...) where {T} = throw(ArgumentError("Unknown ticks type in get_ticks: $T")) # do not specify array item type to also catch e.g. "xlabel=[]" and "xlabel=([],[])" _has_ticks(v::AVec) = !isempty(v) _has_ticks(t::Tuple{AVec,AVec}) = !isempty(t[1]) -_has_ticks(s::Symbol) = s !== :none +_has_ticks(s::Symbol) = s ≢ :none _has_ticks(b::Bool) = b _has_ticks(::Nothing) = false _has_ticks(::Any) = true @@ -44,11 +51,11 @@ function num_minor_intervals(axis) end no_minor_intervals(axis) = - if (n_intervals = axis[:minorticks]) === false + if (n_intervals = axis[:minorticks]) ≡ false true # must be tested with `===` since Bool <: Integer elseif n_intervals ∈ (:none, nothing) true - elseif (n_intervals === :auto && !axis[:minorgrid]) + elseif (n_intervals ≡ :auto && !axis[:minorgrid]) true else false @@ -97,4 +104,8 @@ function get_minor_ticks(sp, axis, ticks_and_labels) minorticks[amin .≤ minorticks .≤ amax] end -end # Ticks +end # module + +# ------------------------------------------------------------------- + +using .Ticks diff --git a/PlotsBase/src/abstract_backend.jl b/PlotsBase/src/abstract_backend.jl deleted file mode 100644 index ba6530f33..000000000 --- a/PlotsBase/src/abstract_backend.jl +++ /dev/null @@ -1,205 +0,0 @@ -const _plots_project = Pkg.Types.read_package(normpath(@__DIR__, "..", "Project.toml")) -const _current_plots_version = _plots_project.version -const _plots_compats = _plots_project.compat - -const _backendSymbol = Dict{DataType,Symbol}(NoBackend => :none) -const _backendType = Dict{Symbol,DataType}(:none => NoBackend) -const _backend_packages = (gaston = :Gaston, gr = :GR, unicodeplots = :UnicodePlots, pgfplotsx = :PGFPlotsX, pythonplot = :PythonPlot, plotly = :Plotly, plotlyjs = :PlotlyJS, hdf5 = :HDF5) -const _initialized_backends = Set{Symbol}() -const _backends = keys(_backend_packages) - -const _plots_deps = let toml = Pkg.TOML.parsefile(normpath(@__DIR__, "..", "Project.toml")) - merge(toml["deps"], toml["extras"]) -end - -function _check_installed(backend::Union{Module,AbstractString,Symbol}; warn = true) - sym = Symbol(lowercase(string(backend))) - if warn && !haskey(_backend_packages, sym) - @warn "backend `$sym` is not compatible with `Plots`." - return - end - # lowercase -> CamelCase, falling back to the given input for `PlotlyBase` ... - str = string(get(_backend_packages, sym, backend)) - str == "Plotly" && (str *= "Base") # FIXME: `Plots` inconsistency, `plotly` should be named `plotlybase` - # check supported - if warn && !haskey(_plots_compats, str) - @warn "backend `$str` is not compatible with `Plots`." - return - end - # check installed - pkg_id = Base.identify_package(str) - version = if pkg_id === nothing - nothing - else - get(Pkg.dependencies(), pkg_id.uuid, (; version = nothing)).version - end - version === nothing && @warn "backend `$str` is not installed." - version -end - -_create_backend_figure(plt::Plot) = nothing -_initialize_subplot(plt::Plot, sp::Subplot) = nothing - -_series_added(plt::Plot, series::Series) = nothing -_series_updated(plt::Plot, series::Series) = nothing - -_before_layout_calcs(plt::Plot) = nothing - -title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt -guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt - -closeall(::AbstractBackend) = nothing - -mutable struct CurrentBackend - sym::Symbol - pkg::AbstractBackend -end - -CurrentBackend(sym::Symbol) = CurrentBackend(sym, _backend_instance(sym)) - -""" -Returns the current plotting package name. Initializes package on first call. -""" -backend() = CURRENT_BACKEND.pkg - -"Returns a list of supported backends" -backends() = _backends - -backend_name() = CURRENT_BACKEND.sym -_backend_instance(sym::Symbol)::AbstractBackend = _backendType[sym]() - -backend_package_name(sym::Symbol = backend_name()) = get(_backend_packages, sym, :None) - -# Traits to be implemented by the extensions -backend_name(::AbstractBackend) = @info "`backend_name(::Backend) not implemented." -backend_package_name(::AbstractBackend) = - @info "`backend_package_name(::Backend) not implemented." - -initialized(sym::Symbol) = sym ∈ _initialized_backends - -""" -Set the plot backend. -""" -function backend(pkg::AbstractBackend) - sym = backend_name(pkg) - if !initialized(sym) - _initialize_backend(pkg) - push!(_initialized_backends, sym) - end - CURRENT_BACKEND.sym = sym - CURRENT_BACKEND.pkg = pkg - pkg -end - -backend(sym::Symbol) = - if sym in _backends - if initialized(sym) - backend(_backend_instance(sym)) - else - name = backend_package_name(sym) - @warn "`:$sym` is not initialized, import it first to trigger the extension --- e.g. $(name === nothing ? '`' : string("`import ", name, ";")) $sym()`." - backend() - end - else - @error "Unsupported backend $sym" - end - -function get_backend_module(name::Symbol) - ext = Base.get_extension(@__MODULE__, Symbol(name, "Ext")) - if !isnothing(ext) - return ext, ext.get_concrete_backend() - else - @error "Extension $name is not loaded yet, run `import $name` to load it" - return nothing - end -end - -# -- Create backend init functions by hand as the corresponding structs do not -# exist yet - -for be in _backends - @eval begin - function $be(; kw...) - default(; reset = false, kw...) - backend(Symbol($be)) - end - export $be - end -end - -# --------------------------------------------------------- -# create the various `is_xxx_supported` and `supported_xxxs` methods -# these methods should be overloaded (dispatched) by each backend in its init_code -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - @eval begin - $f1(::AbstractBackend, $s) = false - $f1(be::AbstractBackend, $s::AbstractVector) = all(v -> $f1(be, v), $s) - $f1($s) = $f1(backend(), $s) - $f2() = $f2(backend()) - end -end -# ----------------------------------------------------------------------------- - -should_warn_on_unsupported(::AbstractBackend) = _plot_defaults[:warn_on_unsupported] - -const _already_warned = Dict{Symbol,Set{Symbol}}() -function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) - _to_warn = Set{Symbol}() - bend = backend_name(pkg) - already_warned = get!(_already_warned, bend) do - Set{Symbol}() - end - extra_kwargs = Dict{Symbol,Any}() - for k in PlotsBase.explicitkeys(plotattributes) - (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue - k in Commons._suppress_warnings && continue - if ismissing(default(k)) - extra_kwargs[k] = pop_kw!(plotattributes, k) - elseif plotattributes[k] != default(k) - k in already_warned || push!(_to_warn, k) - end - end - - if !isempty(_to_warn) && - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) - for k in sort(collect(_to_warn)) - push!(already_warned, k) - if k in keys(Commons._deprecated_attributes) - @warn """ - Keyword argument `$k` is deprecated. - Please use `$(Commons._deprecated_attributes[k])` instead. - """ - else - @warn "Keyword argument $k not supported with $pkg. Choose from: $(join(supported_attrs(pkg), ", "))" - end - end - end - extra_kwargs -end - -function warn_on_unsupported(pkg::AbstractBackend, plotattributes) - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return - is_seriestype_supported(pkg, plotattributes[:seriestype]) || - @warn "seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))" - is_style_supported(pkg, plotattributes[:linestyle]) || - @warn "linestyle $(plotattributes[:linestyle]) is unsupported with $pkg. Choose from: $(supported_styles(pkg))" - is_marker_supported(pkg, plotattributes[:markershape]) || - @warn "markershape $(plotattributes[:markershape]) is unsupported with $pkg. Choose from: $(supported_markers(pkg))" -end - -function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW) - get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return - for k in (:xscale, :yscale, :zscale, :scale) - if haskey(plotattributes, k) - v = plotattributes[k] - if !all(is_scale_supported.(Ref(pkg), v)) - @warn """ - scale $v is unsupported with $pkg. - Choose from: $(supported_scales(pkg)) - """ - end - end - end -end diff --git a/PlotsBase/src/alignment.jl b/PlotsBase/src/alignment.jl index 1588a6dcd..0969a5837 100644 --- a/PlotsBase/src/alignment.jl +++ b/PlotsBase/src/alignment.jl @@ -17,7 +17,7 @@ text_size(lab::PlotText, sz::Number, rot::Number = 0) = text_size(length(lab.str # account for the size/length/rotation of tick labels function tick_padding(sp::Subplot, axis::Axis) - if (ticks = get_ticks(sp, axis)) === nothing + if (ticks = get_ticks(sp, axis)) ≡ nothing 0mm else vals, labs = ticks @@ -26,7 +26,7 @@ function tick_padding(sp::Subplot, axis::Axis) longest_label = maximum(length(lab) for lab in labs) # generalize by "rotating" y labels - rot = axis[:rotation] + (axis[:letter] === :y ? 90 : 0) + rot = axis[:rotation] + (axis[:letter] ≡ :y ? 90 : 0) # # we need to compute the size of the ticks generically # # this means computing the bounding box and then getting the width/height diff --git a/PlotsBase/src/animation.jl b/PlotsBase/src/animation.jl index 434c747b1..859caf35b 100644 --- a/PlotsBase/src/animation.jl +++ b/PlotsBase/src/animation.jl @@ -235,9 +235,9 @@ function _animate(forloop::Expr, args...; type::Symbol = :none) push!(block.args, :($countersym += 1)) # add a final call to `gif(anim)`? - retval = if type === :gif + retval = if type ≡ :gif :(PlotsBase.gif($animsym; $(animationsKwargs...))) - elseif type === :apng + elseif type ≡ :apng :(PlotsBase.apng($animsym; $(animationsKwargs...))) else animsym diff --git a/PlotsBase/src/axes_utils.jl b/PlotsBase/src/axes_utils.jl index 46e0a461a..735ce0c26 100644 --- a/PlotsBase/src/axes_utils.jl +++ b/PlotsBase/src/axes_utils.jl @@ -1,6 +1,6 @@ const _label_func = Dict{Symbol,Function}(:log10 => x -> "10^$x", :log2 => x -> "2^$x", :ln => x -> "e^$x") -labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string) +labelfunc(scale::Symbol, ::AbstractBackend) = get(_label_func, scale, string) const _label_func_tex = Dict{Symbol,Function}( :log10 => x -> "10^{$x}", @@ -23,7 +23,7 @@ function optimal_ticks_and_labels(ticks, alims, scale, formatter) # rather than on the input format # TODO: maybe: non-trivial scale (:ln, :log2, :log10) for date/datetime - if ticks === nothing && noop + if ticks ≡ nothing && noop if formatter == RecipesPipeline.dateformatter # optimize_datetime_ticks returns ticks and labels(!) based on # integers/floats corresponding to the DateTime type. Thus, the axes @@ -41,7 +41,7 @@ function optimal_ticks_and_labels(ticks, alims, scale, formatter) end # get a list of well-laid-out ticks - scaled_ticks = if ticks === nothing + scaled_ticks = if ticks ≡ nothing optimize_ticks( sf(amin), sf(amax); @@ -75,12 +75,12 @@ function optimal_ticks_and_labels(ticks, alims, scale, formatter) unscaled_ticks, labels end -Ticks.get_ticks(ticks::Symbol, cvals::T, dvals, args...) where {T} = - if ticks === :none +Commons.get_ticks(ticks::Symbol, cvals::T, dvals, args...) where {T} = + if ticks ≡ :none T[], String[] elseif !isempty(dvals) n = length(dvals) - if ticks === :all || n < 16 + if ticks ≡ :all || n < 16 cvals, string.(dvals) else Δ = ceil(Int, n / 10) @@ -91,9 +91,9 @@ Ticks.get_ticks(ticks::Symbol, cvals::T, dvals, args...) where {T} = optimal_ticks_and_labels(nothing, args...) end -Ticks.get_ticks(ticks::AVec, cvals, dvals, args...) = +Commons.get_ticks(ticks::AVec, cvals, dvals, args...) = optimal_ticks_and_labels(ticks, args...) -Ticks.get_ticks(ticks::Int, dvals, cvals, args...) = +Commons.get_ticks(ticks::Int, dvals, cvals, args...) = if isempty(dvals) optimal_ticks_and_labels(ticks, args...) else @@ -101,18 +101,18 @@ Ticks.get_ticks(ticks::Int, dvals, cvals, args...) = cvals[rng], string.(dvals[rng]) end -function get_labels(formatter::Symbol, scaled_ticks, scale) +get_labels(formatter::Symbol, scaled_ticks, scale) = if formatter in (:auto, :plain, :scientific, :engineering) - return map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, formatter)) - elseif formatter === :latex - return map( + map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, formatter)) + elseif formatter ≡ :latex + map( l -> string("\$", replace(convert_sci_unicode(l), '×' => "\\times"), "\$"), get_labels(:auto, scaled_ticks, scale), ) - elseif formatter === :none - return String[] + elseif formatter ≡ :none + String[] end -end + function get_labels(formatter::Function, scaled_ticks, scale) sf, invsf, _ = scale_inverse_scale_func(scale) fticks = map(formatter ∘ invsf, scaled_ticks) @@ -120,7 +120,7 @@ function get_labels(formatter::Function, scaled_ticks, scale) # CategoricalArrays's recipe gives "missing" label to those filter!(!ismissing, fticks) eltype(fticks) <: Number && return get_labels(:auto, map(sf, fticks), scale) - return fticks + fticks end # Ticks getter functions @@ -190,7 +190,7 @@ end # whenever we have discrete values, we automatically set the ticks to match. # we return (continuous_value, discrete_index) discrete_value!(plotattributes, letter::Symbol, dv) = - let l = if plotattributes[:permute] !== :none + let l = if plotattributes[:permute] ≢ :none filter(!=(letter), plotattributes[:permute]) |> only else letter @@ -256,21 +256,21 @@ function add_major_or_minor_segments_2d( factor, cond, ) - ticks === nothing && return + ticks ≡ nothing && return if cond f, invf = scale_inverse_scale_func(oax[:scale]) - tick_start, tick_stop = if sp[:framestyle] === :origin + tick_start, tick_stop = if sp[:framestyle] ≡ :origin oamin, oamax = oamM t = invf(f(0) + factor * (f(oamax) - f(oamin))) (-t, t) else - ticks_in = ax[:tick_direction] === :out ? -1 : 1 + ticks_in = ax[:tick_direction] ≡ :out ? -1 : 1 oa1, oa2 = oas t = invf(f(oa1) + factor * (f(oa2) - f(oa1)) * ticks_in) (oa1, t) end end - isy = ax[:letter] === :y + isy = ax[:letter] ≡ :y for tick in ticks (ax[:showaxis] && cond) && push!( tick_segments, @@ -299,23 +299,23 @@ function axis_drawing_info(sp, letter) segments, tick_segments, grid_segments, minorgrid_segments, border_segments = map(_ -> Segments(2), 1:5) - if sp[:framestyle] !== :none - isy = letter === :y + if sp[:framestyle] ≢ :none + isy = letter ≡ :y oa1, oa2 = oas = if sp[:framestyle] in (:origin, :zerolines) 0, 0 else xor(ax[:mirror], oax[:flip]) ? reverse(oamM) : oamM end if ax[:showaxis] - if sp[:framestyle] !== :grid + if sp[:framestyle] ≢ :grid push!(segments, reverse_if((amin, oa1), isy), reverse_if((amax, oa1), isy)) # don't show the 0 tick label for the origin framestyle if ( - sp[:framestyle] === :origin && + sp[:framestyle] ≡ :origin && ticks ∉ (:none, nothing, false) && length(ticks) > 1 ) - if (i = findfirst(==(0), ticks[1])) !== nothing + if (i = findfirst(==(0), ticks[1])) ≢ nothing deleteat!(ticks[1], i) deleteat!(ticks[2], i) end @@ -329,7 +329,7 @@ function axis_drawing_info(sp, letter) ) end if ax[:ticks] ∉ (:none, nothing, false) - ax_length = letter === :x ? height(sp.plotarea).value : width(sp.plotarea).value + ax_length = letter ≡ :x ? height(sp.plotarea).value : width(sp.plotarea).value # add major grid segments add_major_or_minor_segments_2d( @@ -343,9 +343,9 @@ function axis_drawing_info(sp, letter) tick_segments, grid_segments, grid_factor_2d[] / ax_length, - ax[:tick_direction] !== :none, + ax[:tick_direction] ≢ :none, ) - if sp[:framestyle] === :box + if sp[:framestyle] ≡ :box add_major_or_minor_segments_2d( sp, ax, @@ -357,7 +357,7 @@ function axis_drawing_info(sp, letter) tick_segments, grid_segments, grid_factor_2d[] / ax_length, - ax[:tick_direction] !== :none, + ax[:tick_direction] ≢ :none, ) end @@ -376,7 +376,7 @@ function axis_drawing_info(sp, letter) grid_factor_2d[] / 2ax_length, true, ) - if sp[:framestyle] === :box + if sp[:framestyle] ≡ :box add_major_or_minor_segments_2d( sp, ax, @@ -419,16 +419,16 @@ function add_major_or_minor_segments_3d( factor, cond, ) - ticks === nothing && return + ticks ≡ nothing && return if cond f, invf = scale_inverse_scale_func(nax[:scale]) - tick_start, tick_stop = if sp[:framestyle] === :origin + tick_start, tick_stop = if sp[:framestyle] ≡ :origin namin, namax = namM t = invf(f(0) + factor * (f(namax) - f(namin))) (-t, t) else na0, na1 = nas - ticks_in = ax[:tick_direction] === :out ? -1 : 1 + ticks_in = ax[:tick_direction] ≡ :out ? -1 : 1 t = invf(f(na0) + factor * (f(na1) - f(na0)) * ticks_in) (na0, t) end @@ -467,12 +467,12 @@ function axis_drawing_info_3d(sp, letter) segments, tick_segments, grid_segments, minorgrid_segments, border_segments = map(_ -> Segments(3), 1:5) - if sp[:framestyle] !== :none # && letter === :x + if sp[:framestyle] ≢ :none # && letter ≡ :x na0, na1 = nas = if sp[:framestyle] in (:origin, :zerolines) 0, 0 else - reverse_if(reverse_if(namM, letter === :y), xor(ax[:mirror], nax[:flip])) + reverse_if(reverse_if(namM, letter ≡ :y), xor(ax[:mirror], nax[:flip])) end fa0, fa1 = fas = if sp[:framestyle] in (:origin, :zerolines) 0, 0 @@ -480,7 +480,7 @@ function axis_drawing_info_3d(sp, letter) reverse_if(famM, xor(ax[:mirror], fax[:flip])) end if ax[:showaxis] - if sp[:framestyle] !== :grid + if sp[:framestyle] ≢ :grid push!( segments, sort_3d_axes(amin, na0, fa0, letter), @@ -488,11 +488,11 @@ function axis_drawing_info_3d(sp, letter) ) # don't show the 0 tick label for the origin framestyle if ( - sp[:framestyle] === :origin && + sp[:framestyle] ≡ :origin && ticks ∉ (:none, nothing, false) && length(ticks) > 1 ) - if (i = findfirst(==(0), ticks[1])) !== nothing + if (i = findfirst(==(0), ticks[1])) ≢ nothing deleteat!(ticks[1], i) deleteat!(ticks[2], i) end @@ -519,7 +519,7 @@ function axis_drawing_info_3d(sp, letter) tick_segments, grid_segments, grid_factor_3d[], - ax[:tick_direction] !== :none, + ax[:tick_direction] ≢ :none, ) # add minor grid segments diff --git a/PlotsBase/src/backends.jl b/PlotsBase/src/backends.jl new file mode 100644 index 000000000..c1817a502 --- /dev/null +++ b/PlotsBase/src/backends.jl @@ -0,0 +1,259 @@ +const _default_supported_syms = :attr, :seriestype, :marker, :style, :scale + +_f1_sym(sym::Symbol) = Symbol("is_$(sym)_supported") +_f2_sym(sym::Symbol) = Symbol("supported_$(sym)s") + +struct NoBackend <: AbstractBackend end + +backend_name(::NoBackend) = :none +should_warn_on_unsupported(::NoBackend) = false + +for sym in _default_supported_syms + @eval begin + $(_f1_sym(sym))(::NoBackend, $sym::Symbol) = true + $(_f2_sym(sym))(::NoBackend) = Commons.$(Symbol("_all_$(sym)s")) + end +end + +_display(::Plot{NoBackend}) = + @warn "No backend activated yet. Load the backend library and call the activation function to do so.\nE.g. `import GR; gr()` activates the GR backend." + +const _backendSymbol = Dict{DataType,Symbol}(NoBackend => :none) +const _backendType = Dict{Symbol,DataType}(:none => NoBackend) +const _backend_packages = (unicodeplots = :UnicodePlots, pythonplot = :PythonPlot, pgfplotsx = :PGFPlotsX, plotlyjs = :PlotlyJS, gaston = :Gaston, plotly = nothing, none = nothing, hdf5 = :HDF5, gr = :GR) +const _supported_backends = keys(_backend_packages) +const _initialized_backends = Set([:none]) + +function _check_installed(pkg::Union{Module,AbstractString,Symbol}; warn = true) + name = Symbol(lowercase(string(pkg))) + if warn && !haskey(_backend_packages, name) + @warn "backend `$name` is not compatible with `PlotsBase`." + return + end + # lowercase -> CamelCase, falling back to the given input for `PlotlyBase` ... + pkg_str = string(get(_backend_packages, name, pkg)) + pkg_str == "Plotly" && (pkg_str *= "Base") # FIXME: `PlotsBase` inconsistency, `plotly` should be named `plotlybase` + # check supported + if warn && !haskey(_compat, pkg_str) + @warn "package `$pkg_str` is not compatible with `PlotsBase`." + return + end + # check installed + version = if (pkg_id = Base.identify_package(pkg_str)) ≡ nothing + nothing + else + get(Pkg.dependencies(), pkg_id.uuid, (; version = nothing)).version + end + version ≡ nothing && @warn "`package $pkg_str` is not installed." + version +end + +_create_backend_figure(plt::Plot) = nothing +_initialize_subplot(plt::Plot, sp::Subplot) = nothing + +_series_added(plt::Plot, series::Series) = nothing +_series_updated(plt::Plot, series::Series) = nothing + +_before_layout_calcs(plt::Plot) = nothing + +title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt +guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt + +closeall(::AbstractBackend) = nothing + +mutable struct CurrentBackend + name::Symbol + instance::AbstractBackend +end + +@inline backend_type(name::Symbol) = _backendType[name] +@inline backend_instance(name::Symbol) = backend_type(name)() +@inline backend(type::Type{<:AbstractBackend}) = backend(type()) + +CurrentBackend(name::Symbol) = CurrentBackend(name, backend_instance(name)) + +const CURRENT_BACKEND = CurrentBackend(:none) + +"returns the current plotting package backend. Initializes package on first call." +@inline backend() = CURRENT_BACKEND.instance + +"returns a list of supported backends." +@inline backends() = _supported_backends + +@inline backend_name() = CURRENT_BACKEND.name +@inline backend_package_name(name::Symbol = backend_name()) = + get(_backend_packages, name, nothing) + +# Traits to be implemented by the extensions +backend_name(::AbstractBackend) = @info "`backend_name(::Backend) not implemented." +backend_package_name(::AbstractBackend) = + @info "`backend_package_name(::Backend) not implemented." + +"set the plot backend." +function backend(instance::AbstractBackend) + name = backend_name(instance) + if name ∈ _supported_backends + CURRENT_BACKEND.name = name + CURRENT_BACKEND.instance = instance + else + @error "Unsupported backend $name" + end + instance +end + +backend(name::Symbol) = + if name ∈ _supported_backends + if name ∈ _initialized_backends + backend(backend_type(name)) + else + pkg_name = backend_package_name(name) + @warn "`:$name` is not initialized, import it first to trigger the extension --- e.g. `$(pkg_name ≡ nothing ? "" : "import $pkg_name; ")$name()`." + backend() + end + else + @error "Unsupported backend $name" + end + +function get_backend_module(pkg_name::Symbol) + ext = Base.get_extension(@__MODULE__, Symbol("$(pkg_name)Ext")) + concrete_backend = if ext ≡ nothing + @error "Extension $pkg_name is not loaded yet, run `import $pkg_name` to load it" + nothing + else + ext.get_concrete_backend() + end + ext, concrete_backend +end + +# create backend init functions by hand as the corresponding structs do not exist yet +for be in _supported_backends + @eval begin + function $be(; kw...) + default(; reset = false, kw...) + backend(Symbol($be)) + end + export $be + end +end + +# create the various `is_xxx_supported` and `supported_xxxs` methods +# these methods should be overloaded (dispatched) by each backend in its init_code +for sym in _default_supported_syms + f1 = _f1_sym(sym) + f2 = _f2_sym(sym) + @eval begin + $f1(::AbstractBackend, $sym) = false + $f1(be::AbstractBackend, $sym::AbstractVector) = all(v -> $f1(be, v), $sym) + $f1($sym) = $f1(backend(), $sym) + $f2() = $f2(backend()) + end +end + +function backend_defines(be_type::Symbol, be::Symbol) + be_sym = QuoteNode(be) + blk = Expr( + :block, + :(get_concrete_backend() = $be_type), + :(PlotsBase.backend_name(::$be_type)::Symbol = $be_sym), + :( + PlotsBase.backend_package_name(::$be_type)::Symbol = + PlotsBase.backend_package_name($be_sym) + ), + ) + #= + Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods, + results in: + PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool + ... + PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol} + ... + PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} + =# + for sym in _default_supported_syms + be_syms = Symbol("_$(be)_$(sym)s") + push!( + blk.args, + :(PlotsBase.$(_f1_sym(sym))(::$be_type, $sym::Symbol)::Bool = $sym in $be_syms), + :(PlotsBase.$(_f2_sym(sym))(::$be_type)::Vector = sort!(collect($be_syms))), + ) + end + blk +end + +"extra init step for an extension" +extension_init(::AbstractBackend) = nothing + +"generate extension `__init__` function, and common defines" +macro extension_static(be_type, be) + be_sym = QuoteNode(be) + quote + $(PlotsBase.backend_defines(be_type, be)) + function __init__() + PlotsBase._backendType[$be_sym] = $be_type + PlotsBase._backendSymbol[$be_type] = $be_sym + push!(PlotsBase._initialized_backends, $be_sym) + ccall(:jl_generating_output, Cint, ()) == 1 && return + PlotsBase.extension_init($be_type()) + @debug "Initialized $be_type backend in PlotsBase; run `$be()` to activate it." + end + end |> esc +end + +should_warn_on_unsupported(::AbstractBackend) = _plot_defaults[:warn_on_unsupported] + +const _already_warned = Dict{Symbol,Set{Symbol}}() +function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) + _to_warn = Set{Symbol}() + bend = backend_name(pkg) + already_warned = get!(() -> Set{Symbol}(), _already_warned, bend) + extra_kwargs = Dict{Symbol,Any}() + for k in PlotsBase.explicitkeys(plotattributes) + (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue + k in Commons._suppress_warnings && continue + if ismissing(default(k)) + extra_kwargs[k] = pop_kw!(plotattributes, k) + elseif plotattributes[k] != default(k) + k in already_warned || push!(_to_warn, k) + end + end + + if !isempty(_to_warn) && + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) + for k in sort!(collect(_to_warn)) + push!(already_warned, k) + if k in keys(Commons._deprecated_attributes) + @warn """ + Keyword argument `$k` is deprecated. + Please use `$(Commons._deprecated_attributes[k])` instead. + """ + else + @warn "Keyword argument $k not supported with $pkg. Choose from: $(join(supported_attrs(pkg), ", "))" + end + end + end + extra_kwargs +end + +function warn_on_unsupported(pkg::AbstractBackend, plotattributes) + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return + is_seriestype_supported(pkg, plotattributes[:seriestype]) || + @warn "seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))" + is_style_supported(pkg, plotattributes[:linestyle]) || + @warn "linestyle $(plotattributes[:linestyle]) is unsupported with $pkg. Choose from: $(supported_styles(pkg))" + is_marker_supported(pkg, plotattributes[:markershape]) || + @warn "markershape $(plotattributes[:markershape]) is unsupported with $pkg. Choose from: $(supported_markers(pkg))" +end + +function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW) + get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return + for k in (:xscale, :yscale, :zscale, :scale) + haskey(plotattributes, k) || continue + v = plotattributes[k] + if !all(is_scale_supported.(Ref(pkg), v)) + @warn """ + scale $v is unsupported with $pkg. + Choose from: $(supported_scales(pkg)) + """ + end + end +end diff --git a/PlotsBase/src/backends/nobackend.jl b/PlotsBase/src/backends/nobackend.jl deleted file mode 100644 index 0135b9c1b..000000000 --- a/PlotsBase/src/backends/nobackend.jl +++ /dev/null @@ -1,15 +0,0 @@ -struct NoBackend <: AbstractBackend end - -backend_name(::NoBackend) = :none - -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - @eval begin - $f1(::NoBackend, $s::Symbol) = true - $f2(::NoBackend) = $(getproperty(Commons, Symbol("_all_", s, 's'))) - end -end - -_display(::Plot{NoBackend}) = - @info "No backend activated yet. Load the backend library and call the activation function to do so.\nE.g. `import GR; gr()` activates the GR backend." diff --git a/PlotsBase/src/examples.jl b/PlotsBase/src/examples.jl index 96c40facc..f120e2360 100644 --- a/PlotsBase/src/examples.jl +++ b/PlotsBase/src/examples.jl @@ -460,7 +460,7 @@ const _examples = PlotExample[ ), PlotExample( # 29 "Layouts, margins, label rotation, title location", - :(using PlotsBase.PlotMeasures), # for Measures, e.g. mm and px + :(using PlotsBase.Commons), # for Measures, e.g. mm and px quote plot( rand(100, 6), @@ -964,9 +964,9 @@ const _examples = PlotExample[ wireframe( args..., title = "wire-flip-$ax", - xflip = ax === :x, - yflip = ax === :y, - zflip = ax === :z; + xflip = ax ≡ :x, + yflip = ax ≡ :y, + zflip = ax ≡ :z; kw..., ), ) @@ -978,9 +978,9 @@ const _examples = PlotExample[ wireframe( args..., title = "wire-mirror-$ax", - xmirror = ax === :x, - ymirror = ax === :y, - zmirror = ax === :z; + xmirror = ax ≡ :x, + ymirror = ax ≡ :y, + zmirror = ax ≡ :z; kw..., ), ) @@ -1254,9 +1254,11 @@ const _examples = PlotExample[ ] # Some constants for PlotDocs and PlotReferenceImages -_animation_examples = [2, 31] +_animation_examples = [02, 31] _backend_skips = Dict( - :gr => [25, 30], # TODO: add back when StatsPlots is available + :none => Int[], + :pythonplot => Int[], + :gr => [25, 30], # TODO: add back when StatsPlots is available :plotlyjs => [ 21, 24, @@ -1274,7 +1276,7 @@ _backend_skips = Dict( 66, # bar: vector-valued `color` unsupported ], :pgfplotsx => [ - 6, # images + 06, # images 16, # pgfplots thinks the upper panel is too small 32, # spy 49, # polar heatmap @@ -1283,8 +1285,8 @@ _backend_skips = Dict( 62, # fillstyle unsupported ], :unicodeplots => [ - 5, # limits issue - 6, # embedded images supported, but requires `using ImageInTerminal`, disable for docs + 05, # limits issue + 06, # embedded images supported, but requires `using ImageInTerminal`, disable for docs 16, # nested layout unsupported 21, # custom markers unsupported 26, # nested layout unsupported @@ -1313,8 +1315,6 @@ _backend_skips = Dict( ) _backend_skips[:plotly] = _backend_skips[:plotlyjs] -_backend_skips[:pythonplot] = Int[] - # --------------------------------------------------------------------------------- # replace `f(args...)` with `f(rng, args...)` for `f ∈ (rand, randn)` replace_rand(ex) = ex @@ -1371,16 +1371,16 @@ function test_examples( PlotsBase.Commons.debug!($debug) backend($(QuoteNode(pkgname))) rng = $rng - rng === nothing || Random.seed!(rng, PlotsBase.PLOTS_SEED) + rng ≡ nothing || Random.seed!(rng, PlotsBase.PLOTS_SEED) theme(:default) end) - (imp = _examples[i].imports) === nothing || Base.eval(m, imp) + (imp = _examples[i].imports) ≡ nothing || Base.eval(m, imp) exprs = _examples[i].exprs - rng === nothing || (exprs = PlotsBase.replace_rand(exprs)) + rng ≡ nothing || (exprs = PlotsBase.replace_rand(exprs)) Base.eval(m, exprs) disp && Base.eval(m, :(gui(current()))) - callback === nothing || callback(m, pkgname, i) + callback ≡ nothing || callback(m, pkgname, i) m.PlotsBase.current() end @@ -1415,7 +1415,7 @@ function test_examples( end # COV_EXCL_STOP end - sleep === nothing || Base.sleep(sleep) + sleep ≡ nothing || Base.sleep(sleep) end plts end diff --git a/PlotsBase/src/layouts.jl b/PlotsBase/src/layouts.jl index 2f80680f0..b43ad09c3 100644 --- a/PlotsBase/src/layouts.jl +++ b/PlotsBase/src/layouts.jl @@ -1,123 +1,12 @@ -# NOTE: (0,0) is the top-left !!! - -to_pixels(m::AbsoluteLength) = m.value / 0.254 - -origin(bbox::BoundingBox) = left(bbox) + width(bbox) / 2, top(bbox) + height(bbox) / 2 -left(bbox::BoundingBox) = bbox.x0[1] -top(bbox::BoundingBox) = bbox.x0[2] -right(bbox::BoundingBox) = left(bbox) + width(bbox) -bottom(bbox::BoundingBox) = top(bbox) + height(bbox) -Base.size(bbox::BoundingBox) = (width(bbox), height(bbox)) - -# Base.:*{T,N}(m1::Length{T,N}, m2::Length{T,N}) = Length{T,N}(m1.value * m2.value) -ispositive(m::Measure) = m.value > 0 - -# union together bounding boxes -function Base.:+(bb1::BoundingBox, bb2::BoundingBox) - # empty boxes don't change the union - ispositive(width(bb1)) || return bb2 - ispositive(height(bb1)) || return bb2 - ispositive(width(bb2)) || return bb1 - ispositive(height(bb2)) || return bb1 - - l = min(left(bb1), left(bb2)) - t = min(top(bb1), top(bb2)) - r = max(right(bb1), right(bb2)) - b = max(bottom(bb1), bottom(bb2)) - BoundingBox(l, t, r - l, b - t) -end - -# convert x,y coordinates from absolute coords to percentages... -# returns x_pct, y_pct -function xy_mm_to_pcts(x::AbsoluteLength, y::AbsoluteLength, figw, figh, flipy = true) - xmm, ymm = x.value, y.value - if flipy - ymm = figh.value - ymm # flip y when origin in bottom-left - end - xmm / figw.value, ymm / figh.value -end - -# convert a bounding box from absolute coords to percentages... -# returns an array of percentages of figure size: [left, bottom, width, height] -function bbox_to_pcts(bb::BoundingBox, figw, figh, flipy = true) - mms = Float64[f(bb).value for f in (left, bottom, width, height)] - if flipy - mms[2] = figh.value - mms[2] # flip y when origin in bottom-left - end - mms ./ Float64[figw.value, figh.value, figw.value, figh.value] -end - -Base.show(io::IO, bbox::BoundingBox) = print( - io, - "BBox{l,t,r,b,w,h = $(left(bbox)),$(top(bbox)), $(right(bbox)),$(bottom(bbox)), $(width(bbox)),$(height(bbox))}", -) - -# ----------------------------------------------------------- -# AbstractLayout - -Base.show(io::IO, layout::AbstractLayout) = print(io, "$(typeof(layout))$(size(layout))") - -make_measure_hor(n::Number) = n * w -make_measure_hor(m::Measure) = m - -make_measure_vert(n::Number) = n * h -make_measure_vert(m::Measure) = m - """ - bbox(x, y, w, h [,originargs...]) - bbox(layout) + grid(args...; kw...) -Create a bounding box for plotting +Create a grid layout for subplots. `args` specify the dimensions, e.g. +`grid(3,2, widths = (0.6,0.4))` creates a grid with three rows and two +columns of different width. """ -function bbox(x, y, w, h, oarg1::Symbol, originargs::Symbol...) - oargs = vcat(oarg1, originargs...) - orighor = :left - origver = :top - for oarg in oargs - if oarg === :center - orighor = origver = oarg - elseif oarg in (:left, :right, :hcenter) - orighor = oarg - elseif oarg in (:top, :bottom, :vcenter) - origver = oarg - else - @warn "Unused origin arg in bbox construction: $oarg" - end - end - bbox(x, y, w, h; h_anchor = orighor, v_anchor = origver) -end - -# create a new bbox -function bbox(x, y, width, height; h_anchor = :left, v_anchor = :top) - x = make_measure_hor(x) - y = make_measure_vert(y) - width = make_measure_hor(width) - height = make_measure_vert(height) - left = if h_anchor === :left - x - elseif h_anchor in (:center, :hcenter) - 0.5w - 0.5width + x - else - 1w - x - width - end - top = if v_anchor === :top - y - elseif v_anchor in (:center, :vcenter) - 0.5h - 0.5height + y - else - 1h - y - height - end - BoundingBox(left, top, width, height) -end - -# this is the available area for drawing everything in this layout... as percentages of total canvas -bbox(layout::AbstractLayout) = layout.bbox -bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb) - -# layouts are recursive, tree-like structures, and most will have a parent field -Base.parent(layout::AbstractLayout) = layout.parent -parent_bbox(layout::AbstractLayout) = bbox(parent(layout)) +grid(args...; kw...) = GridLayout(args...; kw...) # padding_w(layout::AbstractLayout) = left_padding(layout) + right_padding(layout) # padding_h(layout::AbstractLayout) = bottom_padding(layout) + top_padding(layout) @@ -130,120 +19,17 @@ update_child_bboxes!( kw..., ) = nothing -left(layout::AbstractLayout) = left(bbox(layout)) -top(layout::AbstractLayout) = top(bbox(layout)) -right(layout::AbstractLayout) = right(bbox(layout)) -bottom(layout::AbstractLayout) = bottom(bbox(layout)) -width(layout::AbstractLayout) = width(bbox(layout)) -height(layout::AbstractLayout) = height(bbox(layout)) - # pass these through to the bbox methods if there's no plotarea -plotarea(layout::AbstractLayout) = bbox(layout) -plotarea!(layout::AbstractLayout, bb::BoundingBox) = bbox!(layout, bb) - -attr(layout::AbstractLayout, k::Symbol) = layout.attr[k] -attr(layout::AbstractLayout, k::Symbol, v) = get(layout.attr, k, v) -attr!(layout::AbstractLayout, v, k::Symbol) = (layout.attr[k] = v) -hasattr(layout::AbstractLayout, k::Symbol) = haskey(layout.attr, k) - -leftpad(layout::AbstractLayout) = 0mm -toppad(layout::AbstractLayout) = 0mm -rightpad(layout::AbstractLayout) = 0mm -bottompad(layout::AbstractLayout) = 0mm - -# ----------------------------------------------------------- -# RootLayout - -# this is the parent of the top-level layout -struct RootLayout <: AbstractLayout end - -Base.show(io::IO, layout::RootLayout) = Base.show_default(io, layout) -Base.parent(::RootLayout) = nothing -parent_bbox(::RootLayout) = DEFAULT_BBOX[] -bbox(::RootLayout) = DEFAULT_BBOX[] - -# ----------------------------------------------------------- -# EmptyLayout - -# contains blank space -mutable struct EmptyLayout <: AbstractLayout - parent::AbstractLayout - bbox::BoundingBox - attr::KW # store label, width, and height for initialization - # label # this is the label that the subplot will take (since we create a layout before initialization) -end -EmptyLayout(parent = RootLayout(); kw...) = EmptyLayout(parent, DEFAULT_BBOX[], KW(kw)) - -Base.size(layout::EmptyLayout) = (0, 0) -Base.length(layout::EmptyLayout) = 0 -Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing +Commons.plotarea(layout::AbstractLayout) = bbox(layout) +Commons.plotarea!(layout::AbstractLayout, bb::BoundingBox) = bbox!(layout, bb) _update_min_padding!(layout::EmptyLayout) = nothing _update_inset_padding!(layout::EmptyLayout) = nothing -# ----------------------------------------------------------- -# GridLayout - -# nested, gridded layout with optional size percentages -mutable struct GridLayout <: AbstractLayout - parent::AbstractLayout - minpad::Tuple # leftpad, toppad, rightpad, bottompad - bbox::BoundingBox - grid::Matrix{AbstractLayout} # Nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion - widths::Vector{Measure} - heights::Vector{Measure} - attr::KW -end - -""" - grid(args...; kw...) - -Create a grid layout for subplots. `args` specify the dimensions, e.g. -`grid(3,2, widths = (0.6,0.4))` creates a grid with three rows and two -columns of different width. -""" -grid(args...; kw...) = GridLayout(args...; kw...) - -function GridLayout( - dims...; - parent = RootLayout(), - widths = zeros(dims[2]), - heights = zeros(dims[1]), - kw..., -) - grid = Matrix{AbstractLayout}(undef, dims...) - layout = GridLayout( - parent, - DEFAULT_MINPAD[], - DEFAULT_BBOX[], - grid, - Measure[w * pct for w in widths], - Measure[h * pct for h in heights], - # convert(Vector{Float64}, widths), - # convert(Vector{Float64}, heights), - KW(kw), - ) - for i in eachindex(grid) - grid[i] = EmptyLayout(layout) - end - layout -end - -Base.size(layout::GridLayout) = size(layout.grid) -Base.length(layout::GridLayout) = length(layout.grid) -Base.getindex(layout::GridLayout, r::Int, c::Int) = layout.grid[r, c] -Base.setindex!(layout::GridLayout, v, r::Int, c::Int) = layout.grid[r, c] = v -Base.setindex!(layout::GridLayout, v, ci::CartesianIndex) = layout.grid[ci] = v - -leftpad(pad) = pad[1] -toppad(pad) = pad[2] -rightpad(pad) = pad[3] -bottompad(pad) = pad[4] - -leftpad(layout::GridLayout) = leftpad(layout.minpad) -toppad(layout::GridLayout) = toppad(layout.minpad) -rightpad(layout::GridLayout) = rightpad(layout.minpad) -bottompad(layout::GridLayout) = bottompad(layout.minpad) +attr(layout::AbstractLayout, k::Symbol) = layout.attr[k] +attr(layout::AbstractLayout, k::Symbol, v) = get(layout.attr, k, v) +attr!(layout::AbstractLayout, v, k::Symbol) = (layout.attr[k] = v) +# hasattr(layout::AbstractLayout, k::Symbol) = haskey(layout.attr, k) # here's how this works... first we recursively "update the minimum padding" (which # means to calculate the minimum size needed from the edge of the subplot to plot area) @@ -491,7 +277,7 @@ end function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) nr, nc = size(layout) subplots = Subplot[] - spmap = PlotsPlots.SubplotMap() + spmap = Plots.SubplotMap() empty = isempty(plts) i = 0 for r in 1:nr, c in 1:nc @@ -512,19 +298,19 @@ function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) merge!(spmap, plt.spmap) inc = length(plt.subplots) end - if get(l.attr, :width, :auto) !== :auto + if get(l.attr, :width, :auto) ≢ :auto layout.widths[c] = attr(l, :width) end - if get(l.attr, :height, :auto) !== :auto + if get(l.attr, :height, :auto) ≢ :auto layout.heights[r] = attr(l, :height) end i += inc elseif isa(l, GridLayout) # sub-grid - if get(l.attr, :width, :auto) !== :auto + if get(l.attr, :width, :auto) ≢ :auto layout.widths[c] = attr(l, :width) end - if get(l.attr, :height, :auto) !== :auto + if get(l.attr, :height, :auto) ≢ :auto layout.heights[r] = attr(l, :height) end l, sps, m = build_layout(l, n - i, plts) @@ -598,7 +384,7 @@ function link_axes!(layout::GridLayout, link::Symbol) link_axes!(layout.grid[r, :], :yaxis) end end - if link === :square + if link ≡ :square if (sps = filter(l -> isa(l, Subplot), layout.grid)) |> !isempty base_axis = sps[1][:xaxis] for sp in sps @@ -607,7 +393,7 @@ function link_axes!(layout::GridLayout, link::Symbol) end end end - if link === :all + if link ≡ :all link_axes!(layout.grid, :xaxis) link_axes!(layout.grid, :yaxis) end @@ -623,7 +409,7 @@ function twin(sp, letter) ax = orig_sp[get_attr_symbol(letter, :axis)] ax[:grid] = false # disable the grid (overlaps with twin axis) end - if orig_sp[:framestyle] === :box + if orig_sp[:framestyle] ≡ :box # incompatible with shared axes (see github.com/JuliaPlots/Plots.jl/issues/2894) orig_sp[:framestyle] = :axes end diff --git a/PlotsBase/src/output.jl b/PlotsBase/src/output.jl index c9bab7865..88822678c 100644 --- a/PlotsBase/src/output.jl +++ b/PlotsBase/src/output.jl @@ -163,8 +163,6 @@ Display a plot using the backends' gui window """ gui(plt::Plot = current()) = display(PlotsDisplay(), plt) -function inline end # for IJulia - function Base.display(::PlotsDisplay, plt::Plot) prepare_output(plt) _display(plt) @@ -172,7 +170,7 @@ end _do_plot_show(plt, showval::Bool) = showval && gui(plt) function _do_plot_show(plt, showval::Symbol) - showval === :gui && gui(plt) + showval ≡ :gui && gui(plt) showval in (:inline, :ijulia) && inline(plt) end @@ -184,10 +182,10 @@ const _best_html_output_type = # a backup for html... passes to svg or png depending on the html_output_format arg function _show(io::IO, ::MIME"text/html", plt::Plot) output_type = Symbol(plt.attr[:html_output_format]) - if output_type === :auto + if output_type ≡ :auto output_type = get(_best_html_output_type, backend_name(plt.backend), :svg) end - if output_type === :png + if output_type ≡ :png # @info "writing png to html output" print( io, @@ -195,10 +193,10 @@ function _show(io::IO, ::MIME"text/html", plt::Plot) base64encode(show, MIME("image/png"), plt), "\" />", ) - elseif output_type === :svg + elseif output_type ≡ :svg # @info "writing svg to html output" show(io, MIME("image/svg+xml"), plt) - elseif output_type === :txt + elseif output_type ≡ :txt show(io, MIME("text/plain"), plt) else error("only png or svg allowed. got: $(repr(output_type))") @@ -246,6 +244,20 @@ closeall() = closeall(backend()) Base.show(io::IO, m::MIME"application/prs.juno.plotpane+html", plt::Plot) = showjuno(io, MIME("text/html"), plt) +function inline end # for IJulia + +function hdf5plot_write end +function hdf5plot_read end + +""" +Add extra jupyter mimetypes to display_dict based on the plot backed. + +The default is nothing, except for plotly based backends, where it +adds data for `application/vnd.plotly.v1+json` that is used in +frontends like jupyterlab and nteract. +""" +_ijulia__extra_mime_info!(::Plot, out::Dict) = out + # Atom PlotPane function showjuno(io::IO, m, plt) dpi = plt[:dpi] @@ -270,4 +282,5 @@ _showjuno(io::IO, m::MIME"image/svg+xml", plt) = Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot) = false _showjuno(io::IO, m, plt) = _show(io, m, plt) + # COV_EXCL_STOP diff --git a/PlotsBase/src/pipeline.jl b/PlotsBase/src/pipeline.jl index 93764ebbb..f66a957e0 100644 --- a/PlotsBase/src/pipeline.jl +++ b/PlotsBase/src/pipeline.jl @@ -10,7 +10,7 @@ function RecipesPipeline.warn_on_recipe_aliases!( ) pkeys = keys(plotattributes) for k in pkeys - if (dk = get(Commons._keyAliases, k, nothing)) !== nothing + if (dk = get(Commons._keyAliases, k, nothing)) ≢ nothing kv = RecipesPipeline.pop_kw!(plotattributes, k) dk ∈ pkeys || (plotattributes[dk] = kv) end @@ -63,7 +63,7 @@ end function _preprocess_userrecipe(kw::AKW) Commons._add_markershape(kw) - if get(kw, :permute, default(:permute)) !== :none + if get(kw, :permute, default(:permute)) ≢ :none l1, l2 = kw[:permute] for k in Commons._axis_attrs k1 = Commons._attrsymbolcache[l1][k] @@ -99,7 +99,7 @@ function _add_errorbar_kw(kw_list::Vector{KW}, kw::AKW) errors = (:xerror, :yerror, :zerror) if st ∉ errors for esym in errors - if get(kw, esym, nothing) !== nothing + if get(kw, esym, nothing) ≢ nothing # we make a copy of the KW and apply an errorbar recipe errkw = copy(kw) errkw[:seriestype] = esym @@ -162,7 +162,7 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, err_inds = findall(kw -> get(kw, :seriestype, :path) in (:xerror, :yerror, :zerror), kw_list) for ind in err_inds - if ind > 1 && get(kw_list[ind - 1], :seriestype, :path) === :scatter + if ind > 1 && get(kw_list[ind - 1], :seriestype, :path) ≡ :scatter tmp = copy(kw_list[ind]) kw_list[ind] = copy(kw_list[ind - 1]) kw_list[ind - 1] = tmp @@ -181,10 +181,10 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, kw[:ribbon] = map(rib, kw[:x]) end # convert a ribbon into a fillrange - if rib !== nothing + if rib ≢ nothing make_fillrange_from_ribbon(kw) # map fillrange if it's a Function - elseif fr !== nothing && fr isa Function + elseif fr ≢ nothing && fr isa Function kw[:fillrange] = map(fr, kw[:x]) end end @@ -214,7 +214,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) end # handle inset subplots - if (insets = plt[:inset_subplots]) !== nothing + if (insets = plt[:inset_subplots]) ≢ nothing typeof(insets) <: AVec || (insets = [insets]) for inset in insets parent, bb = is_2tuple(inset) ? inset : (nothing, inset) @@ -247,10 +247,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) sps = get(kw, :subplot, :auto) sp = get_subplot( plt, - _cycle( - sps === :auto ? plt.subplots : plt.subplots[sps], - series_idx(kw_list, kw), - ), + _cycle(sps ≡ :auto ? plt.subplots : plt.subplots[sps], series_idx(kw_list, kw)), ) kw[:subplot] = sp @@ -290,7 +287,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) else get(sp_attrs, sp, KW()) end - PlotsPlots._update_subplot_attrs(plt, sp, attr, idx, false) + Plots._update_subplot_attrs(plt, sp, attr, idx, false) end # do we need to link any axes together? @@ -353,15 +350,15 @@ RecipesPipeline.is_seriestype_supported(plt::Plot, st) = is_seriestype_supported function RecipesPipeline.add_series!(plt::Plot, plotattributes) sp = _prepare_subplot(plt, plotattributes) - if (perm = plotattributes[:permute]) !== :none + if (perm = plotattributes[:permute]) ≢ :none letter1, letter2 = perm ms = plotattributes[:markershape] - if ms === :hline && (perm == (:x, :y) || perm == (:y, :x)) + if ms ≡ :hline && (perm == (:x, :y) || perm == (:y, :x)) plotattributes[:markershape] = :vline - elseif ms === :vline && (perm == (:x, :y) || perm == (:y, :x)) + elseif ms ≡ :vline && (perm == (:x, :y) || perm == (:y, :x)) plotattributes[:markershape] = :hline end - if plotattributes[:seriestype] === :bar # bar calls expand_extrema! in its recipe... + if plotattributes[:seriestype] ≡ :bar # bar calls expand_extrema! in its recipe... sp = plotattributes[:subplot] sp[get_attr_symbol(letter1, :axis)][:lims], sp[get_attr_symbol(letter2, :axis)][:lims] = @@ -381,16 +378,13 @@ end function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where {T} st::Symbol = plotattributes[:seriestype] sp::Subplot{T} = plotattributes[:subplot] - sp_idx = PlotsPlots.get_subplot_index(plt, sp) - PlotsPlots._update_subplot_attrs(plt, sp, plotattributes, sp_idx, true) + sp_idx = Plots.get_subplot_index(plt, sp) + Plots._update_subplot_attrs(plt, sp, plotattributes, sp_idx, true) st = _override_seriestype_check(plotattributes, st) # change to a 3d projection for this subplot? - if ( - RecipesPipeline.needs_3d_axes(st) || - (st === :quiver && plotattributes[:z] !== nothing) - ) + if (RecipesPipeline.needs_3d_axes(st) || (st ≡ :quiver && plotattributes[:z] ≢ nothing)) sp.attr[:projection] = "3d" end @@ -404,7 +398,7 @@ end function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol) # adjust extrema and discrete info - if st === :image + if st ≡ :image xmin, xmax = ignorenan_extrema(plotattributes[:x]) ymin, ymax = ignorenan_extrema(plotattributes[:y]) expand_extrema!(sp[:xaxis], (xmin, xmax)) @@ -426,11 +420,11 @@ function _add_the_series(plt, sp, plotattributes) plt[:extra_plot_kwargs] = get(kw, :plot, KW()) sp[:extra_kwargs] = get(kw, :subplot, KW()) plotattributes[:extra_kwargs] = get(kw, :series, KW()) - elseif kw === :plot + elseif kw ≡ :plot plt[:extra_plot_kwargs] = extra_kwargs - elseif kw === :subplot + elseif kw ≡ :subplot sp[:extra_kwargs] = extra_kwargs - elseif kw === :series + elseif kw ≡ :series plotattributes[:extra_kwargs] = extra_kwargs else throw(ArgumentError("Unsupported type for extra keyword arguments")) @@ -438,9 +432,9 @@ function _add_the_series(plt, sp, plotattributes) warn_on_unsupported(plt.backend, plotattributes) series = Series(plotattributes) push!(plt.series_list, series) - if (z_order = plotattributes[:z_order]) === :front + if (z_order = plotattributes[:z_order]) ≡ :front push!(sp.series_list, series) - elseif z_order === :back + elseif z_order ≡ :back pushfirst!(sp.series_list, series) elseif z_order isa Integer insert!(sp.series_list, z_order, series) diff --git a/PlotsBase/src/plot.jl b/PlotsBase/src/plot.jl index 963eea86c..892df16b0 100644 --- a/PlotsBase/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -5,7 +5,7 @@ mutable struct CurrentPlot end const CURRENT_PLOT = CurrentPlot(nothing) -isplotnull() = CURRENT_PLOT.nullableplot === nothing +isplotnull() = CURRENT_PLOT.nullableplot ≡ nothing """ current() @@ -182,7 +182,7 @@ function plot!( # first apply any args for the subplots for (idx, sp) in enumerate(plt.subplots) - PlotsPlots._update_subplot_attrs( + Plots._update_subplot_attrs( plt, sp, idx == ttl_idx ? KW() : plotattributes, @@ -254,8 +254,7 @@ function prepare_output(plt::Plot) force_minpad = get(plt, :force_minpad, ()) isempty(force_minpad) || for i in eachindex(plt.layout.grid) plt.layout.grid[i].minpad = Tuple( - i === nothing ? j : i for - (i, j) in zip(force_minpad, plt.layout.grid[i].minpad) + i ≡ nothing ? j : i for (i, j) in zip(force_minpad, plt.layout.grid[i].minpad) ) end diff --git a/PlotsBase/src/backends/plotly.jl b/PlotsBase/src/plotly.jl similarity index 92% rename from PlotsBase/src/backends/plotly.jl rename to PlotsBase/src/plotly.jl index 9a97182cf..e7c74d3a7 100644 --- a/PlotsBase/src/backends/plotly.jl +++ b/PlotsBase/src/plotly.jl @@ -8,28 +8,27 @@ import Statistics import UUIDs import JSON +using PlotUtils + +using PlotsBase.Colors: Colorant using PlotsBase.Annotations -using PlotsBase.Axes +using PlotsBase.DataSeries using PlotsBase.Colorbars -using PlotsBase.Colors: Colorant -using PlotsBase.Commons -using PlotsBase.Fonts -using PlotsBase.Fonts: PlotText -using PlotsBase.PlotMeasures -using PlotsBase.PlotsPlots -using PlotsBase.PlotsSeries -using PlotsBase.PlotUtils: PlotUtils, ColorGradient, rgba_string, rgb_string using PlotsBase.Subplots using PlotsBase.Surfaces +using PlotsBase.Commons +using PlotsBase.Plots +using PlotsBase.Fonts using PlotsBase.Ticks +using PlotsBase.Axes struct PlotlyBackend <: PlotsBase.AbstractBackend end + PlotsBase._backendType[:plotly] = PlotlyBackend PlotsBase._backendSymbol[PlotlyBackend] = :plotly - push!(PlotsBase._initialized_backends, :plotly) -PlotsBase.backend_name(::PlotlyBackend) = :plotly -PlotsBase.backend_package_name(::PlotlyBackend) = PlotsBase.backend_package_name(:plotly) + +eval(PlotsBase.backend_defines(:PlotlyBackend, :plotly)) const _plotly_attrs = PlotsBase.merge_with_base_supported([ :annotations, @@ -181,15 +180,6 @@ const _plotly_scales = [:identity, :log10] PlotsBase.default_output_format(plt::Plot{PlotlyBackend}) = "html" -for s in (:attr, :seriestype, :marker, :style, :scale) - f1 = Symbol("is_", s, "_supported") - f2 = Symbol("supported_", s, "s") - v = Symbol("_plotly_", s, "s") - eval(quote - PlotsBase.$f1(::PlotlyBackend, $s::Symbol) = $s in $v - PlotsBase.$f2(::PlotlyBackend) = sort(collect($v)) - end) -end # ---------------------------------------------------------------- function labelfunc(scale::Symbol, backend::PlotlyBackend) @@ -235,8 +225,8 @@ plotly_annotation_dict(x, y, ptxt::PlotText; xref = "paper", yref = "paper") = m plotly_annotation_dict(x, y, ptxt.str; xref = xref, yref = yref), KW( :font => plotly_font(ptxt.font), - :xanchor => ptxt.font.halign === :hcenter ? :center : ptxt.font.halign, - :yanchor => ptxt.font.valign === :vcenter ? :middle : ptxt.font.valign, + :xanchor => ptxt.font.halign ≡ :hcenter ? :center : ptxt.font.halign, + :yanchor => ptxt.font.valign ≡ :vcenter ? :middle : ptxt.font.valign, :rotation => -ptxt.font.rotation, ), ) @@ -253,13 +243,13 @@ plotly_annotation_dict( plotly_annotation_dict(x, y, z, ptxt.str; xref = xref, yref = yref, zref = zref), KW( :font => plotly_font(ptxt.font), - :xanchor => ptxt.font.halign === :hcenter ? :center : ptxt.font.halign, - :yanchor => ptxt.font.valign === :vcenter ? :middle : ptxt.font.valign, + :xanchor => ptxt.font.halign ≡ :hcenter ? :center : ptxt.font.halign, + :yanchor => ptxt.font.valign ≡ :vcenter ? :middle : ptxt.font.valign, :rotation => -ptxt.font.rotation, ), ) -plotly_scale(scale::Symbol) = scale === :log10 ? "log" : "-" +plotly_scale(scale::Symbol) = scale ≡ :log10 ? "log" : "-" function shrink_by(lo, sz, ratio) amt = 0.5(1 - ratio) * sz @@ -267,8 +257,8 @@ function shrink_by(lo, sz, ratio) end function plotly_apply_aspect_ratio(sp::Subplot, plotarea, pcts) - if (aspect_ratio = get_aspect_ratio(sp)) !== :none - aspect_ratio === :equal && (aspect_ratio = 1.0) + if (aspect_ratio = get_aspect_ratio(sp)) ≢ :none + aspect_ratio ≡ :equal && (aspect_ratio = 1.0) xmin, xmax = axis_limits(sp, :x) ymin, ymax = axis_limits(sp, :y) want_ratio = ((xmax - xmin) / (ymax - ymin)) / aspect_ratio @@ -307,30 +297,30 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing) letter = axis[:letter] framestyle = sp[:framestyle] ax = KW( - :visible => framestyle !== :none, + :visible => framestyle ≢ :none, :title => axis[:guide], :showgrid => axis[:grid], :gridcolor => rgba_string(plot_color(axis[:foreground_color_grid], axis[:gridalpha])), :gridwidth => axis[:gridlinewidth], - :zeroline => framestyle === :zerolines, + :zeroline => framestyle ≡ :zerolines, :zerolinecolor => rgba_string(axis[:foreground_color_axis]), :showline => framestyle in (:box, :axes) && axis[:showaxis], :linecolor => rgba_string(plot_color(axis[:foreground_color_axis])), :ticks => - axis[:tick_direction] === :out ? "outside" : - axis[:tick_direction] === :in ? "inside" : "", - :mirror => framestyle === :box, + axis[:tick_direction] ≡ :out ? "outside" : + axis[:tick_direction] ≡ :in ? "inside" : "", + :mirror => framestyle ≡ :box, :showticklabels => axis[:showaxis], ) - anchor === nothing || (ax[:anchor] = anchor) - domain === nothing || (ax[:domain] = domain) + anchor ≡ nothing || (ax[:anchor] = anchor) + domain ≡ nothing || (ax[:domain] = domain) ax[:tickangle] = -axis[:rotation] ax[:type] = plotly_scale(axis[:scale]) lims = axis_limits(sp, letter) - if axis[:ticks] !== :native || axis[:lims] !== :auto + if axis[:ticks] ≢ :native || axis[:lims] ≢ :auto ax[:range] = map(RecipesPipeline.scale_func(axis[:scale]), lims) end @@ -343,13 +333,13 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing) ax[:linecolor] = rgba_string(axis[:foreground_color_axis]) # ticks - if axis[:ticks] !== :native + if axis[:ticks] ≢ :native ticks = PlotsBase.get_ticks(sp, axis) ttype = PlotsBase.ticks_type(ticks) - if ttype === :ticks + if ttype ≡ :ticks ax[:tickmode] = "array" ax[:tickvals] = ticks - elseif ttype === :ticks_and_labels + elseif ttype ≡ :ticks_and_labels ax[:tickmode] = "array" ax[:tickvals], ax[:ticktext] = ticks end @@ -368,7 +358,7 @@ end function plotly_polaraxis(sp::Subplot, axis::Axis) ax = KW(:visible => axis[:showaxis], :showline => axis[:grid]) - if axis[:letter] === :x + if axis[:letter] ≡ :x ax[:range] = rad2deg.(axis_limits(sp, :x)) else ax[:range] = axis_limits(sp, :y) @@ -397,13 +387,13 @@ function plotly_layout(plt::Plot) if sp[:title] != "" bb = plotarea(sp) tpos = sp[:titlelocation] - if tpos === :left + if tpos ≡ :left xmm, ymm = left(bb), top(bbox(sp)) halign, valign = :left, :top - elseif tpos === :center + elseif tpos ≡ :center xmm, ymm = 0.5(left(bb) + right(bb)), top(bbox(sp)) halign, valign = :hcenter, :top - elseif tpos === :right + elseif tpos ≡ :right xmm, ymm = right(bb), top(bbox(sp)) halign, valign = :right, :top else @@ -524,9 +514,9 @@ function plotly_layout(plt::Plot) end function plotly_add_legend!(plotattributes_out::KW, sp::Subplot) - plotattributes_out[:showlegend] = sp[:legend_position] !== :none + plotattributes_out[:showlegend] = sp[:legend_position] ≢ :none legend_position = plotly_legend_pos(sp[:legend_position]) - sp[:legend_position] === :none && return + sp[:legend_position] ≡ :none && return plotattributes_out[:legend] = KW( :bgcolor => rgba_string(sp[:legend_background_color]), :bordercolor => rgba_string(sp[:legend_foreground_color]), @@ -539,7 +529,7 @@ function plotly_add_legend!(plotattributes_out::KW, sp::Subplot) :x => legend_position.coords[1], :y => legend_position.coords[2], :title => KW( - :text => sp[:legend_title] === nothing ? "" : string(sp[:legend_title]), + :text => sp[:legend_title] ≡ nothing ? "" : string(sp[:legend_title]), :font => plotly_font(legendtitlefont(sp)), ), ) @@ -599,7 +589,7 @@ function plotly_legend_pos(v::Tuple{S,Symbol}) where {S<:Real} xanchors = ["left", "center", "right"] yanchors = ["bottom", "middle", "top"] - if v[2] === :inner + if v[2] ≡ :inner rect = 0.07, 0.5, 1.0, 0.07, 0.52, 1.0 xanchor = xanchors[legend_anchor_index(c)] yanchor = yanchors[legend_anchor_index(s)] @@ -632,7 +622,10 @@ function plotly_colorscale(cg::PlotUtils.CategoricalColorGradient, α = nothing) cinds = repeat(1:n, inner = 2) vinds = vcat((i:(i + 1) for i in 1:n)...) map( - i -> [cg.values[vinds[i]], rgba_string(plot_color(color_list(cg)[cinds[i]], α))], + i -> [ + cg.values[vinds[i]], + rgba_string(plot_color(PlotsBase.color_list(cg)[cinds[i]], α)), + ], eachindex(cinds), ) end @@ -678,7 +671,7 @@ end function plotly_data(series::Series, letter::Symbol, data) axis = series[:subplot][get_attr_symbol(letter, :axis)] - data = if axis[:ticks] === :native && data !== nothing + data = if axis[:ticks] ≡ :native && data ≢ nothing plotly_native_data(axis, data) else data @@ -690,7 +683,7 @@ function plotly_data(series::Series, letter::Symbol, data) plotly_data(data) end end -plotly_data(v) = v !== nothing ? collect(v) : v +plotly_data(v) = v ≢ nothing ? collect(v) : v plotly_data(v::AbstractArray) = v plotly_data(surf::Surface) = surf.surf plotly_data(v::AbstractArray{R}) where {R<:Rational} = float(v) @@ -730,7 +723,7 @@ function plotly_series(plt::Plot, series::Series) sp = series[:subplot] clims = get_clims(sp, series) - (st = series[:seriestype]) === :shape && return plotly_series_shapes(plt, series, clims) + (st = series[:seriestype]) ≡ :shape && return plotly_series_shapes(plt, series, clims) plotattributes_out = KW() @@ -748,8 +741,8 @@ function plotly_series(plt::Plot, series::Series) end plotattributes_out[:showlegend] = should_add_to_legend(series) - if st === :straightline - x, y = straightline_data(series, 100) + if st ≡ :straightline + x, y = PlotsBase.straightline_data(series, 100) z = series[:z] else x, y, z = series[:x], series[:y], series[:z] @@ -763,7 +756,7 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:name] = series[:label] isscatter = st in (:scatter, :scatter3d, :scattergl) - hasmarker = isscatter || series[:markershape] !== :none + hasmarker = isscatter || series[:markershape] ≢ :none hasline = st in (:path, :path3d, :straightline) hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && @@ -779,16 +772,16 @@ function plotly_series(plt::Plot, series::Series) if st in (:path, :scatter, :scattergl, :straightline, :path3d, :scatter3d) return plotly_series_segments(series, plotattributes_out, x, y, z, clims) - elseif st === :heatmap - x = heatmap_edges(x, sp[:xaxis][:scale]) - y = heatmap_edges(y, sp[:yaxis][:scale]) + elseif st ≡ :heatmap + x = PlotsBase.heatmap_edges(x, sp[:xaxis][:scale]) + y = PlotsBase.heatmap_edges(y, sp[:yaxis][:scale]) plotattributes_out[:type] = "heatmap" plotattributes_out[:x], plotattributes_out[:y], plotattributes_out[:z] = x, y, z plotattributes_out[:colorscale] = plotly_colorscale(series[:fillcolor], series[:fillalpha]) plotattributes_out[:showscale] = hascolorbar(sp) - elseif st === :contour + elseif st ≡ :contour filled = isfilledcontour(series) plotattributes_out[:type] = "contour" plotattributes_out[:x], plotattributes_out[:y], plotattributes_out[:z] = x, y, z @@ -827,7 +820,7 @@ function plotly_series(plt::Plot, series::Series) elseif st in (:surface, :wireframe) plotattributes_out[:type] = "surface" plotattributes_out[:x], plotattributes_out[:y], plotattributes_out[:z] = x, y, z - if st === :wireframe + if st ≡ :wireframe plotattributes_out[:hidesurface] = true wirelines = KW( :show => true, @@ -842,16 +835,16 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:colorscale] = plotly_colorscale(series[:fillcolor], series[:fillalpha]) plotattributes_out[:opacity] = series[:fillalpha] - if series[:fill_z] !== nothing + if series[:fill_z] ≢ nothing plotattributes_out[:surfacecolor] = handle_surface(series[:fill_z]) end plotattributes_out[:showscale] = hascolorbar(sp) end - elseif st === :mesh3d + elseif st ≡ :mesh3d plotattributes_out[:type] = "mesh3d" plotattributes_out[:x], plotattributes_out[:y], plotattributes_out[:z] = x, y, z - if series[:connections] !== nothing + if series[:connections] ≢ nothing if typeof(series[:connections]) <: Tuple{Array,Array,Array} # 0-based indexing i, j, k = series[:connections] @@ -888,7 +881,7 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:color] = rgba_string(plot_color(series[:fillcolor], series[:fillalpha])) plotattributes_out[:opacity] = series[:fillalpha] - if series[:fill_z] !== nothing + if series[:fill_z] ≢ nothing plotattributes_out[:surfacecolor] = handle_surface(series[:fill_z]) end plotattributes_out[:showscale] = hascolorbar(sp) @@ -960,7 +953,7 @@ function plotly_series_shapes(plt::Plot, series::Series, clims) x, y = ( plotly_data(series, letter, data) for - (letter, data) in zip((:x, :y), shape_data(series, 100)) + (letter, data) in zip((:x, :y), PlotsBase.shape_data(series, 100)) ) for (k, segment) in enumerate(segments) @@ -995,11 +988,11 @@ function plotly_series_shapes(plt::Plot, series::Series, clims) plotly_adjust_hover_label!(plotattributes_out, _cycle(series[:hover], i)) plotattributes_outs[k] = merge(plotattributes_out, series[:extra_kwargs]) end - if series[:fill_z] !== nothing + if series[:fill_z] ≢ nothing push!(plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :fill)) - elseif series[:line_z] !== nothing + elseif series[:line_z] ≢ nothing push!(plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :line)) - elseif series[:marker_z] !== nothing + elseif series[:marker_z] ≢ nothing push!( plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :marker), @@ -1012,7 +1005,7 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z st = series[:seriestype] sp = series[:subplot] isscatter = st in (:scatter, :scatter3d, :scattergl) - hasmarker = isscatter || series[:markershape] !== :none + hasmarker = isscatter || series[:markershape] ≢ :none hasline = st in (:path, :path3d, :straightline) hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && @@ -1032,7 +1025,7 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z # set the type if st in (:path, :scatter, :scattergl, :straightline) - plotattributes_out[:type] = st === :scattergl ? "scattergl" : "scatter" + plotattributes_out[:type] = st ≡ :scattergl ? "scattergl" : "scatter" plotattributes_out[:mode] = if hasmarker hasline ? "lines+markers" : "markers" else @@ -1071,7 +1064,7 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z mcolor = rgba_string( plot_color(get_markercolor(series, clims, i), get_markeralpha(series, i)), ) - mcolor_next = if (mz = series[:marker_z]) !== nothing && i < length(mz) + mcolor_next = if (mz = series[:marker_z]) ≢ nothing && i < length(mz) plot_color( get_markercolor(series, clims, i + 1), get_markeralpha(series, i + 1), @@ -1113,11 +1106,11 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z plot_color(get_linecolor(series, clims, i), get_linealpha(series, i)), ), :width => get_linewidth(series, i), - :shape => if st === :steppre + :shape => if st ≡ :steppre "vh" - elseif st === :stepmid + elseif st ≡ :stepmid "hvh" - elseif st === :steppost + elseif st ≡ :steppost "hv" else "linear" @@ -1154,7 +1147,7 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z # if fillrange is a tuple with upper and lower limit, plotattributes_out_fillrange # is the series that will do the filling plotattributes_out_fillrange[:x], plotattributes_out_fillrange[:y] = - concatenate_fillrange(x[rng], series[:fillrange]) + PlotsBase.concatenate_fillrange(x[rng], series[:fillrange]) plotattributes_out_fillrange[:line][:width] = 0 delete!(plotattributes_out, :fill) delete!(plotattributes_out, :fillcolor) @@ -1168,11 +1161,11 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z plotattributes_outs[k] = merge(plotattributes_outs[k], series[:extra_kwargs]) end - if series[:line_z] !== nothing + if series[:line_z] ≢ nothing push!(plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :line)) - elseif series[:fill_z] !== nothing + elseif series[:fill_z] ≢ nothing push!(plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :fill)) - elseif series[:marker_z] !== nothing + elseif series[:marker_z] ≢ nothing push!( plotattributes_outs, plotly_colorbar_hack(series, plotattributes_base, :marker), @@ -1215,7 +1208,7 @@ plotly_polar!(plotattributes_out::KW, series::Series) = end function plotly_adjust_hover_label!(plotattributes_out::KW, hover) - if hover === nothing + if hover ≡ nothing return elseif all(in([:none, false]), hover) plotattributes_out[:hoverinfo] = "none" @@ -1272,7 +1265,7 @@ function plotly_html_head(plt::Plot) end function plotly_html_body(plt, style = nothing) - if style === nothing + if style ≡ nothing w, h = plt[:size] style = "width:$(w)px;height:$(h)px;" end @@ -1323,4 +1316,12 @@ PlotsBase._show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = PlotsBase._display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) -end # module +function _ijulia__extra_mime_info!(plt::Plot{PlotlyBackend}, out::Dict) + out["application/vnd.plotly.v1+json"] = + Dict(:data => plotly_series(plt), :layout => plotly_layout(plt)) + out +end + +end # module + +using .Plotly diff --git a/PlotsBase/src/preferences.jl b/PlotsBase/src/preferences.jl new file mode 100644 index 000000000..e58f5394a --- /dev/null +++ b/PlotsBase/src/preferences.jl @@ -0,0 +1,49 @@ +# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: +# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" +# ==> this must always be done during precompilation, otherwise +# the cache will not invalidate when preferences change +const DEFAULT_BACKEND = lowercase(load_preference(PlotsBase, "default_backend", "gr")) + +function default_backend() + # environment variable preempts the `Preferences` based mechanism + name = get(ENV, "PLOTSBASE_DEFAULT_BACKEND", DEFAULT_BACKEND) |> lowercase |> Symbol + backend(name) +end + +function set_default_backend!( + backend::Union{Nothing,AbstractString,Symbol} = nothing; + force = true, + kw..., +) + if backend ≡ nothing + delete_preferences!(PlotsBase, "default_backend"; force, kw...) + else + # NOTE: `_check_installed` already throws a warning + if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing + set_preferences!(PlotsBase, "default_backend" => value; force, kw...) + end + end + nothing +end + +function diagnostics(io::IO = stdout) + origin = if has_preference(PlotsBase, "default_backend") + "`Preferences`" + elseif haskey(ENV, "PLOTSBASE_DEFAULT_BACKEND") + "environment variable" + else + "fallback" + end + if (be = backend_name()) ≡ :none + @info "no `PlotsBase` backends currently initialized" + else + pkg_name = string(PlotsBase.backend_package_name(be)) + @info "selected `PlotsBase` backend: $pkg_name, from $origin" + Pkg.status( + ["PlotsBase", "RecipesBase", "RecipesPipeline", pkg_name]; + mode = Pkg.PKGMODE_MANIFEST, + io, + ) + end + nothing +end diff --git a/PlotsBase/src/recipes.jl b/PlotsBase/src/recipes.jl index 0431d2826..eeccb3623 100644 --- a/PlotsBase/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -13,7 +13,7 @@ function seriestype_supported(pkg::AbstractBackend, st::Symbol) supported = true for dep in _series_recipe_deps[st] - if seriestype_supported(pkg, dep) === :no + if seriestype_supported(pkg, dep) ≡ :no supported = false break end @@ -29,7 +29,7 @@ end function all_seriestypes() sts = Set{Symbol}(keys(_series_recipe_deps)) for bsym in _initialized_backends - be = _backend_instance(bsym) + be = backend_instance(bsym) sts = union(sts, Set{Symbol}(supported_seriestypes(be))) end sts |> collect |> sort @@ -199,11 +199,11 @@ function make_steps(x::AbstractArray, st, even) for i in 2:n xindex = xstartindex - 1 + i idx = 2i - 1 - if st === :mid + if st ≡ :mid newx[idx] = newx[idx - 1] = (x[xindex] + x[xindex - 1]) / 2 else newx[idx] = x[xindex] - newx[idx - 1] = x[st === :pre ? xindex : xindex - 1] + newx[idx - 1] = x[st ≡ :pre ? xindex : xindex - 1] end end even && (newx[end] = x[end]) @@ -223,7 +223,7 @@ make_steps(t::Tuple, st, even) = Tuple(make_steps(ti, st, even) for ti in t) plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :pre, false) # create a secondary series for the markers - if plotattributes[:markershape] !== :none + if plotattributes[:markershape] ≢ :none @series begin seriestype := :scatter x := x @@ -248,7 +248,7 @@ end plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :post, true) # create a secondary series for the markers - if plotattributes[:markershape] !== :none + if plotattributes[:markershape] ≢ :none @series begin seriestype := :scatter x := x @@ -273,7 +273,7 @@ end plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :post, false) # create a secondary series for the markers - if plotattributes[:markershape] !== :none + if plotattributes[:markershape] ≢ :none @series begin seriestype := :scatter x := x @@ -294,19 +294,19 @@ end # create vertical line segments from fill @recipe function f(::Type{Val{:sticks}}, x, y, z) # COV_EXCL_LINE n = length(x) - if (fr = plotattributes[:fillrange]) === nothing + if (fr = plotattributes[:fillrange]) ≡ nothing sp = plotattributes[:subplot] - fr = if sp[:yaxis][:scale] === :identity + fr = if sp[:yaxis][:scale] ≡ :identity 0.0 else NaNMath.min(axis_limits(sp, :y)[1], ignorenan_minimum(y)) end end - newx, newy, newz = zeros(3n), zeros(3n), z !== nothing ? zeros(3n) : nothing - for (i, (xi, yi, zi)) in enumerate(zip(x, y, z !== nothing ? z : 1:n)) + newx, newy, newz = zeros(3n), zeros(3n), z ≢ nothing ? zeros(3n) : nothing + for (i, (xi, yi, zi)) in enumerate(zip(x, y, z ≢ nothing ? z : 1:n)) rng = (3i - 2):(3i) newx[rng] = [xi, xi, NaN] - if z !== nothing + if z ≢ nothing newy[rng] = [yi, yi, NaN] newz[rng] = [_cycle(fr, i), zi, NaN] else @@ -315,27 +315,27 @@ end end x := newx y := newy - if z !== nothing + if z ≢ nothing z := newz end fillrange := nothing seriestype := :path if ( - plotattributes[:linecolor] === :auto && - plotattributes[:marker_z] !== nothing && - plotattributes[:line_z] === nothing + plotattributes[:linecolor] ≡ :auto && + plotattributes[:marker_z] ≢ nothing && + plotattributes[:line_z] ≡ nothing ) line_z := plotattributes[:marker_z] end # create a primary series for the markers - if plotattributes[:markershape] !== :none + if plotattributes[:markershape] ≢ :none primary := false @series begin seriestype := :scatter x := x y := y - if z !== nothing + if z ≢ nothing z := z end primary := true @@ -366,35 +366,35 @@ end # create segmented bezier curves in place of line segments @recipe function f(::Type{Val{:curves}}, x, y, z; npoints = 30) # COV_EXCL_LINE - args = z !== nothing ? (x, y, z) : (x, y) + args = z ≢ nothing ? (x, y, z) : (x, y) newx, newy = zeros(0), zeros(0) - newfr = (fr = plotattributes[:fillrange]) !== nothing ? zeros(0) : nothing - newz = z !== nothing ? zeros(0) : nothing + newfr = (fr = plotattributes[:fillrange]) ≢ nothing ? zeros(0) : nothing + newz = z ≢ nothing ? zeros(0) : nothing # for each line segment (point series with no NaNs), convert it into a bezier curve # where the points are the control points of the curve - for rng in PlotsSeries.iter_segments(args...) + for rng in DataSeries.iter_segments(args...) length(rng) < 2 && continue ts = range(0, stop = 1, length = npoints) nanappend!(newx, map(t -> bezier_value(_cycle(x, rng), t), ts)) nanappend!(newy, map(t -> bezier_value(_cycle(y, rng), t), ts)) - if z !== nothing + if z ≢ nothing nanappend!(newz, map(t -> bezier_value(_cycle(z, rng), t), ts)) end - if fr !== nothing + if fr ≢ nothing nanappend!(newfr, map(t -> bezier_value(_cycle(fr, rng), t), ts)) end end x := newx y := newy - if z === nothing + if z ≡ nothing seriestype := :path else seriestype := :path3d z := newz end - if fr !== nothing + if fr ≢ nothing fillrange := newfr end () @@ -422,7 +422,7 @@ end # compute half-width of bars bw = plotattributes[:bar_width] - hw = if bw === nothing + hw = if bw ≡ nothing 0.5Commons._bar_width * if nx > 1 ignorenan_minimum(filter(x -> x > 0, diff(sort(procx)))) else @@ -433,7 +433,7 @@ end end # make fillto a vector... default fills to 0 - if (fillto = plotattributes[:fillrange]) === nothing + if (fillto = plotattributes[:fillrange]) ≡ nothing fillto = 0 end if yscale in _log_scales && !all(_is_positive, fillto) @@ -587,7 +587,7 @@ end @recipe function f(::Type{Val{:barbins}}, x, y, z) # COV_EXCL_LINE edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, x, y) - if plotattributes[:bar_width] === nothing + if plotattributes[:bar_width] ≡ nothing bar_width := diff(edge) end x := _bin_centers(edge) @@ -634,7 +634,7 @@ function _stepbins_path(edge, weights, baseline::Real, xscale::Symbol, yscale::S last_w = eltype(weights)(NaN) - while it_tuple_e !== nothing && it_tuple_w !== nothing + while it_tuple_e ≢ nothing && it_tuple_w ≢ nothing b, it_state_e = it_tuple_e w, it_state_w = it_tuple_w @@ -678,7 +678,7 @@ end xpts, ypts = _stepbins_path(edge, weights, baseline, xscale, yscale) # create a secondary series for the markers - if plotattributes[:markershape] !== :none + if plotattributes[:markershape] ≢ :none @series begin seriestype := :scatter x := _bin_centers(edge) @@ -729,19 +729,19 @@ function _auto_binning_nbins( end v = vs[dim] - mode === :auto && (mode = :fd) + mode ≡ :auto && (mode = :fd) - if mode === :sqrt # Square-root choice + if mode ≡ :sqrt # Square-root choice _cl(sqrt(n_samples)) - elseif mode === :sturges # Sturges' formula + elseif mode ≡ :sturges # Sturges' formula _cl(log2(n_samples) + 1) - elseif mode === :rice # Rice Rule + elseif mode ≡ :rice # Rice Rule _cl(2 * nd) - elseif mode === :scott # Scott's normal reference rule + elseif mode ≡ :scott # Scott's normal reference rule _cl(_span(v) / (3.5 * std(v) / nd)) - elseif mode === :fd # Freedman–Diaconis rule + elseif mode ≡ :fd # Freedman–Diaconis rule _cl(_span(v) / (2 * _iqr(v) / nd)) - elseif mode === :wand + elseif mode ≡ :wand wand_edges(v) # this makes this function not type stable, but the type instability does not propagate else error("Unknown auto-binning mode $mode") @@ -782,7 +782,7 @@ function _make_hist( localvs = _filternans(vs) edges = _hist_edges(localvs, binning) h = float( - weights === nothing ? + weights ≡ nothing ? StatsBase.fit(StatsBase.Histogram, localvs, edges, closed = :left) : StatsBase.fit( StatsBase.Histogram, @@ -856,7 +856,7 @@ end ) seriestype := get(st_map, plotattributes[:seriestype], plotattributes[:seriestype]) - if plotattributes[:seriestype] === :scatterbins + if plotattributes[:seriestype] ≡ :scatterbins # Workaround, error bars currently not set correctly by scatterbins edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, h.edges[1], h.weights) @@ -883,7 +883,7 @@ end float_weights = float(weights) if !plotattributes[:show_empty_bins] - if float_weights === weights + if float_weights ≡ weights float_weights = deepcopy(float_weights) end for (i, c) in enumerate(float_weights) @@ -948,7 +948,7 @@ end @recipe function f(::Type{Val{:mesh3d}}, x, y, z) # COV_EXCL_LINE # As long as no i,j,k are supplied this should work with PyPlot and GR seriestype := :surface - if plotattributes[:connections] !== nothing + if plotattributes[:connections] ≢ nothing "Giving triangles using the connections argument is only supported on Plotly backend." |> ArgumentError |> throw @@ -961,7 +961,7 @@ end @recipe function f(::Type{Val{:scatter3d}}, x, y, z) # COV_EXCL_LINE seriestype := :path3d - if plotattributes[:markershape] === :none + if plotattributes[:markershape] ≡ :none markershape := :circle end linewidth := 0 @@ -977,7 +977,7 @@ lens!(args...; kwargs...) = plot!(args...; seriestype = :lens, kwargs...) export lens! @recipe function f(::Type{Val{:lens}}, plt::AbstractPlot) # COV_EXCL_LINE sp_index, inset_bbox = plotattributes[:inset_subplots] - width(inset_bbox) isa Measures.Length{:w,<:Real} || + width(inset_bbox) isa Commons.Length{:w,<:Real} || throw(ArgumentError("Inset bounding box needs to in relative coordinates.")) sp = plt.subplots[sp_index] xscale = sp[:xaxis][:scale] @@ -1084,9 +1084,9 @@ Commons.@attributes function error_style!(plotattributes::AKW) RecipesPipeline.reset_kw!(plotattributes, :marker_z) haskey(plotattributes, :line_z) && RecipesPipeline.reset_kw!(plotattributes, :line_z) - msc = if (msc = plotattributes[:markerstrokecolor]) === :match + msc = if (msc = plotattributes[:markerstrokecolor]) ≡ :match plotattributes[:subplot][:foreground_color_subplot] - elseif msc === :auto + elseif msc ≡ :auto get_series_color( plotattributes[:linecolor], plotattributes[:subplot], @@ -1137,13 +1137,13 @@ clamp_to_eps!(ary) = (replace!(x -> x <= 0.0 ? Base.eps(Float64) : x, ary); noth Commons.error_style!(plotattributes) markershape := :vline xerr = error_zipit(plotattributes[:xerror]) - if z === nothing + if z ≡ nothing plotattributes[:x], plotattributes[:y] = error_coords(xerr, x, y) else plotattributes[:x], plotattributes[:y], plotattributes[:z] = error_coords(xerr, x, y, z) end - if :xscale ∈ keys(plotattributes) && plotattributes[:xscale] === :log10 + if :xscale ∈ keys(plotattributes) && plotattributes[:xscale] ≡ :log10 clamp_to_eps!(plotattributes[:x]) end () @@ -1154,13 +1154,13 @@ end Commons.error_style!(plotattributes) markershape := :hline yerr = error_zipit(plotattributes[:yerror]) - if z === nothing + if z ≡ nothing plotattributes[:y], plotattributes[:x] = error_coords(yerr, y, x) else plotattributes[:y], plotattributes[:x], plotattributes[:z] = error_coords(yerr, y, x, z) end - if :yscale ∈ keys(plotattributes) && plotattributes[:yscale] === :log10 + if :yscale ∈ keys(plotattributes) && plotattributes[:yscale] ≡ :log10 clamp_to_eps!(plotattributes[:y]) end () @@ -1170,12 +1170,12 @@ end @recipe function f(::Type{Val{:zerror}}, x, y, z) # COV_EXCL_LINE Commons.error_style!(plotattributes) markershape := :hline - if z !== nothing + if z ≢ nothing zerr = error_zipit(plotattributes[:zerror]) plotattributes[:z], plotattributes[:x], plotattributes[:y] = error_coords(zerr, z, x, y) end - if :zscale ∈ keys(plotattributes) && plotattributes[:zscale] === :log10 + if :zscale ∈ keys(plotattributes) && plotattributes[:zscale] ≡ :log10 clamp_to_eps!(plotattributes[:z]) end () @@ -1456,7 +1456,7 @@ end @recipe f(x::AVec, ohlc::AVec{NTuple{N,<:Number}}) where {N} = x, map(t -> OHLC(t...), ohlc) @recipe f(xyuv::AVec{NTuple}) = - get(plotattributes, :seriestype, :path) === :ohlc ? map(t -> OHLC(t...), xyuv) : + get(plotattributes, :seriestype, :path) ≡ :ohlc ? map(t -> OHLC(t...), xyuv) : RecipesPipeline.unzip(xyuv) @recipe function f(x::AVec, v::AVec{OHLC}) # COV_EXCL_LINE diff --git a/PlotsBase/src/utils.jl b/PlotsBase/src/utils.jl index ef38e1baa..d6a37db48 100644 --- a/PlotsBase/src/utils.jl +++ b/PlotsBase/src/utils.jl @@ -76,11 +76,11 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) # update alphas for asym in (:linealpha, :markeralpha, :fillalpha) - if plotattributes[asym] === nothing + if plotattributes[asym] ≡ nothing plotattributes[asym] = plotattributes[:seriesalpha] end end - if plotattributes[:markerstrokealpha] === nothing + if plotattributes[:markerstrokealpha] ≡ nothing plotattributes[:markerstrokealpha] = plotattributes[:markeralpha] end @@ -92,13 +92,13 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) # update other colors (`linecolor`, `markercolor`, `fillcolor`) <- for grep for s in (:line, :marker, :fill) csym, asym = Symbol(s, :color), Symbol(s, :alpha) - plotattributes[csym] = if plotattributes[csym] === :auto - plot_color(if Commons.has_black_border_for_default(stype) && s === :line - sp[:foreground_color_subplot] - else - scolor - end) - elseif plotattributes[csym] === :match + plotattributes[csym] = if plotattributes[csym] ≡ :auto + plot_color(if Commons.has_black_border_for_default(stype) && s ≡ :line + sp[:foreground_color_subplot] + else + scolor + end) + elseif plotattributes[csym] ≡ :match plot_color(scolor) else get_series_color(plotattributes[csym], sp, plotIndex, stype) @@ -106,29 +106,29 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) end # update markerstrokecolor - plotattributes[:markerstrokecolor] = if plotattributes[:markerstrokecolor] === :match + plotattributes[:markerstrokecolor] = if plotattributes[:markerstrokecolor] ≡ :match plot_color(sp[:foreground_color_subplot]) - elseif plotattributes[:markerstrokecolor] === :auto + elseif plotattributes[:markerstrokecolor] ≡ :auto get_series_color(plotattributes[:markercolor], sp, plotIndex, stype) else get_series_color(plotattributes[:markerstrokecolor], sp, plotIndex, stype) end # if marker_z, fill_z or line_z are set, ensure we have a gradient - if plotattributes[:marker_z] !== nothing + if plotattributes[:marker_z] ≢ nothing Commons.ensure_gradient!(plotattributes, :markercolor, :markeralpha) end - if plotattributes[:line_z] !== nothing + if plotattributes[:line_z] ≢ nothing Commons.ensure_gradient!(plotattributes, :linecolor, :linealpha) end - if plotattributes[:fill_z] !== nothing + if plotattributes[:fill_z] ≢ nothing Commons.ensure_gradient!(plotattributes, :fillcolor, :fillalpha) end # scatter plots don't have a line, but must have a shape if plotattributes[:seriestype] in (:scatter, :scatterbins, :scatterhist, :scatter3d) plotattributes[:linewidth] = 0 - if plotattributes[:markershape] === :none + if plotattributes[:markershape] ≡ :none plotattributes[:markershape] = :circle end end @@ -215,7 +215,7 @@ heatmap_edges( scale::Symbol = :identity, isedges::Bool = false, ispolar::Bool = false, -) = _heatmap_edges(Val(scale === :identity), v, scale, isedges, ispolar) +) = _heatmap_edges(Val(scale ≡ :identity), v, scale, isedges, ispolar) function heatmap_edges( x::AVec, @@ -239,8 +239,8 @@ function heatmap_edges( ArgumentError |> throw ( - _heatmap_edges(Val(xscale === :identity), x, xscale, isedges, false), - _heatmap_edges(Val(yscale === :identity), y, yscale, isedges, ispolar), # special handle for `r` in polar plots + _heatmap_edges(Val(xscale ≡ :identity), x, xscale, isedges, false), + _heatmap_edges(Val(yscale ≡ :identity), y, yscale, isedges, ispolar), # special handle for `r` in polar plots ) end @@ -270,27 +270,11 @@ end isijulia() = :IJulia in nameof.(collect(values(Base.loaded_modules))) isatom() = :Atom in nameof.(collect(values(Base.loaded_modules))) -istuple(::Tuple) = true -istuple(::Any) = false -isvector(::AVec) = true -isvector(::Any) = false -ismatrix(::AMat) = true -ismatrix(::Any) = false -isscalar(::Real) = true -isscalar(::Any) = false - -is_2tuple(v) = typeof(v) <: Tuple && length(v) == 2 - -ticks_type(ticks::AVec{<:Real}) = :ticks -ticks_type(ticks::AVec{<:AbstractString}) = :labels -ticks_type(ticks::Tuple{<:Union{AVec,Tuple},<:Union{AVec,Tuple}}) = :ticks_and_labels -ticks_type(ticks) = :invalid - limsType(lims::Tuple{<:Real,<:Real}) = :limits -limsType(lims::Symbol) = lims === :auto ? :auto : :invalid +limsType(lims::Symbol) = lims ≡ :auto ? :auto : :invalid limsType(lims) = :invalid -isautop(sp::Subplot) = sp[:projection_type] === :auto +isautop(sp::Subplot) = sp[:projection_type] ≡ :auto isortho(sp::Subplot) = sp[:projection_type] ∈ (:ortho, :orthographic) ispersp(sp::Subplot) = sp[:projection_type] ∈ (:persp, :perspective) @@ -305,7 +289,7 @@ nanappend!(a::AbstractVector, b) = (push!(a, NaN); append!(a, b); nothing) function nansplit(v::AVec) vs = Vector{eltype(v)}[] while true - if (idx = findfirst(isnan, v)) === nothing + if (idx = findfirst(isnan, v)) ≡ nothing # no nans push!(vs, v) break @@ -339,7 +323,7 @@ function make_fillrange_from_ribbon(kw::AKW) rib1, rib2 = -first(rib), last(rib) # kw[:ribbon] = nothing kw[:fillrange] = make_fillrange_side(y, rib1), make_fillrange_side(y, rib2) - (get(kw, :fillalpha, nothing) === nothing) && (kw[:fillalpha] = 0.5) + (get(kw, :fillalpha, nothing) ≡ nothing) && (kw[:fillalpha] = 0.5) end #turn tuple of fillranges to one path @@ -406,8 +390,8 @@ function Commons.preprocess_attributes!(plotattributes::AKW) if treats_y_as_x(get(plotattributes, :seriestype, :path)) xformatter = get(plotattributes, :xformatter, :auto) yformatter = get(plotattributes, :yformatter, :auto) - yformatter !== :auto && (plotattributes[:xformatter] = yformatter) - xformatter === :auto && + yformatter ≢ :auto && (plotattributes[:xformatter] = yformatter) + xformatter ≡ :auto && haskey(plotattributes, :yformatter) && pop!(plotattributes, :yformatter) end @@ -475,7 +459,7 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end # handle axes args for k in Commons._axis_attrs - if haskey(plotattributes, k) && k !== :link + if haskey(plotattributes, k) && k ≢ :link v = plotattributes[k] for letter in (:x, :y, :z) lk = get_attr_symbol(letter, k) @@ -515,7 +499,7 @@ function Commons.preprocess_attributes!(plotattributes::AKW) if haskey(plotattributes, :markershape) plotattributes[:markershape] = Commons._replace_markershape(plotattributes[:markershape]) - if plotattributes[:markershape] === :none && + if plotattributes[:markershape] ≡ :none && get(plotattributes, :seriestype, :path) in (:scatter, :scatterbins, :scatterhist, :scatter3d) #the default should be :auto, not :none, so that :none can be set explicitly and would be respected plotattributes[:markershape] = :circle @@ -594,33 +578,31 @@ end ``` """ function with(f::Function, args...; scalefonts = nothing, kw...) - newdefs = KW(kw) + new_defs = KW(kw) if :canvas in args - newdefs[:xticks] = nothing - newdefs[:yticks] = nothing - newdefs[:grid] = false - newdefs[:legend_position] = false + new_defs[:xticks] = nothing + new_defs[:yticks] = nothing + new_defs[:grid] = false + new_defs[:legend_position] = false end # dict to store old and new keyword args for anything that changes - olddefs = KW() - for k in keys(newdefs) - olddefs[k] = default(k) + old_defs = KW() + for k in keys(new_defs) + old_defs[k] = default(k) end # save the backend - oldbackend = CURRENT_BACKEND.sym + old_backend = backend_name() for arg in args - # change backend? - if arg isa Symbol - if arg ∈ backends() - if (pkg = backend_package_name(arg)) ≢ nothing # :plotly - @eval Main import $pkg - end - backend(arg) + # change backend ? + arg isa Symbol && if arg ∈ backends() + if (pkg = backend_package_name(arg)) ≢ nothing # :plotly + @eval Main import $pkg end + Base.invokelatest(backend, arg) end # TODO: generalize this strategy to allow args as much as possible @@ -629,57 +611,57 @@ function with(f::Function, args...; scalefonts = nothing, kw...) k = :legend if arg in (k, :leg) - olddefs[k] = default(k) - newdefs[k] = true + old_defs[k] = default(k) + new_defs[k] = true end k = :grid if arg == k - olddefs[k] = default(k) - newdefs[k] = true + old_defs[k] = default(k) + new_defs[k] = true end end # now set all those defaults - default(; newdefs...) + default(; new_defs...) scalefonts ≡ nothing || scalefontsizes(scalefonts) # call the function - ret = f() + ret = Base.invokelatest(f) # put the defaults back scalefonts ≡ nothing || resetfontsizes() - default(; olddefs...) + default(; old_defs...) # revert the backend - CURRENT_BACKEND.sym != oldbackend && backend(oldbackend) + old_backend != backend_name() && backend(old_backend) # return the result of the function ret end # --------------------------------------------------------------- +const _convert_sci_unicode_dict = Dict( + '⁰' => "0", + '¹' => "1", + '²' => "2", + '³' => "3", + '⁴' => "4", + '⁵' => "5", + '⁶' => "6", + '⁷' => "7", + '⁸' => "8", + '⁹' => "9", + '⁻' => "-", + "×10" => "×10^{", +) # converts unicode scientific notation, as returned by Showoff, # to a tex-like format (supported by gr, pyplot, and pgfplots). function convert_sci_unicode(label::AbstractString) - unicode_dict = Dict( - '⁰' => "0", - '¹' => "1", - '²' => "2", - '³' => "3", - '⁴' => "4", - '⁵' => "5", - '⁶' => "6", - '⁷' => "7", - '⁸' => "8", - '⁹' => "9", - '⁻' => "-", - "×10" => "×10^{", - ) - for key in keys(unicode_dict) - label = replace(label, key => unicode_dict[key]) + for key in keys(_convert_sci_unicode_dict) + label = replace(label, key => _convert_sci_unicode_dict[key]) end if occursin("×10^{", label) label = string(label, "}") @@ -947,7 +929,7 @@ Computes the distances of the plot limits to a sample of points at the extremes the ranges, and places the legend at the corner where the maximum distance to the limits is found. """ function _guess_best_legend_position(lp::Symbol, plt) - lp === :best || return lp + lp ≡ :best || return lp _guess_best_legend_position(xlims(plt), ylims(plt), plt) end diff --git a/PlotsBase/src/backends/web.jl b/PlotsBase/src/web.jl similarity index 94% rename from PlotsBase/src/backends/web.jl rename to PlotsBase/src/web.jl index cfa7e454c..1dc382c06 100644 --- a/PlotsBase/src/backends/web.jl +++ b/PlotsBase/src/web.jl @@ -1,5 +1,5 @@ -# NOTE: backend should implement `html_body` and `html_head` +# NOTE: backend should implement `html_body` and `html_head` # CREDIT: parts of this implementation were inspired by @joshday's PlotlyLocal.jl @@ -46,7 +46,7 @@ function standalone_html_window(plt::AbstractPlot) # if we open a browser ourself, we can host local files, so # when we have a local plotly downloaded this is the way to go! _use_local_dependencies[] = - _plotly_local_file_path[] === nothing ? false : isfile(_plotly_local_file_path[]) + _plotly_local_file_path[] ≡ nothing ? false : isfile(_plotly_local_file_path[]) filename = write_temp_html(plt) open_browser_window(filename) # restore for other backends diff --git a/PlotsBase/test/runtests.jl b/PlotsBase/test/runtests.jl index 2d8048ad1..a6f90621d 100644 --- a/PlotsBase/test/runtests.jl +++ b/PlotsBase/test/runtests.jl @@ -1,24 +1,28 @@ const TEST_PACKAGES = - strip.( - split( - get( - ENV, - "PLOTSBASE_TEST_PACKAGES", - "GR,UnicodePlots,PythonPlot,PGFPlotsX,PlotlyJS,Gaston", - ), - ",", + let val = get( + ENV, + "PLOTSBASE_TEST_PACKAGES", + "GR,UnicodePlots,PythonPlot,PGFPlotsX,PlotlyJS,Gaston", ) - ) -const TEST_BACKENDS = Symbol.(lowercase.(TEST_PACKAGES)) + Symbol.(strip.(split(val, ","))) + end +const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p in TEST_PACKAGES) + +get!(ENV, "MPLBACKEND", "agg") using PlotsBase +# always initialize GR +import GR +gr() + # initialize all backends for pkg in TEST_PACKAGES - @eval import $(Symbol(pkg)) # trigger extension - getproperty(PlotsBase, Symbol(lowercase(pkg)))() + @eval begin + import $pkg # trigger extension + $(TEST_BACKENDS[pkg])() + end end -gr() import Unitful: m, s, cm, DimensionError import PlotsBase: PLOTS_SEED, Plot, with @@ -35,6 +39,7 @@ using RecipesPipeline using FilePathsBase using LaTeXStrings using RecipesBase +using Preferences using TestImages using Unitful using FileIO @@ -47,8 +52,19 @@ is_ci() = PlotsBase.bool_env("CI") is_ci() || @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 +ref_name(i) = "ref" * lpad(i, 3, '0') + +const blacklist = if VERSION.major == 1 && VERSION.minor ≥ 9 + [ + 25, + 30, # FIXME: remove, when StatsPlots supports Plots v2 + 41, + ] # FIXME: github.com/JuliaLang/julia/issues/47261 +else + [] +end + for name in ( - "quality", "misc", "utils", "args", @@ -61,12 +77,15 @@ for name in ( "shorthands", "recipes", "unitful", - "hdf5plots", # broken ? + "hdf5plots", "pgfplotsx", "plotly", "animations", "output", + "reference", "backends", + "preferences", + "quality", ) @testset "$name" begin # skip the majority of tests if we only want to update reference images or under `PkgEval` (timeout limit) diff --git a/PlotsBase/test/test_args.jl b/PlotsBase/test/test_args.jl index 1e0da0f8e..a1c4207ee 100644 --- a/PlotsBase/test/test_args.jl +++ b/PlotsBase/test/test_args.jl @@ -16,14 +16,14 @@ x = collect(0.0:10.0) foo = Foo(x, sin.(x)) @testset "Magic attributes" begin - @test plot(foo)[1][1][:markershape] === :+ - @test plot(foo, markershape = :diamond)[1][1][:markershape] === :diamond - @test plot(foo, marker = :diamond)[1][1][:markershape] === :diamond + @test plot(foo)[1][1][:markershape] ≡ :+ + @test plot(foo, markershape = :diamond)[1][1][:markershape] ≡ :diamond + @test plot(foo, marker = :diamond)[1][1][:markershape] ≡ :diamond @test (@test_logs (:warn, "Skipped marker arg diamond.") plot( foo, marker = :diamond, markershape = :diamond, - )[1][1][:markershape]) === :diamond + )[1][1][:markershape]) ≡ :diamond end @testset "Subplot Attributes" begin diff --git a/PlotsBase/test/test_axes.jl b/PlotsBase/test/test_axes.jl index abda149df..9a9bba0f2 100644 --- a/PlotsBase/test/test_axes.jl +++ b/PlotsBase/test/test_axes.jl @@ -21,13 +21,15 @@ @test PlotsBase.labelfunc_tex(:log2)(1) == "2^{1}" @test PlotsBase.labelfunc_tex(:ln)(1) == "e^{1}" - @test PlotsBase.get_labels(:auto, 1:3, :identity) == ["1", "2", "3"] - @test PlotsBase.get_labels(:scientific, float.(500:500:1500), :identity) == - ["5.00×10^{2}", "1.00×10^{3}", "1.50×10^{3}"] - @test PlotsBase.get_labels(:engineering, float.(500:500:1500), :identity) == - ["500.×10^{0}", "1.00×10^{3}", "1.50×10^{3}"] - @test PlotsBase.get_labels(:latex, 1:3, :identity) == ["\$1\$", "\$2\$", "\$3\$"] - # GR is used during tests and it correctly overrides labelfunc(), but PGFPlotsX did not + with(:gr) do # NOTE: GR overrides `labelfunc` + @test PlotsBase.get_labels(:auto, 1:3, :identity) == ["1", "2", "3"] + @test PlotsBase.get_labels(:scientific, float.(500:500:1500), :identity) == + ["5.00×10^{2}", "1.00×10^{3}", "1.50×10^{3}"] + @test PlotsBase.get_labels(:engineering, float.(500:500:1500), :identity) == + ["500.×10^{0}", "1.00×10^{3}", "1.50×10^{3}"] + @test PlotsBase.get_labels(:latex, 1:3, :identity) == ["\$1\$", "\$2\$", "\$3\$"] + end + # GR is used during tests and it correctly overrides `labelfunc`, but PGFPlotsX did not with(:pgfplotsx) do @test PlotsBase.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] end @@ -155,9 +157,9 @@ end @test haskey(PlotsBase.Commons._keyAliases, :x_guide_position) @test !haskey(PlotsBase.Commons._keyAliases, :xguide_position) pl = plot(1:2, xl = "x label") - @test pl[1][:xaxis][:guide] === "x label" + @test pl[1][:xaxis][:guide] ≡ "x label" pl = plot(1:2, xrange = (0, 3)) - @test xlims(pl) === (0, 3) + @test xlims(pl) ≡ (0, 3) pl = plot(1:2, xtick = [1.25, 1.5, 1.75]) @test pl[1][:xaxis][:ticks] == [1.25, 1.5, 1.75] pl = plot(1:2, xlabelfontsize = 4) @@ -165,17 +167,17 @@ end pl = plot(1:2, xgα = 0.07) @test pl[1][:xaxis][:gridalpha] ≈ 0.07 pl = plot(1:2, xgridls = :dashdot) - @test pl[1][:xaxis][:gridstyle] === :dashdot + @test pl[1][:xaxis][:gridstyle] ≡ :dashdot pl = plot(1:2, xgridcolor = :red) - @test pl[1][:xaxis][:foreground_color_grid] === RGBA{Float64}(1.0, 0.0, 0.0, 1.0) + @test pl[1][:xaxis][:foreground_color_grid] ≡ RGBA{Float64}(1.0, 0.0, 0.0, 1.0) pl = plot(1:2, xminorgridcolor = :red) - @test pl[1][:xaxis][:foreground_color_minor_grid] === RGBA{Float64}(1.0, 0.0, 0.0, 1.0) + @test pl[1][:xaxis][:foreground_color_minor_grid] ≡ RGBA{Float64}(1.0, 0.0, 0.0, 1.0) pl = plot(1:2, xgrid_lw = 0.01) @test pl[1][:xaxis][:gridlinewidth] ≈ 0.01 pl = plot(1:2, xminorgrid_lw = 0.01) @test pl[1][:xaxis][:minorgridlinewidth] ≈ 0.01 pl = plot(1:2, xtickor = :out) - @test pl[1][:xaxis][:tick_direction] === :out + @test pl[1][:xaxis][:tick_direction] ≡ :out end @testset "Aliases" begin @@ -186,7 +188,7 @@ end pl = plot(1:2, label = "test") @test compare(pl, :guide, "", ===) pl = plot(1:2, lim = (0, 3)) - @test xlims(pl) === ylims(pl) === zlims(pl) === (0, 3) + @test xlims(pl) ≡ ylims(pl) ≡ zlims(pl) ≡ (0, 3) pl = plot(1:2, tick = [1.25, 1.5, 1.75]) @test compare(pl, :ticks, [1.25, 1.5, 1.75], ==) pl = plot(1:2, labelfontsize = 4) @@ -218,7 +220,7 @@ end let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - PlotsBase.PlotsPlots.scale_lims!(pl, 1.1) + PlotsBase.scale_lims!(pl, 1.1) @test first(xlims(pl)) < first(xl) @test last(xlims(pl)) > last(xl) @test first(ylims(pl)) < first(yl) @@ -237,7 +239,7 @@ end @testset "no labels" begin # github.com/JuliaPlots/Plots.jl/issues/4475 pl = plot(100:100:300, hcat([1, 2, 4], [-1, -2, -4]); yformatter = :none) - @test pl[1][:yaxis][:formatter] === :none + @test pl[1][:yaxis][:formatter] ≡ :none end @testset "minor ticks" begin @@ -245,9 +247,9 @@ end for minor_intervals in (:auto, :none, nothing, false, true, 0, 1, 2, 3, 4, 5) n_minor_ticks_per_major = if minor_intervals isa Bool minor_intervals ? PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 - elseif minor_intervals === :auto + elseif minor_intervals ≡ :auto PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 - elseif minor_intervals === :none || minor_intervals isa Nothing + elseif minor_intervals ≡ :none || minor_intervals isa Nothing 0 else max(0, minor_intervals - 1) @@ -265,9 +267,9 @@ end @test minor_ticks isa Nothing 0 end - elseif minor_intervals === :auto + elseif minor_intervals ≡ :auto length(minor_ticks) - elseif minor_intervals === :none || minor_intervals isa Nothing + elseif minor_intervals ≡ :none || minor_intervals isa Nothing @test minor_ticks isa Nothing 0 else diff --git a/PlotsBase/test/test_backends.jl b/PlotsBase/test/test_backends.jl index b30111944..fc5a4f157 100644 --- a/PlotsBase/test/test_backends.jl +++ b/PlotsBase/test/test_backends.jl @@ -1,143 +1,6 @@ -ci_tol() = - if Sys.islinux() - is_pkgeval() ? "1e-2" : "5e-4" - elseif Sys.isapple() - "1e-3" - else - "1e-1" - end - -const TESTS_MODULE = Module(:PlotsBaseTestModule) -const PLOTS_IMG_TOL = parse(Float64, get(ENV, "PLOTS_IMG_TOL", is_ci() ? ci_tol() : "1e-5")) - -Base.eval(TESTS_MODULE, :(using Random, StableRNGs, PlotsBase)) - -reference_dir(args...) = - if (ref_dir = get(ENV, "PLOTS_REFERENCE_DIR", nothing)) !== nothing - ref_dir - else - joinpath(homedir(), ".julia", "dev", "PlotReferenceImages.jl", args...) - end -reference_path(backend, version) = reference_dir("Plots", string(backend), string(version)) - -function checkout_reference_dir(dn::AbstractString) - mkpath(dn) - local repo - for i in 1:6 - try - repo = LibGit2.clone( - "https://github.com/JuliaPlots/PlotReferenceImages.jl.git", - dn, - ) - break - catch err - @warn err - sleep(20i) - end - end - if (ver = PlotsBase._current_plots_version).prerelease |> isempty - try - tag = LibGit2.GitObject(repo, "v$ver") - hash = string(LibGit2.target(tag)) - LibGit2.checkout!(repo, hash) - catch err - @warn err - end - end - LibGit2.peel(LibGit2.head(repo)) |> println # print some information - nothing -end - -let dn = reference_dir() - isdir(dn) || checkout_reference_dir(dn) -end - -ref_name(i) = "ref" * lpad(i, 3, '0') - -function reference_file(backend, version, i) - # NOTE: keep ref[...].png naming consistent with `PlotDocs` - refdir = reference_dir("Plots", string(backend)) - fn = ref_name(i) * ".png" - reffn = joinpath(refdir, string(version), fn) - for ver in sort(VersionNumber.(readdir(refdir)), rev = true) - if (tmpfn = joinpath(refdir, string(ver), fn)) |> isfile - reffn = tmpfn - break - end - end - return reffn -end - -function image_comparison_tests( - pkg::Symbol, - idx::Int; - debug = false, - popup = !is_ci(), - sigma = [1, 1], - tol = 1e-2, -) - example = PlotsBase._examples[idx] - @info "Testing plot: $pkg:$idx:$(example.header)" - - ver = PlotsBase._current_plots_version - ver = VersionNumber(ver.major, ver.minor, ver.patch) - reffn = reference_file(pkg, ver, idx) - newfn = joinpath(reference_path(pkg, ver), ref_name(idx) * ".png") - - imports = something(example.imports, :()) - exprs = quote - PlotsBase.Commons.debug!($debug) - backend($(QuoteNode(pkg))) - theme(:default) - rng = StableRNG(PlotsBase.PLOTS_SEED) - $(PlotsBase.replace_rand(example.exprs)) - end - @debug imports exprs - - func = fn -> Base.eval.(Ref(TESTS_MODULE), (imports, exprs, :(png($fn)))) - test_images( - VisualTest(func, reffn), - newfn = newfn, - popup = popup, - sigma = sigma, - tol = tol, - ) -end - -function image_comparison_facts( - pkg::Symbol; - skip = [], # skip these examples (int index) - only = nothing, # limit to these examples (int index) - debug = false, # print debug information ? - sigma = [1, 1], # number of pixels to "blur" - tol = 1e-2, # acceptable error (percent) -) - for i in setdiff(1:length(PlotsBase._examples), skip) - if only === nothing || i in only - @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) - end - end -end - -## Uncomment the following lines to update reference images for different backends -#= - -with(:gr) do - image_comparison_facts(:gr, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:gr]) -end - -with(:plotlyjs) do - image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:plotlyjs]) -end - -with(:pgfplotsx) do - image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:pgfplotsx]) -end -=# - @testset "UnicodePlots" begin with(:unicodeplots) do - @test backend() == PlotsBase._backend_instance(:unicodeplots) + @test backend() == PlotsBase.backend_instance(:unicodeplots) io = IOContext(IOBuffer(), :color => true) @@ -181,43 +44,20 @@ end end end -const blacklist = if VERSION.major == 1 && VERSION.minor ∈ (9, 10) - [ - 25, - 30, # FIXME: remove, when StatsPlots supports Plots v2 - 41, - ] # FIXME: github.com/JuliaLang/julia/issues/47261 -else - [] -end - -@testset "GR - reference images" begin - with(:gr) do - # NOTE: use `ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true;` to automatically replace reference images - @test backend() == PlotsBase._backend_instance(:gr) - @test backend_name() === :gr - image_comparison_facts( - :gr, - tol = PLOTS_IMG_TOL, - skip = vcat(PlotsBase._backend_skips[:gr], blacklist), - ) - end -end - -is_pkgeval() || @testset "PlotlyJS" begin +(is_pkgeval() || is_ci()) || @testset "PlotlyJS" begin with(:plotlyjs) do PlotlyJSExt = Base.get_extension(PlotsBase, :PlotlyJSExt) @test backend() == PlotlyJSExt.PlotlyJSBackend() pl = plot(rand(10)) @test pl isa Plot - @test display(pl) isa Nothing + display(pl) end end -is_pkgeval() || @testset "Examples" begin +is_pkgeval() || @testset "Backends" begin callback(m, pkgname, i) = begin - pl = m.PlotsBase.current() save_func = (; pgfplotsx = m.PlotsBase.pdf, unicodeplots = m.PlotsBase.txt) # fastest `savefig` for each backend + pl = m.PlotsBase.current() fn = Base.invokelatest( get(save_func, pkgname, m.PlotsBase.png), pl, diff --git a/PlotsBase/test/test_components.jl b/PlotsBase/test/test_components.jl index 501c43bab..62a84c6ac 100644 --- a/PlotsBase/test/test_components.jl +++ b/PlotsBase/test/test_components.jl @@ -55,7 +55,7 @@ @test square2.y ≈ coordsRotated2[2, :] # unrotate the new square in place - rotate!(square2, 2) + PlotsBase.rotate!(square2, 2) @test square2.x ≈ coords[1, :] @test square2.y ≈ coords[2, :] end @@ -67,7 +67,7 @@ @test coords(myshapes) isa Tuple{Vector{Vector{S}},Vector{Vector{T}}} where {T,S} local pl @test_nowarn pl = plot(myshapes) - @test pl[1][1][:seriestype] === :shape + @test pl[1][1][:seriestype] ≡ :shape end @testset "Misc" begin @@ -102,7 +102,7 @@ end end @testset "Alpha" begin @test brush(0.4).alpha == 0.4 - @test brush(20).alpha === nothing + @test brush(20).alpha ≡ nothing end @testset "Bad Argument" begin # using test_logs because test_warn seems to not work anymore @@ -138,7 +138,7 @@ end @test PlotsBase.series_annotations(["1" "2"; "3" "4"]) isa AbstractMatrix @test PlotsBase.series_annotations(10).strs[1].str == "10" - @test PlotsBase.series_annotations(nothing) === nothing + @test PlotsBase.series_annotations(nothing) ≡ nothing @test PlotsBase.series_annotations(ann) == ann @test PlotsBase.annotations(["1" "2"; "3" "4"]) isa AbstractMatrix diff --git a/PlotsBase/test/test_contours.jl b/PlotsBase/test/test_contours.jl index 5ec0072c8..04c656c00 100644 --- a/PlotsBase/test/test_contours.jl +++ b/PlotsBase/test/test_contours.jl @@ -1,8 +1,8 @@ @testset "check_contour_levels" begin let check_contour_levels = PlotsBase.Commons.check_contour_levels - @test check_contour_levels(2) === nothing - @test check_contour_levels(-1.0:0.2:10.0) === nothing - @test check_contour_levels([-100, -2, -1, 0, 1, 2, 100]) === nothing + @test check_contour_levels(2) ≡ nothing + @test check_contour_levels(-1.0:0.2:10.0) ≡ nothing + @test check_contour_levels([-100, -2, -1, 0, 1, 2, 100]) ≡ nothing @test_throws ArgumentError check_contour_levels(1.0) @test_throws ArgumentError check_contour_levels((1, 2, 3)) @test_throws ArgumentError check_contour_levels(-3) @@ -46,7 +46,7 @@ end @testset "$n contours" for n in (2, 5, 100) p = contour(x, y, z, levels = n) attr = p[1][1].plotattributes - @test attr[:seriestype] === :contour + @test attr[:seriestype] ≡ :contour @test attr[:levels] == n end end diff --git a/PlotsBase/test/test_defaults.jl b/PlotsBase/test/test_defaults.jl index f75fff7af..94cf69614 100644 --- a/PlotsBase/test/test_defaults.jl +++ b/PlotsBase/test/test_defaults.jl @@ -25,16 +25,16 @@ end pl = plot() @test pl[1][:legend_font_family] == "sans-serif" @test pl[1][:legend_font_pointsize] == 8 - @test pl[1][:legend_font_halign] === :hcenter - @test pl[1][:legend_font_valign] === :vcenter + @test pl[1][:legend_font_halign] ≡ :hcenter + @test pl[1][:legend_font_valign] ≡ :vcenter @test pl[1][:legend_font_rotation] == 0.0 @test pl[1][:legend_font_color] == RGB{Colors.N0f8}(0.0, 0.0, 0.0) - @test pl[1][:legend_position] === :best - @test pl[1][:legend_title] === nothing + @test pl[1][:legend_position] ≡ :best + @test pl[1][:legend_title] ≡ nothing @test pl[1][:legend_title_font_family] == "sans-serif" @test pl[1][:legend_title_font_pointsize] == 11 - @test pl[1][:legend_title_font_halign] === :hcenter - @test pl[1][:legend_title_font_valign] === :vcenter + @test pl[1][:legend_title_font_halign] ≡ :hcenter + @test pl[1][:legend_title_font_valign] ≡ :vcenter @test pl[1][:legend_title_font_rotation] == 0.0 @test pl[1][:legend_title_font_color] == RGB{Colors.N0f8}(0.0, 0.0, 0.0) @test pl[1][:legend_background_color] == RGBA{Float64}(1.0, 1.0, 1.0, 1.0) @@ -62,18 +62,18 @@ end ) @test pl[1][:legend_font_family] == "serif" @test pl[1][:legend_font_pointsize] == 12 - @test pl[1][:legend_font_halign] === :left - @test pl[1][:legend_font_valign] === :top + @test pl[1][:legend_font_halign] ≡ :left + @test pl[1][:legend_font_valign] ≡ :top @test pl[1][:legend_font_rotation] == 1.0 - @test pl[1][:legend_font_color] === :red - @test pl[1][:legend_position] === :outertopleft + @test pl[1][:legend_font_color] ≡ :red + @test pl[1][:legend_position] ≡ :outertopleft @test pl[1][:legend_title] == "The legend" @test pl[1][:legend_title_font_family] == "helvetica" @test pl[1][:legend_title_font_pointsize] == 3 - @test pl[1][:legend_title_font_halign] === :right - @test pl[1][:legend_title_font_valign] === :bottom + @test pl[1][:legend_title_font_halign] ≡ :right + @test pl[1][:legend_title_font_valign] ≡ :bottom @test pl[1][:legend_title_font_rotation] == -5.2 - @test pl[1][:legend_title_font_color] === :blue + @test pl[1][:legend_title_font_color] ≡ :blue @test pl[1][:legend_background_color] == RGBA{Float64}(0.0, 1.0, 1.0, 1.0) @test pl[1][:legend_foreground_color] == RGBA{Float64}(0.0, 0.5019607843137255, 0.0, 1.0) @@ -91,7 +91,7 @@ end foreground_color_subplot = :red, )[1] @test PlotsBase.legendfont(sp).pointsize == 12 - @test PlotsBase.legendfont(sp).halign === :left + @test PlotsBase.legendfont(sp).halign ≡ :left # match mechanism @test sp[:legend_font_color] == colorant"black" @test PlotsBase.legendfont(sp).color == colorant"black" diff --git a/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl index b98df9b4e..90cf6f4a1 100644 --- a/PlotsBase/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -11,10 +11,10 @@ end layout = 4, yscale = [:identity :identity :log10 :log10], ) - @test pl[1][:yaxis][:scale] === :identity - @test pl[2][:yaxis][:scale] === :identity - @test pl[3][:yaxis][:scale] === :log10 - @test pl[4][:yaxis][:scale] === :log10 + @test pl[1][:yaxis][:scale] ≡ :identity + @test pl[2][:yaxis][:scale] ≡ :identity + @test pl[3][:yaxis][:scale] ≡ :log10 + @test pl[4][:yaxis][:scale] ≡ :log10 end @testset "Plot title" begin @@ -41,9 +41,9 @@ end @testset "Plots.jl/issues/4083" begin pl = plot(plot(1:2), plot(1:2); border = :grid, plot_title = "abc") - @test pl[1][:framestyle] === :grid - @test pl[2][:framestyle] === :grid - @test pl[3][:framestyle] === :none + @test pl[1][:framestyle] ≡ :grid + @test pl[2][:framestyle] ≡ :grid + @test pl[3][:framestyle] ≡ :none end @testset "Allowed subplot counts" begin @@ -108,16 +108,16 @@ end show(io, PlotsBase.DEFAULT_BBOX[]) show(io, pl.layout) - @test PlotsBase.make_measure_hor(1PlotsBase.mm) == 1PlotsBase.mm - @test PlotsBase.make_measure_vert(1PlotsBase.mm) == 1PlotsBase.mm + @test PlotsBase.Commons.make_measure_hor(1PlotsBase.mm) == 1PlotsBase.mm + @test PlotsBase.Commons.make_measure_vert(1PlotsBase.mm) == 1PlotsBase.mm @test PlotsBase.parent(pl.layout) isa PlotsBase.RootLayout - show(io, PlotsBase.parent_bbox(pl.layout)) + show(io, PlotsBase.Commons.parent_bbox(pl.layout)) rl = PlotsBase.RootLayout() show(io, rl) - @test parent(rl) === nothing - @test PlotsBase.parent_bbox(rl) == PlotsBase.DEFAULT_BBOX[] + @test parent(rl) ≡ nothing + @test PlotsBase.Commons.parent_bbox(rl) == PlotsBase.DEFAULT_BBOX[] @test PlotsBase.bbox(rl) == PlotsBase.DEFAULT_BBOX[] @test PlotsBase.origin(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm) for h_anchor in (:left, :right, :hcenter), v_anchor in (:top, :bottom, :vcenter) @@ -125,10 +125,10 @@ end end el = PlotsBase.EmptyLayout() - @test PlotsBase.update_position!(el) === nothing + @test PlotsBase.update_position!(el) ≡ nothing @test size(el) == (0, 0) @test length(el) == 0 - @test el[1, 1] === nothing + @test el[1, 1] ≡ nothing @test PlotsBase.left(el) == 0PlotsBase.mm @test PlotsBase.top(el) == 0PlotsBase.mm diff --git a/PlotsBase/test/test_misc.jl b/PlotsBase/test/test_misc.jl index 6a53aad68..864082dad 100644 --- a/PlotsBase/test/test_misc.jl +++ b/PlotsBase/test/test_misc.jl @@ -21,7 +21,7 @@ end @testset "NoFail" begin with(:unicodeplots) do - @test backend() == PlotsBase._backend_instance(:unicodeplots) + @test backend() == PlotsBase.backend_instance(:unicodeplots) dsp = TextDisplay(IOContext(IOBuffer(), :color => true)) @@ -124,7 +124,7 @@ end value(m::MyType) = m.val data = MyType.(sort(randn(20))) - # A recipe that puts the axis letter in the title + # a recipe that puts the axis letter in the title @recipe function f(::Type{T}, m::T) where {T<:AbstractArray{<:MyType}} title --> string(plotattributes[:letter]) value.(m) diff --git a/PlotsBase/test/test_output.jl b/PlotsBase/test/test_output.jl index b929e9845..e221c1819 100644 --- a/PlotsBase/test/test_output.jl +++ b/PlotsBase/test/test_output.jl @@ -81,7 +81,7 @@ if Sys.islinux() && Sys.which("pdflatex") ≢ nothing end end -with(:gaston) do +Sys.islinux() && with(:gaston) do @test_save :png @test_save :pdf @test_save :eps diff --git a/PlotsBase/test/test_pgfplotsx.jl b/PlotsBase/test/test_pgfplotsx.jl index 81dde6ea1..07bacbedf 100644 --- a/PlotsBase/test/test_pgfplotsx.jl +++ b/PlotsBase/test/test_pgfplotsx.jl @@ -20,7 +20,7 @@ with(:pgfplotsx) do pl = plot(1:5) axis = first(get_pgf_axes(pl)) @test pl.o.the_plot isa PGFPlotsX.TikzDocument - @test pl.series_list[1].plotattributes[:quiver] === nothing + @test pl.series_list[1].plotattributes[:quiver] ≡ nothing @test count(x -> x isa PGFPlotsX.Plot, axis.contents) == 1 @test !haskey(axis.contents[1].options.dict, "fill") @test occursin("documentclass", PlotsBase.pgfx_preamble(pl)) @@ -60,7 +60,7 @@ with(:pgfplotsx) do pl = plot!(pl, zeros(n), zeros(n), 1:n, w = 10) axis = first(get_pgf_axes(pl)) if @test_nowarn(haskey(axis.options.dict, "colorbar")) - @test axis["colorbar"] === nothing + @test axis["colorbar"] ≡ nothing end end @@ -190,7 +190,7 @@ with(:pgfplotsx) do pl = heatmap(xs, ys, z, aspect_ratio = 1) axis = first(get_pgf_axes(pl)) if @test_nowarn(haskey(axis.options.dict, "colorbar")) - @test axis["colorbar"] === nothing + @test axis["colorbar"] ≡ nothing @test axis["colormap name"] == "plots1" end @@ -336,8 +336,8 @@ with(:pgfplotsx) do @test haskey(plots[1].options.dict, "fill") @test haskey(plots[2].options.dict, "fill") @test !haskey(plots[3].options.dict, "fill") - @test pl.o !== nothing - @test pl.o.the_plot !== nothing + @test pl.o ≢ nothing + @test pl.o.the_plot ≢ nothing end @testset "Markers and Paths" begin @@ -414,14 +414,14 @@ with(:pgfplotsx) do pl = plot(1:5, title = "Test me", titlefont = (2, :left)) @test pl[1][:title] == "Test me" @test pl[1][:titlefontsize] == 2 - @test pl[1][:titlefonthalign] === :left + @test pl[1][:titlefonthalign] ≡ :left ax_opt = first(get_pgf_axes(pl)).options @test ax_opt["title"] == "Test me" @test(haskey(ax_opt.dict, "title style")) isa Test.Pass pl = plot(1:5, plot_title = "Test me", plot_titlefont = (2, :left)) @test pl[:plot_title] == "Test me" @test pl[:plot_titlefontsize] == 2 - @test pl[:plot_titlefonthalign] === :left + @test pl[:plot_titlefonthalign] ≡ :left pl = heatmap( rand(3, 3), colorbar_title = "Test me", @@ -429,7 +429,7 @@ with(:pgfplotsx) do ) @test pl[1][:colorbar_title] == "Test me" @test pl[1][:colorbar_titlefontsize] == 12 - @test pl[1][:colorbar_titlefonthalign] === :right + @test pl[1][:colorbar_titlefonthalign] ≡ :right end @testset "Latexify - LaTeXStrings" begin @@ -455,7 +455,7 @@ with(:pgfplotsx) do plt1 = plot(rand(10, 5)) plt2 = plot(rand(10)) - @test plot(plt1, plt2, layout = (1, 2), plot_titles = ["(a)" "(b)"]) !== nothing + @test plot(plt1, plt2, layout = (1, 2), plot_titles = ["(a)" "(b)"]) ≢ nothing end if Sys.islinux() && Sys.which("pdflatex") ≢ nothing diff --git a/PlotsBase/test/test_preferences.jl b/PlotsBase/test/test_preferences.jl new file mode 100644 index 000000000..ae6cac8b7 --- /dev/null +++ b/PlotsBase/test/test_preferences.jl @@ -0,0 +1,96 @@ +# get `Preferences` set backend, if any +const PREVIOUS_DEFAULT_BACKEND = load_preference(PlotsBase, "default_backend") +# ----------------------------------------------------------------------------- + +PlotsBase.set_default_backend!() # start with empty preferences + +withenv("PLOTSBASE_DEFAULT_BACKEND" => "test_invalid_backend") do + @test_logs (:error, r"Unsupported backend.*") PlotsBase.default_backend() +end +@test_logs (:error, r"Unsupported backend.*") backend(:test_invalid_backend) + +@test PlotsBase.default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() + +withenv("PLOTSBASE_DEFAULT_BACKEND" => "unicodeplots") do + @test_logs (:info, r".*environment variable") PlotsBase.diagnostics(devnull) + @test PlotsBase.default_backend() == + Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() +end + +@test PlotsBase.default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() +@test PlotsBase.backend_package_name() ≡ :GR +@test PlotsBase.backend_name() ≡ :gr + +@test_logs (:info, r".*fallback") PlotsBase.diagnostics(devnull) + +@test PlotsBase.merge_with_base_supported([:annotations, :guide]) isa Set +@test PlotsBase.CurrentBackend(:gr).name ≡ :gr + +@test_logs (:warn, r".*is not compatible with") PlotsBase.set_default_backend!( + :test_invalid_backend, +) + +const DEBUG = false +@testset "persistent backend - restart" begin + # this test mimics a restart, which is needed after a preferences change + PlotsBase.set_default_backend!(:unicodeplots) + script = tempname() + dn = pkgdir(PlotsBase) |> escape_string + write( + script, + """ + using Pkg, Test; io = (devnull, stdout)[1] # toggle for debugging + Pkg.activate(; temp = true, io) + Pkg.develop(; path = joinpath("$dn", "..", "RecipesBase"), io) + Pkg.develop(; path = joinpath("$dn", "..", "RecipesPipeline"), io) + Pkg.develop(; path = "$dn", io) + Pkg.add("UnicodePlots"; io) # checked by Plots + import UnicodePlots + using PlotsBase + unicodeplots() + res = @testset "[subtest] preferences UnicodePlots" begin + @test_logs (:info, r".*Preferences") PlotsBase.diagnostics(io) + @test backend() == Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() + end + exit(res.n_passed == 2 ? 0 : 123) + """, + ) + DEBUG && print(read(script, String)) + @test run(```$(Base.julia_cmd()) $script```) |> success +end + +is_pkgeval() || for pkg in TEST_PACKAGES + @testset "persistent backend $pkg" begin + be = TEST_BACKENDS[pkg] + if is_ci() + (Sys.isapple() && be ≡ :gaston) && continue # FIXME: hangs + (Sys.iswindows() && be ≡ :plotlyjs) && continue # FIXME: OutOfMemory + end + @test_logs PlotsBase.set_default_backend!(be) # test the absence of warnings + rm.(Base.find_all_in_cache_path(Base.module_keys[PlotsBase])) # make sure the compiled cache is removed + script = tempname() + write( + script, + """ + import $pkg + using Test, PlotsBase + $be() + res = @testset "[subtest] persistent backend $pkg" begin + @test PlotsBase.backend_name() ≡ :$be + end + exit(res.n_passed == 1 ? 0 : 123) + """, + ) + DEBUG && print(read(script, String)) + @test run(```$(Base.julia_cmd()) $script```) |> success # test default precompilation + end +end + +PlotsBase.set_default_backend!() # clear `Preferences` key + +# ----------------------------------------------------------------------------- +if PREVIOUS_DEFAULT_BACKEND ≡ nothing + delete_preferences!(PlotsBase, "default_backend") # restore the absence of a preference +else + set_default_backend!(PREVIOUS_DEFAULT_BACKEND) # reset to previous state +end diff --git a/PlotsBase/test/test_quality.jl b/PlotsBase/test/test_quality.jl index aba35594e..8fccb5a1f 100644 --- a/PlotsBase/test/test_quality.jl +++ b/PlotsBase/test/test_quality.jl @@ -1,20 +1,13 @@ @testset "Auto QUality Assurance" begin # JuliaTesting/Aqua.jl/issues/77 # TODO: fix :Contour, :Latexify and :LaTeXStrings stale imports in Plots 2.0 - # :PyCall and :Conda stale deps show up when running CI + # :CondaPkg stale deps show up when running CI Aqua.test_all( PlotsBase; stale_deps = (; - ignore = [ - :GR, - :CondaPkg, - :Contour, - :Latexify, - :LaTeXStrings, - :Requires, - :UnitfulLatexify, - ] + ignore = [:CondaPkg, :Contour, :UnitfulLatexify, :LaTeXStrings, :Latexify] ), + persistent_tasks = false, ambiguities = false, deps_compat = false, # FIXME: fails `CondaPkg` piracies = false, diff --git a/PlotsBase/test/test_recipes.jl b/PlotsBase/test/test_recipes.jl index 86cd0e007..9c4abddde 100644 --- a/PlotsBase/test/test_recipes.jl +++ b/PlotsBase/test/test_recipes.jl @@ -7,16 +7,16 @@ using OffsetArrays (1:3, 1:3) end let pl = pl = plot(LegendPlot(); legend = :right) - @test pl[1][:legend_position] === :right + @test pl[1][:legend_position] ≡ :right end let pl = pl = plot(LegendPlot()) - @test pl[1][:legend_position] === :topleft + @test pl[1][:legend_position] ≡ :topleft end let pl = plot(LegendPlot(); legend = :inline) - @test pl[1][:legend_position] === :inline + @test pl[1][:legend_position] ≡ :inline end let pl = plot(LegendPlot(); legend = :inline, ymirror = true) - @test pl[1][:legend_position] === :inline + @test pl[1][:legend_position] ≡ :inline end end @@ -24,7 +24,7 @@ end pl = plot(1:5) lens!(pl, [1, 2], [1, 2], inset = (1, bbox(0.0, 0.0, 0.2, 0.2)), colorbar = false) @test length(pl.series_list) == 4 - @test pl[2][:colorbar] === :none + @test pl[2][:colorbar] ≡ :none end @testset "vline, vspan" begin @@ -96,10 +96,10 @@ end # TODO: that should cover all seriestypes without the need to have the extension loaded # currently uses plotly seriestypes only @test :surface in PlotsBase.all_seriestypes() - unicode_instance = PlotsBase._backend_instance(:unicodeplots) - @test PlotsBase.seriestype_supported(unicode_instance, :surface) === :native - @test PlotsBase.seriestype_supported(unicode_instance, :hspan) === :recipe - @test PlotsBase.seriestype_supported(PlotsBase.NoBackend(), :line) === :native + unicode_instance = PlotsBase.backend_instance(:unicodeplots) + @test PlotsBase.seriestype_supported(unicode_instance, :surface) ≡ :native + @test PlotsBase.seriestype_supported(unicode_instance, :hspan) ≡ :recipe + @test PlotsBase.seriestype_supported(PlotsBase.NoBackend(), :line) ≡ :native end with(:gr) do diff --git a/PlotsBase/test/test_reference.jl b/PlotsBase/test/test_reference.jl new file mode 100644 index 000000000..203454434 --- /dev/null +++ b/PlotsBase/test/test_reference.jl @@ -0,0 +1,147 @@ +ci_tol() = + if Sys.islinux() + is_pkgeval() ? "1e-2" : "5e-4" + elseif Sys.isapple() + "1e-3" + else + "1e-1" + end + +const TESTS_MODULE = Module(:PlotsBaseTestModule) +const PLOTS_IMG_TOL = parse(Float64, get(ENV, "PLOTS_IMG_TOL", is_ci() ? ci_tol() : "1e-5")) + +Base.eval(TESTS_MODULE, :(using Random, StableRNGs, PlotsBase)) + +reference_dir(args...) = + if (ref_dir = get(ENV, "PLOTS_REFERENCE_DIR", nothing)) ≢ nothing + ref_dir + else + joinpath(homedir(), ".julia", "dev", "PlotReferenceImages.jl", args...) + end +reference_path(backend, version) = reference_dir("Plots", string(backend), string(version)) + +function checkout_reference_dir(dn::AbstractString) + mkpath(dn) + local repo + for i in 1:6 + try + repo = LibGit2.clone( + "https://github.com/JuliaPlots/PlotReferenceImages.jl.git", + dn, + ) + break + catch err + @warn err + sleep(20i) + end + end + if (ver = PlotsBase._version).prerelease |> isempty + try + tag = LibGit2.GitObject(repo, "v$ver") + hash = string(LibGit2.target(tag)) + LibGit2.checkout!(repo, hash) + catch err + @warn err + end + end + LibGit2.peel(LibGit2.head(repo)) |> println # print some information + nothing +end + +let dn = reference_dir() + isdir(dn) || checkout_reference_dir(dn) +end + +function reference_file(backend, version, i) + # NOTE: keep ref[...].png naming consistent with `PlotDocs` + refdir = reference_dir("Plots", string(backend)) + fn = ref_name(i) * ".png" + reffn = joinpath(refdir, string(version), fn) + for ver in sort(VersionNumber.(readdir(refdir)), rev = true) + if (tmpfn = joinpath(refdir, string(ver), fn)) |> isfile + reffn = tmpfn + break + end + end + return reffn +end + +function image_comparison_tests( + pkg::Symbol, + idx::Int; + debug = false, + popup = !is_ci(), + sigma = [1, 1], + tol = 1e-2, +) + example = PlotsBase._examples[idx] + @info "Testing plot: $pkg:$idx:$(example.header)" + + ver = PlotsBase._version + ver = VersionNumber(ver.major, ver.minor, ver.patch) + reffn = reference_file(pkg, ver, idx) + newfn = joinpath(reference_path(pkg, ver), ref_name(idx) * ".png") + + imports = something(example.imports, :()) + exprs = quote + PlotsBase.Commons.debug!($debug) + backend($(QuoteNode(pkg))) + theme(:default) + rng = StableRNG(PlotsBase.PLOTS_SEED) + $(PlotsBase.replace_rand(example.exprs)) + end + @debug imports exprs + + func = fn -> Base.eval.(Ref(TESTS_MODULE), (imports, exprs, :(png($fn)))) + test_images( + VisualTest(func, reffn), + newfn = newfn, + popup = popup, + sigma = sigma, + tol = tol, + ) +end + +function image_comparison_facts( + pkg::Symbol; + skip = [], # skip these examples (int index) + only = nothing, # limit to these examples (int index) + debug = false, # print debug information ? + sigma = [1, 1], # number of pixels to "blur" + tol = 1e-2, # acceptable error (percent) +) + for i in setdiff(1:length(PlotsBase._examples), skip) + if only ≡ nothing || i in only + @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) + end + end +end + +## Uncomment the following lines to update reference images for different backends +#= + +with(:gr) do + image_comparison_facts(:gr, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:gr]) +end + +with(:plotlyjs) do + image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:plotlyjs]) +end + +with(:pgfplotsx) do + image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = PlotsBase._backend_skips[:pgfplotsx]) +end +=# + +@testset "GR - reference images" begin + with(:gr) do + # NOTE: use `ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true;` to automatically replace reference images + @test backend() == PlotsBase.backend_instance(:gr) + @test backend_name() ≡ :gr + image_comparison_facts( + :gr, + tol = PLOTS_IMG_TOL, + skip = vcat(PlotsBase._backend_skips[:gr], blacklist), + ) + end +end diff --git a/PlotsBase/test/test_shorthands.jl b/PlotsBase/test/test_shorthands.jl index fb6a96703..69a9cdf28 100644 --- a/PlotsBase/test/test_shorthands.jl +++ b/PlotsBase/test/test_shorthands.jl @@ -97,7 +97,7 @@ end pl = plot3d([1, 2], [1, 2], [1, 2]) plot3d!(pl, [3, 4], [3, 4], [3, 4]) - @test PlotsBase.series_list(pl[1])[1][:seriestype] === :path3d + @test PlotsBase.series_list(pl[1])[1][:seriestype] ≡ :path3d end @testset "Set Ticks" begin diff --git a/PlotsBase/test/test_utils.jl b/PlotsBase/test/test_utils.jl index e3d9ab01b..ea179267a 100644 --- a/PlotsBase/test/test_utils.jl +++ b/PlotsBase/test/test_utils.jl @@ -49,12 +49,12 @@ @test PlotsBase.nansplit([1, 2, NaN, 3, 4]) == [[1.0, 2.0], [3.0, 4.0]] @test PlotsBase.nanvcat([1, NaN]) |> length == 4 - @test PlotsBase.PlotMeasures.inch2px(1) isa AbstractFloat - @test PlotsBase.PlotMeasures.px2inch(1) isa AbstractFloat - @test PlotsBase.PlotMeasures.inch2mm(1) isa AbstractFloat - @test PlotsBase.PlotMeasures.mm2inch(1) isa AbstractFloat - @test PlotsBase.PlotMeasures.px2mm(1) isa AbstractFloat - @test PlotsBase.PlotMeasures.mm2px(1) isa AbstractFloat + @test PlotsBase.Commons.inch2px(1) isa AbstractFloat + @test PlotsBase.Commons.px2inch(1) isa AbstractFloat + @test PlotsBase.Commons.inch2mm(1) isa AbstractFloat + @test PlotsBase.Commons.mm2inch(1) isa AbstractFloat + @test PlotsBase.Commons.px2mm(1) isa AbstractFloat + @test PlotsBase.Commons.mm2px(1) isa AbstractFloat pl = plot() @test xlims() isa Tuple @@ -102,25 +102,25 @@ push!(pl, 1:2, 2:3, 3:4) pl = plot([1, 2, 3], [4, 5, 6]) - @test PlotsBase.PlotsPlots.xmin(pl) == 1 - @test PlotsBase.PlotsPlots.xmax(pl) == 3 + @test PlotsBase.Plots.xmin(pl) == 1 + @test PlotsBase.Plots.xmax(pl) == 3 @test PlotsBase.Commons.ignorenan_extrema(pl) == (1, 3) - @test PlotsBase.Commons.get_attr_symbol(:x, "lims") === :xlims - @test PlotsBase.Commons.get_attr_symbol(:x, :lims) === :xlims + @test PlotsBase.Commons.get_attr_symbol(:x, "lims") ≡ :xlims + @test PlotsBase.Commons.get_attr_symbol(:x, :lims) ≡ :xlims @test contains(PlotsBase._document_argument(:bar_position), "bar_position") - @test PlotsBase.limsType((1, 1)) === :limits - @test PlotsBase.limsType(:undefined) === :invalid - @test PlotsBase.limsType(:auto) === :auto - @test PlotsBase.limsType(NaN) === :invalid + @test PlotsBase.limsType((1, 1)) ≡ :limits + @test PlotsBase.limsType(:undefined) ≡ :invalid + @test PlotsBase.limsType(:auto) ≡ :auto + @test PlotsBase.limsType(NaN) ≡ :invalid - @test PlotsBase.ticks_type([1, 2]) === :ticks - @test PlotsBase.ticks_type(["1", "2"]) === :labels - @test PlotsBase.ticks_type(([1, 2], ["1", "2"])) === :ticks_and_labels - @test PlotsBase.ticks_type(((1, 2), ("1", "2"))) === :ticks_and_labels - @test PlotsBase.ticks_type(:undefined) === :invalid + @test PlotsBase.ticks_type([1, 2]) ≡ :ticks + @test PlotsBase.ticks_type(["1", "2"]) ≡ :labels + @test PlotsBase.ticks_type(([1, 2], ["1", "2"])) ≡ :ticks_and_labels + @test PlotsBase.ticks_type(((1, 2), ("1", "2"))) ≡ :ticks_and_labels + @test PlotsBase.ticks_type(:undefined) ≡ :invalid pl = plot(1:2, 1:2, 1:2, proj_type = :ortho) @test PlotsBase.isortho(first(pl.subplots)) @@ -130,23 +130,23 @@ let pl = plot(1:2) series = first(pl.series_list) label = "fancy label" - PlotsBase.PlotsSeries.attr!(series; label) + PlotsBase.attr!(series; label) @test series[:label] == label - @test PlotsBase.PlotsSeries.attr(series, :label) == label + @test PlotsBase.attr(series, :label) == label label = "another label" - PlotsBase.PlotsSeries.attr!(series, label, :label) - @test PlotsBase.PlotsSeries.attr(series, :label) == label + PlotsBase.attr!(series, label, :label) + @test PlotsBase.attr(series, :label) == label sp = first(pl.subplots) title = "fancy title" - PlotsBase.Subplots.attr!(sp; title) + PlotsBase.attr!(sp; title) @test sp[:title] == title end end @testset "NaN-separated Segments" begin - segments(args...) = collect(PlotsBase.PlotsSeries.iter_segments(args...)) + segments(args...) = collect(PlotsBase.DataSeries.iter_segments(args...)) nan10 = fill(NaN, 10) @test segments(11:20) == [1:10] @@ -195,45 +195,45 @@ end pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(-3)) pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(+3)) pl = plot(x, x, label = "linear") pl = plot!(x, x .^ 2, label = "quadratic") pl = plot!(x, x .^ 3, label = "cubic") - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft x = 0:0.01:2 pl = plot(x, -x, label = "linear") pl = plot!(x, -x .^ 2, label = "quadratic") pl = plot!(x, -x .^ 3, label = "cubic") - @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :bottomleft x = OffsetArrays.OffsetArray(0:0.01:2, OffsetArrays.Origin(-3)) pl = plot(x, -x, label = "linear") pl = plot!(x, -x .^ 2, label = "quadratic") pl = plot!(x, -x .^ 3, label = "cubic") - @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :bottomleft x = [0, 1, 0, 1] y = [0, 0, 1, 1] pl = scatter(x, y, xlims = [0.0, 1.3], ylims = [0.0, 1.3], label = "test") - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(x, y, xlims = [-0.3, 1.0], ylims = [-0.3, 1.0], label = "test") - @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :bottomleft pl = scatter(x, y, xlims = [0.0, 1.3], ylims = [-0.3, 1.0], label = "test") - @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :bottomright pl = scatter(x, y, xlims = [-0.3, 1.0], ylims = [0.0, 1.3], label = "test") - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft y1 = [ 0.6640202072697099, @@ -250,48 +250,48 @@ end y2 = [0.40089741940615464, 0.6687326060649715, 0.6844117863127116] pl = plot(1:10, y1) pl = plot!(1:3, y2, xlims = (0, 10), ylims = (0, 1)) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright # test empty plot pl = plot([]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright # test that we didn't overlap other placements - @test PlotsBase._guess_best_legend_position(:bottomleft, pl) === :bottomleft + @test PlotsBase._guess_best_legend_position(:bottomleft, pl) ≡ :bottomleft # test singleton pl = plot(1:1) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright # test cycling indexes x = 0.0:0.1:1 y = [1, 2, 3] pl = scatter(x, y) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright # Test step plot with variable limits x = 0:0.001:1 y = vcat([0.0 for _ in 1:100], [1.0 for _ in 101:200], [0.5 for _ in 201:1001]) pl = scatter(x, y) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(x, y, xlims = [0, 0.25]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft pl = scatter(x, y, xlims = [0.1, 0.25]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(x, y, xlims = [0.18, 0.25]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(x, y, ylims = [-1, 0.75]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :bottomright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :bottomright pl = scatter(x, y, ylims = [0.25, 0.75]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(-x, y, ylims = [0.25, 0.75]) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(-x, y) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft pl = scatter(-x, -y) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topleft + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topleft pl = scatter(x, -y) - @test PlotsBase._guess_best_legend_position(:best, pl) === :topright + @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright end @testset "dispatch" begin diff --git a/Project.toml b/Project.toml index 430295e24..cb4539f3b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,24 +1,20 @@ name = "Plots" uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" author = ["Tom Breloff (@tbreloff)"] -version = "1.41.0" +version = "2.0.0" [deps] GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -Preferences = "21216c6a-2e73-6563-6e65-726566657250" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" [compat] -GR = "0.69.5 - 0.73" -Pkg = "1" -PlotsBase = "1.41" +GR = "0, 1" +PlotsBase = "0.1" PrecompileTools = "1" -Preferences = "1" -Reexport = "0.2, 1" -julia = "1.6" +Reexport = "1" +julia = "1.9" [extras] PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" diff --git a/RecipesBase/src/RecipesBase.jl b/RecipesBase/src/RecipesBase.jl index 5f3410aff..aa5103caa 100644 --- a/RecipesBase/src/RecipesBase.jl +++ b/RecipesBase/src/RecipesBase.jl @@ -73,10 +73,10 @@ _is_arrow_tuple(expr::Expr) = expr.head ≡ :tuple && !isempty(expr.args) && isa(expr.args[1], Expr) && - expr.args[1].head === :(-->) + expr.args[1].head ≡ :(-->) -_equals_symbol(x::Symbol, sym::Symbol) = x === sym -_equals_symbol(x::QuoteNode, sym::Symbol) = x.value === sym +_equals_symbol(x::Symbol, sym::Symbol) = x ≡ sym +_equals_symbol(x::QuoteNode, sym::Symbol) = x.value ≡ sym _equals_symbol(x, sym::Symbol) = false # build an apply_recipe function header from the recipe function header @@ -112,7 +112,7 @@ function create_kw_body(func_signature::Expr) if isa(arg1, Expr) && arg1.head ≡ :parameters for kwpair in arg1.args k, v = kwpair.args - if isa(k, Expr) && k.head === :(::) + if isa(k, Expr) && k.head ≡ :(::) k = k.args[1] @warn """ Type annotations on keyword arguments not currently supported in recipes. @@ -163,14 +163,14 @@ function process_recipe_body!(expr::Expr) # 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 === :(:=) + if e.head ≡ :(:=) force = true e.head = :(-->) end # we are going to recursively swap out `a --> b, flags...` commands # note: this means "x may become 5" - if e.head === :(-->) + if e.head ≡ :(-->) k, v = e.args if isa(k, Symbol) k = QuoteNode(k) @@ -298,7 +298,7 @@ macro recipe(funcexpr::Expr) $cleanup_body series_list = $RecipesBase.RecipeData[] func_return = $func_body - func_return === nothing || push!( + func_return ≡ nothing || push!( series_list, $RecipesBase.RecipeData( plotattributes, diff --git a/RecipesPipeline/src/user_recipe.jl b/RecipesPipeline/src/user_recipe.jl index 4c0fcbaaa..00321d7ca 100644 --- a/RecipesPipeline/src/user_recipe.jl +++ b/RecipesPipeline/src/user_recipe.jl @@ -115,9 +115,9 @@ end @recipe function f(x, y, z) # COV_EXCL_LINE wrap_surfaces!(plotattributes, x, y, z) did_replace = false - did_replace |= x !== (newx = _apply_type_recipe(plotattributes, x, :x)) - did_replace |= y !== (newy = _apply_type_recipe(plotattributes, y, :y)) - did_replace |= z !== (newz = _apply_type_recipe(plotattributes, z, :z)) + did_replace |= x ≢ (newx = _apply_type_recipe(plotattributes, x, :x)) + did_replace |= y ≢ (newy = _apply_type_recipe(plotattributes, y, :y)) + did_replace |= z ≢ (newz = _apply_type_recipe(plotattributes, z, :z)) if did_replace newx, newy, newz else @@ -127,8 +127,8 @@ end @recipe function f(x, y) # COV_EXCL_LINE wrap_surfaces!(plotattributes, x, y) did_replace = false - did_replace |= x !== (newx = _apply_type_recipe(plotattributes, x, :x)) - did_replace |= y !== (newy = _apply_type_recipe(plotattributes, y, :y)) + did_replace |= x ≢ (newx = _apply_type_recipe(plotattributes, x, :x)) + did_replace |= y ≢ (newy = _apply_type_recipe(plotattributes, y, :y)) if did_replace newx, newy else @@ -137,7 +137,7 @@ end end @recipe function f(y) # COV_EXCL_LINE wrap_surfaces!(plotattributes, y) - if y !== (newy = _apply_type_recipe(plotattributes, y, :y)) + if y ≢ (newy = _apply_type_recipe(plotattributes, y, :y)) newy else SliceIt, nothing, y, nothing @@ -150,7 +150,7 @@ end did_replace = false newargs = map( v -> begin - did_replace |= v !== (newv = _apply_type_recipe(plotattributes, v, :unknown)) + did_replace |= v ≢ (newv = _apply_type_recipe(plotattributes, v, :unknown)) newv end, (v1, v2, v3, v4, vrest...), @@ -167,7 +167,7 @@ wrap_surfaces!(plotattributes, x::AVec, y::AVec, z::AMat) = wrap_surfaces!(plota wrap_surfaces!(plotattributes, x::AVec, y::AVec, z::Surface) = wrap_surfaces!(plotattributes) wrap_surfaces!(plotattributes) = - if (v = get(plotattributes, :fill_z, nothing)) !== nothing + if (v = get(plotattributes, :fill_z, nothing)) ≢ nothing v isa Surface || (plotattributes[:fill_z] = Surface(v)) end diff --git a/ci/downstream.jl b/ci/downstream.jl new file mode 100644 index 000000000..faf53f45f --- /dev/null +++ b/ci/downstream.jl @@ -0,0 +1,78 @@ +using Pkg + +const LibGit2 = Pkg.GitTools.LibGit2 +const TOML = Pkg.TOML + +failsafe_clone_checkout(path, url) = begin + local repo + for i in 1:6 + try + repo = Pkg.GitTools.ensure_clone(stdout, path, url) + break + catch err + @warn err + sleep(20i) + end + end + + @assert isfile(joinpath(path, "Project.toml")) "spurious network error: clone failed, bailing out" + + name, _ = splitext(basename(url)) + registries = joinpath(first(DEPOT_PATH), "registries") + general = joinpath(registries, "General") + versions = joinpath(general, name[1:1], name, "Versions.toml") + if !isfile(versions) + mkpath(general) + run(setenv(`tar xf $general.tar.gz`; dir = general)) + end + @assert isfile(versions) + + version_dict = TOML.parse(read(versions, String)) + stable = VersionNumber.(keys(version_dict)) |> maximum + tag = LibGit2.GitObject(repo, "v$stable") + hash = string(LibGit2.target(tag)) + LibGit2.checkout!(repo, hash) + nothing +end + +pkg_version(name) = + Pkg.Types.read_package(normpath(@__DIR__, "..", name, "Project.toml")).version |> string + +maybe_pin_version!(dict::AbstractDict, name::AbstractString, ver::AbstractString) = + haskey(dict, name) && (dict[name] = "=$ver") + +"fake supported Plots ecosystem versions for using `Pkg.develop`" +fake_supported_versions!(path) = begin + toml = joinpath(path, "Project.toml") + parsed_toml = TOML.parse(read(toml, String)) + compat = parsed_toml["compat"] + maybe_pin_version!(compat, "RecipesBase", pkg_version("RecipesBase")) + maybe_pin_version!(compat, "RecipesPipeline", pkg_version("RecipesPipeline")) + maybe_pin_version!(compat, "PlotsBase", pkg_version("PlotsBase")) + maybe_pin_version!(compat, "Plots", pkg_version("")) + open(toml, "w") do io + TOML.print(io, parsed_toml) + end + # print(read(toml, String)) # debug + nothing +end + +test_stable(pkg::AbstractString) = begin + Pkg.activate(; temp = true) + mktempdir() do tmpd + for dn in ("RecipesBase", "RecipesPipeline", "PlotsBase", "") + Pkg.develop(; path = joinpath(@__DIR__, "..", dn)) + end + + pkg_dir = joinpath(tmpd, "$pkg.jl") + failsafe_clone_checkout(pkg_dir, "https://github.com/JuliaPlots/$pkg.jl") + fake_supported_versions!(pkg_dir) + + Pkg.develop(; path = pkg_dir) + Pkg.test(pkg) + end + nothing +end + +test_stable("GraphRecipes") +test_stable("StatsPlots") diff --git a/ci/matplotlib.jl b/ci/matplotlib.jl new file mode 100644 index 000000000..4c657a3b0 --- /dev/null +++ b/ci/matplotlib.jl @@ -0,0 +1,25 @@ +using Pkg +Pkg.add("CondaPkg") + +using CondaPkg +CondaPkg.resolve() + +libgcc = if Sys.islinux() + # see discourse.julialang.org/t/glibcxx-version-not-found/82209/8 + # julia 1.8.3 is built with libstdc++.so.6.0.29, so we must restrict to this version (gcc 11.3.0, not gcc 12.2.0) + # see gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html + specs = Dict( + v"3.4.29" => ">=11.1,<12.1", + v"3.4.30" => ">=12.1,<13.1", + v"3.4.31" => ">=13.1,<14.1", + v"3.4.32" => ">=14.1,<15.1", + v"3.4.33" => ">=15.1,<16.1", + # ... keep this up-to-date with gcc 16 + )[Base.BinaryPlatforms.detect_libstdcxx_version()] + ("libgcc-ng$specs", "libstdcxx-ng$specs") +else + () +end + +CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) +CondaPkg.status() diff --git a/src/Plots.jl b/src/Plots.jl index 3d9c6e9f6..dace3c1fd 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -1,121 +1,92 @@ module Plots + using PrecompileTools -using Preferences using Reexport -using Pkg @reexport using PlotsBase -function __init__() - ccall(:jl_generating_output, Cint, ()) == 1 && return - load_default_backend() -end - -# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: -# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" -# ==> this must always be done during precompilation, otherwise -# the cache will not invalidate when preferences change -const PLOTS_DEFAULT_BACKEND = lowercase(load_preference(Plots, "default_backend", "gr")) - -function load_default_backend() - # environment variable preempts the `Preferences` based mechanism - PlotsBase.CURRENT_BACKEND.sym = - get(ENV, "PLOTS_DEFAULT_BACKEND", PLOTS_DEFAULT_BACKEND) |> lowercase |> Symbol - if (pkg_name = PlotsBase.backend_package_name()) ≡ :GR - @eval import GR - end - Base.invokelatest(PlotsBase.backend, PlotsBase.CURRENT_BACKEND.sym) +if PlotsBase.DEFAULT_BACKEND == "gr" + @debug "loading default GR" + import GR end -function set_default_backend!( - backend::Union{Nothing,AbstractString,Symbol} = nothing; - force = true, - kw..., -) - if backend ≡ nothing - delete_preferences!(Plots, "default_backend"; force, kw...) - else - # NOTE: `_check_installed` already throws a warning - if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing - set_preferences!(Plots, "default_backend" => value; force, kw...) - end - end - nothing -end +function __init__() + ccall(:jl_generating_output, Cint, ()) == 1 && return + PlotsBase.default_backend() -function diagnostics(io::IO = stdout) - origin = if has_preference(Plots, "default_backend") - "`Preferences`" - elseif haskey(ENV, "PLOTS_DEFAULT_BACKEND") - "environment variable" - else - "fallback" - end - if (be = backend_name()) ≡ :none - @info "no `Plots` backends currently initialized" - else - be_name = string(PlotsBase.backend_package_name(be)) - @info "selected `Plots` backend: $be_name, from $origin" - Pkg.status( - ["Plots", "PlotsBase", "RecipesBase", "RecipesPipeline", be_name]; - mode = Pkg.PKGMODE_MANIFEST, - io, - ) - end nothing end # COV_EXCL_START -@setup_workload begin - load_default_backend() - @debug PlotsBase.backend_package_name() - n = length(PlotsBase._examples) - imports = sizehint!(Expr[], n) - examples = sizehint!(Expr[], 10n) - scratch_dir = mktempdir() - for i in setdiff( - 1:n, - PlotsBase._backend_skips[backend_name()], - PlotsBase._animation_examples, - ) - PlotsBase._examples[i].external && continue - (imp = PlotsBase._examples[i].imports) ≡ nothing || - push!(imports, PlotsBase.replace_module(imp)) - func = gensym(string(i)) - push!( - examples, - quote - $func() = begin # evaluate each example in a local scope - $(PlotsBase._examples[i].exprs) - $i == 1 || return # only for one example - fn = joinpath(scratch_dir, tempname()) - pl = current() - show(devnull, pl) - # FIXME: pgfplotsx requires bug - backend_name() ≡ :pgfplotsx && return - if backend_name() ≡ :unicodeplots - savefig(pl, "$fn.txt") - return - end - showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") - showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") - if showable(MIME"image/svg+xml"(), pl) - show(IOBuffer(), MIME"image/svg+xml"(), pl) - end - nothing - end - $func() - end, +if PlotsBase.DEFAULT_BACKEND == "gr" # FIXME: Creating a new global in closed module `Main` (`UnicodePlots`) breaks incremental compilation because the side effects will not be permanent. + @setup_workload begin + #= + if PlotsBase.DEFAULT_BACKEND == "gr" + import GR + elseif PlotsBase.DEFAULT_BACKEND == "unicodeplots" + @eval Main import UnicodePlots + elseif PlotsBase.DEFAULT_BACKEND == "pythonplot" + @eval Main import PythonPlot + elseif PlotsBase.DEFAULT_BACKEND == "pgfplotsx" + @eval Main import PGFPlotsX + elseif PlotsBase.DEFAULT_BACKEND == "plotlyjs" + @eval Main import PlotlyJS + elseif PlotsBase.DEFAULT_BACKEND == "gaston" + @eval Main import Gaston + elseif PlotsBase.DEFAULT_BACKEND == "hdf5" + @eval Main import HDF5 + end + =# + PlotsBase.default_backend() + @debug PlotsBase.backend_package_name() + n = length(PlotsBase._examples) + imports = sizehint!(Expr[], n) + examples = sizehint!(Expr[], 10n) + scratch_dir = mktempdir() + for i in setdiff( + 1:n, + PlotsBase._backend_skips[backend_name()], + PlotsBase._animation_examples, ) - end - withenv("GKSwstype" => "nul") do - @compile_workload begin - load_default_backend() - eval.(imports) - eval.(examples) + PlotsBase._examples[i].external && continue + (imp = PlotsBase._examples[i].imports) ≡ nothing || + push!(imports, PlotsBase.replace_module(imp)) + func = gensym(string(i)) + push!( + examples, + quote + $func() = begin # evaluate each example in a local scope + $(PlotsBase._examples[i].exprs) + $i == 1 || return # trigger display only for one example + fn = joinpath(scratch_dir, tempname()) + pl = current() + show(devnull, pl) + # FIXME: pgfplotsx requires bug + backend_name() ≡ :pgfplotsx && return + if backend_name() ≡ :unicodeplots + savefig(pl, "$fn.txt") + return + end + showable(MIME"image/png"(), pl) && savefig(pl, "$fn.png") + showable(MIME"application/pdf"(), pl) && savefig(pl, "$fn.pdf") + if showable(MIME"image/svg+xml"(), pl) + show(PipeBuffer(), MIME"image/svg+xml"(), pl) + end + nothing + end + $func() + end, + ) end + withenv("GKSwstype" => "nul", "MPLBACKEND" => "agg") do + @compile_workload begin + PlotsBase.default_backend() + eval.(imports) + eval.(examples) + end + end + PlotsBase.CURRENT_PLOT.nullableplot = nothing end - PlotsBase.CURRENT_PLOT.nullableplot = nothing end # COV_EXCL_STOP -end +end # module diff --git a/test/preferences.jl b/test/preferences.jl deleted file mode 100644 index 6f5101ffb..000000000 --- a/test/preferences.jl +++ /dev/null @@ -1,75 +0,0 @@ - -@testset "Preferences" begin - Plots.set_default_backend!() # start with empty preferences - - withenv("PLOTS_DEFAULT_BACKEND" => "invalid") do - @test_logs (:error, r"Unsupported backend.*") Plots.load_default_backend() - end - @test_logs (:error, r"Unsupported backend.*") backend(:invalid) - - @test Plots.load_default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() - - withenv("PLOTS_DEFAULT_BACKEND" => "unicodeplots") do - @test_logs (:info, r".*environment variable") Plots.diagnostics(devnull) - @test Plots.load_default_backend() == - Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() - end - - @test Plots.load_default_backend() == Base.get_extension(PlotsBase, :GRExt).GRBackend() - @test Plots.PlotsBase.backend_package_name() ≡ :GR - @test Plots.backend_name() ≡ :gr - - @test_logs (:info, r".*fallback") Plots.diagnostics(devnull) - - @test Plots.PlotsBase.merge_with_base_supported([:annotations, :guide]) isa Set - @test Plots.PlotsBase.CurrentBackend(:gr).sym ≡ :gr - - @test_logs (:warn, r".*is not compatible with") Plots.set_default_backend!(:invalid) - - @testset "persistent backend" begin - # this test mimics a restart, which is needed after a preferences change - Plots.set_default_backend!(:unicodeplots) - script = tempname() - write( - script, - """ - using Pkg, Test; io = (devnull, stdout)[1] # toggle for debugging - Pkg.activate(; temp = true, io) - Pkg.develop(; path = "$(escape_string(pkgdir(Plots)))", io) - Pkg.add("UnicodePlots"; io) # checked by Plots - import UnicodePlots - using Plots - res = @testset "Preferences UnicodePlots" begin - @test_logs (:info, r".*Preferences") Plots.diagnostics(io) - @test backend() == Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlotsBackend() - end - exit(res.n_passed == 2 ? 0 : 123) - """, - ) - @test success(run(```$(Base.julia_cmd()) $script```)) - end - - is_pkgeval() || for pkg in TEST_PACKAGES - be = Symbol(lowercase(pkg)) - (Sys.isapple() && be ≡ :gaston) && continue # FIXME: hangs - (Sys.iswindows() && be ≡ :plotlyjs && is_ci()) && continue # FIXME: OutOfMemory - @test_logs Plots.set_default_backend!(be) # test the absence of warnings - rm.(Base.find_all_in_cache_path(Base.module_keys[Plots])) # make sure the compiled cache is removed - script = tempname() - write( - script, - """ - import $pkg - using Test, Plots - $be() - res = @testset "Persistent backend $pkg" begin - @test Plots.backend_name() ≡ :$be - end - exit(res.n_passed == 1 ? 0 : 123) - """, - ) - @test success(run(```$(Base.julia_cmd()) $script```)) # test default precompilation - end - - Plots.set_default_backend!() # clear `Preferences` key -end diff --git a/test/runtests.jl b/test/runtests.jl index 054a40117..c1d2b40b1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,29 +1,28 @@ const TEST_PACKAGES = - strip.(split(get(ENV, "PLOTS_TEST_PACKAGES", "GR,UnicodePlots,PythonPlot"), ",")) + let val = get(ENV, "PLOTS_TEST_PACKAGES", "GR,UnicodePlots,PythonPlot") + Symbol.(strip.(split(val, ","))) + end +const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p in TEST_PACKAGES) + using PlotsBase # initialize all backends for pkg in TEST_PACKAGES - @eval import $(Symbol(pkg)) # trigger extension - getproperty(PlotsBase, Symbol(lowercase(pkg)))() + @eval begin + import $pkg # trigger extension + $(TEST_BACKENDS[pkg])() + end end gr() -using Preferences using Plots using Test -is_auto() = Plots.PlotsBase.bool_env("VISUAL_REGRESSION_TESTS_AUTO") -is_pkgeval() = Plots.PlotsBase.bool_env("JULIA_PKGEVAL") -is_ci() = Plots.PlotsBase.bool_env("CI") - -# get `Preferences` set backend, if any -const PREVIOUS_DEFAULT_BACKEND = load_preference(Plots, "default_backend") - -include("preferences.jl") - -if PREVIOUS_DEFAULT_BACKEND === nothing - delete_preferences!(Plots, "default_backend") # restore the absence of a preference -else - Plots.set_default_backend!(PREVIOUS_DEFAULT_BACKEND) # reset to previous state +for pkg in TEST_PACKAGES + @testset "simple plots using $pkg" begin + @eval $(TEST_BACKENDS[pkg])() + pl = plot(1:2) + @test pl isa PlotsBase.Plot + show(devnull, pl) + end end From a5a028b460ba8a0572e31df8339bdbfb91cb03e2 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 14:46:59 +0200 Subject: [PATCH 04/85] restore ci on v2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a996b021..ebdd6a21d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: ci on: pull_request: push: - branches: [master] + branches: [master, v2] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From 2979ab5bd4420b1cda685eed618a48949f203de0 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 14:47:20 +0200 Subject: [PATCH 05/85] restore format check on v2 --- .github/workflows/format_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index afb15383b..91be67414 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -3,7 +3,7 @@ name: format on: pull_request: push: - branches: [master] + branches: [master, v2] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From 5014e6c7d5c919ab0d497b6d32ad5c17c46ff754 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 14:58:49 +0200 Subject: [PATCH 06/85] empty commit From 82f57ba6237af4a33561f0ea119745de46934500 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 15:08:51 +0200 Subject: [PATCH 07/85] make downstream test conditional --- .github/workflows/ci.yml | 4 +++- ci/downstream.jl | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebdd6a21d..f8b2c1e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,9 @@ jobs: ' - name: Test downstream packages if: startsWith(matrix.os, 'ubuntu') - run: xvfb-run julia --color=yes ci/downstream.jl + run: | + xvfb-run julia --color=yes ci/downstream.jl GraphRecipes || true + xvfb-run julia --color=yes ci/downstream.jl StatsPlots || true - uses: julia-actions/julia-processcoverage@latest if: startsWith(matrix.os, 'ubuntu') diff --git a/ci/downstream.jl b/ci/downstream.jl index faf53f45f..070dd9033 100644 --- a/ci/downstream.jl +++ b/ci/downstream.jl @@ -74,5 +74,4 @@ test_stable(pkg::AbstractString) = begin nothing end -test_stable("GraphRecipes") -test_stable("StatsPlots") +test_stable.(ARGS) From 023b64338daf57890c9de32c4d7c2179a842ba82 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 7 Apr 2024 22:09:53 +0200 Subject: [PATCH 08/85] cleanup --- PlotsBase/src/Annotations.jl | 28 +++++++++++++++++++- PlotsBase/src/Arrows.jl | 2 ++ PlotsBase/src/Axes.jl | 13 ++++----- PlotsBase/src/BezierCurves.jl | 2 +- PlotsBase/src/Colorbars.jl | 2 +- PlotsBase/src/DataSeries.jl | 2 +- PlotsBase/src/Fonts.jl | 2 +- PlotsBase/src/Plots.jl | 29 ++++++++++---------- PlotsBase/src/PlotsBase.jl | 1 - PlotsBase/src/Shapes.jl | 8 +++--- PlotsBase/src/Strokes.jl | 2 +- PlotsBase/src/Subplots.jl | 7 +++-- PlotsBase/src/Surfaces.jl | 2 +- PlotsBase/src/Ticks.jl | 2 +- PlotsBase/src/alignment.jl | 23 +++++++++------- PlotsBase/src/backends.jl | 8 +++--- PlotsBase/src/init.jl | 50 +++++++++++++++++++++++++++++++++++ PlotsBase/src/layouts.jl | 35 +++++++++++------------- PlotsBase/src/legend.jl | 8 ++---- PlotsBase/src/output.jl | 6 ++--- PlotsBase/src/pipeline.jl | 19 ++++++------- PlotsBase/src/plot.jl | 5 +++- PlotsBase/src/plotattr.jl | 4 +-- PlotsBase/src/preferences.jl | 49 ---------------------------------- PlotsBase/src/recipes.jl | 2 +- PlotsBase/src/shorthands.jl | 26 ------------------ PlotsBase/src/utils.jl | 13 +++------ PlotsBase/src/web.jl | 2 +- 28 files changed, 169 insertions(+), 183 deletions(-) delete mode 100644 PlotsBase/src/preferences.jl diff --git a/PlotsBase/src/Annotations.jl b/PlotsBase/src/Annotations.jl index e73a095c9..798fb30f8 100644 --- a/PlotsBase/src/Annotations.jl +++ b/PlotsBase/src/Annotations.jl @@ -253,6 +253,32 @@ locate_annotation(sp::Subplot, pos::Symbol, label::PlotText) = end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- + +""" + annotate!(anns) + annotate!(anns::Tuple...) + annotate!(x, y, txt) + +Add annotations to an existing plot. +Annotations are specified either as a vector of tuples, each of the form `(x,y,txt)`, +or as three vectors, `x, y, txt`. +Each `txt` can be a `String`, `PlotText` PlotText (created with `text(args...)`), +or a tuple of arguments to `text` (e.g., `("Label", 8, :red, :top)`). + +# Example +```julia-repl +julia> plot(1:10) +julia> annotate!([(7,3,"(7,3)"),(3,7,text("hey", 14, :left, :top, :green))]) +julia> annotate!([(4, 4, ("More text", 8, 45.0, :bottom, :red))]) +julia> annotate!([2,5], [6,3], ["text at (2,6)", "text at (5,3)"]) +``` +""" +annotate!(anns...; kw...) = plot!(; annotation = anns, kw...) +annotate!(anns::Tuple...; kw...) = plot!(; annotation = collect(anns), kw...) +annotate!(anns::AVec{<:Tuple}; kw...) = plot!(; annotation = anns, kw...) +annotate!(plt::PlotOrSubplot, anns...; kw...) = plot!(plt; annotations = anns, kw...) +annotate!(plt::PlotOrSubplot, anns::Tuple...; kw...) = plot!(plt; annotations = collect(anns), kw...) +annotate!(plt::PlotOrSubplot, anns::AVec{<:Tuple}; kw...) = plot!(plt; annotations = anns, kw...) using .Annotations diff --git a/PlotsBase/src/Arrows.jl b/PlotsBase/src/Arrows.jl index 4bc470480..597f126b8 100644 --- a/PlotsBase/src/Arrows.jl +++ b/PlotsBase/src/Arrows.jl @@ -63,4 +63,6 @@ end end # module +# ----------------------------------------------------------------------------- + using .Arrows diff --git a/PlotsBase/src/Axes.jl b/PlotsBase/src/Axes.jl index ea099aeeb..32b28ee4f 100644 --- a/PlotsBase/src/Axes.jl +++ b/PlotsBase/src/Axes.jl @@ -35,11 +35,12 @@ const _widen_seriestypes = ( :scatter3d, ) -# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place +"simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place" mutable struct Axis sps::Vector{Subplot} plotattributes::DefaultsDict end + function Axis(sp::Subplot, letter::Symbol, args...; kw...) explicit = KW( :letter => letter, @@ -57,7 +58,7 @@ function Axis(sp::Subplot, letter::Symbol, args...; kw...) attr!(Axis([sp], attr), args...; kw...) end -# properly retrieve from axis.attr, passing `:match` to the correct key +"properly retrieve from axis.attr, passing `:match` to the correct key" Base.getindex(axis::Axis, k::Symbol) = if (v = axis.plotattributes[k]) ≡ :match if haskey(Commons._match_map2, k) @@ -77,7 +78,7 @@ mutable struct Extrema end Extrema() = Extrema(Inf, -Inf) -# ------------------------------------------------------------------------- + sort_3d_axes(x, y, z, letter) = if letter ≡ :x x, y, z @@ -338,7 +339,7 @@ function PlotsBase.attr!(axis::Axis, args...; kw...) axis end -# ------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- Base.show(io::IO, axis::Axis) = Commons.dumpdict(io, axis.plotattributes, "Axis") ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) @@ -466,8 +467,8 @@ function PlotsBase.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} ex end -end # Axes +end # module -# ------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Axes diff --git a/PlotsBase/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl index 1b7cc051e..af7585265 100644 --- a/PlotsBase/src/BezierCurves.jl +++ b/PlotsBase/src/BezierCurves.jl @@ -21,6 +21,6 @@ PlotsBase.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .BezierCurves diff --git a/PlotsBase/src/Colorbars.jl b/PlotsBase/src/Colorbars.jl index 17a55a800..a3f27063f 100644 --- a/PlotsBase/src/Colorbars.jl +++ b/PlotsBase/src/Colorbars.jl @@ -145,6 +145,6 @@ _update_subplot_colorbars(sp::Subplot, series::Series) = update_clims(sp, series end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Colorbars diff --git a/PlotsBase/src/DataSeries.jl b/PlotsBase/src/DataSeries.jl index 17043f0d0..04391669d 100644 --- a/PlotsBase/src/DataSeries.jl +++ b/PlotsBase/src/DataSeries.jl @@ -317,7 +317,7 @@ end end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .DataSeries diff --git a/PlotsBase/src/Fonts.jl b/PlotsBase/src/Fonts.jl index 2434bc018..5534c5ed9 100644 --- a/PlotsBase/src/Fonts.jl +++ b/PlotsBase/src/Fonts.jl @@ -178,6 +178,6 @@ is_horizontal(t::PlotText) = abs(sind(t.font.rotation)) ≤ sind(45) end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- @reexport using .Fonts diff --git a/PlotsBase/src/Plots.jl b/PlotsBase/src/Plots.jl index 7eae830ec..028c82954 100644 --- a/PlotsBase/src/Plots.jl +++ b/PlotsBase/src/Plots.jl @@ -27,7 +27,7 @@ const SubplotMap = Dict{Any,Subplot} mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} backend::T # the backend type n::Int # number of series - attr::DefaultsDict # arguments for the whole plot + attr::DefaultsDict # arguments for the whole plot series_list::Vector{Series} # arguments for each series o # the backend's plot object subplots::Vector{Subplot} @@ -107,8 +107,7 @@ function Base.push!(plt::Plot, y::AVec) plt end -# push y[i] to the ith series -# same x for each series +"push y[i] to the ith series, same x for each series" Base.push!(plt::Plot, x::Real, y::AVec) = push!(plt, [x], y) # push (x[i], y[i]) to the ith series @@ -121,7 +120,7 @@ function Base.push!(plt::Plot, x::AVec, y::AVec) plt end -# push (x[i], y[i], z[i]) to the ith series +"push (x[i], y[i], z[i]) to the ith series" function Base.push!(plt::Plot, x::AVec, y::AVec, z::AVec) nx = length(x) ny = length(y) @@ -167,28 +166,28 @@ Base.get(plt::Plot, k::Symbol, v) = get(plt.attr, k, v) Base.size(plt::Plot) = size(plt.layout) Base.size(plt::Plot, i::Integer) = size(plt.layout)[i] -Base.ndims(plt::Plot) = 2 +Base.ndims(::Plot) = 2 # clear out series list, but retain subplots Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) -Commons.get_subplot(plt::Plot, sp::Subplot) = sp +Commons.get_subplot(::Plot, sp::Subplot) = sp Commons.get_subplot(plt::Plot, i::Integer) = plt.subplots[i] Commons.get_subplot(plt::Plot, k) = plt.spmap[k] Commons.series_list(plt::Plot) = plt.series_list -Commons.get_ticks(p::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), p.subplots) +Commons.get_ticks(plt::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), plt.subplots) get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x ≡ sp, plt.subplots) RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = Commons.preprocess_attributes!(plotattributes) -plottitlefont(p::Plot) = font(; - family = p[:plot_titlefontfamily], - pointsize = p[:plot_titlefontsize], - valign = p[:plot_titlefontvalign], - halign = p[:plot_titlefonthalign], - rotation = p[:plot_titlefontrotation], - color = p[:plot_titlefontcolor], +plottitlefont(plt::Plot) = font(; + family = plt[:plot_titlefontfamily], + pointsize = v[:plot_titlefontsize], + valign = plt[:plot_titlefontvalign], + halign = plt[:plot_titlefonthalign], + rotation = plt[:plot_titlefontrotation], + color = plt[:plot_titlefontcolor], ) # update attr from an input dictionary @@ -289,6 +288,6 @@ Commons.get_thickness_scaling(plt::Plot) = get_thickness_scaling(plt.attr) end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Plots diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl index 09ad15f27..2e9d18b20 100644 --- a/PlotsBase/src/PlotsBase.jl +++ b/PlotsBase/src/PlotsBase.jl @@ -161,7 +161,6 @@ include("shorthands.jl") include("backends.jl") include("web.jl") include("plotly.jl") -include("preferences.jl") include("init.jl") include("users.jl") diff --git a/PlotsBase/src/Shapes.jl b/PlotsBase/src/Shapes.jl index 2c8b9e55e..2b3843a70 100644 --- a/PlotsBase/src/Shapes.jl +++ b/PlotsBase/src/Shapes.jl @@ -30,8 +30,6 @@ nanappend!(a::AVec{P3}, b) = (push!(a, (NaN, NaN, NaN)); append!(a, b); nothing) compute_angle(v::P2) = (angle = atan(v[2], v[1]); angle < 0 ? 2π - angle : angle) -# ------------------------------------------------------------- - struct Shape{X<:Number,Y<:Number} x::Vector{X} y::Vector{Y} @@ -59,8 +57,8 @@ PlotsBase.coords(shape::Shape) = shape.x, shape.y PlotsBase.coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shapes)) "get an array of tuples of points on a circle with radius `r`" -partialcircle(start_θ, end_θ, n = 20, r = 1) = - [(r * cos(u), r * sin(u)) for u in range(start_θ, stop = end_θ, length = n)] +partialcircle(start_angle, end_angle, n = 20, r = 1) = + [(r * cos(u), r * sin(u)) for u in range(start_angle, end_angle, n)] "interleave 2 vectors into each other (like a zipper's teeth)" function weave(x, y; ordering = Vector[x, y]) @@ -207,7 +205,7 @@ rotate_y(x::Real, y::Real, θ::Real, centerx::Real, centery::Real) = end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Shapes diff --git a/PlotsBase/src/Strokes.jl b/PlotsBase/src/Strokes.jl index 840f42a55..2c9f87782 100644 --- a/PlotsBase/src/Strokes.jl +++ b/PlotsBase/src/Strokes.jl @@ -81,6 +81,6 @@ end end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Strokes diff --git a/PlotsBase/src/Subplots.jl b/PlotsBase/src/Subplots.jl index c41b4c1b2..dc7a1f324 100644 --- a/PlotsBase/src/Subplots.jl +++ b/PlotsBase/src/Subplots.jl @@ -63,7 +63,7 @@ Base.lastindex(sp::Subplot) = length(series_list(sp)) Base.empty!(sp::Subplot) = empty!(sp.series_list) Base.get(sp::Subplot, k::Symbol, v) = get(sp.attr, k, v) -# ----------------------------------------------------------------------- +# ----------------------------------------------------------------------------- Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}") @@ -219,8 +219,7 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) end # expand for fillrange - fr = plotattributes[:fillrange] - if fr ≡ nothing && plotattributes[:seriestype] ≡ :bar + if (fr = plotattributes[:fillrange]) ≡ nothing && plotattributes[:seriestype] ≡ :bar fr = 0.0 end if fr ≢ nothing && !RecipesPipeline.is3d(plotattributes) @@ -267,7 +266,7 @@ Commons.get_thickness_scaling(sp::Subplot) = get_thickness_scaling(sp.plt) end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Subplots diff --git a/PlotsBase/src/Surfaces.jl b/PlotsBase/src/Surfaces.jl index 12eb688be..dbcae93a1 100644 --- a/PlotsBase/src/Surfaces.jl +++ b/PlotsBase/src/Surfaces.jl @@ -23,6 +23,6 @@ Commons.handle_surface(z::Surface) = permutedims(z.surf) end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Surfaces diff --git a/PlotsBase/src/Ticks.jl b/PlotsBase/src/Ticks.jl index e5c9f2b9d..7abf4f9c8 100644 --- a/PlotsBase/src/Ticks.jl +++ b/PlotsBase/src/Ticks.jl @@ -106,6 +106,6 @@ end end # module -# ------------------------------------------------------------------- +# ----------------------------------------------------------------------------- using .Ticks diff --git a/PlotsBase/src/alignment.jl b/PlotsBase/src/alignment.jl index 0969a5837..f3ad63ba1 100644 --- a/PlotsBase/src/alignment.jl +++ b/PlotsBase/src/alignment.jl @@ -15,7 +15,7 @@ text_size(lab::AbstractString, sz::Number, rot::Number = 0) = text_size(length(lab), sz, rot) text_size(lab::PlotText, sz::Number, rot::Number = 0) = text_size(length(lab.str), sz, rot) -# account for the size/length/rotation of tick labels +"account for the size/length/rotation of tick labels" function tick_padding(sp::Subplot, axis::Axis) if (ticks = get_ticks(sp, axis)) ≡ nothing 0mm @@ -28,21 +28,24 @@ function tick_padding(sp::Subplot, axis::Axis) # generalize by "rotating" y labels rot = axis[:rotation] + (axis[:letter] ≡ :y ? 90 : 0) - # # we need to compute the size of the ticks generically - # # this means computing the bounding box and then getting the width/height - # labelwidth = 0.8longest_label * ptsz - # - # - # # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles - # hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm + #= + # we need to compute the size of the ticks generically + # this means computing the bounding box and then getting the width/height + labelwidth = 0.8longest_label * ptsz + + # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles + hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm + =# # get the height of the rotated label text_size(longest_label, axis[:tickfontsize], rot)[2] end end -# Set the (left, top, right, bottom) minimum padding around the plot area -# to fit ticks, tick labels, guides, colorbars, etc. +""" +Set the (left, top, right, bottom) minimum padding around the plot area +to fit ticks, tick labels, guides, colorbars, etc. +""" function _update_min_padding!(sp::Subplot) # TODO: something different when `RecipesPipeline.is3d(sp) == true` leftpad = tick_padding(sp, sp[:yaxis]) + sp[:left_margin] + guide_padding(sp[:yaxis]) diff --git a/PlotsBase/src/backends.jl b/PlotsBase/src/backends.jl index c1817a502..1c30f98a2 100644 --- a/PlotsBase/src/backends.jl +++ b/PlotsBase/src/backends.jl @@ -48,11 +48,11 @@ function _check_installed(pkg::Union{Module,AbstractString,Symbol}; warn = true) version end -_create_backend_figure(plt::Plot) = nothing -_initialize_subplot(plt::Plot, sp::Subplot) = nothing +_create_backend_figure(::Plot) = nothing +_initialize_subplot(::Plot, ::Subplot) = nothing -_series_added(plt::Plot, series::Series) = nothing -_series_updated(plt::Plot, series::Series) = nothing +_series_added(::Plot, ::Series) = nothing +_series_updated(::Plot, ::Series) = nothing _before_layout_calcs(plt::Plot) = nothing diff --git a/PlotsBase/src/init.jl b/PlotsBase/src/init.jl index bac48ea2b..256529ef2 100644 --- a/PlotsBase/src/init.jl +++ b/PlotsBase/src/init.jl @@ -59,3 +59,53 @@ function __init__() nothing end + +# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: +# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" +# ==> this must always be done during precompilation, otherwise +# the cache will not invalidate when preferences change +const DEFAULT_BACKEND = lowercase(load_preference(PlotsBase, "default_backend", "gr")) + +function default_backend() + # environment variable preempts the `Preferences` based mechanism + name = get(ENV, "PLOTSBASE_DEFAULT_BACKEND", DEFAULT_BACKEND) |> lowercase |> Symbol + backend(name) +end + +function set_default_backend!( + backend::Union{Nothing,AbstractString,Symbol} = nothing; + force = true, + kw..., +) + if backend ≡ nothing + delete_preferences!(PlotsBase, "default_backend"; force, kw...) + else + # NOTE: `_check_installed` already throws a warning + if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing + set_preferences!(PlotsBase, "default_backend" => value; force, kw...) + end + end + nothing +end + +function diagnostics(io::IO = stdout) + origin = if has_preference(PlotsBase, "default_backend") + "`Preferences`" + elseif haskey(ENV, "PLOTSBASE_DEFAULT_BACKEND") + "environment variable" + else + "fallback" + end + if (be = backend_name()) ≡ :none + @info "no `PlotsBase` backends currently initialized" + else + pkg_name = string(PlotsBase.backend_package_name(be)) + @info "selected `PlotsBase` backend: $pkg_name, from $origin" + Pkg.status( + ["PlotsBase", "RecipesBase", "RecipesPipeline", pkg_name]; + mode = Pkg.PKGMODE_MANIFEST, + io, + ) + end + nothing +end diff --git a/PlotsBase/src/layouts.jl b/PlotsBase/src/layouts.jl index b43ad09c3..27193f411 100644 --- a/PlotsBase/src/layouts.jl +++ b/PlotsBase/src/layouts.jl @@ -69,7 +69,7 @@ end update_position!(layout::GridLayout) = map(update_position!, layout.grid) -# some lengths are fixed... we have to split up the free space among the list v +"some lengths are fixed... we have to split up the free space among the list v" function recompute_lengths(v) # dump(v) tot = 0pct @@ -88,8 +88,7 @@ function recompute_lengths(v) ) end - # now fill in the blanks - map(x -> x == 0pct ? leftover / cnt : x, v) + map(x -> x == 0pct ? leftover / cnt : x, v) # fill in the blanks end # recursively compute the bounding boxes for the layout and plotarea (relative to canvas!) @@ -163,8 +162,10 @@ function update_child_bboxes!(layout::GridLayout, minimum_perimeter = [0mm, 0mm, end end -# for each inset (floating) subplot, resolve the relative position -# to absolute canvas coordinates, relative to the parent's plotarea +""" +For each inset (floating) subplot, resolve the relative position +to absolute canvas coordinates, relative to the parent's plotarea. +""" update_inset_bboxes!(plt::Plot) = for sp in plt.inset_subplots p_area = Measures.resolve(plotarea(sp.parent), sp[:relative_bbox]) @@ -374,23 +375,17 @@ function link_axes!(l::AbstractLayout, link::Symbol) end # process a GridLayout, recursively linking axes according to the link symbol function link_axes!(layout::GridLayout, link::Symbol) nr, nc = size(layout) - if link in (:x, :both) - for c in 1:nc - link_axes!(layout.grid[:, c], :xaxis) - end + link in (:x, :both) && for c in 1:nc + link_axes!(layout.grid[:, c], :xaxis) end - if link in (:y, :both) - for r in 1:nr - link_axes!(layout.grid[r, :], :yaxis) - end + link in (:y, :both) && for r in 1:nr + link_axes!(layout.grid[r, :], :yaxis) end - if link ≡ :square - if (sps = filter(l -> isa(l, Subplot), layout.grid)) |> !isempty - base_axis = sps[1][:xaxis] - for sp in sps - link_axes!(base_axis, sp[:xaxis]) - link_axes!(base_axis, sp[:yaxis]) - end + link ≡ :square && if (sps = filter(l -> isa(l, Subplot), layout.grid)) |> !isempty + base_axis = sps[1][:xaxis] + for sp in sps + link_axes!(base_axis, sp[:xaxis]) + link_axes!(base_axis, sp[:yaxis]) end end if link ≡ :all diff --git a/PlotsBase/src/legend.jl b/PlotsBase/src/legend.jl index 86b7ddb60..8e0fbec11 100644 --- a/PlotsBase/src/legend.jl +++ b/PlotsBase/src/legend.jl @@ -1,5 +1,3 @@ -### Legend - @add_attributes subplot struct Legend background_color = :match foreground_color = :match @@ -31,13 +29,11 @@ function legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax) return (xcenter + A * c, ycenter + A * s) end -""" -Split continuous range `[-1,1]` evenly into an integer `[1,2,3]` -""" +"Split continuous range `[-1,1]` evenly into an integer `[1,2,3]`." function legend_anchor_index(x) x < -1 // 3 && return 1 x < 1 // 3 && return 2 - return 3 + 3 end """ diff --git a/PlotsBase/src/output.jl b/PlotsBase/src/output.jl index 88822678c..2ea0f9412 100644 --- a/PlotsBase/src/output.jl +++ b/PlotsBase/src/output.jl @@ -159,7 +159,7 @@ savefig(fn) = savefig(current(), fn) """ gui([plot]) -Display a plot using the backends' gui window +Display a plot using the backends' gui window. """ gui(plt::Plot = current()) = display(PlotsDisplay(), plt) @@ -239,7 +239,7 @@ closeall() = closeall(backend()) # COV_EXCL_START -# Base.showable(::MIME"text/html", plt::Plot{UnicodePlotsBackend}) = false # Pluto +# Base.showable(::MIME"text/html", ::Plot{UnicodePlotsBackend}) = false # Pluto Base.show(io::IO, m::MIME"application/prs.juno.plotpane+html", plt::Plot) = showjuno(io, MIME("text/html"), plt) @@ -279,7 +279,7 @@ _showjuno(io::IO, m::MIME"image/svg+xml", plt) = _show(io, m, plt) end -Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot) = false +Base.showable(::MIME"application/prs.juno.plotpane+html", ::Plot) = false _showjuno(io::IO, m, plt) = _show(io, m, plt) diff --git a/PlotsBase/src/pipeline.jl b/PlotsBase/src/pipeline.jl index f66a957e0..09ff51118 100644 --- a/PlotsBase/src/pipeline.jl +++ b/PlotsBase/src/pipeline.jl @@ -33,9 +33,7 @@ RecipesPipeline.split_attribute(plt::Plot, key, val::SeriesAnnotations, indices) ## Preprocessing attributes function RecipesPipeline.preprocess_axis_attrs!(plt::Plot, plotattributes, letter) # Fix letter for seriestypes that are x only but data gets passed as y - if treats_y_as_x(get(plotattributes, :seriestype, :path)) - letter = :x - end + treats_y_as_x(get(plotattributes, :seriestype, :path)) && (letter = :x) plotattributes[:letter] = letter RecipesPipeline.preprocess_axis_attrs!(plt, plotattributes) @@ -139,7 +137,7 @@ RecipesPipeline.get_axis_limits(plt::Plot, letter) = axis_limits(plt[1], letter, ## Plot recipes -RecipesPipeline.type_alias(plt::Plot, st) = get(Commons._typeAliases, st, st) +RecipesPipeline.type_alias(::Plot, st) = get(Commons._typeAliases, st, st) ## Plot setup @@ -149,7 +147,7 @@ function RecipesPipeline.plot_setup!(plt::Plot, plotattributes, kw_list) nothing end -function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, kw_list) +function RecipesPipeline.process_sliced_series_attributes!(::Plot, kw_list) # determine global extrema xe = ye = ze = NaN, NaN for kw in kw_list @@ -177,9 +175,8 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, rib = get(kw, :ribbon, default(:ribbon)) fr = get(kw, :fillrange, default(:fillrange)) # map ribbon if it's a Function - if rib isa Function - kw[:ribbon] = map(rib, kw[:x]) - end + rib isa Function && (kw[:ribbon] = map(rib, kw[:x])) + # convert a ribbon into a fillrange if rib ≢ nothing make_fillrange_from_ribbon(kw) @@ -191,7 +188,7 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::PlotsBase.Plot, nothing end -# TODO: Should some of this logic be moved to RecipesPipeline? +# TODO: Should some of this logic be moved to RecipesPipeline ? function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # merge in anything meant for the Plot for kw in kw_list, (k, v) in kw @@ -344,9 +341,9 @@ function RecipesPipeline.slice_series_attributes!(plt::Plot, kw_list, kw) nothing end -RecipesPipeline.series_defaults(plt::Plot) = _series_defaults # in args.jl +RecipesPipeline.series_defaults(::Plot) = _series_defaults # in args.jl -RecipesPipeline.is_seriestype_supported(plt::Plot, st) = is_seriestype_supported(st) +RecipesPipeline.is_seriestype_supported(::Plot, st) = is_seriestype_supported(st) function RecipesPipeline.add_series!(plt::Plot, plotattributes) sp = _prepare_subplot(plt, plotattributes) diff --git a/PlotsBase/src/plot.jl b/PlotsBase/src/plot.jl index 892df16b0..dd696c1ff 100644 --- a/PlotsBase/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -1,5 +1,6 @@ struct PlaceHolder end + mutable struct CurrentPlot nullableplot::Union{AbstractPlot,Nothing} end @@ -20,7 +21,9 @@ current(plot::AbstractPlot) = (CURRENT_PLOT.nullableplot = plot) # --------------------------------------------------------- Base.string(plt::Plot) = "Plot{$(plt.backend) n=$(plt.n)}" + Base.print(io::IO, plt::Plot) = print(io, string(plt)) + function Base.show(io::IO, plt::Plot) print(io, string(plt)) sp_ekwargs = getindex.(plt.subplots, :extra_kwargs) @@ -57,7 +60,7 @@ function Base.show(io::IO, plt::Plot) end getplot(plt::Plot) = plt -getattr(plt::Plot, idx::Int = 1) = plt.attr +getattr(plt::Plot, ::Int = 1) = plt.attr # --------------------------------------------------------- diff --git a/PlotsBase/src/plotattr.jl b/PlotsBase/src/plotattr.jl index 972dcf2e3..9ef9efa07 100644 --- a/PlotsBase/src/plotattr.jl +++ b/PlotsBase/src/plotattr.jl @@ -52,13 +52,13 @@ function plotattr() end d = default(attr) - print(""" + """ # $letter$attr - $attrtype attribute - Default: `$(d isa Symbol ? string(':', d) : d)`. - $(_argument_description(attr)) - """) + """ |> print end # COV_EXCL_STOP diff --git a/PlotsBase/src/preferences.jl b/PlotsBase/src/preferences.jl deleted file mode 100644 index e58f5394a..000000000 --- a/PlotsBase/src/preferences.jl +++ /dev/null @@ -1,49 +0,0 @@ -# from github.com/JuliaPackaging/Preferences.jl/blob/master/README.md: -# "Preferences that are accessed during compilation are automatically marked as compile-time preferences" -# ==> this must always be done during precompilation, otherwise -# the cache will not invalidate when preferences change -const DEFAULT_BACKEND = lowercase(load_preference(PlotsBase, "default_backend", "gr")) - -function default_backend() - # environment variable preempts the `Preferences` based mechanism - name = get(ENV, "PLOTSBASE_DEFAULT_BACKEND", DEFAULT_BACKEND) |> lowercase |> Symbol - backend(name) -end - -function set_default_backend!( - backend::Union{Nothing,AbstractString,Symbol} = nothing; - force = true, - kw..., -) - if backend ≡ nothing - delete_preferences!(PlotsBase, "default_backend"; force, kw...) - else - # NOTE: `_check_installed` already throws a warning - if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing - set_preferences!(PlotsBase, "default_backend" => value; force, kw...) - end - end - nothing -end - -function diagnostics(io::IO = stdout) - origin = if has_preference(PlotsBase, "default_backend") - "`Preferences`" - elseif haskey(ENV, "PLOTSBASE_DEFAULT_BACKEND") - "environment variable" - else - "fallback" - end - if (be = backend_name()) ≡ :none - @info "no `PlotsBase` backends currently initialized" - else - pkg_name = string(PlotsBase.backend_package_name(be)) - @info "selected `PlotsBase` backend: $pkg_name, from $origin" - Pkg.status( - ["PlotsBase", "RecipesBase", "RecipesPipeline", pkg_name]; - mode = Pkg.PKGMODE_MANIFEST, - io, - ) - end - nothing -end diff --git a/PlotsBase/src/recipes.jl b/PlotsBase/src/recipes.jl index eeccb3623..c8d196615 100644 --- a/PlotsBase/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -424,7 +424,7 @@ end bw = plotattributes[:bar_width] hw = if bw ≡ nothing 0.5Commons._bar_width * if nx > 1 - ignorenan_minimum(filter(x -> x > 0, diff(sort(procx)))) + ignorenan_minimum(filter(>(0), diff(sort(procx)))) else 1 end diff --git a/PlotsBase/src/shorthands.jl b/PlotsBase/src/shorthands.jl index 09fa9fd2b..7e9022e9a 100644 --- a/PlotsBase/src/shorthands.jl +++ b/PlotsBase/src/shorthands.jl @@ -528,32 +528,6 @@ for letter in ("x", "y", "z") end end -""" - annotate!(anns) - annotate!(anns::Tuple...) - annotate!(x, y, txt) - -Add annotations to an existing plot. -Annotations are specified either as a vector of tuples, each of the form `(x,y,txt)`, -or as three vectors, `x, y, txt`. -Each `txt` can be a `String`, `PlotText` PlotText (created with `text(args...)`), -or a tuple of arguments to `text` (e.g., `("Label", 8, :red, :top)`). - -# Example -```julia-repl -julia> plot(1:10) -julia> annotate!([(7,3,"(7,3)"),(3,7,text("hey", 14, :left, :top, :green))]) -julia> annotate!([(4, 4, ("More text", 8, 45.0, :bottom, :red))]) -julia> annotate!([2,5], [6,3], ["text at (2,6)", "text at (5,3)"]) -``` -""" -annotate!(anns...; kw...) = plot!(; annotation = anns, kw...) -annotate!(anns::Tuple...; kw...) = plot!(; annotation = collect(anns), kw...) -annotate!(anns::AVec{<:Tuple}; kw...) = plot!(; annotation = anns, kw...) -annotate!(plt::PlotOrSubplot, anns...; kw...) = plot!(plt; annotations = anns, kw...) -annotate!(plt::PlotOrSubplot, anns::Tuple...; kw...) = plot!(plt; annotations = collect(anns), kw...) -annotate!(plt::PlotOrSubplot, anns::AVec{<:Tuple}; kw...) = plot!(plt; annotations = anns, kw...) - @doc """ abline!([plot,] a, b; kwargs...) diff --git a/PlotsBase/src/utils.jl b/PlotsBase/src/utils.jl index d6a37db48..7c2958a1a 100644 --- a/PlotsBase/src/utils.jl +++ b/PlotsBase/src/utils.jl @@ -1,5 +1,4 @@ -# --------------------------------------------------------------- bool_env(x, default::String = "0")::Bool = tryparse(Bool, get(ENV, x, default)) treats_y_as_x(seriestype) = @@ -11,8 +10,6 @@ function replace_image_with_heatmap(z::AbstractMatrix{<:Colorant}) reshape(1:(n * m), n, m), colors end -# --------------------------------------------------------------- - "Build line segments for plotting" mutable struct Segments{T} pts::Vector{T} @@ -521,7 +518,6 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end # convert into strokes and brushes - if haskey(plotattributes, :arrow) a = plotattributes[:arrow] plotattributes[:arrow] = if a == true @@ -640,7 +636,6 @@ function with(f::Function, args...; scalefonts = nothing, kw...) ret end -# --------------------------------------------------------------- const _convert_sci_unicode_dict = Dict( '⁰' => "0", '¹' => "1", @@ -663,9 +658,7 @@ function convert_sci_unicode(label::AbstractString) for key in keys(_convert_sci_unicode_dict) label = replace(label, key => _convert_sci_unicode_dict[key]) end - if occursin("×10^{", label) - label = string(label, "}") - end + occursin("×10^{", label) && (label = string(label, "}")) label end @@ -921,7 +914,7 @@ function _guess_best_legend_position(xl, yl, plt, weight = 100) u[ibest] ≈ u[4] && return :topright u[ibest] ≈ u[3] && return :topleft u[ibest] ≈ u[2] && return :bottomright - return :bottomleft + :bottomleft end """ @@ -953,7 +946,7 @@ julia> plot([0,1]u"m", [1,2]u"m/s^2", xlabel="This label will display units") ``` """ macro P_str(s) - return protectedstring(s) + protectedstring(s) end # for `PGFPlotsx` together with `UnitfulExt` diff --git a/PlotsBase/src/web.jl b/PlotsBase/src/web.jl index 1dc382c06..4197f1a94 100644 --- a/PlotsBase/src/web.jl +++ b/PlotsBase/src/web.jl @@ -36,7 +36,7 @@ end function write_temp_html(plt::AbstractPlot) html = standalone_html(plt; title = plt.attr[:window_title]) - filename = string(tempname(), ".html") + filename = tempname() * ".html" write(filename, html) filename end From a5992baec666fca142a96fe89a9296ecbc35eb87 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Mon, 8 Apr 2024 00:01:20 +0200 Subject: [PATCH 09/85] fix typos --- PlotsBase/src/Plots.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PlotsBase/src/Plots.jl b/PlotsBase/src/Plots.jl index 028c82954..ea3fe908e 100644 --- a/PlotsBase/src/Plots.jl +++ b/PlotsBase/src/Plots.jl @@ -74,9 +74,9 @@ struct InputWrapper{T} obj::T end protect(obj::T) where {T} = InputWrapper{T}(obj) -Base.isempty(wrapper::InputWrapper) = false -_cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj -_cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj +Base.isempty(::InputWrapper) = false +_cycle(wrapper::InputWrapper, ::Int) = wrapper.obj +_cycle(wrapper::InputWrapper, ::AVec{Int}) = wrapper.obj # ----------------------------------------------------------- @@ -178,12 +178,12 @@ Commons.series_list(plt::Plot) = plt.series_list Commons.get_ticks(plt::Plot, s::Symbol) = map(sp -> get_ticks(sp, s), plt.subplots) get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x ≡ sp, plt.subplots) -RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes::AKW) = +RecipesPipeline.preprocess_attributes!(::Plot, plotattributes::AKW) = Commons.preprocess_attributes!(plotattributes) plottitlefont(plt::Plot) = font(; family = plt[:plot_titlefontfamily], - pointsize = v[:plot_titlefontsize], + pointsize = plt[:plot_titlefontsize], valign = plt[:plot_titlefontvalign], halign = plt[:plot_titlefonthalign], rotation = plt[:plot_titlefontrotation], From f27c801f3c87164658077b9a73adc0bf65c17b9d Mon Sep 17 00:00:00 2001 From: t-bltg Date: Fri, 12 Apr 2024 13:57:12 +0200 Subject: [PATCH 10/85] fix test --- PlotsBase/test/test_layouts.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl index 39ce40652..9bfca3e97 100644 --- a/PlotsBase/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -26,7 +26,7 @@ end background_color = :darkgray, background_color_inside = :lightgray, ) - @test pl.layout.heights == [0.05Plots.pct, 0.95Plots.pct] + @test pl.layout.heights == [0.05PlotsBase.pct, 0.95PlotsBase.pct] @test pl[:plot_title] == "My title" @test pl[:plot_titleindex] == 5 From 9d70fa91efc157c216f0a5e049ea1279cd346452 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Fri, 12 Apr 2024 14:16:19 +0200 Subject: [PATCH 11/85] fixes --- PlotsBase/src/Commons/layouts.jl | 38 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl index c23161637..f344552a8 100644 --- a/PlotsBase/src/Commons/layouts.jl +++ b/PlotsBase/src/Commons/layouts.jl @@ -1,7 +1,7 @@ -make_measure_hor(n::Number) = n * w +make_measure_hor(n::Number) = n * Measures.w make_measure_hor(m::Measure) = m -make_measure_vert(n::Number) = n * h +make_measure_vert(n::Number) = n * Measures.h make_measure_vert(m::Measure) = m """ @@ -30,10 +30,8 @@ end # create a new bbox function bbox(x, y, width, height; h_anchor = :left, v_anchor = :top) - x = make_measure_hor(x) - y = make_measure_vert(y) - width = make_measure_hor(width) - height = make_measure_vert(height) + x, y = make_measure_hor(x), make_measure_vert(y) + width, height = make_measure_hor(width), make_measure_vert(height) left = if h_anchor ≡ :left x elseif h_anchor in (:center, :hcenter) @@ -69,10 +67,10 @@ bottom(layout::AbstractLayout) = bottom(bbox(layout)) width(layout::AbstractLayout) = width(bbox(layout)) height(layout::AbstractLayout) = height(bbox(layout)) -leftpad(layout::AbstractLayout) = 0mm -toppad(layout::AbstractLayout) = 0mm -rightpad(layout::AbstractLayout) = 0mm -bottompad(layout::AbstractLayout) = 0mm +leftpad(::AbstractLayout) = 0mm +toppad(::AbstractLayout) = 0mm +rightpad(::AbstractLayout) = 0mm +bottompad(::AbstractLayout) = 0mm leftpad(pad) = pad[1] toppad(pad) = pad[2] @@ -83,7 +81,7 @@ Base.show(io::IO, layout::AbstractLayout) = print(io, "$(typeof(layout))$(size(l # this is the available area for drawing everything in this layout... as percentages of total canvas bbox(layout::AbstractLayout) = layout.bbox -bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb) +bbox!(layout::AbstractLayout, bb::BoundingBox) = layout.bbox = bb # layouts are recursive, tree-like structures, and most will have a parent field Base.parent(layout::AbstractLayout) = layout.parent @@ -112,9 +110,9 @@ mutable struct EmptyLayout <: AbstractLayout end EmptyLayout(parent = RootLayout(); kw...) = EmptyLayout(parent, DEFAULT_BBOX[], KW(kw)) -Base.size(layout::EmptyLayout) = (0, 0) -Base.length(layout::EmptyLayout) = 0 -Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing +Base.size(::EmptyLayout) = (0, 0) +Base.length(::EmptyLayout) = 0 +Base.getindex(::EmptyLayout, ::Int, ::Int) = nothing # ----------------------------------------------------------- # GridLayout @@ -122,9 +120,9 @@ Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing # nested, gridded layout with optional size percentages mutable struct GridLayout <: AbstractLayout parent::AbstractLayout - minpad::Tuple # leftpad, toppad, rightpad, bottompad + minpad::Tuple # leftpad, toppad, rightpad, bottompad bbox::BoundingBox - grid::Matrix{AbstractLayout} # Nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion + grid::Matrix{AbstractLayout} # nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion widths::Vector{Measure} heights::Vector{Measure} attr::KW @@ -138,8 +136,8 @@ bottompad(layout::GridLayout) = bottompad(layout.minpad) function GridLayout( dims...; parent = RootLayout(), - widths = zeros(dims[2]), - heights = zeros(dims[1]), + heights = nothing, + widths = nothing, kw..., ) # Check the values for heights and widths if values are provided @@ -150,7 +148,7 @@ function GridLayout( else heights = zeros(dims[1]) end - if widths ≢ nothing + if heights ≢ nothing && widths ≢ nothing sum(widths) == 1 || error("The sum of widths must be 1!") all(x -> 0 < x < 1, widths) || error("Values for widths must be in the range (0, 1)!") @@ -165,8 +163,6 @@ function GridLayout( grid, Measure[w * pct for w in widths], Measure[h * pct for h in heights], - # convert(Vector{Float64}, widths), - # convert(Vector{Float64}, heights), KW(kw), ) for i in eachindex(grid) From b377179beb9cc776d79c732e388a724ae4fec599 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Fri, 12 Apr 2024 23:07:39 +0200 Subject: [PATCH 12/85] remove `using` --- PlotsBase/test/test_layouts.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl index 9bfca3e97..d39ec7d18 100644 --- a/PlotsBase/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -1,4 +1,3 @@ -using Plots, Test @testset "Plotting plots" begin pl = @test_nowarn plot(plot(1:2), plot(1:2, size = (1_200, 400))) @test pl[:size] == (1_200, 400) From f55ee234c2627a0a21311d0fa1d94506e12312db Mon Sep 17 00:00:00 2001 From: t-bltg Date: Fri, 12 Apr 2024 23:39:06 +0200 Subject: [PATCH 13/85] fix test --- PlotsBase/src/Commons/layouts.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl index f344552a8..79ccc93a0 100644 --- a/PlotsBase/src/Commons/layouts.jl +++ b/PlotsBase/src/Commons/layouts.jl @@ -141,17 +141,17 @@ function GridLayout( kw..., ) # Check the values for heights and widths if values are provided - if heights ≢ nothing && widths ≢ nothing - sum(heights) == 1 || error("The sum of heights must be 1!") + if heights ≢ nothing + sum(heights) == 1 || error("The sum of heights must be 1 !") all(x -> 0 < x < 1, heights) || - error("Values for heights must be in the range (0, 1)!") + error("Values for heights must be in the range (0, 1) !") else heights = zeros(dims[1]) end - if heights ≢ nothing && widths ≢ nothing - sum(widths) == 1 || error("The sum of widths must be 1!") + if widths ≢ nothing + sum(widths) == 1 || error("The sum of widths must be 1 !") all(x -> 0 < x < 1, widths) || - error("Values for widths must be in the range (0, 1)!") + error("Values for widths must be in the range (0, 1) !") else widths = zeros(dims[2]) end From 3bea623de088f98c7bd9a990b649ca640433a5c8 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Thu, 18 Apr 2024 09:39:08 +0200 Subject: [PATCH 14/85] =?UTF-8?q?stable=20Plots=20v2=20-=20checkpoint=20CI?= =?UTF-8?q?=20=E2=9C=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ .github/workflows/format_check.yml | 21 +++------------------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8b2c1e9d..5769a9193 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,8 @@ jobs: - uses: codecov/codecov-action@v4 if: startsWith(matrix.os, 'ubuntu') with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false file: lcov.info Skip: diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 91be67414..155982b16 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -18,10 +18,7 @@ jobs: - name: Install dependencies run: | using Pkg - Pkg.add([ - PackageSpec("JuliaFormatter"), - PackageSpec(url = "https://github.com/tkf/JuliaProjectFormatter.jl.git"), - ]) + Pkg.add("JuliaFormatter") shell: julia --color=yes {0} - name: Format Julia files @@ -29,7 +26,9 @@ jobs: using JuliaFormatter format(["RecipesBase", "RecipesPipeline", "PlotsBase", "src", "test"]) shell: julia --color=yes --compile=min -O0 {0} + - name: suggester / JuliaFormatter + if: success() && github.ref == 'refs/heads/master' uses: reviewdog/action-suggester@v1 with: tool_name: JuliaFormatter @@ -42,17 +41,3 @@ jobs: git checkout -- . git clean --force shell: bash - - # temporarily disable `JuliaProjectFormatter` until github.com/tkf/JuliaProjectFormatter.jl/pull/7 is merged - # - name: Format Julia project files - # if: success() || failure() - # run: | - # using JuliaProjectFormatter - # format_projects() - # shell: julia --color=yes --compile=min -O0 {0} - # - name: suggester / JuliaProjectFormatter - # if: success() || failure() - # uses: reviewdog/action-suggester@v1 - # with: - # tool_name: JuliaProjectFormatter - # fail_on_error: true From 9f185adc0d630807616e2deacb21d8dc24c9f4d1 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Thu, 25 Apr 2024 11:37:35 +0200 Subject: [PATCH 15/85] use `@static` for `PythonCall` --- PlotsBase/ext/PythonPlotExt.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PlotsBase/ext/PythonPlotExt.jl b/PlotsBase/ext/PythonPlotExt.jl index 90a5298b2..d10423086 100644 --- a/PlotsBase/ext/PythonPlotExt.jl +++ b/PlotsBase/ext/PythonPlotExt.jl @@ -5,8 +5,11 @@ import PythonPlot import NaNMath const PythonCall = PythonPlot.PythonCall -const pyisnone = - isdefined(PythonCall, :pyisnone) ? PythonCall.pyisnone : PythonCall.Core.pyisnone +const pyisnone = @static if isdefined(PythonCall, :pyisnone) + PythonCall.pyisnone +else + PythonCall.Core.pyisnone +end const mpl_toolkits = PythonCall.pynew() const numpy = PythonCall.pynew() From 0ab074b03fcbd4e5e69a76f4f40bb2bb79be2841 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sat, 4 May 2024 12:48:07 +0200 Subject: [PATCH 16/85] format --- .JuliaFormatter.toml | 2 +- PlotsBase/ext/GRExt.jl | 60 +++++++-------- PlotsBase/ext/GastonExt.jl | 30 ++++---- PlotsBase/ext/HDF5Ext.jl | 26 +++---- PlotsBase/ext/ImageInTerminalExt.jl | 2 +- PlotsBase/ext/PGFPlotsXExt.jl | 44 +++++------ PlotsBase/ext/PlotlyJSExt.jl | 4 +- PlotsBase/ext/PlotlyKaleidoExt.jl | 2 +- PlotsBase/ext/PythonPlotExt.jl | 64 ++++++++-------- PlotsBase/ext/UnicodePlotsExt.jl | 30 ++++---- PlotsBase/src/Annotations.jl | 8 +- PlotsBase/src/Arrows.jl | 4 +- PlotsBase/src/Axes.jl | 10 +-- PlotsBase/src/BezierCurves.jl | 2 +- PlotsBase/src/Colorbars.jl | 6 +- PlotsBase/src/Commons/Commons.jl | 8 +- PlotsBase/src/Commons/attrs.jl | 32 ++++---- PlotsBase/src/Commons/layouts.jl | 8 +- PlotsBase/src/Commons/measures.jl | 2 +- PlotsBase/src/Commons/postprocess_attrs.jl | 6 +- PlotsBase/src/DataSeries.jl | 24 +++--- PlotsBase/src/Fonts.jl | 16 ++-- PlotsBase/src/Plots.jl | 18 ++--- PlotsBase/src/PlotsBase.jl | 2 +- PlotsBase/src/Shapes.jl | 14 ++-- PlotsBase/src/Strokes.jl | 4 +- PlotsBase/src/Subplots.jl | 14 ++-- PlotsBase/src/Ticks.jl | 4 +- PlotsBase/src/alignment.jl | 2 +- PlotsBase/src/animation.jl | 2 +- PlotsBase/src/axes_utils.jl | 10 +-- PlotsBase/src/backends.jl | 14 ++-- PlotsBase/src/examples.jl | 34 ++++----- PlotsBase/src/layouts.jl | 30 ++++---- PlotsBase/src/output.jl | 4 +- PlotsBase/src/pipeline.jl | 30 ++++---- PlotsBase/src/plot.jl | 26 +++---- PlotsBase/src/plotattr.jl | 2 +- PlotsBase/src/plotly.jl | 29 ++++---- PlotsBase/src/recipes.jl | 56 +++++++------- PlotsBase/src/shorthands.jl | 2 +- PlotsBase/src/themes.jl | 4 +- PlotsBase/src/utils.jl | 85 +++++++++++----------- PlotsBase/test/runtests.jl | 6 +- PlotsBase/test/test_animations.jl | 18 ++--- PlotsBase/test/test_args.jl | 4 +- PlotsBase/test/test_axes.jl | 20 ++--- PlotsBase/test/test_backends.jl | 2 +- PlotsBase/test/test_components.jl | 22 +++--- PlotsBase/test/test_contours.jl | 2 +- PlotsBase/test/test_dates.jl | 12 +-- PlotsBase/test/test_layouts.jl | 2 +- PlotsBase/test/test_misc.jl | 10 +-- PlotsBase/test/test_pgfplotsx.jl | 6 +- PlotsBase/test/test_preferences.jl | 2 +- PlotsBase/test/test_recipes.jl | 4 +- PlotsBase/test/test_reference.jl | 6 +- PlotsBase/test/test_unitful.jl | 16 ++-- PlotsBase/test/test_utils.jl | 4 +- src/Plots.jl | 2 +- test/runtests.jl | 6 +- 61 files changed, 457 insertions(+), 463 deletions(-) diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index 3a33b0ff3..92d7deead 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -1,5 +1,5 @@ -always_for_in = true for_in_replacement = "∈" +always_for_in = true import_to_using = false align_pair_arrow = true align_assignment = true diff --git a/PlotsBase/ext/GRExt.jl b/PlotsBase/ext/GRExt.jl index 58211a305..a1dfabfa5 100644 --- a/PlotsBase/ext/GRExt.jl +++ b/PlotsBase/ext/GRExt.jl @@ -366,7 +366,7 @@ function gr_polyline(x, y, func = GR.polyline; arrowside = :none, arrowstyle = : iend = 0 while iend < n - 1 istart = -1 # set istart to the first index that is finite - for j in (iend + 1):n + for j ∈ (iend + 1):n if ok(x[j], y[j]) istart = j break @@ -374,7 +374,7 @@ function gr_polyline(x, y, func = GR.polyline; arrowside = :none, arrowstyle = : end if istart > 0 iend = -1 # iend is the last finite index - for j in (istart + 1):n + for j ∈ (istart + 1):n if ok(x[j], y[j]) iend = j else @@ -404,7 +404,7 @@ function gr_polyline3d(x, y, z, func = GR.polyline3d) n = length(x) while iend < n - 1 istart = -1 # set istart to the first index that is finite - for j in (iend + 1):n + for j ∈ (iend + 1):n if ok(x[j], y[j], z[j]) istart = j break @@ -412,7 +412,7 @@ function gr_polyline3d(x, y, z, func = GR.polyline3d) end if istart > 0 iend = -1 # iend is the last finite index - for j in (istart + 1):n + for j ∈ (istart + 1):n if ok(x[j], y[j], z[j]) iend = j else @@ -467,7 +467,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) sp, ) gr_set_transparency(xaxis[:foreground_color_grid], xaxis[:gridalpha]) - for i in eachindex(α) + for i ∈ eachindex(α) GR.polyline([sinf[i], 0], [cosf[i], 0]) end end @@ -481,7 +481,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) sp, ) gr_set_transparency(yaxis[:foreground_color_grid], yaxis[:gridalpha]) - for i in eachindex(rtick_values) + for i ∈ eachindex(rtick_values) r = (rtick_values[i] - rmin) / (rmax - rmin) (r ≤ 1 && r ≥ 0) && GR.drawarc(-r, r, -r, r, 0, 359) end @@ -496,14 +496,14 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) # draw angular ticks if xaxis[:showaxis] GR.drawarc(-1, 1, -1, 1, 0, 359) - for i in eachindex(α) + for i ∈ eachindex(α) x, y = GR.wctondc(1.1sinf[i], 1.1cosf[i]) GR.textext(x, y, string((360 - α[i]) % 360, "^o")) end end # draw radial ticks - yaxis[:showaxis] && for i in eachindex(rtick_values) + yaxis[:showaxis] && for i ∈ eachindex(rtick_values) r = (rtick_values[i] - rmin) / (rmax - rmin) (r ≤ 1 && r ≥ 0) && gr_text(GR.wctondc(0.05, r)..., _cycle(rtick_labels, i)) end @@ -706,7 +706,7 @@ end function _cbar_unique(values, propname) out = last(values) - if any(x != out for x in values) + if any(x != out for x ∈ values) @warn """ Multiple series with different $propname share a colorbar. Colorbar may not reflect all series correctly. @@ -763,7 +763,7 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport) push!(levels, z_max) end colors = gr_colorbar_colors(last(series), clims) - for (from, to, color) in zip(levels[1:(end - 1)], levels[2:end], colors) + for (from, to, color) ∈ zip(levels[1:(end - 1)], levels[2:end], colors) GR.setfillcolorind(color) GR.fillrect(x_min, x_max, from, to) end @@ -781,7 +781,7 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport) gr_set_transparency(_cbar_unique(get_linealpha.(series), "line alpha")) levels = _cbar_unique(contour_levels.(series, Ref(clims)), "levels") colors = gr_colorbar_colors(last(series), clims) - for (line, color) in zip(levels, colors) + for (line, color) ∈ zip(levels, colors) GR.setlinecolorind(color) GR.polyline([x_min, x_max], [line, line]) end @@ -825,7 +825,7 @@ alignment(symb) = function gr_set_gradient(c) grad = _as_gradient(c) - for (i, z) in enumerate(range(0, 1; length = 256)) + for (i, z) ∈ enumerate(range(0, 1; length = 256)) c = grad[z] GR.setcolorrep(999 + i, red(c), green(c), blue(c)) end @@ -925,7 +925,7 @@ end function gr_get_ticks_size(ticks, rot) w, h = 0.0, 0.0 - for (cv, dv) in zip(ticks...) + for (cv, dv) ∈ zip(ticks...) wi, hi = gr_text_size(dv, rot) w = NaNMath.max(w, wi) h = NaNMath.max(h, hi) @@ -996,7 +996,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) if gr_is3d(sp) # Add margin for x and y ticks m = 0mm - for (ax, tc) in ((xaxis, xticks), (yaxis, yticks)) + for (ax, tc) ∈ ((xaxis, xticks), (yaxis, yticks)) isempty(first(tc)) && continue rot = ax[:rotation] gr_set_tickfont( @@ -1027,7 +1027,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) # Add margin for x or y label m = 0mm - for ax in (xaxis, yaxis) + for ax ∈ (xaxis, yaxis) (guide = ax[:guide] == "") && continue gr_set_font(guidefont(ax), sp) l = last(gr_text_size(guide)) @@ -1045,7 +1045,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{GRBackend}) end else # Add margin for x/y ticks & labels - for (ax, tc, (a, b)) in + for (ax, tc, (a, b)) ∈ ((xaxis, xticks, (:top, :bottom)), (yaxis, yticks, (:right, :left))) if !isempty(first(tc)) isy = ax[:letter] ≡ :y @@ -1142,7 +1142,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport) # init the colorbar cbar = GRColorbar() - for series in series_list(sp) + for series ∈ series_list(sp) gr_add_series(sp, series) gr_update_colorbar!(cbar, series) end @@ -1154,7 +1154,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport) gr_add_legend(sp, leg, vp_plt) # add annotations - for ann in sp[:annotations] + for ann ∈ sp[:annotations] x, y = if PlotsBase.is3d(sp) x, y, z, val = locate_annotation(sp, ann...) GR.setwindow(-1, 1, -1, 1) @@ -1222,7 +1222,7 @@ function gr_add_legend(sp, leg, viewport_area) nentry = 1 - for series in series_list(sp) + for series ∈ series_list(sp) should_add_to_legend(series) || continue st = series[:seriestype] clims = gr_clims(sp, series) @@ -1395,7 +1395,7 @@ function gr_get_legend_geometry(vp, sp) textw = r - l end gr_set_font(legendfont(sp), sp) - for series in series_list(sp) + for series ∈ series_list(sp) should_add_to_legend(series) || continue (l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(series[:label]))) texth = max(texth, t - b) @@ -1715,7 +1715,7 @@ function gr_label_ticks(sp, letter, ticks) else rot < -270 || -90 < rot < 90 || rot > 270 ? 1 : -1 end - for (cv, dv) in zip(ticks...) + for (cv, dv) ∈ zip(ticks...) x, y = GR.wctondc(reverse_if((cv, ov), isy)...) sz_rot, sz = gr_text_size(dv, rot), gr_text_size(dv) x_off, y_off = x_offset, y_offset @@ -1790,7 +1790,7 @@ function gr_label_ticks_3d(sp, letter, ticks) end GR.setwindow(-1, 1, -1, 1) - for (cv, dv) in zip((ax[:flip] ? reverse(cvs) : cvs, dvs)...) + for (cv, dv) ∈ zip((ax[:flip] ? reverse(cvs) : cvs, dvs)...) xi, yi = gr_w3tondc(sort_3d_axes(cv, nt, ft, letter)...) sz_rot, sz = gr_text_size(dv, rot), gr_text_size(dv) x_off = x_offset + 0.5(sgn2a * first(sz_rot) + sgn3 * last(sz) * sind(rot)) @@ -1944,7 +1944,7 @@ function gr_add_series(sp, series) end # this is all we need to add the series_annotations text - for (xi, yi, str, fnt) in EachAnn(series[:series_annotations], x, y) + for (xi, yi, str, fnt) ∈ EachAnn(series[:series_annotations], x, y) gr_set_font(fnt, sp) gr_text(GR.wctondc(xi, yi)..., str) end @@ -1975,7 +1975,7 @@ function gr_draw_segments(series, x, y, z, fillrange, clims) # draw the line(s) st = series[:seriestype] - for segment in series_segments(series, st; check = true) + for segment ∈ series_segments(series, st; check = true) i, rng = segment.attr_index, segment.range isempty(rng) && continue is3d = st ≡ :path3d && z ≢ nothing @@ -2016,14 +2016,14 @@ function gr_draw_markers( isempty(x) && return GR.setfillintstyle(GR.INTSTYLE_SOLID) (shapes = series[:markershape]) ≡ :none && return - for segment in series_segments(series, :scatter) + for segment ∈ series_segments(series, :scatter) rng = intersect(eachindex(IndexLinear(), x), segment.range) isempty(rng) && continue i = segment.attr_index ms = get_thickness_scaling(series) * _cycle(msize, i) msw = get_thickness_scaling(series) * _cycle(strokewidth, i) shape = _cycle(shapes, i) - for j in rng + for j ∈ rng gr_draw_marker( series, _cycle(x, j), @@ -2041,7 +2041,7 @@ end function gr_draw_shapes(series, clims) x, y = PlotsBase.shape_data(series) - for segment in series_segments(series, :shape) + for segment ∈ series_segments(series, :shape) i, rng = segment.attr_index, segment.range if length(rng) > 1 # connect to the beginning @@ -2181,7 +2181,7 @@ function gr_draw_heatmap(series, x, y, z, clims) z_log, z_normalized = gr_z_normalized_log_scaled(scale, z, clims) z_log, plot_color.(map(z -> get(fillgrad, z), z_normalized), series[:fillalpha]) end - for i in eachindex(colors) + for i ∈ eachindex(colors) isnan(_z[i]) && (colors[i] = set_RGBA_alpha(0, colors[i])) end GR.drawimage(first(x), last(x), last(y), first(y), w, h, gr_color.(colors)) @@ -2196,7 +2196,7 @@ function gr_draw_heatmap(series, x, y, z, clims) end rgba = map(x -> round(Int32, 1_000 + 255x), z_normalized) bg_rgba = gr_getcolorind(plot_color(sp[:background_color_inside])) - for i in eachindex(rgba) + for i ∈ eachindex(rgba) isnan(_z[i]) && (rgba[i] = bg_rgba) end if ispolar(series) @@ -2221,7 +2221,7 @@ end # ---------------------------------------------------------------- -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/pdf" => "pdf", "image/png" => "png", "application/postscript" => "ps", diff --git a/PlotsBase/ext/GastonExt.jl b/PlotsBase/ext/GastonExt.jl index 980d3c9b4..09f5e3fb6 100644 --- a/PlotsBase/ext/GastonExt.jl +++ b/PlotsBase/ext/GastonExt.jl @@ -143,9 +143,9 @@ function PlotsBase._before_layout_calcs(plt::Plot{GastonBackend}) # then add the series (curves in gaston) foreach(series -> gaston_add_series(plt, series), plt.series_list) - for sp in plt.subplots + for sp ∈ plt.subplots sp ≡ nothing && continue - for ann in sp[:annotations] + for ann ∈ sp[:annotations] x, y, val = locate_annotation(sp, ann...) sp.o.axesconf *= "; set label '$(val.str)' at $x,$y $(gaston_font(val.font))" end @@ -167,7 +167,7 @@ function PlotsBase._update_plot_object(plt::Plot{GastonBackend}) nothing end -for (mime, term) in ( +for (mime, term) ∈ ( "application/eps" => "epscairo", "image/eps" => "epscairo", "application/pdf" => "pdfcairo", @@ -231,7 +231,7 @@ end function gaston_get_subplots(n, plt_subplots, layout) nr, nc = size(layout) sps = Array{Any}(nothing, nr, nc) - for r in 1:nr, c in 1:nc # NOTE: col major + for r ∈ 1:nr, c ∈ 1:nc # NOTE: col major sps[r, c] = if (l = layout[r, c]) isa GridLayout n, sub = gaston_get_subplots(n, plt_subplots, l) size(sub) == (1, 1) ? only(sub) : sub @@ -249,7 +249,7 @@ end function gaston_init_subplots(plt, sps) sz = nr, nc = size(sps) - for c in 1:nc, r in 1:nr # NOTE: row major + for c ∈ 1:nc, r ∈ 1:nr # NOTE: row major if (sp = sps[r, c]) isa Subplot || sp ≡ nothing gaston_init_subplot(plt, sp) else @@ -270,7 +270,7 @@ function gaston_init_subplot( dims = RecipesPipeline.is3d(sp) || sp[:projection] == "3d" || needs_any_3d_axes(sp) ? 3 : 2 any_label = false - for series in series_list(sp) + for series ∈ series_list(sp) if dims == 2 && series[:seriestype] ∈ (:heatmap, :contour) dims = 3 # we need heatmap/contour to use splot, not plot end @@ -286,7 +286,7 @@ end function gaston_multiplot_pos_size(layout, parent_xy_wh) nr, nc = size(layout) dat = Array{Any}(nothing, nr, nc) - for r in 1:nr, c in 1:nc + for r ∈ 1:nr, c ∈ 1:nc l = layout[r, c] # width and height (pct) are multiplicative (parent) w = layout.widths[c].value * parent_xy_wh[3] @@ -314,7 +314,7 @@ end function gaston_multiplot_pos_size!(dat) nr, nc = size(dat) - for r in 1:nr, c in 1:nc + for r ∈ 1:nr, c ∈ 1:nc if (xy_wh_sp = dat[r, c]) isa Array gaston_multiplot_pos_size!(xy_wh_sp) elseif xy_wh_sp isa Tuple @@ -336,10 +336,10 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) st = series[:seriestype] curves = Gaston.Curve[] if gsp.dims == 2 && z ≡ nothing - for (n, seg) in enumerate(series_segments(series, st; check = true)) + for (n, seg) ∈ 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, n == 1, i) + for sc ∈ gaston_seriesconf!(sp, series, n == 1, i) push!(curves, Gaston.Curve(x[rng], y[rng], nothing, fr, sc)) end end @@ -369,12 +369,12 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series) z = reshape(z, length(y), length(x)) end end - for sc in gaston_seriesconf!(sp, series, true, 1) + for sc ∈ gaston_seriesconf!(sp, series, true, 1) push!(curves, Gaston.Curve(x, y, z, supp, sc)) end end - for c in curves + for c ∈ curves append = length(gsp.curves) > 0 push!(gsp.curves, c) Gaston.write_data(c, gsp.dims, gsp.datafile; append) @@ -524,7 +524,7 @@ function gaston_parse_axes_attrs( polar = ispolar(sp) && dims == 2 # cannot splot in polar coordinates fs = sp[:framestyle] - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) (letter ≡ :z && dims == 2) && continue axis = sp[get_attr_symbol(letter, :axis)] @@ -679,7 +679,7 @@ function gaston_parse_axes_attrs( gaston_ticks = if (ttype = PlotsBase.ticks_type(rticks)) ≡ :ticks string.(rticks) elseif ttype ≡ :ticks_and_labels - ["'$l' $t" for (t, l) in zip(rticks...)] + ["'$l' $t" for (t, l) ∈ zip(rticks...)] end push!( axesconf, @@ -799,7 +799,7 @@ function gaston_font(f; rot = true, align = true, color = true, scale = 1) end gaston_palette(gradient) = - let palette = ["$(n - 1) $(c.r) $(c.g) $(c.b)" for (n, c) in enumerate(gradient)] + let palette = ["$(n - 1) $(c.r) $(c.g) $(c.b)" for (n, c) ∈ enumerate(gradient)] '(' * join(palette, ", ") * ')' end diff --git a/PlotsBase/ext/HDF5Ext.jl b/PlotsBase/ext/HDF5Ext.jl index cbd39498c..a2a7a5280 100644 --- a/PlotsBase/ext/HDF5Ext.jl +++ b/PlotsBase/ext/HDF5Ext.jl @@ -171,7 +171,7 @@ if length(HDF5PLOT_MAP_TELEM2STR) < 1 merge!(HDF5PLOT_MAP_STR2TELEM, _telem2str) # Faster to create than push!()?? merge!( HDF5PLOT_MAP_TELEM2STR, - Dict{Type,String}(v => k for (k, v) in HDF5PLOT_MAP_STR2TELEM), + Dict{Type,String}(v => k for (k, v) ∈ HDF5PLOT_MAP_STR2TELEM), ) end @@ -180,7 +180,7 @@ end h5plotpath(name::String) = "plots/$name" _hdf5_merge!(dest::AKW, src::AKW) = - for (k, v) in src + for (k, v) ∈ src if isa(v, Axis) _hdf5_merge!(dest[k].plotattributes, v.plotattributes) else @@ -259,7 +259,7 @@ function _write_harray(grp::Group, name::String, v::Array) sgrp = HDF5.create_group(grp, name) lidx = LinearIndices(size(v)) - for iter in eachindex(v) + for iter ∈ eachindex(v) coord = lidx[iter] elem = v[iter] idxstr = join(coord, "_") @@ -272,7 +272,7 @@ end # Write Dict without tagging with type: _write(grp::Group, name::String, d::AbstractDict) = let sgrp = HDF5.create_group(grp, name) - for (k, v) in d + for (k, v) ∈ d kstr = string(k) _write_typed(sgrp, kstr, v) end @@ -280,7 +280,7 @@ _write(grp::Group, name::String, d::AbstractDict) = # Write out arbitrary `struct`s: _writestructgeneric(grp::Group, obj::T) where {T} = - for fname in fieldnames(T) + for fname ∈ fieldnames(T) v = getfield(obj, fname) _write_typed(grp, String(fname), v) end @@ -351,7 +351,7 @@ function _write(grp::Group, sp::Subplot{HDF5Backend}) listgrp = HDF5.create_group(grp, "series_list") _write_length_attrs(listgrp, sp.series_list) - for (i, series) in enumerate(sp.series_list) + for (i, series) ∈ enumerate(sp.series_list) # Just write .plotattributes part: _write(listgrp, "$i", series.plotattributes) end @@ -362,7 +362,7 @@ function _write(grp::Group, plt::Plot{HDF5Backend}) listgrp = HDF5.create_group(grp, "subplots") _write_length_attrs(listgrp, plt.subplots) - for (i, sp) in enumerate(plt.subplots) + for (i, sp) ∈ enumerate(plt.subplots) sgrp = HDF5.create_group(listgrp, "$i") _write(sgrp, sp) end @@ -406,7 +406,7 @@ end # _readstructgeneric: Needs object values to be written out with _write_typed(): function _readstructgeneric(::Type{T}, grp::Group) where {T} vlist = Array{Any}(nothing, fieldcount(T)) - for (i, fname) in enumerate(fieldnames(T)) + for (i, fname) ∈ enumerate(fieldnames(T)) vlist[i] = _read_typed(grp, String(fname)) end T(vlist...) @@ -416,7 +416,7 @@ end function _read(::Type{KW}, grp::Group) d = KW() gkeys = keys(grp) - for k in gkeys + for k ∈ gkeys try v = _read_typed(grp, k) d[Symbol(k)] = v @@ -438,7 +438,7 @@ function _read(::Type{Array}, grp::Group) # Array{Any} result = Array{Any}(undef, sz) lidx = LinearIndices(sz) - for iter in eachindex(result) + for iter ∈ eachindex(result) coord = lidx[iter] idxstr = join(coord, "_") result[iter] = _read_typed(grp, "v$idxstr") @@ -446,7 +446,7 @@ function _read(::Type{Array}, grp::Group) # Array{Any} # Hack: Implicitly make Julia detect element type. # (Should probably write it explicitly to file) - result = [elem for elem in result] # Potentially make more specific + result = [elem for elem ∈ result] # Potentially make more specific reshape(result, sz) end @@ -486,7 +486,7 @@ function _read(grp::Group, sp::Subplot) listgrp = HDF5.open_group(grp, "series_list") nseries = _read_length_attrs(Vector, listgrp) - for i in 1:nseries + for i ∈ 1:nseries sgrp = HDF5.open_group(listgrp, "$i") seriesinfo = _read(KW, sgrp) @@ -512,7 +512,7 @@ function _read_plot(grp::Group) agrp = HDF5.open_group(grp, "attr") _hdf5_merge!(plt.attr, _read(KW, agrp)) - for (i, sp) in enumerate(plt.subplots) + for (i, sp) ∈ enumerate(plt.subplots) sgrp = HDF5.open_group(listgrp, "$i") _read(sgrp, sp) end diff --git a/PlotsBase/ext/ImageInTerminalExt.jl b/PlotsBase/ext/ImageInTerminalExt.jl index 7dd49ca83..0fd305ac3 100644 --- a/PlotsBase/ext/ImageInTerminalExt.jl +++ b/PlotsBase/ext/ImageInTerminalExt.jl @@ -5,7 +5,7 @@ import PlotsBase if ImageInTerminal.ENCODER_BACKEND[] == :Sixel get!(ENV, "GKSwstype", "nul") # disable `gr` output, we display in the terminal instead - for be in ( + for be ∈ ( PlotsBase.GRBackend, PlotsBase.PythonPlotBackend, # PlotsBase.UnicodePlotsBackend, # better and faster as MIME("text/plain") in terminal diff --git a/PlotsBase/ext/PGFPlotsXExt.jl b/PlotsBase/ext/PGFPlotsXExt.jl index a9789befc..9e3f03df9 100644 --- a/PlotsBase/ext/PGFPlotsXExt.jl +++ b/PlotsBase/ext/PGFPlotsXExt.jl @@ -259,7 +259,7 @@ function surface_to_vecs(x::AVec, y::AVec, s::Union{AMat,Surface}) xn = Vector{eltype(x)}(undef, length(a)) yn = Vector{eltype(y)}(undef, length(a)) zn = Vector{eltype(s)}(undef, length(a)) - for (n, (i, j)) in enumerate(Tuple.(CartesianIndices(a))) + for (n, (i, j)) ∈ enumerate(Tuple.(CartesianIndices(a))) if length(x) == size(s, 1) i, j = j, i end @@ -313,7 +313,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) ) end - for sp in plt.subplots + for sp ∈ plt.subplots bb2 = bbox(sp) dx, dy = bb2.x0 sp_w, sp_h = width(bb2), height(bb2) @@ -364,7 +364,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) ) sp_w > 0mm && push!(axis_opt, "width" => string(sp_w - (rpad + lpad))) sp_h > 0mm && push!(axis_opt, "height" => string(sp_h - (tpad + bpad))) - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) if letter ≢ :z || RecipesPipeline.is3d(sp) pgfx_axis!(axis_opt, sp, letter) end @@ -375,7 +375,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) # It's also possible to assign the colormap to the series itself but # then the colormap needs to be added twice, once for the axis and once for the series. # 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 series ∈ series_list(sp) if hascolorbar(series) cg = get_colorgradient(series) cm = pgfx_colormap(cg) @@ -464,7 +464,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) ), ) end - for (series_index, series) in enumerate(series_list(sp)) + for (series_index, series) ∈ enumerate(series_list(sp)) # give each series a uuid for fillbetween series_id = uuid4() _pgfplotsx_series_ids[Symbol("$series_index")] = series_id @@ -502,7 +502,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) end # add series annotations anns = series[:series_annotations] - for (xi, yi, str, fnt) in EachAnn(anns, series[:x], series[:y]) + for (xi, yi, str, fnt) ∈ EachAnn(anns, series[:x], series[:y]) pgfx_add_annotation!( axis, (xi, yi), @@ -512,7 +512,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) end end # for series # add subplot annotations - for ann in sp[:annotations] + for ann ∈ sp[:annotations] # [1:end-1] -> coordinates, [end] is string loc_val = locate_annotation(sp, ann...) pgfx_add_annotation!( @@ -544,7 +544,7 @@ end function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, opt) # treat segments segments = collect(series_segments(series, series[:seriestype]; check = true)) - for (k, segment) in enumerate(segments) + for (k, segment) ∈ enumerate(segments) i, rng = segment.attr_index, segment.range segment_opt = pgfx_linestyle(opt, i) if opt[:markershape] ≢ :none @@ -572,7 +572,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o if sf isa Number || sf isa AVec pgfx_fillrange_series!(axis, series, series_func, i, _cycle(sf, rng), rng) elseif sf isa Tuple && series[:ribbon] ≢ nothing - for sfi in sf + for sfi ∈ sf pgfx_fillrange_series!( axis, series, @@ -616,8 +616,8 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o coords = Table([ :x => x_arrow[1:2:(end - 1)], :y => y_arrow[1:2:(end - 1)], - :u => [x_arrow[i] - x_arrow[i - 1] for i in 2:2:lastindex(x_arrow)], - :v => [y_arrow[i] - y_arrow[i - 1] for i in 2:2:lastindex(y_arrow)], + :u => [x_arrow[i] - x_arrow[i - 1] for i ∈ 2:2:lastindex(x_arrow)], + :v => [y_arrow[i] - y_arrow[i - 1] for i ∈ 2:2:lastindex(y_arrow)], ]) arrow_plot = series_func(merge(series_opt, arrow_opt), coords) push!(series_opt, "forget plot" => nothing) @@ -700,7 +700,7 @@ function pgfx_add_series!(::Val{:heatmap}, axis, series_opt, series, series_func ) args = pgfx_series_arguments(series, opt) meta = map(r -> any(!isfinite, r) ? NaN : r[3], zip(args...)) - for arg in args + for arg ∈ args arg[(!isfinite).(arg)] .= 0 end table = Table([ @@ -1287,16 +1287,16 @@ function pgfx_sanitize_string(s::AbstractString) end function pgfx_sanitize_plot!(plt) - for (key, value) in plt.attr + for (key, value) ∈ plt.attr if value isa Union{AbstractString,AVec{<:AbstractString}} plt.attr[key] = pgfx_sanitize_string.(value) end end - for subplot in plt.subplots - for (key, value) in subplot.attr + for subplot ∈ plt.subplots + for (key, value) ∈ subplot.attr if key ≡ :annotations && subplot.attr[:annotations] ≢ nothing old_ann = subplot.attr[key] - for i in eachindex(old_ann) + for i ∈ eachindex(old_ann) # [1:end-1] is a tuple of coordinates, [end] - text subplot.attr[key][i] = (old_ann[i][1:(end - 1)]..., pgfx_sanitize_string(old_ann[i][end])) @@ -1304,7 +1304,7 @@ function pgfx_sanitize_plot!(plt) elseif value isa Union{AbstractString,AVec{<:AbstractString}} subplot.attr[key] = pgfx_sanitize_string.(value) elseif value isa Axis - for (k, v) in value.plotattributes + for (k, v) ∈ value.plotattributes if v isa Union{AbstractString,AVec{<:AbstractString}} value.plotattributes[k] = pgfx_sanitize_string.(v) end @@ -1312,12 +1312,12 @@ function pgfx_sanitize_plot!(plt) end end end - for series in plt.series_list - for (key, value) in series.plotattributes + for series ∈ plt.series_list + for (key, value) ∈ series.plotattributes if key ≡ :series_annotations && series.plotattributes[:series_annotations] ≢ nothing old_ann = series.plotattributes[key].strs - for i in eachindex(old_ann) + for i ∈ eachindex(old_ann) series.plotattributes[key].strs[i] = pgfx_sanitize_string(old_ann[i]) end elseif value isa Union{AbstractString,AVec{<:AbstractString}} @@ -1343,7 +1343,7 @@ end wrap_power_labels(labels::AVec{LaTeXString}) = labels function wrap_power_labels(labels::AVec{<:AbstractString}) new_labels = similar(labels) - for (i, label) in enumerate(labels) + for (i, label) ∈ enumerate(labels) new_labels[i] = wrap_power_label(label) end new_labels @@ -1566,7 +1566,7 @@ PlotsBase._series_added(plt::Plot{PGFPlotsXBackend}, series::Series) = PlotsBase._update_plot_object(plt::Plot{PGFPlotsXBackend}) = plt.o(plt) -for mime in ("application/pdf", "image/svg+xml", "image/png") +for mime ∈ ("application/pdf", "image/svg+xml", "image/png") @eval function PlotsBase._show( io::IO, mime::MIME{Symbol($mime)}, diff --git a/PlotsBase/ext/PlotlyJSExt.jl b/PlotsBase/ext/PlotlyJSExt.jl index 3b3acea36..2dab848e4 100644 --- a/PlotsBase/ext/PlotlyJSExt.jl +++ b/PlotsBase/ext/PlotlyJSExt.jl @@ -20,7 +20,7 @@ function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) plt[:overwrite_figure] && PlotsBase.closeall() plt.o = PlotlyJS.plot() traces = PlotlyJS.GenericTrace[] - for series_dict in plotly_series(plt) + for series_dict ∈ plotly_series(plt) plotly_type = pop!(series_dict, :type) series_dict[:transpose] = false push!(traces, PlotlyJS.GenericTrace(plotly_type; series_dict...)) @@ -34,7 +34,7 @@ end # ------------------------------------------------------------------------------ -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/pdf" => "pdf", "image/png" => "png", "image/svg+xml" => "svg", diff --git a/PlotsBase/ext/PlotlyKaleidoExt.jl b/PlotsBase/ext/PlotlyKaleidoExt.jl index 1a86add86..b893ecafa 100644 --- a/PlotsBase/ext/PlotlyKaleidoExt.jl +++ b/PlotsBase/ext/PlotlyKaleidoExt.jl @@ -11,7 +11,7 @@ function __init__() end end -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/pdf" => "pdf", "image/png" => "png", "image/svg+xml" => "svg", diff --git a/PlotsBase/ext/PythonPlotExt.jl b/PlotsBase/ext/PythonPlotExt.jl index d10423086..a3c3218d7 100644 --- a/PlotsBase/ext/PythonPlotExt.jl +++ b/PlotsBase/ext/PythonPlotExt.jl @@ -50,9 +50,9 @@ function PlotsBase.extension_init(::PythonPlotBackend) PythonPlot.ioff() # we don't want every command to update the figure # WARNING: matplotlib uses a reverse convention: `labeltop` instead of `toplabel` - for keyword in (:linthresh, :base, :label) + for keyword ∈ (:linthresh, :base, :label) Commons.new_attr_dict!(keyword) - for letter in (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) + for letter ∈ (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) Commons.set_attr_symbol!(keyword, string(letter)) end end @@ -251,7 +251,7 @@ function _py_marker(marker::Shape) x, y = coords(marker) n = length(x) mat = zeros(n + 1, 2) - @inbounds for i in eachindex(x) + @inbounds for i ∈ eachindex(x) mat[i, 1] = x[i] mat[i, 2] = y[i] end @@ -413,7 +413,7 @@ _py_bbox(::Nothing) = BoundingBox(0mm, 0mm) # get the bounding box of the union of the objects function _py_bbox(v::AVec) bbox_union = DEFAULT_BBOX[] - for obj in v + for obj ∈ v bbox_union += _py_bbox(obj) end bbox_union @@ -444,7 +444,7 @@ end # bounding box: axis title function _py_bbox_title(ax) bb = DEFAULT_BBOX[] - for s in (:title, :_left_title, :_right_title) + for s ∈ (:title, :_left_title, :_right_title) bb += _py_bbox(getproperty(ax, s)) end bb @@ -505,7 +505,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) fix_xy_lengths!(plt, series) # ax = getAxis(plt, series) - x, y, z = (_py_handle_surface(series[letter]) for letter in (:x, :y, :z)) + x, y, z = (_py_handle_surface(series[letter]) for letter ∈ (:x, :y, :z)) if st ≡ :straightline x, y = PlotsBase.straightline_data(series) elseif st ≡ :shape @@ -513,7 +513,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end # make negative radii positive and flip the angle (PythonPlot ignores negative radii) - ispolar(series) && for i in eachindex(y) + ispolar(series) && for i ∈ eachindex(y) if y[i] < 0 y[i] = -y[i] x[i] -= π @@ -559,7 +559,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # add lines ? if st ∈ _py_line_series && maximum(series[:linewidth]) > 0 - for (k, segment) in enumerate(series_segments(series, st; check = true)) + for (k, segment) ∈ enumerate(series_segments(series, st; check = true)) i, rng = segment.attr_index, segment.range ax.plot( map(arg -> arg[rng], xyargs)...; @@ -605,7 +605,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # add markers ? if series[:markershape] ≢ :none && st ∈ _py_marker_series - for segment in series_segments(series, :scatter) + for segment ∈ series_segments(series, :scatter) i, rng = segment.attr_index, segment.range args = x[rng], y[rng] RecipesPipeline.is3d(sp) && (args = (args..., z[rng])) @@ -630,7 +630,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end if st ≡ :shape - for segment in series_segments(series) + for segment ∈ series_segments(series) i, rng = segment.attr_index, segment.range if length(rng) > 1 lc = get_linecolor(series, clims, i, cbar_scale) @@ -734,7 +734,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) X, Y, Z = PlotsBase.mesh3d_triangles(x, y, z, cns) ntris = length(cns[1]) polys = sizehint!(Matrix{eltype(x)}[], ntris) - for n in 1:ntris + for n ∈ 1:ntris m = 4(n - 1) + 1 push!( polys, @@ -845,7 +845,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) ) |> push_h # contours on the axis planes - series[:contours] && for (zdir, mat) in (("x", x), ("y", y), ("z", z)) + series[:contours] && for (zdir, mat) ∈ (("x", x), ("y", y), ("z", z)) offset = zdir == "y" ? ignorenan_maximum(mat) : ignorenan_minimum(mat) ax.contourf( x, @@ -882,7 +882,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) # handle area filling if (fillrange = series[:fillrange]) ≢ nothing && st ≢ :contour - for segment in series_segments(series) + for segment ∈ series_segments(series) i, rng = segment.attr_index, segment.range f, dim1, dim2 = :fill_between, x[rng], y[rng] n = length(dim1) @@ -915,7 +915,7 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end # this is all we need to add the series_annotations text - for (xi, yi, str, fnt) in EachAnn(series[:series_annotations], x, y) + for (xi, yi, str, fnt) ∈ EachAnn(series[:series_annotations], x, y) _py_add_annotations(sp, xi, yi, PlotText(str, fnt)) end end @@ -932,7 +932,7 @@ function _py_set_ticks(sp, ax, ticks, letter) axis = getproperty(ax, get_attr_symbol(letter, :axis)) if ticks ≡ :none || ticks ≡ nothing || ticks == false kw = KW() - for dir in (:top, :bottom, :left, :right) + for dir ∈ (:top, :bottom, :left, :right) kw[dir] = kw[get_attr_symbol(:label, dir)] = false end axis.set_tick_params(; which = "both", kw...) @@ -954,7 +954,7 @@ end function _py_compute_axis_minval(sp::Subplot, axis::Axis) # compute the smallest absolute value for the log scale's linear threshold minval = 1.0 - for sp in axis.sps, series in series_list(sp) + for sp ∈ axis.sps, series ∈ series_list(sp) (v = series.plotattributes[axis[:letter]]) |> isempty && continue minval = NaNMath.min(minval, ignorenan_minimum(abs.(v))) end @@ -1031,7 +1031,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) foreach(series -> _py_add_series(plt, series), plt.series_list) # update subplots - for sp in plt.subplots + for sp ∈ plt.subplots (ax = sp.o) ≡ nothing && continue xaxis, yaxis = sp[:xaxis], sp[:yaxis] @@ -1075,7 +1075,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) kw[:boundaries] = vcat(0, kw[:values] + 0.5) cbar_series[:serieshandle][end] elseif any( - cbar_series[attr] ≢ nothing for attr in (:line_z, :fill_z, :marker_z) + cbar_series[attr] ≢ nothing for attr ∈ (:line_z, :fill_z, :marker_z) ) cmin, cmax = get_clims(sp) norm = if cbar_scale ≡ :identity @@ -1084,7 +1084,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) mpl.colors.LogNorm(vmin = cmin, vmax = cmax) end cmap = nothing - for func in (_py_linecolormap, _py_fillcolormap, _py_markercolormap) + for func ∈ (_py_linecolormap, _py_fillcolormap, _py_markercolormap) (cmap = func(cbar_series)) ≡ nothing || break end c_map = mpl.cm.ScalarMappable(; cmap, norm) @@ -1147,7 +1147,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) _py_set_scale(cbar.ax, sp, sp[:colorbar_scale], ticks_letter) sp[:colorbar_ticks] ≡ :native || _py_set_ticks(sp, cbar.ax, ticks, ticks_letter) - for lab in cbar_axis.get_ticklabels() + for lab ∈ cbar_axis.get_ticklabels() lab.set_fontsize(_py_thickness_scale(plt, sp[:colorbar_tickfontsize])) lab.set_family(sp[:colorbar_tickfontfamily]) lab.set_math_fontfamily( @@ -1173,7 +1173,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) framestyle = sp[:framestyle] if !ispolar(sp) && !RecipesPipeline.is3d(sp) - for pos in ("left", "right", "top", "bottom") + for pos ∈ ("left", "right", "top", "bottom") # Scale all axes by default first getproperty(ax.spines, pos).set_linewidth(_py_thickness_scale(plt, 1)) end @@ -1193,7 +1193,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) ax.tick_params(top = true) # Add ticks too ax.tick_params(right = true) # Add ticks too elseif framestyle ∈ (:axes, :origin) - for loc in + for loc ∈ (xaxis[:mirror] ? "bottom" : "top", yaxis[:mirror] ? "left" : "right") getproperty(ax.spines, loc).set_visible(false) end @@ -1229,7 +1229,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) end # axis attributes - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) axis_sym = get_attr_symbol(letter, :axis) hasproperty(ax, axis_sym) || continue axis = sp[axis_sym] @@ -1276,7 +1276,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) positions = getproperty(ax, get_axis(letter, :ticks))() pyaxis.set_major_locator(mpl.ticker.FixedLocator(positions)) kw = if RecipesPipeline.is3d(sp) - NamedTuple(Symbol(k) => v for (k, v) in fontProperties) + NamedTuple(Symbol(k) => v for (k, v) ∈ fontProperties) else (; fontdict = PythonPlot.PyDict(fontProperties)) end @@ -1358,7 +1358,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) if !xaxis[:showaxis] kw = KW() ispolar(sp) && ax.spines.polar.set_visible(false) - for dir in (:top, :bottom) + for dir ∈ (:top, :bottom) ispolar(sp) || getproperty(ax.spines, string(dir)).set_visible(false) kw[dir] = kw[get_attr_symbol(:label, dir)] = false end @@ -1366,7 +1366,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{PythonPlotBackend}) end if !yaxis[:showaxis] kw = KW() - for dir in (:left, :right) + for dir ∈ (:left, :right) ispolar(sp) || getproperty(ax.spines, string(dir)).set_visible(false) kw[dir] = kw[get_attr_symbol(:label, dir)] = false end @@ -1431,7 +1431,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) # figure out how much the axis components and title "stick out" from the plot area padding = [0mm, 0mm, 0mm, 0mm] # leftpad, toppad, rightpad, bottompad - for bb in ( + for bb ∈ ( _py_bbox_axis(ax, :x), _py_bbox_axis(ax, :y), _py_bbox_title(ax), @@ -1441,7 +1441,7 @@ function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) end if haskey(sp.attr, :cbar_ax) # Treat colorbar the same way cbar_ax = sp.attr[:cbar_handle].ax - for bb in ( + for bb ∈ ( _py_bbox_axis(cbar_ax, :x), _py_bbox_axis(cbar_ax, :y), _py_bbox_title(cbar_ax), @@ -1522,7 +1522,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) push_h(x) = push!(handles, x) nseries = 0 - for series in series_list(sp) + for series ∈ series_list(sp) should_add_to_legend(series) || continue clims = get_clims(sp, series) nseries += 1 @@ -1638,7 +1638,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax) ) end - for txt in leg.get_texts() + for txt ∈ leg.get_texts() PythonPlot.setp( txt, color = _py_color(sp[:legend_font_color]), @@ -1654,7 +1654,7 @@ end # Use the bounding boxes (and methods left/top/right/bottom/width/height) `sp.bbox` and `sp.plotarea` to # position the subplot in the backend. function PlotsBase._update_plot_object(plt::Plot{PythonPlotBackend}) - for sp in plt.subplots + for sp ∈ plt.subplots (ax = sp.o) ≡ nothing && return figw, figh = sp.plt[:size] .* px @@ -1683,7 +1683,7 @@ end PlotsBase._display(plt::Plot{PythonPlotBackend}) = plt.o.show() -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/eps" => "eps", "image/eps" => "eps", "application/pdf" => "pdf", diff --git a/PlotsBase/ext/UnicodePlotsExt.jl b/PlotsBase/ext/UnicodePlotsExt.jl index 9d306cc4e..4fdb44835 100644 --- a/PlotsBase/ext/UnicodePlotsExt.jl +++ b/PlotsBase/ext/UnicodePlotsExt.jl @@ -116,7 +116,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend}) up_height = UnicodePlots.DEFAULT_HEIGHT[] has_layout = prod(size(plt.layout)) > 1 - for sp in plt.subplots + for sp ∈ plt.subplots sp_kw = sp[:extra_kwargs] xaxis = sp[:xaxis] yaxis = sp[:yaxis] @@ -160,7 +160,7 @@ function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend}) blend = get(sp_kw, :blend, true) grid = xaxis[:grid] && yaxis[:grid] quiver = contour = false - for series in series_list(sp) + for series ∈ series_list(sp) st = series[:seriestype] blend &= get(series[:extra_kwargs], :blend, true) quiver |= series[:arrow] isa Arrow # post-pipeline detection (:quiver -> :path) @@ -220,11 +220,11 @@ function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend}) ) o = UnicodePlots.Plot(x, y, plot_3d ? z : nothing, _canvas_map[canvas]; kw...) - for series in series_list(sp) + for series ∈ series_list(sp) o = addUnicodeSeries!(sp, o, kw, series, sp[:legend_position] ≢ :none, plot_3d) end - for ann in sp[:annotations] + for ann ∈ sp[:annotations] x, y, val = locate_annotation(sp, ann...) o = UnicodePlots.annotate!( o, @@ -333,7 +333,7 @@ function addUnicodeSeries!( label = addlegend ? series[:label] : "" - for (n, segment) in enumerate(series_segments(series, st; check = true)) + for (n, segment) ∈ enumerate(series_segments(series, st; check = true)) i, rng = segment.attr_index, segment.range lc = get_linecolor(series, i) up = func( @@ -347,7 +347,7 @@ function addUnicodeSeries!( ) end - for (xi, yi, str, fnt) in EachAnn(series[:series_annotations], x, y) + for (xi, yi, str, fnt) ∈ EachAnn(series[:series_annotations], x, y) up = UnicodePlots.annotate!( up, xi, @@ -385,7 +385,7 @@ function PlotsBase._show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBacken canvas_type = nothing imgs = [] sps = 0 - for r in 1:nr, c in 1:nc + for r ∈ 1:nr, c ∈ 1:nc if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) unsupported_layout_error() else @@ -405,9 +405,9 @@ function PlotsBase._show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBacken length(img) == 0 && return # early return on failing fonts sps = 0 n1 = 1 - for r in 1:nr + for r ∈ 1:nr n2 = 1 - for c in 1:nc + for c ∈ 1:nc h, w = (sp = imgs[sps += 1]) |> size img[n1:(n1 + (h - 1)), n2:(n2 + (w - 1))] = sp n2 += m2[c] @@ -429,7 +429,7 @@ function PlotsBase._show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBacke nr, nc = size(plt.layout) if nr == 1 && nc == 1 # fast path n = length(plt.o) - for (i, p) in enumerate(plt.o) + for (i, p) ∈ enumerate(plt.o) show(io, p) i < n && println(io) end @@ -441,9 +441,9 @@ function PlotsBase._show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBacke w_max = zeros(Int, nc) nsp = length(plt.o) sps = 0 - for r in 1:nr + for r ∈ 1:nr lmax = 0 - for c in 1:nc + for c ∈ 1:nc if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) unsupported_layout_error() else @@ -465,9 +465,9 @@ function PlotsBase._show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBacke l_max[r] = lmax end empty = map(w -> ' '^w, w_max) - for r in 1:nr - for n in 1:l_max[r] - for c in 1:nc + for r ∈ 1:nr + for n ∈ 1:l_max[r] + for c ∈ 1:nc pre = c == 1 ? '\0' : ' ' if (lc = lines_colored[r, c]) ≡ nothing || length(lc) < n print(io, pre, empty[c]) diff --git a/PlotsBase/src/Annotations.jl b/PlotsBase/src/Annotations.jl index 798fb30f8..8ce7af232 100644 --- a/PlotsBase/src/Annotations.jl +++ b/PlotsBase/src/Annotations.jl @@ -43,7 +43,7 @@ function series_annotations(anns::AMat, outer_attrs...) # whole_series types can only be in a row vector if size(anns, 1) > 1 - for ann in Iterators.filter(ann -> ann isa whole_series, anns) + for ann ∈ Iterators.filter(ann -> ann isa whole_series, anns) "Given series annotation must be the only element in its column:\n$ann" |> ArgumentError |> throw @@ -65,7 +65,7 @@ function series_annotations(strs::AVec, args...) fnt = font() shp = nothing scalefactor = 1, 1 - for arg in args + for arg ∈ args if isa(arg, Shape) || (isa(arg, AVec) && eltype(arg) == Shape) shp = arg elseif isa(arg, Font) @@ -94,7 +94,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) msw, msh = anns.scalefactor msize = Float64[] shapes = Vector{Shape}(undef, length(anns.strs)) - for i in eachindex(anns.strs) + for i ∈ eachindex(anns.strs) str = _cycle(anns.strs, i) # get the width and height of the string (in mm) @@ -195,7 +195,7 @@ _process_annotation_3d( function _process_annotation(sp::Subplot, ann, annotation_processor::Function) ann = makevec.(ann) - [annotation_processor(sp, _cycle.(ann, i)...) for i in 1:maximum(length.(ann))] + [annotation_processor(sp, _cycle.(ann, i)...) for i ∈ 1:maximum(length.(ann))] end # Expand arrays of coordinates, positions and labels into individual annotations diff --git a/PlotsBase/src/Arrows.jl b/PlotsBase/src/Arrows.jl index 597f126b8..5d46785cd 100644 --- a/PlotsBase/src/Arrows.jl +++ b/PlotsBase/src/Arrows.jl @@ -22,7 +22,7 @@ function arrow(args...) style, side = :simple, :head headlength = headwidth = 0.3 setlength = false - for arg in args + for arg ∈ args T = typeof(arg) if T == Symbol if arg in (:head, :tail, :both) @@ -49,7 +49,7 @@ end # allow for do-block notation which gets called on every valid start/end pair which # we need to draw an arrow function add_arrows(func::Function, x::AVec, y::AVec) - for i in 2:length(x) + for i ∈ 2:length(x) xyprev = (x[i - 1], y[i - 1]) xy = (x[i], y[i]) if ok(xyprev) && ok(xy) diff --git a/PlotsBase/src/Axes.jl b/PlotsBase/src/Axes.jl index 32b28ee4f..a2c00036d 100644 --- a/PlotsBase/src/Axes.jl +++ b/PlotsBase/src/Axes.jl @@ -204,7 +204,7 @@ function widen_factor(axis::Axis; factor = default_widen_factor[]) # automatic behavior: widen if limits aren't specified and series type is appropriate lims = process_limits(axis[:lims], axis) (lims isa Tuple || lims ≡ :round) && return - for sp in axis.sps, series in series_list(sp) + for sp ∈ axis.sps, series ∈ series_list(sp) series.plotattributes[:seriestype] in _widen_seriestypes && return factor end nothing @@ -320,7 +320,7 @@ function PlotsBase.attr!(axis::Axis, args...; kw...) PlotsBase.Commons.preprocess_attributes!(KW(kw)) # then override for any keywords... only those keywords that already exists in plotattributes - for (k, v) in kw + for (k, v) ∈ kw haskey(plotattributes, k) || continue if k ≡ :discrete_values foreach(x -> discrete_value!(axis, x), v) # add these discrete values to the axis @@ -370,7 +370,7 @@ function _update_axis( ) # build the KW of arguments from the letter version (i.e. xticks --> ticks) kw = KW() - for k in Commons._all_axis_attrs + for k ∈ Commons._all_axis_attrs # first get the args without the letter: `tickfont = font(10)` # note: we don't pop because we want this to apply to all axes! (delete after all have finished) if haskey(plotattributes_in, k) @@ -432,10 +432,10 @@ function Commons.get_ticks( end function reset_extrema!(sp::Subplot) - for asym in (:x, :y, :z) + for asym ∈ (:x, :y, :z) sp[get_attr_symbol(asym, :axis)][:extrema] = Extrema() end - for series in sp.series_list + for series ∈ sp.series_list expand_extrema!(sp, series.plotattributes) end end diff --git a/PlotsBase/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl index af7585265..705143e82 100644 --- a/PlotsBase/src/BezierCurves.jl +++ b/PlotsBase/src/BezierCurves.jl @@ -10,7 +10,7 @@ end function (bc::BezierCurve)(t::Real) p = (0.0, 0.0) n = length(bc.control_points) - 1 - for i in 0:n + for i ∈ 0:n p = p .+ bc.control_points[i + 1] .* binomial(n, i) .* (1 - t)^(n - i) .* t^i end p diff --git a/PlotsBase/src/Colorbars.jl b/PlotsBase/src/Colorbars.jl index a3f27063f..f29708cbf 100644 --- a/PlotsBase/src/Colorbars.jl +++ b/PlotsBase/src/Colorbars.jl @@ -30,7 +30,7 @@ get_clims(sp::Subplot, series::Series)::Tuple{Float64,Float64} = function update_clims(sp::Subplot, op = process_clims(sp[:clims]))::Tuple{Float64,Float64} zmin, zmax = Inf, -Inf - for series in series_list(sp) + for series ∈ series_list(sp) if series[:colorbar_entry]::Bool # Avoid calling the inner `update_clims` if at all possible; dynamic dispatch hell if (series[:seriestype] ∈ Commons._z_colored_series && series[:z] ≢ nothing) || @@ -115,7 +115,7 @@ function colorbar_style(series::Series) elseif iscontour(series) cbar_lines elseif series[:seriestype] ∈ (:heatmap, :surface) || - any(series[z] ≢ nothing for z in (:marker_z, :line_z, :fill_z)) + any(series[z] ≢ nothing for z ∈ (:marker_z, :line_z, :fill_z)) cbar_gradient else nothing @@ -124,7 +124,7 @@ end hascolorbar(series::Series) = colorbar_style(series) ≢ nothing hascolorbar(sp::Subplot) = - sp[:colorbar] ≢ :none && any(hascolorbar(s) for s in series_list(sp)) + sp[:colorbar] ≢ :none && any(hascolorbar(s) for s ∈ series_list(sp)) function get_colorbar_ticks(sp::Subplot; update = true, formatter = sp[:colorbar_formatter]) if update || !haskey(sp.attr, :colorbar_optimized_ticks) diff --git a/PlotsBase/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl index a58e53b30..573953f9a 100644 --- a/PlotsBase/src/Commons/Commons.jl +++ b/PlotsBase/src/Commons/Commons.jl @@ -168,10 +168,10 @@ macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) Expr( :(:), Expr(:., :., :., parent), - (Expr(:., s isa Expr ? s.args[1] : s) for s in symbols)..., + (Expr(:., s isa Expr ? s.args[1] : s) for s ∈ symbols)..., ), ) - export_ex = Expr(:export, (s isa Expr ? s.args[1] : s for s in symbols)...) + export_ex = Expr(:export, (s isa Expr ? s.args[1] : s for s ∈ symbols)...) Expr(:module, true, mod, Expr(:block, import_ex, export_ex)) |> esc end @@ -318,7 +318,7 @@ function get_aspect_ratio(sp) check_aspect_ratio(ar) if ar ≡ :auto ar = :none - for series in series_list(sp) + for series ∈ series_list(sp) if series[:seriestype] ≡ :image ar = :equal end @@ -339,7 +339,7 @@ function dumpdict(io::IO, plotattributes::AKW, prefix = "") _debug[] || return println(io) prefix == "" || println(io, prefix, ":") - for k in sort(collect(keys(plotattributes))) + for k ∈ sort(collect(keys(plotattributes))) Printf.@printf(io, "%14s: ", k) debugshow(io, plotattributes[k]) println(io) diff --git a/PlotsBase/src/Commons/attrs.jl b/PlotsBase/src/Commons/attrs.jl index 6e9ad60c4..48da09c12 100644 --- a/PlotsBase/src/Commons/attrs.jl +++ b/PlotsBase/src/Commons/attrs.jl @@ -4,7 +4,7 @@ make_non_underscore(s::Symbol) = Symbol(replace(string(s), "_" => "")) const _keyAliases = Dict{Symbol,Symbol}() function add_aliases(sym::Symbol, aliases::Symbol...) - for alias in aliases + for alias ∈ aliases (haskey(_keyAliases, alias) || alias ≡ sym) && return _keyAliases[alias] = sym end @@ -14,13 +14,13 @@ end function add_axes_aliases(sym::Symbol, aliases::Symbol...; generic::Bool = true) sym in keys(_axis_defaults) || throw(ArgumentError("Invalid `$sym`")) generic && add_aliases(sym, aliases...) - for letter in (:x, :y, :z) - add_aliases(Symbol(letter, sym), (Symbol(letter, a) for a in aliases)...) + for letter ∈ (:x, :y, :z) + add_aliases(Symbol(letter, sym), (Symbol(letter, a) for a ∈ aliases)...) end end function add_non_underscore_aliases!(aliases::Dict{Symbol,Symbol}) - for (k, v) in aliases + for (k, v) ∈ aliases if '_' in string(k) aliases[make_non_underscore(k)] = v end @@ -523,9 +523,9 @@ const _axis_defaults = KW( const _axis_defaults_byletter = KW() reset_axis_defaults_byletter!() = - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) _axis_defaults_byletter[letter] = KW() - for (k, v) in _axis_defaults + for (k, v) ∈ _axis_defaults _axis_defaults_byletter[letter][k] = v end end @@ -573,7 +573,7 @@ const _all_magic_attrs = const _all_axis_attrs = union(_axis_attrs, _magic_axis_attrs) const _lettered_all_axis_attrs = - Set([Symbol(letter, kw) for letter in (:x, :y, :z) for kw in _all_axis_attrs]) + Set([Symbol(letter, kw) for letter ∈ (:x, :y, :z) for kw ∈ _all_axis_attrs]) const _all_subplot_attrs = union(_subplot_attrs, _magic_subplot_attrs) const _all_series_attrs = union(_series_attrs, _magic_series_attrs) const _all_plot_attrs = _plot_attrs @@ -647,9 +647,9 @@ const _base_supported_attrs = [ function merge_with_base_supported(v::AVec) v = vcat(v, _base_supported_attrs) - for vi in v + for vi ∈ v if haskey(_axis_defaults, vi) - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) push!(v, get_attr_symbol(letter, vi)) end end @@ -670,7 +670,7 @@ include("aliases.jl") function parse_axis_kw(s::Symbol) s = string(s) - for letter in ('x', 'y', 'z') + for letter ∈ ('x', 'y', 'z') startswith(s, letter) && return (Symbol(letter), Symbol(chop(s, head = 1, tail = 0))) end @@ -691,7 +691,7 @@ end """ function default(k::Symbol) k = get(_keyAliases, k, k) - for defaults in _all_defaults + for defaults ∈ _all_defaults haskey(defaults, k) && return defaults[k] end haskey(_axis_defaults, k) && return _axis_defaults[k] @@ -705,7 +705,7 @@ end function default(k::Symbol, v) k = get(_keyAliases, k, k) - for defaults in _all_defaults + for defaults ∈ _all_defaults if haskey(defaults, k) defaults[k] = v return v @@ -727,7 +727,7 @@ function default(; reset = true, kw...) (reset && isempty(kw)) && reset_defaults() kw = KW(kw) preprocess_attributes!(kw) - for (k, v) in kw + for (k, v) ∈ kw default(k, v) end end @@ -1195,7 +1195,7 @@ macro add_attributes(level, expr, match_table) _splitdef!(expr.args[3], key_dict) insert_block = Expr(:block) - for (key, value) in key_dict + for (key, value) ∈ key_dict # e.g. _series_defaults[key] = value exp_key = Symbol(lowercase(string(T)), "_", key) pl_key = makeplural(exp_key) @@ -1228,7 +1228,7 @@ macro add_attributes(level, expr, match_table) end function _splitdef!(blk, key_dict) - for i in eachindex(blk.args) + for i ∈ eachindex(blk.args) if (ei = blk.args[i]) isa Symbol # var continue @@ -1243,7 +1243,7 @@ function _splitdef!(blk, key_dict) var = lhs.args[1] type = lhs.args[2] if @isdefined type - for field in fieldnames(getproperty(PlotsBase, type)) + for field ∈ fieldnames(getproperty(PlotsBase, type)) key_dict[Symbol(var, "_", field)] = :(getfield($(ei.args[2]), $(QuoteNode(field)))) end diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl index 79ccc93a0..05f4907d9 100644 --- a/PlotsBase/src/Commons/layouts.jl +++ b/PlotsBase/src/Commons/layouts.jl @@ -14,7 +14,7 @@ function bbox(x, y, w, h, oarg1::Symbol, originargs::Symbol...) oargs = vcat(oarg1, originargs...) orighor = :left origver = :top - for oarg in oargs + for oarg ∈ oargs if oarg ≡ :center orighor = origver = oarg elseif oarg in (:left, :right, :hcenter) @@ -161,11 +161,11 @@ function GridLayout( DEFAULT_MINPAD[], DEFAULT_BBOX[], grid, - Measure[w * pct for w in widths], - Measure[h * pct for h in heights], + Measure[w * pct for w ∈ widths], + Measure[h * pct for h ∈ heights], KW(kw), ) - for i in eachindex(grid) + for i ∈ eachindex(grid) grid[i] = EmptyLayout(layout) end layout diff --git a/PlotsBase/src/Commons/measures.jl b/PlotsBase/src/Commons/measures.jl index 127991391..9de4bff47 100644 --- a/PlotsBase/src/Commons/measures.jl +++ b/PlotsBase/src/Commons/measures.jl @@ -30,7 +30,7 @@ end # convert a bounding box from absolute coords to percentages... # returns an array of percentages of figure size: [left, bottom, width, height] function bbox_to_pcts(bb::BoundingBox, figw, figh, flipy = true) - mms = Float64[f(bb).value for f in (left, bottom, width, height)] + mms = Float64[f(bb).value for f ∈ (left, bottom, width, height)] if flipy mms[2] = figh.value - mms[2] # flip y when origin in bottom-left end diff --git a/PlotsBase/src/Commons/postprocess_attrs.jl b/PlotsBase/src/Commons/postprocess_attrs.jl index 67e48da7f..76f6de869 100644 --- a/PlotsBase/src/Commons/postprocess_attrs.jl +++ b/PlotsBase/src/Commons/postprocess_attrs.jl @@ -3,15 +3,15 @@ foreach(arg -> add_aliases(arg, makeplural(arg)), _all_attrs) # fill symbol cache -for letter in (:x, :y, :z) +for letter ∈ (:x, :y, :z) new_attr_dict!(letter) - for keyword in _axis_attrs + for keyword ∈ _axis_attrs # populate attribute cache letter_keyword = set_attr_symbol!(letter, string(keyword)) # allow the underscore version too: `xguide` or `x_guide` add_aliases(letter_keyword, Symbol(letter, "_", keyword)) end - for keyword in (_magic_axis_attrs..., :(_discrete_indices)) + for keyword ∈ (_magic_axis_attrs..., :(_discrete_indices)) _attrsymbolcache[letter][keyword] = Symbol(letter, keyword) end end diff --git a/PlotsBase/src/DataSeries.jl b/PlotsBase/src/DataSeries.jl index 04391669d..bb360eacd 100644 --- a/PlotsBase/src/DataSeries.jl +++ b/PlotsBase/src/DataSeries.jl @@ -87,7 +87,7 @@ end function copy_series!(series, letter) plt = series[:plot_object] - for s in plt.series_list, l in (:x, :y, :z) + for s ∈ plt.series_list, l ∈ (:x, :y, :z) if (s ≢ series || l ≢ letter) && s[l] ≡ series[letter] series[letter] = copy(series[letter]) end @@ -103,7 +103,7 @@ extend_by_data!(v::AbstractVector, x) = isimmutable(v) ? vcat(v, x) : push!(v, x extend_by_data!(v::AbstractVector, x::AbstractVector) = isimmutable(v) ? vcat(v, x) : append!(v, x) -for comp in (:line, :fill, :marker) +for comp ∈ (:line, :fill, :marker) compcolor = string(comp, :color) get_compcolor = Symbol(:get_, compcolor) comp_z = string(comp, :_z) @@ -235,10 +235,10 @@ end has_attribute_segments(series::Series) = any( series[attr] isa AbstractVector && length(series[attr]) > 1 for - attr in PlotsBase.Commons._segmenting_vector_attributes + attr ∈ PlotsBase.Commons._segmenting_vector_attributes ) || any( series[attr] isa AbstractArray for - attr in PlotsBase.Commons._segmenting_array_attributes + attr ∈ PlotsBase.Commons._segmenting_array_attributes ) function series_segments(series::Series, seriestype::Symbol = :path; check = false) @@ -250,10 +250,10 @@ function series_segments(series::Series, seriestype::Symbol = :path; check = fal if check scales = :xscale, :yscale, :zscale - for (n, s) in enumerate(args) + for (n, s) ∈ enumerate(args) (scale = get(series, scales[n], :identity)) ∈ PlotsBase.Commons._log_scales || continue - for (i, v) in enumerate(s) + for (i, v) ∈ enumerate(s) if v <= 0 @warn "Invalid negative or zero value $v found at series index $i for $scale based $(scales[n])" @debug "" exception = (DomainError(v), stacktrace()) @@ -269,13 +269,13 @@ function series_segments(series::Series, seriestype::Symbol = :path; check = fal warn_on_inconsistent_shape_attrs(series, x, y, z, r) (SeriesSegment(r, first(r)),) elseif seriestype in (:scatter, :scatter3d) - (SeriesSegment(i:i, i) for i in r) + (SeriesSegment(i:i, i) for i ∈ r) else - (SeriesSegment(i:(i + 1), i) for i in first(r):(last(r) - 1)) + (SeriesSegment(i:(i + 1), i) for i ∈ first(r):(last(r) - 1)) end end |> Iterators.flatten else - (SeriesSegment(r, 1) for r in nan_segments) + (SeriesSegment(r, 1) for r ∈ nan_segments) end warn_on_attr_dim_mismatch(series, x, y, z, segments) @@ -288,7 +288,7 @@ function warn_on_attr_dim_mismatch(series, x, y, z, segments) minimum(map(seg -> first(seg.range), segments)), maximum(map(seg -> last(seg.range), segments)), ) - for attr in PlotsBase.Commons._segmenting_vector_attributes + for attr ∈ PlotsBase.Commons._segmenting_vector_attributes if (v = get(series, attr, nothing)) isa PlotsBase.Commons.AVec && eachindex(v) != seg_range @warn "Indices $(eachindex(v)) of attribute `$attr` does not match data indices $seg_range." @@ -306,7 +306,7 @@ function warn_on_attr_dim_mismatch(series, x, y, z, segments) end function warn_on_inconsistent_shape_attrs(series, x, y, z, r) - for attr in PlotsBase.Commons._segmenting_vector_attributes + for attr ∈ PlotsBase.Commons._segmenting_vector_attributes v = get(series, attr, nothing) if v isa PlotsBase.Commons.AVec && length(unique(v[r])) > 1 @warn "Different values of `$attr` specified for different shape vertices. Only first one will be used." @@ -327,7 +327,7 @@ attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) function attr!(series::Series; kw...) plotattributes = KW(kw) Commons.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes + for (k, v) ∈ plotattributes if haskey(_series_defaults, k) series[k] = v else diff --git a/PlotsBase/src/Fonts.jl b/PlotsBase/src/Fonts.jl index 5534c5ed9..ff9041bfa 100644 --- a/PlotsBase/src/Fonts.jl +++ b/PlotsBase/src/Fonts.jl @@ -43,7 +43,7 @@ function font(args...; kw...) rotation = 0 color = colorant"black" - for arg in args + for arg ∈ args T = typeof(arg) @assert arg ≢ :match @@ -78,7 +78,7 @@ function font(args...; kw...) end end - for sym in keys(kw) + for sym ∈ keys(kw) if sym ≡ :family family = string(kw[sym]) elseif sym ≡ :pointsize @@ -116,12 +116,12 @@ end Scales all **current** font sizes by `factor`. For example `scalefontsizes(1.1)` increases all current font sizes by 10%. To reset to initial sizes, use `scalefontsizes()` """ function scalefontsizes(factor::Number) - for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) + for k ∈ keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) scalefontsize(k, factor) end - for letter in (:x, :y, :z) - for k in keys(_initial_ax_fontsizes) + for letter ∈ (:x, :y, :z) + for k ∈ keys(_initial_ax_fontsizes) scalefontsize(get_attr_symbol(letter, k), factor) end end @@ -133,7 +133,7 @@ end Resets font sizes to initial default values. """ function scalefontsizes() - for k in keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) + for k ∈ keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) f = default(k) if k in keys(_initial_fontsizes) factor = f / _initial_fontsizes[k] @@ -141,8 +141,8 @@ function scalefontsizes() end end - for letter in (:x, :y, :z) - for k in keys(_initial_ax_fontsizes) + for letter ∈ (:x, :y, :z) + for k ∈ keys(_initial_ax_fontsizes) if k in keys(_initial_fontsizes) f = default(get_attr_symbol(letter, k)) factor = f / _initial_fontsizes[k] diff --git a/PlotsBase/src/Plots.jl b/PlotsBase/src/Plots.jl index ea3fe908e..59a3047c3 100644 --- a/PlotsBase/src/Plots.jl +++ b/PlotsBase/src/Plots.jl @@ -101,7 +101,7 @@ Base.append!(plt::Plot, i::Integer, t::Tuple) = append!(plt, i, t...) # push y[i] to the ith series function Base.push!(plt::Plot, y::AVec) ny = length(y) - for i in 1:(plt.n) + for i ∈ 1:(plt.n) push!(plt, i, y[mod1(i, ny)]) end plt @@ -114,7 +114,7 @@ Base.push!(plt::Plot, x::Real, y::AVec) = push!(plt, [x], y) function Base.push!(plt::Plot, x::AVec, y::AVec) nx = length(x) ny = length(y) - for i in 1:(plt.n) + for i ∈ 1:(plt.n) push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)]) end plt @@ -125,7 +125,7 @@ function Base.push!(plt::Plot, x::AVec, y::AVec, z::AVec) nx = length(x) ny = length(y) nz = length(z) - for i in 1:(plt.n) + for i ∈ 1:(plt.n) push!(plt, i, x[mod1(i, nx)], y[mod1(i, ny)], z[mod1(i, nz)]) end plt @@ -135,11 +135,11 @@ end "smallest x in plot" xmin(plt::Plot) = ignorenan_minimum([ - ignorenan_minimum(series.plotattributes[:x]) for series in plt.series_list + ignorenan_minimum(series.plotattributes[:x]) for series ∈ plt.series_list ]) "largest x in plot" xmax(plt::Plot) = ignorenan_maximum([ - ignorenan_maximum(series.plotattributes[:x]) for series in plt.series_list + ignorenan_maximum(series.plotattributes[:x]) for series ∈ plt.series_list ]) "extrema of x-values in plot" @@ -192,7 +192,7 @@ plottitlefont(plt::Plot) = font(; # update attr from an input dictionary function _update_plot_attrs(plt::Plot, plotattributes_in::AKW) - for (k, v) in PlotsBase._plot_defaults + for (k, v) ∈ PlotsBase._plot_defaults PlotsBase.slice_arg!(plotattributes_in, plt.attr, k, 1, true) end @@ -206,7 +206,7 @@ function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol) # handle linking here. if we're passed a list of # other subplots to link to, link them together (link = axis[:link]) |> isempty && return - for other_sp in link + for other_sp ∈ link link_axes!(axis, get_axis(get_subplot(plt, other_sp), letter)) end axis.plotattributes[:link] = [] @@ -246,7 +246,7 @@ function _update_subplot_attrs( anns = RecipesPipeline.pop_kw!(sp.attr, :annotations) # grab those args which apply to this subplot - for k in keys(_subplot_defaults) + for k ∈ keys(_subplot_defaults) PlotsBase.slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair) end @@ -259,7 +259,7 @@ function _update_subplot_attrs( end lims_warned = false - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) Axes._update_axis(plt, sp, plotattributes_in, letter, subplot_index) lk = get_attr_symbol(letter, :lims) diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl index 2e9d18b20..14dbc92f9 100644 --- a/PlotsBase/src/PlotsBase.jl +++ b/PlotsBase/src/PlotsBase.jl @@ -171,7 +171,7 @@ include("users.jl") imports = sizehint!(Expr[], n) examples = sizehint!(Expr[], 10n) scratch_dir = mktempdir() - for i in setdiff(1:n, _backend_skips[backend_name()], _animation_examples) + for i ∈ setdiff(1:n, _backend_skips[backend_name()], _animation_examples) _examples[i].external && continue (imp = _examples[i].imports) ≡ nothing || push!(imports, imp) func = gensym(string(i)) diff --git a/PlotsBase/src/Shapes.jl b/PlotsBase/src/Shapes.jl index 2b3843a70..1feef553e 100644 --- a/PlotsBase/src/Shapes.jl +++ b/PlotsBase/src/Shapes.jl @@ -58,14 +58,14 @@ PlotsBase.coords(shapes::AVec{<:Shape}) = RecipesPipeline.unzip(map(coords, shap "get an array of tuples of points on a circle with radius `r`" partialcircle(start_angle, end_angle, n = 20, r = 1) = - [(r * cos(u), r * sin(u)) for u in range(start_angle, end_angle, n)] + [(r * cos(u), r * sin(u)) for u ∈ range(start_angle, end_angle, n)] "interleave 2 vectors into each other (like a zipper's teeth)" function weave(x, y; ordering = Vector[x, y]) ret = eltype(x)[] done = false while !done - for o in ordering + for o ∈ ordering try push!(ret, popfirst!(o)) catch @@ -147,12 +147,12 @@ function center(shape::Shape) x, y = coords(shape) n = length(x) A, Cx, Cy = 0, 0, 0 - for i in 1:n + for i ∈ 1:n ip1 = i == n ? 1 : i + 1 A += x[i] * y[ip1] - x[ip1] * y[i] end A *= 0.5 - for i in 1:n + for i ∈ 1:n ip1 = i == n ? 1 : i + 1 m = (x[i] * y[ip1] - x[ip1] * y[i]) Cx += (x[i] + x[ip1]) * m @@ -164,7 +164,7 @@ end function scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) sx, sy = coords(shape) cx, cy = c - for i in eachindex(sx) + for i ∈ eachindex(sx) sx[i] = (sx[i] - cx) * x + cx sy[i] = (sy[i] - cy) * y + cy end @@ -182,7 +182,7 @@ scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) = function translate!(shape::Shape, x::Real, y::Real = x) sx, sy = coords(shape) - for i in eachindex(sx) + for i ∈ eachindex(sx) sx[i] += x sy[i] += y end @@ -214,7 +214,7 @@ rotate(x::Real, y::Real, θ::Real, c) = function rotate!(shape::Shape, θ::Real, c = center(shape)) x, y = coords(shape) - for i in eachindex(x) + for i ∈ eachindex(x) xi = Shapes.rotate_x(x[i], y[i], θ, c...) yi = Shapes.rotate_y(x[i], y[i], θ, c...) x[i], y[i] = xi, yi diff --git a/PlotsBase/src/Strokes.jl b/PlotsBase/src/Strokes.jl index 2c9f87782..605e8559f 100644 --- a/PlotsBase/src/Strokes.jl +++ b/PlotsBase/src/Strokes.jl @@ -22,7 +22,7 @@ function stroke(args...; alpha = nothing) color = :black style = :solid - for arg in args + for arg ∈ args T = typeof(arg) # if arg in _all_styles @@ -57,7 +57,7 @@ function brush(args...; alpha = nothing) size = 1 color = :black - for arg in args + for arg ∈ args T = typeof(arg) if T <: Colorant diff --git a/PlotsBase/src/Subplots.jl b/PlotsBase/src/Subplots.jl index dc7a1f324..1658514e8 100644 --- a/PlotsBase/src/Subplots.jl +++ b/PlotsBase/src/Subplots.jl @@ -138,7 +138,7 @@ legendtitlefont(sp::Subplot) = font(; function _update_subplot_periphery(sp::Subplot, anns::AVec) # extend annotations, and ensure we always have a (x,y,PlotText) tuple newanns = [] - for ann in vcat(anns, sp[:annotations]) + for ann ∈ vcat(anns, sp[:annotations]) append!(newanns, PlotsBase.process_annotation(sp, ann)) end sp.attr[:annotations] = newanns @@ -167,7 +167,7 @@ function _update_subplot_colors(sp::Subplot) end _update_margins(sp::Subplot) = - for sym in (:margin, :left_margin, :top_margin, :right_margin, :bottom_margin) + for sym ∈ (:margin, :left_margin, :top_margin, :right_margin, :bottom_margin) if (margin = get(sp.attr, sym, nothing)) isa Tuple # transform e.g. (1, :mm) => 1 * PlotsBase.mm sp.attr[sym] = margin[1] * getfield(@__MODULE__, margin[2]) @@ -177,18 +177,18 @@ _update_margins(sp::Subplot) = needs_any_3d_axes(sp::Subplot) = any( RecipesPipeline.needs_3d_axes( _override_seriestype_check(s.plotattributes, s.plotattributes[:seriestype]), - ) for s in series_list(sp) + ) for s ∈ series_list(sp) ) function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) # first expand for the data - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) data = plotattributes[letter] if ( letter ≢ :z && plotattributes[:seriestype] ≡ :straightline && - any(series[:seriestype] ≢ :straightline for series in series_list(sp)) && + any(series[:seriestype] ≢ :straightline for series ∈ series_list(sp)) && length(data) > 1 && data[1] != data[2] ) @@ -247,7 +247,7 @@ function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) # expand for heatmaps if plotattributes[:seriestype] ≡ :heatmap - for letter in (:x, :y) + for letter ∈ (:x, :y) data = plotattributes[letter] axis = sp[get_attr_symbol(letter, :axis)] scale = get(plotattributes, get_attr_symbol(letter, :scale), :identity) @@ -278,7 +278,7 @@ Commons.bottompad(sp::Subplot) = sp.minpad[4] function attr!(sp::Subplot; kw...) plotattributes = KW(kw) PlotsBase.Commons.preprocess_attributes!(plotattributes) - for (k, v) in plotattributes + for (k, v) ∈ plotattributes if haskey(_subplot_defaults, k) sp[k] = v else diff --git a/PlotsBase/src/Ticks.jl b/PlotsBase/src/Ticks.jl index 7abf4f9c8..d1b6bcc17 100644 --- a/PlotsBase/src/Ticks.jl +++ b/PlotsBase/src/Ticks.jl @@ -84,12 +84,12 @@ function get_minor_ticks(sp, axis, ticks_and_labels) n_minor_intervals = num_minor_intervals(axis) minorticks = sizehint!(eltype(ticks)[], n_minor_intervals * sub * length(ticks)) - for i in 2:length(ticks) + for i ∈ 2:length(ticks) lo = ticks[i - 1] hi = ticks[i] (isfinite(lo) && isfinite(hi) && hi > lo) || continue if log_scaled - for e in 1:sub + for e ∈ 1:sub lo_ = lo * base^(e - 1) hi_ = lo_ * base step = (hi_ - lo_) / n_minor_intervals diff --git a/PlotsBase/src/alignment.jl b/PlotsBase/src/alignment.jl index f3ad63ba1..5e54dd4e0 100644 --- a/PlotsBase/src/alignment.jl +++ b/PlotsBase/src/alignment.jl @@ -23,7 +23,7 @@ function tick_padding(sp::Subplot, axis::Axis) vals, labs = ticks isempty(labs) && return 0mm # ptsz = axis[:tickfont].pointsize * pt - longest_label = maximum(length(lab) for lab in labs) + longest_label = maximum(length(lab) for lab ∈ labs) # generalize by "rotating" y labels rot = axis[:rotation] + (axis[:letter] ≡ :y ? 90 : 0) diff --git a/PlotsBase/src/animation.jl b/PlotsBase/src/animation.jl index 859caf35b..e3b5cd911 100644 --- a/PlotsBase/src/animation.jl +++ b/PlotsBase/src/animation.jl @@ -45,7 +45,7 @@ Animate from an iterator which returns the plot args each iteration. """ function animate(fitr::FrameIterator, fn = giffn(); kw...) anim = Animation() - for (i, plotargs) in enumerate(fitr.itr) + for (i, plotargs) ∈ enumerate(fitr.itr) if mod1(i, fitr.every) == 1 plot(wraptuple(plotargs)...; fitr.kw...) frame(anim) diff --git a/PlotsBase/src/axes_utils.jl b/PlotsBase/src/axes_utils.jl index 735ce0c26..244e207a8 100644 --- a/PlotsBase/src/axes_utils.jl +++ b/PlotsBase/src/axes_utils.jl @@ -124,7 +124,7 @@ function get_labels(formatter::Function, scaled_ticks, scale) end # Ticks getter functions -for l in (:x, :y, :z) +for l ∈ (:x, :y, :z) axis = string(l, "-axis") # "x-axis" ticks = string(l, "ticks") # "xticks" f = Symbol(ticks) # :xticks @@ -220,7 +220,7 @@ discrete_value!(axis::Axis, cv::Number) = (cv, -1) function discrete_value!(axis::Axis, v::AVec) cvec = zeros(axes(v)) discrete_indices = similar(Array{Int}, axes(v)) - for i in eachindex(v) + for i ∈ eachindex(v) cvec[i], discrete_indices[i] = discrete_value!(axis, v[i]) end cvec, discrete_indices @@ -230,7 +230,7 @@ end function discrete_value!(axis::Axis, v::AMat) cmat = zeros(axes(v)) discrete_indices = similar(Array{Int}, axes(v)) - for I in eachindex(v) + for I ∈ eachindex(v) cmat[I], discrete_indices[I] = discrete_value!(axis, v[I]) end cmat, discrete_indices @@ -271,7 +271,7 @@ function add_major_or_minor_segments_2d( end end isy = ax[:letter] ≡ :y - for tick in ticks + for tick ∈ ticks (ax[:showaxis] && cond) && push!( tick_segments, reverse_if((tick, tick_start), isy), @@ -439,7 +439,7 @@ function add_major_or_minor_segments_3d( ga0_, ga1_ = reverse_if(gas, ax[:mirror]) end letter = ax[:letter] - for tick in ticks + for tick ∈ ticks (ax[:showaxis] && cond) && push!( tick_segments, sort_3d_axes(tick, tick_start, first(fas), letter), diff --git a/PlotsBase/src/backends.jl b/PlotsBase/src/backends.jl index 1c30f98a2..169316a45 100644 --- a/PlotsBase/src/backends.jl +++ b/PlotsBase/src/backends.jl @@ -8,7 +8,7 @@ struct NoBackend <: AbstractBackend end backend_name(::NoBackend) = :none should_warn_on_unsupported(::NoBackend) = false -for sym in _default_supported_syms +for sym ∈ _default_supported_syms @eval begin $(_f1_sym(sym))(::NoBackend, $sym::Symbol) = true $(_f2_sym(sym))(::NoBackend) = Commons.$(Symbol("_all_$(sym)s")) @@ -126,7 +126,7 @@ function get_backend_module(pkg_name::Symbol) end # create backend init functions by hand as the corresponding structs do not exist yet -for be in _supported_backends +for be ∈ _supported_backends @eval begin function $be(; kw...) default(; reset = false, kw...) @@ -138,7 +138,7 @@ end # create the various `is_xxx_supported` and `supported_xxxs` methods # these methods should be overloaded (dispatched) by each backend in its init_code -for sym in _default_supported_syms +for sym ∈ _default_supported_syms f1 = _f1_sym(sym) f2 = _f2_sym(sym) @eval begin @@ -169,7 +169,7 @@ function backend_defines(be_type::Symbol, be::Symbol) ... PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol} =# - for sym in _default_supported_syms + for sym ∈ _default_supported_syms be_syms = Symbol("_$(be)_$(sym)s") push!( blk.args, @@ -207,7 +207,7 @@ function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) bend = backend_name(pkg) already_warned = get!(() -> Set{Symbol}(), _already_warned, bend) extra_kwargs = Dict{Symbol,Any}() - for k in PlotsBase.explicitkeys(plotattributes) + for k ∈ PlotsBase.explicitkeys(plotattributes) (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue k in Commons._suppress_warnings && continue if ismissing(default(k)) @@ -219,7 +219,7 @@ function warn_on_unsupported_attrs(pkg::AbstractBackend, plotattributes) if !isempty(_to_warn) && get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) - for k in sort!(collect(_to_warn)) + for k ∈ sort!(collect(_to_warn)) push!(already_warned, k) if k in keys(Commons._deprecated_attributes) @warn """ @@ -246,7 +246,7 @@ end function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW) get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return - for k in (:xscale, :yscale, :zscale, :scale) + for k ∈ (:xscale, :yscale, :zscale, :scale) haskey(plotattributes, k) || continue v = plotattributes[k] if !all(is_scale_supported.(Ref(pkg), v)) diff --git a/PlotsBase/src/examples.jl b/PlotsBase/src/examples.jl index f120e2360..86c02710e 100644 --- a/PlotsBase/src/examples.jl +++ b/PlotsBase/src/examples.jl @@ -39,7 +39,7 @@ const _examples = PlotExample[ quote p = plot([sin, cos], zeros(0), leg = false, xlims = (0, 2π), ylims = (-1, 1)) anim = Animation() - for x in range(0, stop = 2π, length = 20) + for x ∈ range(0, stop = 2π, length = 20) push!(p, x, Float64[sin(x), cos(x)]) frame(anim) end @@ -268,7 +268,7 @@ const _examples = PlotExample[ bot[i] + hgt[i], bot[i], closepct[i] * hgt[i] + bot[i], - ) for i in 1:n + ) for i ∈ 1:n ] ohlc(y) end, @@ -452,8 +452,8 @@ const _examples = PlotExample[ PlotExample( # 28 "Heatmap, categorical axes, and aspect_ratio", quote - xs = [string("x", i) for i in 1:10] - ys = [string("y", i) for i in 1:4] + xs = [string("x", i) for i ∈ 1:10] + ys = [string("y", i) for i ∈ 1:4] z = float((1:4) * reshape(1:10, 1, :)) heatmap(xs, ys, z, aspect_ratio = 1) end, @@ -503,7 +503,7 @@ const _examples = PlotExample[ ) anim = Animation() - for x in range(1, stop = 2π, length = 20) + for x ∈ range(1, stop = 2π, length = 20) plot(push!(p, x, Float64[sin(x), cos(x), atan(x), cos(x), log(x)])) frame(anim) end @@ -847,8 +847,8 @@ const _examples = PlotExample[ xs = collect(0.1:0.05:2.0) ys = collect(0.2:0.1:2.0) - X = [x for x in xs for _ in ys] - Y = [y for _ in xs for y in ys] + X = [x for x ∈ xs for _ ∈ ys] + Y = [y for _ ∈ xs for y ∈ ys] Z = (x, y) -> 1 / x + y * x^2 @@ -889,13 +889,13 @@ const _examples = PlotExample[ θs = range(0, π, length = 25) θqs = range(1, π - 1, length = 25) - x = vec([sin(θ) * cos(ϕ) for (ϕ, θ) in Iterators.product(ϕs, θs)]) - y = vec([sin(θ) * sin(ϕ) for (ϕ, θ) in Iterators.product(ϕs, θs)]) - z = vec([cos(θ) for (ϕ, θ) in Iterators.product(ϕs, θs)]) + x = vec([sin(θ) * cos(ϕ) for (ϕ, θ) ∈ Iterators.product(ϕs, θs)]) + y = vec([sin(θ) * sin(ϕ) for (ϕ, θ) ∈ Iterators.product(ϕs, θs)]) + z = vec([cos(θ) for (ϕ, θ) ∈ Iterators.product(ϕs, θs)]) - u = 0.1vec([sin(θ) * cos(ϕ) for (ϕ, θ) in Iterators.product(ϕs, θqs)]) - v = 0.1vec([sin(θ) * sin(ϕ) for (ϕ, θ) in Iterators.product(ϕs, θqs)]) - w = 0.1vec([cos(θ) for (ϕ, θ) in Iterators.product(ϕs, θqs)]) + u = 0.1vec([sin(θ) * cos(ϕ) for (ϕ, θ) ∈ Iterators.product(ϕs, θqs)]) + v = 0.1vec([sin(θ) * sin(ϕ) for (ϕ, θ) ∈ Iterators.product(ϕs, θqs)]) + w = 0.1vec([cos(θ) for (ϕ, θ) ∈ Iterators.product(ϕs, θqs)]) quiver(x, y, z, quiver = (u, v, w)) end, @@ -958,7 +958,7 @@ const _examples = PlotExample[ plots = [wireframe(args..., title = "wire"; kw...)] - for ax in (:x, :y, :z) + for ax ∈ (:x, :y, :z) push!( plots, wireframe( @@ -972,7 +972,7 @@ const _examples = PlotExample[ ) end - for ax in (:x, :y, :z) + for ax ∈ (:x, :y, :z) push!( plots, wireframe( @@ -1334,7 +1334,7 @@ replace_module(ex) = ex function replace_module(ex::Expr) if Meta.isexpr(ex, :import) || Meta.isexpr(ex, :using) expr = Expr(ex.head) - for arg in ex.args + for arg ∈ ex.args mod = last(arg.args) new_arg = if Meta.isexpr(arg, :.) mod ≡ :PlotsBase ? arg : Expr(:., :PlotsBase, mod) @@ -1401,7 +1401,7 @@ function test_examples( strict = false, ) plts = Dict() - for i in eachindex(_examples) + for i ∈ eachindex(_examples) i ∈ something(only, (i,)) || continue i ∈ skip && continue try diff --git a/PlotsBase/src/layouts.jl b/PlotsBase/src/layouts.jl index 27193f411..67586a2a3 100644 --- a/PlotsBase/src/layouts.jl +++ b/PlotsBase/src/layouts.jl @@ -48,7 +48,7 @@ compute_minpad(args...) = map(maximum, paddings(args...)) _update_inset_padding!(layout::GridLayout) = map(_update_inset_padding!, layout.grid) _update_inset_padding!(sp::Subplot) = - for isp in sp.plt.inset_subplots + for isp ∈ sp.plt.inset_subplots parent(isp) == sp || continue _update_min_padding!(isp) sp.minpad = max.(sp.minpad, isp.minpad) @@ -74,7 +74,7 @@ function recompute_lengths(v) # dump(v) tot = 0pct cnt = 0 - for vi in v + for vi ∈ v if vi == 0pct cnt += 1 else @@ -128,7 +128,7 @@ function update_child_bboxes!(layout::GridLayout, minimum_perimeter = [0mm, 0mm, layout.heights = recompute_lengths(layout.heights) # we have all the data we need... lets compute the plot areas and set the bounding boxes - for r in 1:nr, c in 1:nc + for r ∈ 1:nr, c ∈ 1:nc child = layout[r, c] # get the top-left corner of this child... the first one is top-left of the parent (i.e. layout) @@ -167,7 +167,7 @@ For each inset (floating) subplot, resolve the relative position to absolute canvas coordinates, relative to the parent's plotarea. """ update_inset_bboxes!(plt::Plot) = - for sp in plt.inset_subplots + for sp ∈ plt.inset_subplots p_area = Measures.resolve(plotarea(sp.parent), sp[:relative_bbox]) plotarea!(sp, p_area) # NOTE: `lens` example, `pgfplotsx` for non-regression @@ -255,7 +255,7 @@ function layout_attrs(m::AbstractVecOrMat) nr = first(sz) nc = get(sz, 2, 1) gl = GridLayout(nr, nc) - for ci in CartesianIndices(m) + for ci ∈ CartesianIndices(m) gl[ci] = layout_attrs(m[ci])[1] end layout_attrs(gl) @@ -281,7 +281,7 @@ function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) spmap = Plots.SubplotMap() empty = isempty(plts) i = 0 - for r in 1:nr, c in 1:nc + for r ∈ 1:nr, c ∈ 1:nc l = layout[r, c] if isa(l, EmptyLayout) && !get(l.attr, :blank, false) if empty @@ -333,16 +333,16 @@ end # merge subplot lists. function link_axes!(axes::Axis...) a1 = axes[1] - for i in 2:length(axes) + for i ∈ 2:length(axes) a2 = axes[i] expand_extrema!(a1, Axes.ignorenan_extrema(a2)) - for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) + for k ∈ (:extrema, :discrete_values, :continuous_values, :discrete_map) a2[k] = a1[k] end # make a2's subplot list refer to a1's and add any missing values sps2 = a2.sps - for sp in sps2 + for sp ∈ sps2 sp in a1.sps || push!(a1.sps, sp) end a2.sps = a1.sps @@ -352,7 +352,7 @@ end # figure out which subplots to link function link_subplots(a::AbstractArray{AbstractLayout}, axissym::Symbol) subplots = [] - for l in a + for l ∈ a if isa(l, Subplot) push!(subplots, l) elseif isa(l, GridLayout) && size(l) == (1, 1) @@ -365,7 +365,7 @@ end # for some vector or matrix of layouts, filter only the Subplots and link those axes function link_axes!(a::AbstractArray{AbstractLayout}, axissym::Symbol) subplots = link_subplots(a, axissym) - axes = [sp.attr[axissym] for sp in subplots] + axes = [sp.attr[axissym] for sp ∈ subplots] length(axes) > 0 && link_axes!(axes...) end @@ -375,15 +375,15 @@ function link_axes!(l::AbstractLayout, link::Symbol) end # process a GridLayout, recursively linking axes according to the link symbol function link_axes!(layout::GridLayout, link::Symbol) nr, nc = size(layout) - link in (:x, :both) && for c in 1:nc + link in (:x, :both) && for c ∈ 1:nc link_axes!(layout.grid[:, c], :xaxis) end - link in (:y, :both) && for r in 1:nr + link in (:y, :both) && for r ∈ 1:nr link_axes!(layout.grid[r, :], :yaxis) end link ≡ :square && if (sps = filter(l -> isa(l, Subplot), layout.grid)) |> !isempty base_axis = sps[1][:xaxis] - for sp in sps + for sp ∈ sps link_axes!(base_axis, sp[:xaxis]) link_axes!(base_axis, sp[:yaxis]) end @@ -400,7 +400,7 @@ end function twin(sp, letter) plt = sp.plt orig_sp = first(plt.subplots) - for letter in filter(!=(letter), axes_letters(orig_sp, letter)) + for letter ∈ filter(!=(letter), axes_letters(orig_sp, letter)) ax = orig_sp[get_attr_symbol(letter, :axis)] ax[:grid] = false # disable the grid (overlaps with twin axis) end diff --git a/PlotsBase/src/output.jl b/PlotsBase/src/output.jl index 2ea0f9412..68464aee3 100644 --- a/PlotsBase/src/output.jl +++ b/PlotsBase/src/output.jl @@ -108,7 +108,7 @@ const _savemap = Dict( "txt" => txt, ) -for out in Symbol.(unique(values(_savemap))) +for out ∈ Symbol.(unique(values(_savemap))) @eval @doc """ $($out)([plot,], filename) Save plot as $($out)-file. @@ -211,7 +211,7 @@ _display(plt::Plot) = @warn "_display is not defined for this backend." Base.show(io::IO, m::MIME"text/plain", plt::Plot) = show(io, plt) # for writing to io streams... first prepare, then callback -for mime in ( +for mime ∈ ( "text/html", "text/latex", "image/png", diff --git a/PlotsBase/src/pipeline.jl b/PlotsBase/src/pipeline.jl index 09ff51118..5db22273c 100644 --- a/PlotsBase/src/pipeline.jl +++ b/PlotsBase/src/pipeline.jl @@ -9,7 +9,7 @@ function RecipesPipeline.warn_on_recipe_aliases!( @nospecialize(args) ) pkeys = keys(plotattributes) - for k in pkeys + for k ∈ pkeys if (dk = get(Commons._keyAliases, k, nothing)) ≢ nothing kv = RecipesPipeline.pop_kw!(plotattributes, k) dk ∈ pkeys || (plotattributes[dk] = kv) @@ -63,7 +63,7 @@ function _preprocess_userrecipe(kw::AKW) if get(kw, :permute, default(:permute)) ≢ :none l1, l2 = kw[:permute] - for k in Commons._axis_attrs + for k ∈ Commons._axis_attrs k1 = Commons._attrsymbolcache[l1][k] k2 = Commons._attrsymbolcache[l2][k] kwk = keys(kw) @@ -96,7 +96,7 @@ function _add_errorbar_kw(kw_list::Vector{KW}, kw::AKW) st = get(kw, :seriestype, :none) errors = (:xerror, :yerror, :zerror) if st ∉ errors - for esym in errors + for esym ∈ errors if get(kw, esym, nothing) ≢ nothing # we make a copy of the KW and apply an errorbar recipe errkw = copy(kw) @@ -150,7 +150,7 @@ end function RecipesPipeline.process_sliced_series_attributes!(::Plot, kw_list) # determine global extrema xe = ye = ze = NaN, NaN - for kw in kw_list + for kw ∈ kw_list xe = ignorenan_min_max(get(kw, :x, nothing), xe) ye = ignorenan_min_max(get(kw, :y, nothing), ye) ze = ignorenan_min_max(get(kw, :z, nothing), ze) @@ -159,7 +159,7 @@ function RecipesPipeline.process_sliced_series_attributes!(::Plot, kw_list) # swap errors err_inds = findall(kw -> get(kw, :seriestype, :path) in (:xerror, :yerror, :zerror), kw_list) - for ind in err_inds + for ind ∈ err_inds if ind > 1 && get(kw_list[ind - 1], :seriestype, :path) ≡ :scatter tmp = copy(kw_list[ind]) kw_list[ind] = copy(kw_list[ind - 1]) @@ -167,7 +167,7 @@ function RecipesPipeline.process_sliced_series_attributes!(::Plot, kw_list) end end - for kw in kw_list + for kw ∈ kw_list kw[:x_extrema] = xe kw[:y_extrema] = ye kw[:z_extrema] = ze @@ -191,7 +191,7 @@ end # TODO: Should some of this logic be moved to RecipesPipeline ? function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # merge in anything meant for the Plot - for kw in kw_list, (k, v) in kw + for kw ∈ kw_list, (k, v) ∈ kw haskey(_plot_defaults, k) && (plotattributes[k] = pop!(kw, k)) end @@ -202,7 +202,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # create the layout and subplots from the inputs plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr) - for (idx, sp) in enumerate(plt.subplots) + for (idx, sp) ∈ enumerate(plt.subplots) sp.plt = plt sp.attr[:subplot_index] = idx end @@ -213,7 +213,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # handle inset subplots if (insets = plt[:inset_subplots]) ≢ nothing typeof(insets) <: AVec || (insets = [insets]) - for inset in insets + for inset ∈ insets parent, bb = is_2tuple(inset) ? inset : (nothing, inset) parent = if (P = typeof(parent)) <: Integer plt.subplots[parent] @@ -239,7 +239,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # Subplot object which they belong to. # TODO: allow matrices to still apply to all subplots sp_attrs = Dict{Subplot,Any}() - for kw in kw_list + for kw ∈ kw_list # get the Subplot object to which the series belongs. sps = get(kw, :subplot, :auto) sp = get_subplot( @@ -250,7 +250,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) # extract subplot/axis attributes from kw and add to sp_attr attr = KW() - for (k, v) in collect(kw) + for (k, v) ∈ collect(kw) if Commons.is_subplot_attrs(k) || Commons.is_axis_attrs(k) v = pop!(kw, k) if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps) @@ -263,12 +263,12 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps) v = v[series_idx(kw_list, kw)] end - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) attr[get_attr_symbol(letter, k)] = v end end end - for k in (:scale,), letter in (:x, :y, :z) + for k ∈ (:scale,), letter ∈ (:x, :y, :z) # Series recipes may need access to this information lk = get_attr_symbol(letter, k) haskey(attr, lk) && (kw[lk] = attr[lk]) @@ -278,7 +278,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW}) _add_plot_title!(plt) # override subplot/axis args. `sp_attrs` take precedence - for (idx, sp) in enumerate(plt.subplots) + for (idx, sp) ∈ enumerate(plt.subplots) attr = if !haskey(plotattributes, :subplot) || plotattributes[:subplot] == idx merge(plotattributes, get(sp_attrs, sp, KW())) else @@ -323,7 +323,7 @@ function _add_plot_title!(plt) # propagate arguments plt[:plot_titleXXX] --> subplot[:titleXXX] plot_titleindex = plt[:plot_titleindex] subplot = plt.subplots[plot_titleindex] - for sym in filter(x -> startswith(string(x), "plot_title"), keys(_plot_defaults)) + for sym ∈ filter(x -> startswith(string(x), "plot_title"), keys(_plot_defaults)) subplot[Symbol(string(sym)[(length("plot_") + 1):end])] = plt[sym] end end diff --git a/PlotsBase/src/plot.jl b/PlotsBase/src/plot.jl index dd696c1ff..0044a7b1a 100644 --- a/PlotsBase/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -35,22 +35,22 @@ function Base.show(io::IO, plt::Plot) ) && return print(io, "\nCaptured extra kwargs:\n") do_show = true - for (key, value) in plt[:extra_plot_kwargs] + for (key, value) ∈ plt[:extra_plot_kwargs] do_show && println(io, " Plot:") println(io, " "^4, key, ": ", value) do_show = false end do_show = true - for (i, ekwargs) in enumerate(sp_ekwargs) - for (key, value) in ekwargs + for (i, ekwargs) ∈ enumerate(sp_ekwargs) + for (key, value) ∈ ekwargs do_show && println(io, " SubplotPlot{$i}:") println(io, " "^4, key, ": ", value) do_show = false end do_show = true end - for (i, ekwargs) in enumerate(s_ekwargs) - for (key, value) in ekwargs + for (i, ekwargs) ∈ enumerate(s_ekwargs) + for (key, value) ∈ ekwargs do_show && println(io, " Series{$i}:") println(io, " "^4, key, ": ", value) do_show = false @@ -132,7 +132,7 @@ function plot!( # compute the layout layout = layout_attrs(plotattributes, n)[1] - num_sp = sum(length(p.subplots) for p in plts) + num_sp = sum(length(p.subplots) for p ∈ plts) # create a new plot object, with subplot list/map made of existing subplots. # note: we create a new backend figure for this new plot object @@ -143,7 +143,7 @@ function plot!( # TODO: replace this with proper processing from a merged user_attrs KW # update plot args - for p in plts + for p ∈ plts plt.attr = merge(p.attr, plt.attr) # plt.attr preempts p.attr (for `twinx`) plt.n += p.n end @@ -155,7 +155,7 @@ function plot!( plt.init = true series_attrs = KW() - for (k, v) in plotattributes + for (k, v) ∈ plotattributes Commons.is_series_attrs(k) && (series_attrs[k] = pop!(plotattributes, k)) end @@ -167,13 +167,13 @@ function plot!( # initialize the subplots cmdidx = 1 - for (idx, sp) in enumerate(plt.subplots) + for (idx, sp) ∈ enumerate(plt.subplots) _initialize_subplot(plt, sp) serieslist = series_list(sp) append!(plt.inset_subplots, sp.plt.inset_subplots) sp.plt = plt sp.attr[:subplot_index] = idx - for series in serieslist + for series ∈ serieslist merge!(series.plotattributes, series_attrs) _slice_series_attrs!(series.plotattributes, plt, sp, cmdidx) push!(plt.series_list, series) @@ -184,7 +184,7 @@ function plot!( ttl_idx = _add_plot_title!(plt) # first apply any args for the subplots - for (idx, sp) in enumerate(plt.subplots) + for (idx, sp) ∈ enumerate(plt.subplots) Plots._update_subplot_attrs( plt, sp, @@ -255,9 +255,9 @@ function prepare_output(plt::Plot) # specific to :plot_title see _add_plot_title! force_minpad = get(plt, :force_minpad, ()) - isempty(force_minpad) || for i in eachindex(plt.layout.grid) + isempty(force_minpad) || for i ∈ eachindex(plt.layout.grid) plt.layout.grid[i].minpad = Tuple( - i ≡ nothing ? j : i for (i, j) in zip(force_minpad, plt.layout.grid[i].minpad) + i ≡ nothing ? j : i for (i, j) ∈ zip(force_minpad, plt.layout.grid[i].minpad) ) end diff --git a/PlotsBase/src/plotattr.jl b/PlotsBase/src/plotattr.jl index 9ef9efa07..3933c9c07 100644 --- a/PlotsBase/src/plotattr.jl +++ b/PlotsBase/src/plotattr.jl @@ -70,7 +70,7 @@ end function plotattr(attribute::AbstractString) attribute = Symbol(attribute) attribute = get(Commons._keyAliases, attribute, attribute) - for (k, v) in _attribute_defaults + for (k, v) ∈ _attribute_defaults attribute ∈ keys(v) && return plotattr(k, attribute) end error("There is no attribute named $attribute") diff --git a/PlotsBase/src/plotly.jl b/PlotsBase/src/plotly.jl index e7c74d3a7..ffde970f9 100644 --- a/PlotsBase/src/plotly.jl +++ b/PlotsBase/src/plotly.jl @@ -379,7 +379,7 @@ function plotly_layout(plt::Plot) multiple_subplots = length(plt.subplots) > 1 - for sp in plt.subplots + for sp ∈ plt.subplots spidx = multiple_subplots ? sp[:subplot_index] : "" x_idx, y_idx = multiple_subplots ? plotly_link_indicies(plt, sp) : ("", "") @@ -462,7 +462,7 @@ function plotly_layout(plt::Plot) plotly_add_legend!(plotattributes_out, sp) # annotations - for ann in sp[:annotations] + for ann ∈ sp[:annotations] append!( plotattributes_out[:annotations], KW[plotly_annotation_dict( @@ -473,9 +473,9 @@ function plotly_layout(plt::Plot) ) end # series_annotations - for series in series_list(sp) + for series ∈ series_list(sp) anns = series[:series_annotations] - for (xi, yi, str, fnt) in EachAnn(anns, series[:x], series[:y]) + for (xi, yi, str, fnt) ∈ EachAnn(anns, series[:x], series[:y]) push!( plotattributes_out[:annotations], plotly_annotation_dict( @@ -620,7 +620,7 @@ plotly_colorscale(c::AbstractVector{<:Colorant}, α = nothing) = function plotly_colorscale(cg::PlotUtils.CategoricalColorGradient, α = nothing) n = length(cg) cinds = repeat(1:n, inner = 2) - vinds = vcat((i:(i + 1) for i in 1:n)...) + vinds = vcat((i:(i + 1) for i ∈ 1:n)...) map( i -> [ cg.values[vinds[i]], @@ -661,7 +661,7 @@ end # we split by NaNs and then construct/destruct the shapes to get the closed coords function plotly_close_shapes(x, y) xs, ys = nansplit(x), nansplit(y) - for i in eachindex(xs) + for i ∈ eachindex(xs) shape = Shape(xs[i], ys[i]) xs[i], ys[i] = coords(shape) end @@ -749,8 +749,7 @@ function plotly_series(plt::Plot, series::Series) end x, y, z = ( - plotly_data(series, letter, data) for - (letter, data) in zip((:x, :y, :z), (x, y, z)) + plotly_data(series, letter, data) for (letter, data) ∈ zip((:x, :y, :z), (x, y, z)) ) plotattributes_out[:name] = series[:label] @@ -860,10 +859,8 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:k] = k elseif typeof(series[:connections]) <: AbstractVector{NTuple{3,Int}} # 1-based indexing - i, j, k = broadcast( - i -> [inds[i] - 1 for inds in series[:connections]], - (1, 2, 3), - ) + i, j, k = + broadcast(i -> [inds[i] - 1 for inds ∈ series[:connections]], (1, 2, 3)) plotattributes_out[:i] = i plotattributes_out[:j] = j plotattributes_out[:k] = k @@ -953,10 +950,10 @@ function plotly_series_shapes(plt::Plot, series::Series, clims) x, y = ( plotly_data(series, letter, data) for - (letter, data) in zip((:x, :y), PlotsBase.shape_data(series, 100)) + (letter, data) ∈ zip((:x, :y), PlotsBase.shape_data(series, 100)) ) - for (k, segment) in enumerate(segments) + for (k, segment) ∈ enumerate(segments) i, rng = segment.attr_index, segment.range length(rng) < 2 && continue @@ -1016,7 +1013,7 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z needs_scatter_fix = !isscatter && hasmarker && !any(isnan, y) && length(segments) > 1 - for (k, segment) in enumerate(segments) + for (k, segment) ∈ enumerate(segments) i, rng = segment.attr_index, segment.range plotattributes_out = deepcopy(plotattributes_base) @@ -1222,7 +1219,7 @@ end # get a list of dictionaries, each representing the series params function plotly_series(plt::Plot) isempty(plt.series_list) && return KW[] - reduce(vcat, plotly_series(plt, series) for series in plt.series_list) + reduce(vcat, plotly_series(plt, series) for series ∈ plt.series_list) end # get json string for a list of dictionaries, each representing the series params diff --git a/PlotsBase/src/recipes.jl b/PlotsBase/src/recipes.jl index c8d196615..597977e0a 100644 --- a/PlotsBase/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -12,7 +12,7 @@ function seriestype_supported(pkg::AbstractBackend, st::Symbol) haskey(_series_recipe_deps, st) || return :no supported = true - for dep in _series_recipe_deps[st] + for dep ∈ _series_recipe_deps[st] if seriestype_supported(pkg, dep) ≡ :no supported = false break @@ -28,7 +28,7 @@ end # get a list of all seriestypes function all_seriestypes() sts = Set{Symbol}(keys(_series_recipe_deps)) - for bsym in _initialized_backends + for bsym ∈ _initialized_backends be = backend_instance(bsym) sts = union(sts, Set{Symbol}(supported_seriestypes(be))) end @@ -75,7 +75,7 @@ const POTENTIAL_VECTOR_ARGUMENTS = [ y := y[indices] # sort vector arguments - for arg in POTENTIAL_VECTOR_ARGUMENTS + for arg ∈ POTENTIAL_VECTOR_ARGUMENTS if typeof(plotattributes[arg]) <: AVec plotattributes[arg] = _cycle(plotattributes[arg], indices) end @@ -98,7 +98,7 @@ end @recipe function f(::Type{Val{:hline}}, x, y, z) # COV_EXCL_LINE n = length(y) newx = repeat(Float64[1, 2, NaN], n) - newy = vec(Float64[yi for i in 1:3, yi in y]) + newy = vec(Float64[yi for i ∈ 1:3, yi ∈ y]) x := newx y := newy seriestype := :straightline @@ -108,7 +108,7 @@ end @recipe function f(::Type{Val{:vline}}, x, y, z) # COV_EXCL_LINE n = length(y) - newx = vec(Float64[yi for i in 1:3, yi in y]) + newx = vec(Float64[yi for i ∈ 1:3, yi ∈ y]) x := newx y := repeat(Float64[1, 2, NaN], n) seriestype := :straightline @@ -196,7 +196,7 @@ function make_steps(x::AbstractArray, st, even) newx = zeros(2n - (even ? 0 : 1)) xstartindex = firstindex(x) newx[1] = x[xstartindex] - for i in 2:n + for i ∈ 2:n xindex = xstartindex - 1 + i idx = 2i - 1 if st ≡ :mid @@ -209,7 +209,7 @@ function make_steps(x::AbstractArray, st, even) even && (newx[end] = x[end]) return newx end -make_steps(t::Tuple, st, even) = Tuple(make_steps(ti, st, even) for ti in t) +make_steps(t::Tuple, st, even) = Tuple(make_steps(ti, st, even) for ti ∈ t) @nospecialize @@ -303,7 +303,7 @@ end end end newx, newy, newz = zeros(3n), zeros(3n), z ≢ nothing ? zeros(3n) : nothing - for (i, (xi, yi, zi)) in enumerate(zip(x, y, z ≢ nothing ? z : 1:n)) + for (i, (xi, yi, zi)) ∈ enumerate(zip(x, y, z ≢ nothing ? z : 1:n)) rng = (3i - 2):(3i) newx[rng] = [xi, xi, NaN] if z ≢ nothing @@ -356,7 +356,7 @@ end function bezier_value(pts::AVec, t::Real) val = 0.0 n = length(pts) - 1 - for (i, p) in enumerate(pts) + for (i, p) ∈ enumerate(pts) val += p * binomial(n, i - 1) * (1 - t)^(n - i + 1) * t^(i - 1) end val @@ -373,7 +373,7 @@ end # for each line segment (point series with no NaNs), convert it into a bezier curve # where the points are the control points of the curve - for rng in DataSeries.iter_segments(args...) + for rng ∈ DataSeries.iter_segments(args...) length(rng) < 2 && continue ts = range(0, stop = 1, length = npoints) nanappend!(newx, map(t -> bezier_value(_cycle(x, rng), t), ts)) @@ -451,7 +451,7 @@ end xseg, yseg = map(_ -> Segments(), 1:2) valid_i = isfinite.(procx) .& isfinite.(procy) - for i in 1:ny + for i ∈ 1:ny valid_i[i] || continue yi = procy[i] center = procx[i] @@ -475,7 +475,7 @@ end x := xseg.pts y := yseg.pts # expand attributes to match indices in new series data - for k in _segmenting_vector_attributes ∪ _segmenting_array_attributes + for k ∈ _segmenting_vector_attributes ∪ _segmenting_array_attributes if (v = get(plotattributes, k, nothing)) isa AVec if eachindex(v) != eachindex(y) @warn "Indices $(eachindex(v)) of attribute `$k` do not match data indices $(eachindex(y))." @@ -508,7 +508,7 @@ end m, n = size(z.surf) x_pts, y_pts = fill(NaN, 6m * n), fill(NaN, 6m * n) fz = zeros(m * n) - for i in 1:m, j in 1:n # i ≡ y, j ≡ x + for i ∈ 1:m, j ∈ 1:n # i ≡ y, j ≡ x k = (j - 1) * m + i inds = (6(k - 1) + 1):(6k - 1) x_pts[inds] .= [xe[j], xe[j + 1], xe[j + 1], xe[j], xe[j]] @@ -768,7 +768,7 @@ _hist_norm_mode(mode::Bool) = mode ? :pdf : :none _filternans(vs::NTuple{1,AbstractVector}) = filter!.(isfinite, vs) function _filternans(vs::NTuple{N,AbstractVector}) where {N} - _invertedindex(v, not) = [j for (i, j) in enumerate(v) if !(i ∈ not)] + _invertedindex(v, not) = [j for (i, j) ∈ enumerate(v) if !(i ∈ not)] nots = union(Set.(findall.(!isfinite, vs))...) _invertedindex.(vs, Ref(nots)) end @@ -869,7 +869,7 @@ end end @recipe f(hv::AbstractVector{H}) where {H<:StatsBase.Histogram} = # COV_EXCL_LINE - for h in hv + for h ∈ hv @series begin h end @@ -886,7 +886,7 @@ end if float_weights ≡ weights float_weights = deepcopy(float_weights) end - for (i, c) in enumerate(float_weights) + for (i, c) ∈ enumerate(float_weights) c == 0 && (float_weights[i] = NaN) end end @@ -927,7 +927,7 @@ end s = sum(y) θ = 0 colors = plotattributes[:seriescolor] - for i in eachindex(y) + for i ∈ eachindex(y) θ_new = θ + 2π * y[i] / s coords = [(0.0, 0.0); partialcircle(θ, θ_new, 50)] @series begin @@ -1031,7 +1031,7 @@ export lens! () end # add subplot - for series in sp.series_list + for series ∈ sp.series_list @series begin plotattributes = merge(backup, copy(series.plotattributes)) subplot := lens_index @@ -1114,8 +1114,8 @@ error_tuple(x::Tuple) = x function error_coords(errorbar, errordata, otherdata...) ed = Vector{float_extended_type(errordata)}(undef, 0) od = map(odi -> Vector{float_extended_type(odi)}(undef, 0), otherdata) - for (i, edi) in enumerate(errordata) - for (j, odj) in enumerate(otherdata) + for (i, edi) ∈ enumerate(errordata) + for (j, odj) ∈ enumerate(otherdata) odi = _cycle(odj, i) nanappend!(od[j], [odi, odi]) end @@ -1202,7 +1202,7 @@ function quiver_using_arrows(plotattributes::AKW) # for each point, we create an arrow of velocity vi, translated to the x/y coordinates x, y = zeros(0), zeros(0) is_3d && (z = zeros(0)) - for i in 1:max(length(xorig), length(yorig), is_3d ? 0 : length(zorig)) + for i ∈ 1:max(length(xorig), length(yorig), is_3d ? 0 : length(zorig)) # get the starting position xi = _cycle(xorig, i) yi = _cycle(yorig, i) @@ -1250,7 +1250,7 @@ function quiver_using_hack(plotattributes::AKW) # for each point, we create an arrow of velocity vi, translated to the x/y coordinates pts = P2[] - for i in 1:max(length(xorig), length(yorig)) + for i ∈ 1:max(length(xorig), length(yorig)) # get the starting position xi = _cycle(xorig, i) @@ -1303,7 +1303,7 @@ end # images - grays function clamp_greys!(mat::AMat{<:Gray}) - for i in eachindex(mat) + for i ∈ eachindex(mat) mat[i].val < 0 && (mat[i] = Gray(0)) mat[i].val > 1 && (mat[i] = Gray(1)) end @@ -1357,7 +1357,7 @@ end seriestype --> :shape # For backwards compatibility, column vectors of segmenting attributes are # interpreted as having one element per shape - for attr in union(_segmenting_array_attributes, _segmenting_vector_attributes) + for attr ∈ union(_segmenting_array_attributes, _segmenting_vector_attributes) v = get(plotattributes, attr, nothing) if v isa AVec || v isa AMat && size(v, 2) == 1 @warn """ @@ -1372,7 +1372,7 @@ end @recipe function f(shapes::AMat{<:Shape}) # COV_EXCL_LINE seriestype --> :shape - for j in axes(shapes, 2) + for j ∈ axes(shapes, 2) @series coords(vec(shapes[:, j])) end end @@ -1439,7 +1439,7 @@ end function get_xy(v::AVec{OHLC}, x = eachindex(v)) xdiff = 0.3ignorenan_mean(abs.(diff(x))) x_out, y_out = zeros(0), zeros(0) - for (i, ohlc) in enumerate(v) + for (i, ohlc) ∈ enumerate(v) ox, oy = get_xy(ohlc, x[i], xdiff) nanappend!(x_out, ox) nanappend!(y_out, oy) @@ -1577,7 +1577,7 @@ end seriestype := :shape # create a filled polygon for each item - for c in axes(weights, 2) + for c ∈ axes(weights, 2) sx = vcat(weights[:, c], c == 1 ? zeros(n) : reverse(weights[:, c - 1])) sy = vcat(returns, reverse(returns)) @series (sx, sy) @@ -1589,7 +1589,7 @@ end @recipe function f(a::AreaPlot; seriestype = :line) # COV_EXCL_LINE data = cumsum(a.args[end], dims = 2) x = length(a.args) == 1 ? (axes(data, 1)) : a.args[1] - for i in axes(data, 2) + for i ∈ axes(data, 2) @series begin fillrange := i > 1 ? data[:, i - 1] : 0 x, data[:, i] diff --git a/PlotsBase/src/shorthands.jl b/PlotsBase/src/shorthands.jl index 7e9022e9a..89b43f69d 100644 --- a/PlotsBase/src/shorthands.jl +++ b/PlotsBase/src/shorthands.jl @@ -455,7 +455,7 @@ plot3d!(args...; kw...) = plot!(args...; kw..., seriestype = :path3d) title!(plt::PlotOrSubplot, s::AbstractString; kw...) = plot!(plt; title = s, kw...) title!(s::AbstractString; kw...) = plot!(; title = s, kw...) -for letter in ("x", "y", "z") +for letter ∈ ("x", "y", "z") @eval begin """Add $($(letter))label to an existing plot""" $(Symbol(letter, :label!))(s::AbstractString; kw...) = diff --git a/PlotsBase/src/themes.jl b/PlotsBase/src/themes.jl index e2feb711e..7aeabc7a3 100644 --- a/PlotsBase/src/themes.jl +++ b/PlotsBase/src/themes.jl @@ -57,7 +57,7 @@ _get_showtheme_attrs(thm::Symbol, func::Symbol) = thm, get(_color_functions, fun cp = cfunc.(RGB.(cp)) # apply the theme - for k in keys(defaults) + for k ∈ keys(defaults) k in (:colorgradient, :palette) && continue def = defaults[k] arg = get(Commons._keyAliases, k, k) @@ -78,7 +78,7 @@ _get_showtheme_attrs(thm::Symbol, func::Symbol) = thm, get(_color_functions, fun colorbar := false layout := (2, 3) - for j in 1:4 + for j ∈ 1:4 @series begin subplot := 1 color_palette := cp diff --git a/PlotsBase/src/utils.jl b/PlotsBase/src/utils.jl index 7c2958a1a..fb266145e 100644 --- a/PlotsBase/src/utils.jl +++ b/PlotsBase/src/utils.jl @@ -72,7 +72,7 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) ) # update alphas - for asym in (:linealpha, :markeralpha, :fillalpha) + for asym ∈ (:linealpha, :markeralpha, :fillalpha) if plotattributes[asym] ≡ nothing plotattributes[asym] = plotattributes[:seriesalpha] end @@ -87,7 +87,7 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) plotattributes[:seriescolor] = scolor = get_series_color(scolor, sp, plotIndex, stype) # update other colors (`linecolor`, `markercolor`, `fillcolor`) <- for grep - for s in (:line, :marker, :fill) + for s ∈ (:line, :marker, :fill) csym, asym = Symbol(s, :color), Symbol(s, :alpha) plotattributes[csym] = if plotattributes[csym] ≡ :auto plot_color(if Commons.has_black_border_for_default(stype) && s ≡ :line @@ -179,7 +179,7 @@ function _slice_series_attrs!( sp::Subplot, commandIndex::Int, ) - for k in keys(_series_defaults) + for k ∈ keys(_series_defaults) haskey(plotattributes, k) && slice_arg!(plotattributes, plotattributes, k, commandIndex, false) end @@ -258,7 +258,7 @@ fakedata(sz::Int...) = fakedata(Random.seed!(PLOTS_SEED), sz...) function fakedata(rng::AbstractRNG, sz...) y = zeros(sz...) - for r in 2:size(y, 1) + for r ∈ 2:size(y, 1) y[r, :] = 0.95vec(y[r - 1, :]) + randn(rng, size(y, 2)) end y @@ -307,7 +307,7 @@ end # compute one side of a fill range from a ribbon function make_fillrange_side(y::AVec, rib) frs = zeros(axes(y)) - for (i, yi) in pairs(y) + for (i, yi) ∈ pairs(y) frs[i] = yi + _cycle(rib, i) end frs @@ -366,17 +366,17 @@ function Commons.preprocess_attributes!(plotattributes::AKW) # handle axis args common to all axis args = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :axis, ())) showarg = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :showaxis, ())) - for arg in wraptuple((args..., showarg...)) - for letter in (:x, :y, :z) + for arg ∈ wraptuple((args..., showarg...)) + for letter ∈ (:x, :y, :z) process_axis_arg!(plotattributes, arg, letter) end end # handle axis args - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) asym = get_attr_symbol(letter, :axis) args = RecipesPipeline.pop_kw!(plotattributes, asym, ()) if !(typeof(args) <: Axis) - for arg in wraptuple(args) + for arg ∈ wraptuple(args) process_axis_arg!(plotattributes, arg, letter) end end @@ -396,39 +396,39 @@ function Commons.preprocess_attributes!(plotattributes::AKW) # handle grid args common to all axes processGridArg! = Commons.process_grid_attr! args = RecipesPipeline.pop_kw!(plotattributes, :grid, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) + for arg ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) processGridArg!(plotattributes, arg, letter) end end # handle individual axes grid args - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) gridsym = get_attr_symbol(letter, :grid) args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) - for arg in wraptuple(args) + for arg ∈ wraptuple(args) processGridArg!(plotattributes, arg, letter) end end # handle minor grid args common to all axes args = RecipesPipeline.pop_kw!(plotattributes, :minorgrid, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) + for arg ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) Commons.process_minor_grid_attr!(plotattributes, arg, letter) end end # handle individual axes grid args - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) gridsym = get_attr_symbol(letter, :minorgrid) args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) - for arg in wraptuple(args) + for arg ∈ wraptuple(args) Commons.process_minor_grid_attr!(plotattributes, arg, letter) end end # handle font args common to all axes - for fontname in (:tickfont, :guidefont) + for fontname ∈ (:tickfont, :guidefont) args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) - for arg in wraptuple(args) - for letter in (:x, :y, :z) + for arg ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) Commons.process_font_attr!( plotattributes, get_attr_symbol(letter, fontname), @@ -438,14 +438,14 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end end # handle individual axes font args - for letter in (:x, :y, :z) - for fontname in (:tickfont, :guidefont) + for letter ∈ (:x, :y, :z) + for fontname ∈ (:tickfont, :guidefont) args = RecipesPipeline.pop_kw!( plotattributes, get_attr_symbol(letter, fontname), (), ) - for arg in wraptuple(args) + for arg ∈ wraptuple(args) Commons.process_font_attr!( plotattributes, get_attr_symbol(letter, fontname), @@ -455,10 +455,10 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end end # handle axes args - for k in Commons._axis_attrs + for k ∈ Commons._axis_attrs if haskey(plotattributes, k) && k ≢ :link v = plotattributes[k] - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) lk = get_attr_symbol(letter, k) if !is_explicit(plotattributes, lk) plotattributes[lk] = v @@ -468,16 +468,16 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end # fonts - for fontname in + for fontname ∈ (:titlefont, :legend_title_font, :plot_titlefont, :colorbar_titlefont, :legend_font) args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) - for arg in wraptuple(args) + for arg ∈ wraptuple(args) Commons.process_font_attr!(plotattributes, fontname, arg) end end # handle line args - for arg in wraptuple(RecipesPipeline.pop_kw!(plotattributes, :line, ())) + for arg ∈ wraptuple(RecipesPipeline.pop_kw!(plotattributes, :line, ())) Commons.process_line_attr(plotattributes, arg) end @@ -488,7 +488,7 @@ function Commons.preprocess_attributes!(plotattributes::AKW) # handle marker args... default to ellipse if shape not set anymarker = false - for arg in wraptuple(get(plotattributes, :marker, ())) + for arg ∈ wraptuple(get(plotattributes, :marker, ())) Commons.process_marker_attr(plotattributes, arg) anymarker = true end @@ -506,7 +506,7 @@ function Commons.preprocess_attributes!(plotattributes::AKW) end # handle fill - for arg in wraptuple(get(plotattributes, :fill, ())) + for arg ∈ wraptuple(get(plotattributes, :fill, ())) Commons.process_fill_attr(plotattributes, arg) end RecipesPipeline.reset_kw!(plotattributes, :fill) @@ -585,14 +585,14 @@ function with(f::Function, args...; scalefonts = nothing, kw...) # dict to store old and new keyword args for anything that changes old_defs = KW() - for k in keys(new_defs) + for k ∈ keys(new_defs) old_defs[k] = default(k) end # save the backend old_backend = backend_name() - for arg in args + for arg ∈ args # change backend ? arg isa Symbol && if arg ∈ backends() if (pkg = backend_package_name(arg)) ≢ nothing # :plotly @@ -655,7 +655,7 @@ const _convert_sci_unicode_dict = Dict( # to a tex-like format (supported by gr, pyplot, and pgfplots). function convert_sci_unicode(label::AbstractString) - for key in keys(_convert_sci_unicode_dict) + for key ∈ keys(_convert_sci_unicode_dict) label = replace(label, key => _convert_sci_unicode_dict[key]) end occursin("×10^{", label) && (label = string(label, "}")) @@ -678,8 +678,7 @@ function ___straightline_data(xl, yl, x, y, exp_fact) a = (y[1] - y[2]) / (x[1] - x[2]) # get the data values xdata = [ - clamp(x[1] + (x[1] - x[2]) * (ylim - y[1]) / (y[1] - y[2]), xl...) for - ylim in yl + clamp(x[1] + (x[1] - x[2]) * (ylim - y[1]) / (y[1] - y[2]), xl...) for ylim ∈ yl ] xdata, a .* xdata .+ b @@ -699,7 +698,7 @@ __straightline_data(xl, yl, x, y, exp_fact) = k, r = divrem(n, 3) @assert r == 0 "Misformed data. `straightline_data` either accepts vectors of length 2 or 3k. The provided series has length $n" xdata, ydata = fill(NaN, n), fill(NaN, n) - for i in 1:k + for i ∈ 1:k inds = (3i - 2):(3i - 1) xdata[inds], ydata[inds] = ___straightline_data(xl, yl, x[inds], y[inds], exp_fact) @@ -749,7 +748,7 @@ function straightline_data(series, expansion_factor = 1) end function _shape_data!(::Val{false}, xf::Function, xinvf::Function, x, xl, exp_fact) - @inbounds for i in eachindex(x) + @inbounds for i ∈ eachindex(x) if x[i] == -Inf x[i] = xinvf(xf(xl[1]) - exp_fact * (xf(xl[2]) - xf(xl[1]))) elseif x[i] == +Inf @@ -760,7 +759,7 @@ function _shape_data!(::Val{false}, xf::Function, xinvf::Function, x, xl, exp_fa end function _shape_data!(::Val{true}, ::Function, ::Function, x, xl, exp_fact) - @inbounds for i in eachindex(x) + @inbounds for i ∈ eachindex(x) if x[i] == -Inf x[i] = xl[1] - exp_fact * (xl[2] - xl[1]) elseif x[i] == +Inf @@ -808,7 +807,7 @@ function mesh3d_triangles(x, y, z, cns::Tuple{Array,Array,Array}) X = zeros(eltype(x), 4length(ci)) Y = zeros(eltype(y), 4length(cj)) Z = zeros(eltype(z), 4length(ck)) - @inbounds for I in eachindex(ci) # connections are 0-based + @inbounds for I ∈ eachindex(ci) # connections are 0-based _add_triangle!(I, ci[I] + 1, cj[I] + 1, ck[I] + 1, x, y, z, X, Y, Z) end X, Y, Z @@ -818,7 +817,7 @@ function mesh3d_triangles(x, y, z, cns::AbstractVector{NTuple{3,Int}}) X = zeros(eltype(x), 4length(cns)) Y = zeros(eltype(y), 4length(cns)) Z = zeros(eltype(z), 4length(cns)) - @inbounds for I in eachindex(cns) # connections are 1-based + @inbounds for I ∈ eachindex(cns) # connections are 1-based _add_triangle!(I, cns[I]..., x, y, z, X, Y, Z) end X, Y, Z @@ -892,13 +891,13 @@ function _guess_best_legend_position(xl, yl, plt, weight = 100) ((0.00, 0.25), (0.75, 1.00)), # topleft ((0.75, 1.00), (0.75, 1.00)), # topright ) - for series in plt.series_list + for series ∈ plt.series_list x = series[:x] y = series[:y] yoffset = firstindex(y) - firstindex(x) - for (i, lim) in enumerate(Iterators.product(xl, yl)) + for (i, lim) ∈ enumerate(Iterators.product(xl, yl)) lim = lim ./ scale - for ix in eachindex(x) + for ix ∈ eachindex(x) xi, yi = x[ix], _cycle(y, ix + yoffset) # ignore y points outside quadrant visible quadrant xi < xl[1] + quadrants[i][1][1] * (xl[2] - xl[1]) && continue diff --git a/PlotsBase/test/runtests.jl b/PlotsBase/test/runtests.jl index a6f90621d..85b987e21 100644 --- a/PlotsBase/test/runtests.jl +++ b/PlotsBase/test/runtests.jl @@ -6,7 +6,7 @@ const TEST_PACKAGES = ) Symbol.(strip.(split(val, ","))) end -const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p in TEST_PACKAGES) +const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p ∈ TEST_PACKAGES) get!(ENV, "MPLBACKEND", "agg") @@ -17,7 +17,7 @@ import GR gr() # initialize all backends -for pkg in TEST_PACKAGES +for pkg ∈ TEST_PACKAGES @eval begin import $pkg # trigger extension $(TEST_BACKENDS[pkg])() @@ -64,7 +64,7 @@ else [] end -for name in ( +for name ∈ ( "misc", "utils", "args", diff --git a/PlotsBase/test/test_animations.jl b/PlotsBase/test/test_animations.jl index 068e2e4ce..ca22cf6db 100644 --- a/PlotsBase/test/test_animations.jl +++ b/PlotsBase/test/test_animations.jl @@ -10,7 +10,7 @@ end end @testset "Empty anim" begin - anim = @animate for i in [] + anim = @animate for i ∈ [] end @test_throws ArgumentError gif(anim, show_msg = false) end @@ -33,7 +33,7 @@ end x = sin.(t) y = cos.(t) - anim = @animate for i in 1:n + anim = @animate for i ∈ 1:n circleplot(x, y, i) end @test filesize(gif(anim, show_msg = false).filename) > 10_000 @@ -42,22 +42,22 @@ end @test filesize(webm(anim, show_msg = false).filename) > 10_000 @test filesize(PlotsBase.apng(anim, show_msg = false).filename) > 10_000 - @gif for i in 1:n + @gif for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end every 5 - @gif for i in 1:n + @gif for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end when i % 5 == 0 - @gif for i in 1:n + @gif for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end when i % 5 == 0 fps = 10 @test_throws LoadError macroexpand( @__MODULE__, quote - @gif for i in 1:n + @gif for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end when i % 5 == 0 every 10 # cannot use every and when together end, @@ -66,13 +66,13 @@ end @test_nowarn macroexpand( @__MODULE__, quote - @gif for i in 1:n + @gif for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end asdf = bla #asdf is allowed end, ) - anim = PlotsBase.@apng for i in 1:n + anim = PlotsBase.@apng for i ∈ 1:n circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end every 5 @test showable(MIME("image/png"), anim) @@ -81,7 +81,7 @@ end @testset "html" begin pl = plot([sin, cos], zeros(0), leg = false, xlims = (0, 2π), ylims = (-1, 1)) anim = Animation() - for x in range(0, stop = 2π, length = 10) + for x ∈ range(0, stop = 2π, length = 10) push!(pl, x, Float64[sin(x), cos(x)]) frame(anim) end diff --git a/PlotsBase/test/test_args.jl b/PlotsBase/test/test_args.jl index a1c4207ee..99c52997e 100644 --- a/PlotsBase/test/test_args.jl +++ b/PlotsBase/test/test_args.jl @@ -48,7 +48,7 @@ end @testset "Axis Attributes" begin pl = @test_nowarn plot(; tickfont = font(10, "Times")) - for axis in (:xaxis, :yaxis, :zaxis) + for axis ∈ (:xaxis, :yaxis, :zaxis) @test pl[1][axis][:tickfontsize] == 10 @test pl[1][axis][:tickfontfamily] == "Times" end @@ -84,7 +84,7 @@ end @testset "aspect_ratio" begin fn = tempname() - for aspect_ratio in (1, 1.0, 1 // 10, :auto, :none, true) + for aspect_ratio ∈ (1, 1.0, 1 // 10, :auto, :none, true) @test_nowarn png(plot(1:2; aspect_ratio), fn) end @test_throws ArgumentError png(plot(1:2; aspect_ratio = :invalid_ar), fn) diff --git a/PlotsBase/test/test_axes.jl b/PlotsBase/test/test_axes.jl index 9a9bba0f2..77df32304 100644 --- a/PlotsBase/test/test_axes.jl +++ b/PlotsBase/test/test_axes.jl @@ -12,7 +12,7 @@ @test PlotsBase.Axes.ignorenan_extrema(axis) == (0.5, 7.5) # github.com/JuliaPlots/Plots.jl/issues/4375 - for lab in ("foo", :foo) + for lab ∈ ("foo", :foo) pl = plot(1:2, xlabel = lab, ylabel = lab, title = lab) show(devnull, pl) end @@ -53,7 +53,7 @@ end @testset "Showaxis" begin - for value in PlotsBase.Commons._all_showaxis_attrs + for value ∈ PlotsBase.Commons._all_showaxis_attrs @test plot(1:5, showaxis = value)[1][:yaxis][:showaxis] isa Bool end @test plot(1:5, showaxis = :y)[1][:yaxis][:showaxis] @@ -97,7 +97,7 @@ end pl = plot([1.05, 2.0, 2.95], ylims = :round) @test PlotsBase.ylims(pl) == (1, 3) - for x in (1:3, -10:10), xlims in ((1, 5), [1, 5]) + for x ∈ (1:3, -10:10), xlims ∈ ((1, 5), [1, 5]) pl = plot(x; xlims) @test PlotsBase.xlims(pl) == (1, 5) pl = plot(x; xlims, widen = true) @@ -107,7 +107,7 @@ end pl = plot(1:5, lims = :symmetric, widen = false) @test PlotsBase.xlims(pl) == PlotsBase.ylims(pl) == (-5, 5) - for xlims in (0, 0.0, false, true, plot()) + for xlims ∈ (0, 0.0, false, true, plot()) pl = plot(1:5; xlims) plims = @test_logs (:warn, r"Invalid limits for x axis") match_mode = :any PlotsBase.xlims( @@ -117,20 +117,20 @@ end end @testset "#4379" begin - for ylims in ((-5, :auto), [-5, :auto]) + for ylims ∈ ((-5, :auto), [-5, :auto]) pl = plot([-2, 3], ylims = ylims, widen = false) @test PlotsBase.ylims(pl) == (-5.0, 3.0) end - for ylims in ((:auto, 4), [:auto, 4]) + for ylims ∈ ((:auto, 4), [:auto, 4]) pl = plot([-2, 3], ylims = ylims, widen = false) @test PlotsBase.ylims(pl) == (-2.0, 4.0) end - for xlims in ((-3, :auto), [-3, :auto]) + for xlims ∈ ((-3, :auto), [-3, :auto]) pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false) @test PlotsBase.xlims(pl) == (-3.0, 3.0) end - for xlims in ((:auto, 4), [:auto, 4]) + for xlims ∈ ((:auto, 4), [:auto, 4]) pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false) @test PlotsBase.xlims(pl) == (-2.0, 4.0) end @@ -244,7 +244,7 @@ end @testset "minor ticks" begin # FIXME in 2.0: this is awful to read, because `minorticks` represent the number of `intervals` - for minor_intervals in (:auto, :none, nothing, false, true, 0, 1, 2, 3, 4, 5) + for minor_intervals ∈ (:auto, :none, nothing, false, true, 0, 1, 2, 3, 4, 5) n_minor_ticks_per_major = if minor_intervals isa Bool minor_intervals ? PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 elseif minor_intervals ≡ :auto @@ -256,7 +256,7 @@ end end pl = plot(1:4; minorgrid = true, minorticks = minor_intervals) sp = first(pl) - for axis in (:xaxis, :yaxis) + for axis ∈ (:xaxis, :yaxis) ticks = PlotsBase.get_ticks(sp, sp[axis], update = false) n_expected_minor_ticks = (length(first(ticks)) - 1) * n_minor_ticks_per_major minor_ticks = PlotsBase.get_minor_ticks(sp, sp[axis], ticks) diff --git a/PlotsBase/test/test_backends.jl b/PlotsBase/test/test_backends.jl index fc5a4f157..6c83ac87b 100644 --- a/PlotsBase/test/test_backends.jl +++ b/PlotsBase/test/test_backends.jl @@ -65,7 +65,7 @@ is_pkgeval() || @testset "Backends" begin ) @test filesize(fn) > 1_000 end - Sys.islinux() && for be in TEST_BACKENDS + Sys.islinux() && for be ∈ TEST_BACKENDS skip = vcat(PlotsBase._backend_skips[be], blacklist) PlotsBase.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage closeall() diff --git a/PlotsBase/test/test_components.jl b/PlotsBase/test/test_components.jl index 62a84c6ac..5999a27ab 100644 --- a/PlotsBase/test/test_components.jl +++ b/PlotsBase/test/test_components.jl @@ -63,7 +63,7 @@ @testset "Plot" begin ang = range(0, 2π, length = 60) ellipse(x, y, w, h) = Shape(w * sin.(ang) .+ x, h * cos.(ang) .+ y) - myshapes = [ellipse(x, rand(), rand(), rand()) for x in 1:4] + myshapes = [ellipse(x, rand(), rand(), rand()) for x ∈ 1:4] @test coords(myshapes) isa Tuple{Vector{Vector{S}},Vector{Vector{T}}} where {T,S} local pl @test_nowarn pl = plot(myshapes) @@ -123,7 +123,7 @@ end @test text("bar", f).str == "bar" @test text(true).str == "true" - for rotation in -180:5:180 + for rotation ∈ -180:5:180 t = text("foo"; rotation) if abs(rotation) ≤ 45 || abs(rotation) ≥ 135 @test PlotsBase.is_horizontal(t) @@ -169,19 +169,19 @@ end annotate!(sp = 2, (0.03, 0.95), text("Cats&Dogs", :left)) end - for scale in PlotsBase._log_scales + for scale ∈ PlotsBase._log_scales pl = plot(xlim = (1, 10), xscale = scale) annotate!(pl, (0.5, 0.5), "hello") end let pl = plot(1:2) - for loc in + for loc ∈ (:topleft, :topcenter, :topright, :bottomleft, :bottomcenter, :bottomright) annotate!(pl, loc, string(loc)) end end let pl = plot(1:2) - for loc in (:N, :NE, :E, :SE, :S, :SW, :W, :NW, :N) + for loc ∈ (:N, :NE, :E, :SE, :S, :SW, :W, :NW, :N) annotate!(pl, loc, string(loc)) end end @@ -201,20 +201,20 @@ end :zguidefontsize, ] # get initial font sizes - initialSizes = [PlotsBase.default(s) for s in sizesToCheck] + initialSizes = [PlotsBase.default(s) for s ∈ sizesToCheck] #scale up font sizes scalefontsizes(2) # get initial font sizes - doubledSizes = [PlotsBase.default(s) for s in sizesToCheck] + doubledSizes = [PlotsBase.default(s) for s ∈ sizesToCheck] @test doubledSizes == initialSizes * 2 # reset font sizes resetfontsizes() - finalSizes = [PlotsBase.default(s) for s in sizesToCheck] + finalSizes = [PlotsBase.default(s) for s ∈ sizesToCheck] @test finalSizes == initialSizes end @@ -251,14 +251,14 @@ end xlims = (0, 5), series_annotations = permutedims([["1/1"], ["1/2"], ["1/3"], ["1/4"], ["1/5"]]), ) - for i in 1:5 + for i ∈ 1:5 @test only(spl.series_list[i].plotattributes[:series_annotations].strs).str == "1/$i" end series_anns(pl, series) = pl.series_list[series].plotattributes[:series_annotations] - ann_strings(ann) = [s.str for s in ann.strs] - ann_pointsizes(ann) = [s.font.pointsize for s in ann.strs] + ann_strings(ann) = [s.str for s ∈ ann.strs] + ann_pointsizes(ann) = [s.font.pointsize for s ∈ ann.strs] let pl = plot(ones(3, 2), series_annotations = ["a" "d"; "b" "e"; "c" "f"]) ann1 = series_anns(pl, 1) diff --git a/PlotsBase/test/test_contours.jl b/PlotsBase/test/test_contours.jl index 04c656c00..65a6e0b65 100644 --- a/PlotsBase/test/test_contours.jl +++ b/PlotsBase/test/test_contours.jl @@ -43,7 +43,7 @@ end end @testset "Number" begin - @testset "$n contours" for n in (2, 5, 100) + @testset "$n contours" for n ∈ (2, 5, 100) p = contour(x, y, z, levels = n) attr = p[1][1].plotattributes @test attr[:seriestype] ≡ :contour diff --git a/PlotsBase/test/test_dates.jl b/PlotsBase/test/test_dates.jl index 031c3931a..cfba7351b 100644 --- a/PlotsBase/test/test_dates.jl +++ b/PlotsBase/test/test_dates.jl @@ -1,6 +1,6 @@ @testset "Limits" begin - y = [1.0 * i * i for i in 1:10] - x = [Date(2019, 11, i) for i in 1:10] + y = [1.0 * i * i for i ∈ 1:10] + x = [Date(2019, 11, i) for i ∈ 1:10] rx = [x[3], x[5]] @@ -14,8 +14,8 @@ end @testset "Date xlims" begin - y = [1.0 * i * i for i in 1:10] - x = [Date(2019, 11, i) for i in 1:10] + y = [1.0 * i * i for i ∈ 1:10] + x = [Date(2019, 11, i) for i ∈ 1:10] span = (Date(2019, 10, 31), Date(2019, 11, 11)) ref_xlims = map(date -> date.instant.periods.value, span) @@ -24,8 +24,8 @@ end end @testset "DateTime xlims" begin - y = [1.0 * i * i for i in 1:10] - x = [DateTime(2019, 11, i, 11) for i in 1:10] + y = [1.0 * i * i for i ∈ 1:10] + x = [DateTime(2019, 11, i, 11) for i ∈ 1:10] span = (DateTime(2019, 10, 31, 11, 59, 59), DateTime(2019, 11, 11, 12, 01, 15)) ref_xlims = map(date -> date.instant.periods.value, span) diff --git a/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl index d39ec7d18..67d87a30f 100644 --- a/PlotsBase/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -121,7 +121,7 @@ end @test PlotsBase.Commons.parent_bbox(rl) == PlotsBase.DEFAULT_BBOX[] @test PlotsBase.bbox(rl) == PlotsBase.DEFAULT_BBOX[] @test PlotsBase.origin(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm) - for h_anchor in (:left, :right, :hcenter), v_anchor in (:top, :bottom, :vcenter) + for h_anchor ∈ (:left, :right, :hcenter), v_anchor ∈ (:top, :bottom, :vcenter) @test PlotsBase.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa PlotsBase.BoundingBox end diff --git a/PlotsBase/test/test_misc.jl b/PlotsBase/test/test_misc.jl index 864082dad..06f594cef 100644 --- a/PlotsBase/test/test_misc.jl +++ b/PlotsBase/test/test_misc.jl @@ -26,7 +26,7 @@ end dsp = TextDisplay(IOContext(IOBuffer(), :color => true)) @testset "plot" begin - for pl in [ + for pl ∈ [ histogram([1, 0, 0, 0, 0, 0]), plot([missing]), plot([missing, missing]), @@ -130,11 +130,11 @@ end value.(m) end - @testset "$f" for f in (hline, hspan) + @testset "$f" for f ∈ (hline, hspan) @test f(data).subplots[1].attr[:title] == "y" end - @testset "$f" for f in (vline, vspan) + @testset "$f" for f ∈ (vline, vspan) @test f(data).subplots[1].attr[:title] == "x" end end @@ -161,8 +161,8 @@ end data4 = rand(4, 4) mat = reshape(1:8, 2, 4) sp = plot(data4, ribbon = (mat, mat))[1] - for i in axes(data4, 1) - for attribute in (:fillrange, :ribbon) + for i ∈ axes(data4, 1) + for attribute ∈ (:fillrange, :ribbon) nt = NamedTuple{tuple(attribute)} get_attrs(pl) = pl[1][i][attribute] @test plot(data4; nt(0)...) |> get_attrs == 0 diff --git a/PlotsBase/test/test_pgfplotsx.jl b/PlotsBase/test/test_pgfplotsx.jl index 07bacbedf..af51bde12 100644 --- a/PlotsBase/test/test_pgfplotsx.jl +++ b/PlotsBase/test/test_pgfplotsx.jl @@ -184,8 +184,8 @@ with(:pgfplotsx) do end @testset "Heatmap-like" begin - xs = [string("x", i) for i in 1:10] - ys = [string("y", i) for i in 1:4] + xs = [string("x", i) for i ∈ 1:10] + ys = [string("y", i) for i ∈ 1:4] z = float((1:4) * reshape(1:10, 1, :)) pl = heatmap(xs, ys, z, aspect_ratio = 1) axis = first(get_pgf_axes(pl)) @@ -236,7 +236,7 @@ with(:pgfplotsx) do markerstrokewidth = 0, ticks = -2:2, ) - for (i, axis) in enumerate(get_pgf_axes(pl)) + for (i, axis) ∈ enumerate(get_pgf_axes(pl)) opts = axis.options # just check by indexing (not defined -> throws) opts["x axis line style"] diff --git a/PlotsBase/test/test_preferences.jl b/PlotsBase/test/test_preferences.jl index ae6cac8b7..23e7514ce 100644 --- a/PlotsBase/test/test_preferences.jl +++ b/PlotsBase/test/test_preferences.jl @@ -59,7 +59,7 @@ const DEBUG = false @test run(```$(Base.julia_cmd()) $script```) |> success end -is_pkgeval() || for pkg in TEST_PACKAGES +is_pkgeval() || for pkg ∈ TEST_PACKAGES @testset "persistent backend $pkg" begin be = TEST_BACKENDS[pkg] if is_ci() diff --git a/PlotsBase/test/test_recipes.jl b/PlotsBase/test/test_recipes.jl index 9c4abddde..0c843fbaa 100644 --- a/PlotsBase/test/test_recipes.jl +++ b/PlotsBase/test/test_recipes.jl @@ -80,8 +80,8 @@ end pl = plot(-1:1, -1:1, -1:1) sp = pl.subplots[1] defaultret = PlotsBase.axis_drawing_info_3d(sp, :x) - for letter in (:x, :y, :z) - for framestyle in [:box :semi :origin :zerolines :grid :none] + for letter ∈ (:x, :y, :z) + for framestyle ∈ [:box :semi :origin :zerolines :grid :none] prevha = UInt64(0) push!(sp.attr, :framestyle => framestyle) ret = PlotsBase.axis_drawing_info_3d(sp, letter) diff --git a/PlotsBase/test/test_reference.jl b/PlotsBase/test/test_reference.jl index 203454434..a03389b30 100644 --- a/PlotsBase/test/test_reference.jl +++ b/PlotsBase/test/test_reference.jl @@ -23,7 +23,7 @@ reference_path(backend, version) = reference_dir("Plots", string(backend), strin function checkout_reference_dir(dn::AbstractString) mkpath(dn) local repo - for i in 1:6 + for i ∈ 1:6 try repo = LibGit2.clone( "https://github.com/JuliaPlots/PlotReferenceImages.jl.git", @@ -57,7 +57,7 @@ function reference_file(backend, version, i) refdir = reference_dir("Plots", string(backend)) fn = ref_name(i) * ".png" reffn = joinpath(refdir, string(version), fn) - for ver in sort(VersionNumber.(readdir(refdir)), rev = true) + for ver ∈ sort(VersionNumber.(readdir(refdir)), rev = true) if (tmpfn = joinpath(refdir, string(ver), fn)) |> isfile reffn = tmpfn break @@ -110,7 +110,7 @@ function image_comparison_facts( sigma = [1, 1], # number of pixels to "blur" tol = 1e-2, # acceptable error (percent) ) - for i in setdiff(1:length(PlotsBase._examples), skip) + for i ∈ setdiff(1:length(PlotsBase._examples), skip) if only ≡ nothing || i in only @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) end diff --git a/PlotsBase/test/test_unitful.jl b/PlotsBase/test/test_unitful.jl index 69f3a2d40..e9b141b14 100644 --- a/PlotsBase/test/test_unitful.jl +++ b/PlotsBase/test/test_unitful.jl @@ -179,7 +179,7 @@ end end @testset "More plots" begin - @testset "data as $dtype" for dtype in + @testset "data as $dtype" for dtype ∈ [:Vectors, :Matrices, Symbol("Vectors of vectors")] if dtype == :Vectors x, y, z = randn(10), randn(10), randn(10) @@ -237,21 +237,19 @@ end @testset "Unitful/unitless combinations" begin mystr(x::Array{<:Quantity}) = "Q" mystr(x::Array) = "A" - @testset "plot($(mystr(xs)), $(mystr(ys)))" for xs in [x, x * m], - ys in [y, y * s] - + @testset "plot($(mystr(xs)), $(mystr(ys)))" for xs ∈ [x, x * m], ys ∈ [y, y * s] @test plot(xs, ys) isa PlotsBase.Plot end - @testset "plot($(mystr(xs)), $(mystr(ys)), $(mystr(zs)))" for xs in [x, x * m], - ys in [y, y * s], - zs in [z, z * (m / s)] + @testset "plot($(mystr(xs)), $(mystr(ys)), $(mystr(zs)))" for xs ∈ [x, x * m], + ys ∈ [y, y * s], + zs ∈ [z, z * (m / s)] @test plot(xs, ys, zs) isa PlotsBase.Plot end end end - @testset "scatter(x::$(us[1]), y::$(us[2]))" for us in collect( + @testset "scatter(x::$(us[1]), y::$(us[2]))" for us ∈ collect( Iterators.product(fill([1, u"m", u"s"], 2)...), ) x, y = rand(10) * us[1], rand(10) * us[2] @@ -260,7 +258,7 @@ end @test scatter(x, y, line_z = x) isa PlotsBase.Plot end - @testset "contour(x::$(us[1]), y::$(us[2]))" for us in collect( + @testset "contour(x::$(us[1]), y::$(us[2]))" for us ∈ collect( Iterators.product(fill([1, u"m", u"s"], 2)...), ) x, y = (1:0.01:2) * us[1], (1:0.02:2) * us[2] diff --git a/PlotsBase/test/test_utils.jl b/PlotsBase/test/test_utils.jl index ea179267a..c973c743d 100644 --- a/PlotsBase/test/test_utils.jl +++ b/PlotsBase/test/test_utils.jl @@ -11,7 +11,7 @@ [(missing, missing)], [(missing, missing, missing), ("a", "b", "c")], ) - for z in zipped + for z ∈ zipped @test isequal(collect(zip(PlotsBase.unzip(z)...)), z) @test isequal(collect(zip(PlotsBase.unzip(GeometryBasics.Point.(z))...)), z) end @@ -271,7 +271,7 @@ end # Test step plot with variable limits x = 0:0.001:1 - y = vcat([0.0 for _ in 1:100], [1.0 for _ in 101:200], [0.5 for _ in 201:1001]) + y = vcat([0.0 for _ ∈ 1:100], [1.0 for _ ∈ 101:200], [0.5 for _ ∈ 201:1001]) pl = scatter(x, y) @test PlotsBase._guess_best_legend_position(:best, pl) ≡ :topright pl = scatter(x, y, xlims = [0, 0.25]) diff --git a/src/Plots.jl b/src/Plots.jl index dace3c1fd..4a1a613e1 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -42,7 +42,7 @@ if PlotsBase.DEFAULT_BACKEND == "gr" # FIXME: Creating a new global in closed m imports = sizehint!(Expr[], n) examples = sizehint!(Expr[], 10n) scratch_dir = mktempdir() - for i in setdiff( + for i ∈ setdiff( 1:n, PlotsBase._backend_skips[backend_name()], PlotsBase._animation_examples, diff --git a/test/runtests.jl b/test/runtests.jl index c1d2b40b1..f6da926aa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,12 +2,12 @@ const TEST_PACKAGES = let val = get(ENV, "PLOTS_TEST_PACKAGES", "GR,UnicodePlots,PythonPlot") Symbol.(strip.(split(val, ","))) end -const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p in TEST_PACKAGES) +const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p ∈ TEST_PACKAGES) using PlotsBase # initialize all backends -for pkg in TEST_PACKAGES +for pkg ∈ TEST_PACKAGES @eval begin import $pkg # trigger extension $(TEST_BACKENDS[pkg])() @@ -18,7 +18,7 @@ gr() using Plots using Test -for pkg in TEST_PACKAGES +for pkg ∈ TEST_PACKAGES @testset "simple plots using $pkg" begin @eval $(TEST_BACKENDS[pkg])() pl = plot(1:2) From 83346a6f75262fe2cf673e281d08b708f89f68ad Mon Sep 17 00:00:00 2001 From: Alexander Dunlap Date: Mon, 13 May 2024 08:59:53 -0400 Subject: [PATCH 17/85] use deterministic series IDs in PGFPlotsX (#4933) --- PlotsBase/ext/PGFPlotsXExt.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/PlotsBase/ext/PGFPlotsXExt.jl b/PlotsBase/ext/PGFPlotsXExt.jl index 9e3f03df9..242646bee 100644 --- a/PlotsBase/ext/PGFPlotsXExt.jl +++ b/PlotsBase/ext/PGFPlotsXExt.jl @@ -3,7 +3,6 @@ module PGFPlotsXExt import PlotsBase: PlotsBase, pgfx_sanitize_string import LaTeXStrings: LaTeXString import Printf: @sprintf -import UUIDs: uuid4 import RecipesPipeline import PlotUtils @@ -465,8 +464,8 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) ) end for (series_index, series) ∈ enumerate(series_list(sp)) - # give each series a uuid for fillbetween - series_id = uuid4() + # give each series an id for fillbetween + series_id = maximum(values(_pgfplotsx_series_ids), init = 0) + 1 _pgfplotsx_series_ids[Symbol("$series_index")] = series_id opt = series.plotattributes st = series[:seriestype] From bb08fa0fc4167524a31dc76a21626682ecb0d191 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 15:10:38 +0200 Subject: [PATCH 18/85] Bump julia-actions/cache from 1 to 2 (#4938) Bumps [julia-actions/cache](https://github.com/julia-actions/cache) from 1 to 2. - [Release notes](https://github.com/julia-actions/cache/releases) - [Commits](https://github.com/julia-actions/cache/compare/v1...v2) --- updated-dependencies: - dependency-name: julia-actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5769a9193..b65d12a81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: with: version: ${{ matrix.version }} - - uses: julia-actions/cache@v1 + - uses: julia-actions/cache@v2 - name: Develop RecipesBase, RecipesPipeline, PlotsBase, Plots env: From 52d35b9935c1b6eb58012347e1360b56a4afb4b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 15:10:54 +0200 Subject: [PATCH 19/85] Bump actions/cache from 3 to 4 (#4937) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9c6e80ae8..b74399ab3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,7 @@ jobs: repository: JuliaPlots/PlotDocs.jl - uses: julia-actions/setup-julia@latest - name: Cache artifacts - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-artifacts with: From 3db0408a927774c00fb64ba5302bc75c389c095f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 15:11:07 +0200 Subject: [PATCH 20/85] Bump peter-evans/create-pull-request from 5 to 6 (#4936) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 6. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v6) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/format_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml index 724a13842..14979d97e 100644 --- a/.github/workflows/format_pr.yml +++ b/.github/workflows/format_pr.yml @@ -19,7 +19,7 @@ jobs: - name: Create Pull Request if: ${{ failure() }} id: cpr - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "Format .jl files [skip ci]" From f32247a819bc938a133bb3403770814194821408 Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Thu, 23 May 2024 18:11:31 +0200 Subject: [PATCH 21/85] add recipe for series on Plots v2 (#4941) * add recipe * add import * handle errorbars, add tests * improve test * simplify test * fix test --- PlotsBase/src/DataSeries.jl | 10 ++++++++++ PlotsBase/test/test_recipes.jl | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/PlotsBase/src/DataSeries.jl b/PlotsBase/src/DataSeries.jl index bb360eacd..507c05c6e 100644 --- a/PlotsBase/src/DataSeries.jl +++ b/PlotsBase/src/DataSeries.jl @@ -21,17 +21,27 @@ export get_linestyle, get_markercolor, get_markeralpha +import Base.show import ..Commons: get_gradient, get_subplot, _series_defaults import ..PlotsBase using ..PlotsBase: DefaultsDict, RecipesPipeline, get_attr_symbol, KW using ..PlotUtils: ColorGradient, plot_color +using ..RecipesBase: @recipe using ..Commons mutable struct Series plotattributes::DefaultsDict end +@recipe function f(s::Series) + for (k, v) in s.plotattributes + k in (:subplot, :yerror, :xerror, :zerror) && continue + plotattributes[k] = v + end + () +end + Base.getindex(series::Series, k::Symbol) = series.plotattributes[k] Base.setindex!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v) Base.get(series::Series, k::Symbol, v) = get(series.plotattributes, k, v) diff --git a/PlotsBase/test/test_recipes.jl b/PlotsBase/test/test_recipes.jl index 0c843fbaa..5666e668b 100644 --- a/PlotsBase/test/test_recipes.jl +++ b/PlotsBase/test/test_recipes.jl @@ -20,6 +20,12 @@ using OffsetArrays end end +@testset "Series" begin + pl = plot(1:3, yerror = 1) + @test plot(pl[1][1])[1][1][:primary] == true + @test plot(pl[1][2])[1][1][:primary] == false + @test isequal(plot(pl[1][2])[1][1][:y], pl[1][2][:y]) +end @testset "lens!" begin pl = plot(1:5) lens!(pl, [1, 2], [1, 2], inset = (1, bbox(0.0, 0.0, 0.2, 0.2)), colorbar = false) From 617b8aae33552a11221f4aa7b501643c0d713e65 Mon Sep 17 00:00:00 2001 From: dd0 Date: Sat, 13 Jul 2024 11:16:05 +0200 Subject: [PATCH 22/85] GR: Draw all gridlines before axis (v2) (#4960) * GR: Draw all gridlines before axis Separates grid drawing from the remainder of gr_draw_axis, so that all gridlines are drawn below the axis. Previously, y-axis gridlines were drawn after the x-axis and could overlap it. Fixes issue #4202 for GR. * Added qualified PlotsBase names missing from v1 port --- PlotsBase/ext/GRExt.jl | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/PlotsBase/ext/GRExt.jl b/PlotsBase/ext/GRExt.jl index a1dfabfa5..2e4490803 100644 --- a/PlotsBase/ext/GRExt.jl +++ b/PlotsBase/ext/GRExt.jl @@ -1585,6 +1585,8 @@ function gr_draw_axes(sp, vp) x_bg, y_bg = RecipesPipeline.unzip(GR.wc3towc.(area_x, area_y, area_z)) GR.fillarea(x_bg, y_bg) + foreach(letter -> gr_draw_axis_minorgrid_3d(sp, letter, vp), (:x, :y, :z)) + foreach(letter -> gr_draw_axis_grid_3d(sp, letter, vp), (:x, :y, :z)) foreach(letter -> gr_draw_axis_3d(sp, letter, vp), (:x, :y, :z)) elseif ispolar(sp) r = gr_set_viewport_polar(vp) @@ -1592,19 +1594,43 @@ function gr_draw_axes(sp, vp) rmin, rmax = axis_limits(sp, :y) gr_polaraxes(rmin, rmax, sp) elseif sp[:framestyle] ≢ :none + foreach(letter -> gr_draw_axis_minorgrid(sp, letter, vp), (:x, :y)) + foreach(letter -> gr_draw_axis_grid(sp, letter, vp), (:x, :y)) foreach(letter -> gr_draw_axis(sp, letter, vp), (:x, :y)) end GR.settransparency(1.0) nothing end +function gr_draw_axis_minorgrid_3d(sp, letter, vp) + ax = PlotsBase.axis_drawing_info_3d(sp, letter) + axis = sp[get_attr_symbol(letter, :axis)] + gr_draw_minorgrid(sp, axis, ax.minorgrid_segments, gr_polyline3d) +end + +function gr_draw_axis_grid_3d(sp, letter, vp) + ax = PlotsBase.axis_drawing_info_3d(sp, letter) + axis = sp[get_attr_symbol(letter, :axis)] + gr_draw_grid(sp, axis, ax.grid_segments, gr_polyline3d) +end + +function gr_draw_axis_minorgrid(sp, letter, vp) + ax = PlotsBase.axis_drawing_info(sp, letter) + axis = sp[get_attr_symbol(letter, :axis)] + gr_draw_minorgrid(sp, axis, ax.minorgrid_segments) +end + +function gr_draw_axis_grid(sp, letter, vp) + ax = PlotsBase.axis_drawing_info(sp, letter) + axis = sp[get_attr_symbol(letter, :axis)] + gr_draw_grid(sp, axis, ax.grid_segments) +end + function gr_draw_axis(sp, letter, vp) ax = PlotsBase.axis_drawing_info(sp, letter) axis = sp[get_attr_symbol(letter, :axis)] # draw segments - gr_draw_grid(sp, axis, ax.grid_segments) - gr_draw_minorgrid(sp, axis, ax.minorgrid_segments) gr_draw_spine(sp, axis, ax.segments) gr_draw_border(sp, axis, ax.border_segments) gr_draw_ticks(sp, axis, ax.tick_segments) @@ -1620,8 +1646,6 @@ function gr_draw_axis_3d(sp, letter, vp) axis = sp[get_attr_symbol(letter, :axis)] # draw segments - gr_draw_grid(sp, axis, ax.grid_segments, gr_polyline3d) - gr_draw_minorgrid(sp, axis, ax.minorgrid_segments, gr_polyline3d) gr_draw_spine(sp, axis, ax.segments, gr_polyline3d) gr_draw_border(sp, axis, ax.border_segments, gr_polyline3d) gr_draw_ticks(sp, axis, ax.tick_segments, gr_polyline3d) From f75d21aa4502d33f32857ed94f5e86d9cb70002d Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Fri, 23 Aug 2024 11:27:10 +0100 Subject: [PATCH 23/85] [v2] Fix floating point equality comparison in layouts (#4973) * Fix floating point equality comparison in layouts [v2] * Add tests for grid layout widths/heights [v2] * Add myself to .zenodo.json [v2] --- .zenodo.json | 5 +++++ PlotsBase/src/Commons/layouts.jl | 13 +++++++------ PlotsBase/test/test_layouts.jl | 11 +++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 539036d03..42fbc2c8a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -770,6 +770,11 @@ "name": "Syver Døving Agdestein", "orcid": "0000-0002-1589-2916", "type": "Other" + }, + { + "affiliation": "The Alan Turing Institute", + "name": "Penelope Yong", + "type": "Other" } ], "upload_type": "software" diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl index 05f4907d9..e9b5d0647 100644 --- a/PlotsBase/src/Commons/layouts.jl +++ b/PlotsBase/src/Commons/layouts.jl @@ -141,17 +141,18 @@ function GridLayout( kw..., ) # Check the values for heights and widths if values are provided + all_between_one(xs) = all(x -> 0 < x < 1, xs) if heights ≢ nothing - sum(heights) == 1 || error("The sum of heights must be 1 !") - all(x -> 0 < x < 1, heights) || - error("Values for heights must be in the range (0, 1) !") + sum(heights) ≈ 1 || error("The heights provided ($(heights)) must sum to 1.") + all_between_one(heights) || + error("The heights provided ($(heights)) must be in the range (0, 1).") else heights = zeros(dims[1]) end if widths ≢ nothing - sum(widths) == 1 || error("The sum of widths must be 1 !") - all(x -> 0 < x < 1, widths) || - error("Values for widths must be in the range (0, 1) !") + sum(widths) ≈ 1 || error("The widths provided ($(widths)) must sum to 1.") + all_between_one(widths) || + error("The widths provided ($(widths)) must be in the range (0, 1).") else widths = zeros(dims[2]) end diff --git a/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl index 67d87a30f..660e3123c 100644 --- a/PlotsBase/test/test_layouts.jl +++ b/PlotsBase/test/test_layouts.jl @@ -64,6 +64,17 @@ end @test_throws ErrorException plot(map(_ -> plot(1:2), 1:5)...; layout = grid(2, 2)) end +@testset "Allowed grid widths/heights" begin + @test_nowarn grid(2, 1, heights = [0.5, 0.5]) + @test_nowarn grid(4, 1, heights = [0.3, 0.3, 0.3, 0.1]) + @test_nowarn grid(1, 2, widths = [0.5, 0.5]) + @test_nowarn grid(1, 4, widths = [0.3, 0.3, 0.3, 0.1]) + @test_throws ErrorException grid(2, 1, heights = [0.5, 0.4]) + @test_throws ErrorException grid(4, 1, heights = [1.5, -0.5]) + @test_throws ErrorException grid(1, 2, widths = [0.5, 0.4]) + @test_throws ErrorException grid(1, 4, widths = [1.5, -0.5]) +end + @testset "Invalid viewport" begin # github.com/JuliaPlots/Plots.jl/issues/2804 pl = plot(1, layout = (10, 2)) From 8bb247ab4c5f91d9d2e028d0692c6c4e901699e8 Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Wed, 28 Aug 2024 17:48:34 +0200 Subject: [PATCH 24/85] Add docs folder (#4963) * add documentation folder * use 'pre' in ci * move make.jl * update for Documenter v1 * handle some more errors * add workflow dispatch triggers to ci * add backend imports * add PlotsBase on CI * earlier * RecipesPipeline too * fix syntax * remove from Project * add ImageIO ImageMagick * add StaticArrays * OffsetArrays * restore old project * remove InspectDR * RecipesBase * use this repo * resolve in one go --- .github/workflows/CompatHelper.yml | 1 + .github/workflows/benchmark.yml | 1 + .github/workflows/ci.yml | 6 +- .github/workflows/docs.yml | 4 +- .github/workflows/format_check.yml | 1 + .github/workflows/format_pr.yml | 1 + .github/workflows/invalidations.yml | 3 +- .gitignore | 5 + PlotsBase/src/plot.jl | 4 +- docs/Project.toml | 43 + docs/ci_build.sh | 92 ++ docs/gallery/gaston/config.json | 6 + docs/gallery/gaston/index.md | 12 + docs/gallery/gr/config.json | 6 + docs/gallery/gr/index.md | 12 + docs/gallery/inspectdr/config.json | 6 + docs/gallery/inspectdr/index.md | 15 + docs/gallery/pgfplotsx/config.json | 6 + docs/gallery/pgfplotsx/index.md | 12 + docs/gallery/plotlyjs/config.json | 6 + docs/gallery/plotlyjs/index.md | 12 + docs/gallery/pythonplot/config.json | 6 + docs/gallery/pythonplot/index.md | 12 + docs/gallery/unicodeplots/config.json | 6 + docs/gallery/unicodeplots/index.md | 12 + docs/make.jl | 824 ++++++++++++++++++ docs/src/GraphRecipes/examples.md | 183 ++++ docs/src/GraphRecipes/introduction.md | 25 + docs/src/RecipesBase/api.md | 3 + docs/src/RecipesBase/index.md | 15 + docs/src/RecipesBase/internals.md | 149 ++++ docs/src/RecipesBase/syntax.md | 121 +++ docs/src/RecipesBase/types.md | 401 +++++++++ docs/src/RecipesPipeline/api.md | 3 + docs/src/RecipesPipeline/index.md | 5 + docs/src/UnitfulExt/unitfulext.md | 25 + docs/src/UnitfulExt/unitfulext_examples.jl | 215 +++++ docs/src/UnitfulExt/unitfulext_plots.jl | 196 +++++ docs/src/animations.md | 76 ++ docs/src/api.md | 70 ++ docs/src/assets/axis_logo.png | Bin 0 -> 73378 bytes docs/src/assets/axis_logo.svg | 146 ++++ docs/src/assets/axis_logo_600x400.png | Bin 0 -> 16253 bytes docs/src/assets/favicon.ico | Bin 0 -> 2533 bytes docs/src/assets/hdf5_samplestruct.png | Bin 0 -> 22391 bytes docs/src/assets/logo.png | Bin 0 -> 73378 bytes docs/src/assets/old_batman_logo.png | Bin 0 -> 6702 bytes docs/src/attributes.md | 141 +++ docs/src/backends.md | 523 +++++++++++ docs/src/basics.md | 64 ++ docs/src/colors.md | 85 ++ docs/src/colorschemes.md | 84 ++ docs/src/contributing.md | 317 +++++++ docs/src/democards/bulmagridtheme.css | 12 + docs/src/ecosystem.md | 135 +++ docs/src/index.md | 143 +++ docs/src/input_data.md | 263 ++++++ docs/src/install.md | 78 ++ docs/src/layouts.md | 120 +++ docs/src/learning.md | 33 + docs/src/output.md | 75 ++ docs/src/pipeline.md | 168 ++++ docs/src/plot_objects.md | 11 + docs/src/recipes.md | 493 +++++++++++ docs/src/series_types/contour.md | 146 ++++ docs/src/series_types/histogram.md | 125 +++ docs/src/tutorial.md | 538 ++++++++++++ docs/user_gallery/config.json | 9 + docs/user_gallery/index.md | 5 + docs/user_gallery/misc/config.json | 3 + docs/user_gallery/misc/double_pendulum.jl | 102 +++ docs/user_gallery/misc/gr_lorenz_attractor.jl | 56 ++ 72 files changed, 6466 insertions(+), 10 deletions(-) create mode 100644 docs/Project.toml create mode 100644 docs/ci_build.sh create mode 100644 docs/gallery/gaston/config.json create mode 100644 docs/gallery/gaston/index.md create mode 100644 docs/gallery/gr/config.json create mode 100644 docs/gallery/gr/index.md create mode 100644 docs/gallery/inspectdr/config.json create mode 100644 docs/gallery/inspectdr/index.md create mode 100644 docs/gallery/pgfplotsx/config.json create mode 100644 docs/gallery/pgfplotsx/index.md create mode 100644 docs/gallery/plotlyjs/config.json create mode 100644 docs/gallery/plotlyjs/index.md create mode 100644 docs/gallery/pythonplot/config.json create mode 100644 docs/gallery/pythonplot/index.md create mode 100644 docs/gallery/unicodeplots/config.json create mode 100644 docs/gallery/unicodeplots/index.md create mode 100644 docs/make.jl create mode 100644 docs/src/GraphRecipes/examples.md create mode 100644 docs/src/GraphRecipes/introduction.md create mode 100644 docs/src/RecipesBase/api.md create mode 100644 docs/src/RecipesBase/index.md create mode 100644 docs/src/RecipesBase/internals.md create mode 100644 docs/src/RecipesBase/syntax.md create mode 100644 docs/src/RecipesBase/types.md create mode 100644 docs/src/RecipesPipeline/api.md create mode 100644 docs/src/RecipesPipeline/index.md create mode 100644 docs/src/UnitfulExt/unitfulext.md create mode 100644 docs/src/UnitfulExt/unitfulext_examples.jl create mode 100644 docs/src/UnitfulExt/unitfulext_plots.jl create mode 100644 docs/src/animations.md create mode 100644 docs/src/api.md create mode 100755 docs/src/assets/axis_logo.png create mode 100755 docs/src/assets/axis_logo.svg create mode 100644 docs/src/assets/axis_logo_600x400.png create mode 100644 docs/src/assets/favicon.ico create mode 100644 docs/src/assets/hdf5_samplestruct.png create mode 100755 docs/src/assets/logo.png create mode 100644 docs/src/assets/old_batman_logo.png create mode 100644 docs/src/attributes.md create mode 100644 docs/src/backends.md create mode 100644 docs/src/basics.md create mode 100644 docs/src/colors.md create mode 100644 docs/src/colorschemes.md create mode 100644 docs/src/contributing.md create mode 100644 docs/src/democards/bulmagridtheme.css create mode 100644 docs/src/ecosystem.md create mode 100644 docs/src/index.md create mode 100644 docs/src/input_data.md create mode 100644 docs/src/install.md create mode 100644 docs/src/layouts.md create mode 100644 docs/src/learning.md create mode 100644 docs/src/output.md create mode 100644 docs/src/pipeline.md create mode 100644 docs/src/plot_objects.md create mode 100644 docs/src/recipes.md create mode 100644 docs/src/series_types/contour.md create mode 100644 docs/src/series_types/histogram.md create mode 100644 docs/src/tutorial.md create mode 100644 docs/user_gallery/config.json create mode 100644 docs/user_gallery/index.md create mode 100644 docs/user_gallery/misc/config.json create mode 100644 docs/user_gallery/misc/double_pendulum.jl create mode 100644 docs/user_gallery/misc/gr_lorenz_attractor.jl diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index d4d64580a..db6f4c60e 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,6 +1,7 @@ name: CompatHelper on: + workflow_dispatch: schedule: - cron: '00 00 * * *' diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ef72f78bf..7ac728b57 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,6 +1,7 @@ name: benchmarks on: + workflow_dispatch: pull_request: concurrency: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b65d12a81..4ab7f3dd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: ci on: + workflow_dispatch: pull_request: push: branches: [master, v2] @@ -37,10 +38,7 @@ jobs: include: - os: ubuntu-latest experimental: true - version: '~1.11.0-0' # upcoming julia version (`alpha`, `beta` or `rc`) - - os: ubuntu-latest - experimental: true - version: 'nightly' + version: 'pre' # upcoming julia version (`alpha`, `beta` or `rc`) steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b74399ab3..bdd67b72f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: docs on: workflow_dispatch: push: - branches: [master] + branches: [v2] tags: '*' jobs: @@ -11,8 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - repository: JuliaPlots/PlotDocs.jl - uses: julia-actions/setup-julia@latest - name: Cache artifacts uses: actions/cache@v4 diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 155982b16..10de85ed1 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -1,6 +1,7 @@ name: format on: + workflow_dispatch: pull_request: push: branches: [master, v2] diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml index 14979d97e..96e41e6e7 100644 --- a/.github/workflows/format_pr.yml +++ b/.github/workflows/format_pr.yml @@ -1,6 +1,7 @@ name: format on: + workflow_dispatch: schedule: - cron: '0 0 1 * *' diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml index 98d42f9cf..cee213dcf 100644 --- a/.github/workflows/invalidations.yml +++ b/.github/workflows/invalidations.yml @@ -1,8 +1,9 @@ name: invalidations on: + workflow_dispatch: pull_request: push: - branches: [master] + branches: [v2] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.gitignore b/.gitignore index 609972e94..35558ab62 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ test/tmpplotsave.hdf5 /benchmark/*.json .vscode/ .CondaPkg/ + +# docs +**/generated/ +docs/build/ +docs/work/ \ No newline at end of file diff --git a/PlotsBase/src/plot.jl b/PlotsBase/src/plot.jl index 0044a7b1a..a3ed73a92 100644 --- a/PlotsBase/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -42,7 +42,7 @@ function Base.show(io::IO, plt::Plot) end do_show = true for (i, ekwargs) ∈ enumerate(sp_ekwargs) - for (key, value) ∈ ekwargs + for (key, value) ∈ pairs(ekwargs) do_show && println(io, " SubplotPlot{$i}:") println(io, " "^4, key, ": ", value) do_show = false @@ -50,7 +50,7 @@ function Base.show(io::IO, plt::Plot) do_show = true end for (i, ekwargs) ∈ enumerate(s_ekwargs) - for (key, value) ∈ ekwargs + for (key, value) ∈ pairs(ekwargs) do_show && println(io, " Series{$i}:") println(io, " "^4, key, ": ", value) do_show = false diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 000000000..94fdaffe3 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,43 @@ +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +DemoCards = "311a05b2-6137-4a5a-b473-18580a3d38b5" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" +Gaston = "4b11ee91-296f-5714-9832-002c20994614" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" +GraphRecipes = "bd48cda9-67a9-57be-86fa-5b3c104eda73" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" +PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5" +PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" +PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" +RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/docs/ci_build.sh b/docs/ci_build.sh new file mode 100644 index 000000000..7104249a2 --- /dev/null +++ b/docs/ci_build.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -e + +key_unset=false +if [ -z "$DOCUMENTER_KEY" ]; then + echo '`DOCUMENTER_KEY` is missing' + key_unset=true +fi + +tok_unset=false +if [ -z "$GITHUB_TOKEN" ]; then + echo '`GITHUB_TOKEN` is missing' + tok_unset=true +fi + +if $key_unset && $tok_unset; then + echo 'either `GITHUB_TOKEN` or `DOCUMENTER_KEY` must be set for `Documenter` !' + exit 1 +fi + +echo '== install system dependencies ==' +sudo apt -y update +sudo apt -y install \ + texlive-{latex-{base,extra},binaries,pictures,luatex} \ + ttf-mscorefonts-installer \ + poppler-utils \ + ghostscript-x \ + qtbase5-dev \ + pdf2svg \ + gnuplot \ + g++ + +echo '== install fonts ==' +mkdir -p ~/.fonts +repo="https://github.com/cormullion/juliamono" +ver="$(git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' "$repo.git" | tail -n 1 | awk '{ print $2 }' | sed 's,refs/tags/,,')" +url="$repo/releases/download/$ver/JuliaMono-ttf.tar.gz" +echo "downloading & extract url=$url" +wget -q "$url" -O - | tar -xz -C ~/.fonts +sudo fc-cache -vr +fc-list | grep 'JuliaMono' + +echo "== install julia dependencies ==" +if true; then + export JULIA_DEBUG='Documenter,Literate,DemoCards' + export DOCUMENTER_DEBUG=true # Democards.jl +fi + +export LD_PRELOAD=$(g++ --print-file-name=libstdc++.so) +export GKSwstype=nul # Plots.jl/issues/3664 +export COLORTERM=truecolor # UnicodePlots.jl +export PLOTDOCS_ANSICOLOR=true +export JULIA_CONDAPKG_BACKEND=MicroMamba + +julia='xvfb-run -a julia --color=yes --project=docs' + +# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="RecipesBase"));' #FIXME: not needed when registered +# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="RecipesPipeline"));' #FIXME: not needed when registered +# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="PlotsBase"));' #FIXME: not needed when registered +$julia -e 'using Pkg; Pkg.develop([(;path="."), (;path="./RecipesBase"), (;path="./RecipesPipeline"), (;path="./PlotsBase")]);' #FIXME: not needed when registered +$julia -e ' + using Pkg; Pkg.add("CondaPkg") + using CondaPkg; CondaPkg.resolve() + libgcc = if Sys.islinux() + # see discourse.julialang.org/t/glibcxx-version-not-found/82209/8 + # julia 1.8.3 is built with libstdc++.so.6.0.29, so we must restrict to this version (gcc 11.3.0, not gcc 12.2.0) + # see gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html + specs = Dict( + v"3.4.29" => ">=11.1,<12.1", + v"3.4.30" => ">=12.1,<13.1", + v"3.4.31" => ">=13.1,<14.1", + # ... keep this up-to-date with gcc 14 + )[Base.BinaryPlatforms.detect_libstdcxx_version()] + ("libgcc-ng$specs", "libstdcxx-ng$specs") + else + () + end + CondaPkg.PkgREPL.add([libgcc..., "matplotlib"]) + CondaPkg.status() +' + +echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $GITHUB_ACTOR on $GITHUB_EVENT_NAME ==" +if [ "$GITHUB_REPOSITORY" == 'JuliaPlots/PlotDocs.jl' ]; then + $julia -e 'using Pkg; Pkg.add(PackageSpec(name="Plots", rev="master"))' + $julia docs/make.jl +elif [ "$GITHUB_REPOSITORY" == 'JuliaPlots/Plots.jl' ]; then + $julia -e 'using Pkg; Pkg.add(PackageSpec(name="Plots", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3])); Pkg.instantiate()' + $julia docs/make.jl +else + echo "something is wrong with $GITHUB_REPOSITORY" + exit 1 +fi diff --git a/docs/gallery/gaston/config.json b/docs/gallery/gaston/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/gaston/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/gaston/index.md b/docs/gallery/gaston/index.md new file mode 100644 index 000000000..53eb141d7 --- /dev/null +++ b/docs/gallery/gaston/index.md @@ -0,0 +1,12 @@ +# Gaston + +To switch to the `Gaston` backend, you can use: + +```julia +using Plots +gaston() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/gr/config.json b/docs/gallery/gr/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/gr/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/gr/index.md b/docs/gallery/gr/index.md new file mode 100644 index 000000000..e1a44c5b5 --- /dev/null +++ b/docs/gallery/gr/index.md @@ -0,0 +1,12 @@ +# GR + +`GR` is the default backend for `Plots`. To explicitly specify the `GR` backend, you can use: + +```julia +using Plots +gr() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/inspectdr/config.json b/docs/gallery/inspectdr/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/inspectdr/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/inspectdr/index.md b/docs/gallery/inspectdr/index.md new file mode 100644 index 000000000..dce74ce83 --- /dev/null +++ b/docs/gallery/inspectdr/index.md @@ -0,0 +1,15 @@ +# InspectDR + +!!! warn + `InspectDR` currently does not precompile on julia 1.10+. + +To switch to the `InspectDR` backend, you can use: + +```julia +using Plots +# inspectdr() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/pgfplotsx/config.json b/docs/gallery/pgfplotsx/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/pgfplotsx/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/pgfplotsx/index.md b/docs/gallery/pgfplotsx/index.md new file mode 100644 index 000000000..285c43e3f --- /dev/null +++ b/docs/gallery/pgfplotsx/index.md @@ -0,0 +1,12 @@ +# PGFPlotsX + +To switch to the `PGFPlotsX` backend, you can use: + +```julia +using Plots +pgfplotsx() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/plotlyjs/config.json b/docs/gallery/plotlyjs/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/plotlyjs/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/plotlyjs/index.md b/docs/gallery/plotlyjs/index.md new file mode 100644 index 000000000..b2f8085a7 --- /dev/null +++ b/docs/gallery/plotlyjs/index.md @@ -0,0 +1,12 @@ +# PlotlyJS + +To switch to the `PlotlyJS` backend, you can use: + +```julia +using Plots +plotlyjs() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/pythonplot/config.json b/docs/gallery/pythonplot/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/pythonplot/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/pythonplot/index.md b/docs/gallery/pythonplot/index.md new file mode 100644 index 000000000..c35ce7f9e --- /dev/null +++ b/docs/gallery/pythonplot/index.md @@ -0,0 +1,12 @@ +# PythonPlot + +To switch to the `PythonPlot` backend, you can use: + +```julia +using Plots +pythonplot() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/gallery/unicodeplots/config.json b/docs/gallery/unicodeplots/config.json new file mode 100644 index 000000000..d90906481 --- /dev/null +++ b/docs/gallery/unicodeplots/config.json @@ -0,0 +1,6 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + } +} diff --git a/docs/gallery/unicodeplots/index.md b/docs/gallery/unicodeplots/index.md new file mode 100644 index 000000000..32737ecea --- /dev/null +++ b/docs/gallery/unicodeplots/index.md @@ -0,0 +1,12 @@ +# UnicodePlots + +To switch to the `UnicodePlots` backend, you can use: + +```julia +using Plots +unicodeplots() +``` + +The demos are generated from `Plots._examples`. Empty demos are features that this backend does not support. + +{{{democards}}} diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 000000000..01f3d114f --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,824 @@ +using DataFrames, OrderedCollections, Dates +using MacroTools: rmlines +using PlotThemes, Plots, RecipesBase, RecipesPipeline +using Documenter, DemoCards, Literate, StableRNGs, Glob +using JSON +import PythonPlot +import PGFPlotsX +import PlotlyJS +import Gaston +import UnicodePlots +# import StatsPlots + +const SRC_DIR = joinpath(@__DIR__, "src") +const WORK_DIR = joinpath(@__DIR__, "work") +const GEN_DIR = joinpath(WORK_DIR, "generated") + +const ATTRIBUTE_SEARCH = Dict{String,Any}() # search terms + +# monkey patch `Documenter` - note that this could break on minor `Documenter` releases +@eval Documenter.Writers.HTMLWriter domify(ctx, navnode) = begin + # github.com/JuliaDocs/Documenter.jl/blob/327d155f992ec7c63e35fa2cb08f7f7c2d33409a/src/Writers/HTMLWriter.jl#L1448-L1455 + page = getpage(ctx, navnode) + map(page.elements) do elem + rec = SearchRecord(ctx, navnode, elem) + ############################################################ + # begin addition + info = "[src=$(rec.src) fragment=$(rec.fragment) title=$(rec.title) page_title=$(rec.page_title)]" + if (m = match(r"generated/attributes_(\w+)", lowercase(rec.src))) ≢ nothing + # fix attributes search terms: `Series`, `Plot`, `Subplot` and `Axis` (github.com/JuliaPlots/Plots.jl/issues/2337) + @info "$info: fix attribute search" + for (attr, alias) ∈ $(ATTRIBUTE_SEARCH)[first(m.captures)] + push!( + ctx.search_index, + SearchRecord(rec.src, rec.page, rec.fragment, rec.category, rec.title, rec.page_title, attr * ' ' * alias) + ) + end + else + add_to_index = if (m = match(r"gallery/(\w+)/", lowercase(rec.src))) ≢ nothing + first(m.captures) == "gr" # only add `GR` gallery pages to `search_index` (github.com/JuliaPlots/Plots.jl/issues/4157) + else + true + end + if add_to_index + push!(ctx.search_index, rec) + else + @info "$info: skip adding to `search_index`" + end + end + # end addition + ############################################################ + domify(ctx, navnode, page.mapping[elem]) + end +end + +@eval DemoCards get_logopath() = $(joinpath(SRC_DIR, "assets", "axis_logo_600x400.png")) + +# ---------------------------------------------------------------------- + +edit_url(args...) = + "https://github.com/JuliaPlots/PlotDocs.jl/blob/master/docs/" * if length(args) == 0 + "make.jl" + else + joinpath("src", args...) + end + +autogenerated() = "(Automatically generated: " * Dates.format(now(), RFC1123Format) * ')' + +author() = "[PlotDocs.jl](https://github.com/JuliaPlots/PlotDocs.jl)" + +recursive_rmlines(x) = x +function recursive_rmlines(x::Expr) + x = rmlines(x) + x.args .= recursive_rmlines.(x.args) + x +end + +pretty_print_expr(io::IO, expr::Expr) = + if expr.head ≡ :block + foreach(arg -> println(io, arg), recursive_rmlines(expr).args) + else + println(io, recursive_rmlines(expr)) + end + +markdown_code_to_string(arr, prefix = "") = + surround_backticks(prefix, join(sort(map(string, arr)), "`, `$prefix")) + +markdown_symbols_to_string(arr) = isempty(arr) ? "" : markdown_code_to_string(arr, ":") + +# ---------------------------------------------------------------------- + +# NOTE: keep consistent with `Plots` +ref_name(i) = "ref" * lpad(i, 3, '0') + +function generate_cards( + prefix::AbstractString, backend::Symbol, slice; + skip = get(Plots.PlotsBase._backend_skips, backend, Int[]) +) + @show backend + # create folder: for each backend we generate a DemoSection "generated" under "gallery" + cardspath = mkpath(joinpath(prefix, "$backend", "generated")) + sec_config = Dict{String, Any}("order" => []) + + needs_rng_fix = Dict{Int,Bool}() + + for (i, example) ∈ enumerate(Plots.PlotsBase._examples) + (slice ≢ nothing && i ∉ slice) && continue + # write out the header, description, code block, and image link + jlname = "$backend-$(ref_name(i)).jl" + jl = PipeBuffer() + if !isempty(example.header) + push!(sec_config["order"], jlname) + # start a new demo file + @debug "generate demo \"$(example.header)\" - writing `$jlname`" + + # DemoCards YAML frontmatter + # https://johnnychen94.github.io/DemoCards.jl/stable/quickstart/usage_example/julia_demos/1.julia_demo/#juliademocard_example + asset = if i ∈ Plots.PlotsBase._animation_examples + "anim_$(backend)_$(ref_name(i)).gif" + else + "$(backend)_$(ref_name(i)).png" + end + extra = if backend ≡ :unicodeplots + "import FileIO, FreeType #hide" # weak deps for png export + else + "" + end + write(jl, """ + # --- + # title: $(example.header) + # id: $(backend)_$(ref_name(i)) $(i ∈ skip ? "" : "\n# cover: assets/$asset") + # author: "$(author())" + # description: "" + # date: $(now()) + # --- + + using Plots + $backend() + $extra + """ + ) + + i ∈ skip && @goto write_file + write(jl, """ + Plots.Commons.reset_defaults() #hide + using StableRNGs #hide + rng = StableRNG($(Plots.PLOTS_SEED)) #hide + nothing #hide + """ + ) + end + # DemoCards use Literate.jl syntax with extra leading `#` as markdown lines + write(jl, "# $(replace(example.desc, "\n" => "\n # "))\n") + isnothing(example.imports) || pretty_print_expr(jl, example.imports) + needs_rng_fix[i] = (exprs_rng = Plots.PlotsBase.replace_rand(example.exprs)) != example.exprs + pretty_print_expr(jl, exprs_rng) + + # NOTE: the supported `Literate.jl` syntax is `#src` and `#hide` NOT `# src` !! + # from the docs: """ + # #src and #hide are quite similar. The only difference is that #src lines are filtered out before execution (if execute=true) and #hide lines are filtered out after execution. + # """ + asset = if i ∈ Plots.PlotsBase._animation_examples + "gif(anim, \"assets/anim_$(backend)_$(ref_name(i)).gif\")\n" # NOTE: must not be hidden, for appearance in the rendered `html` + else + "png(\"assets/$(backend)_$(ref_name(i)).png\") #src\n" + end + write(jl, """ + mkpath("assets") #src + $asset + """ + ) + backend ≡ :plotlyjs && write(jl, """ + nothing #hide + # ![plot](assets/$(backend)_$(ref_name(i)).png) + """ + ) + + @label write_file + fn, mode = if isempty(example.header) + "$backend-$(ref_name(i - 1)).jl", "a" # continued example + else + jlname, "w" + end + card = joinpath(cardspath, fn) + # @info "writing" card + open(card, mode) do io + write(io, read(jl, String)) + end + # DEBUG: sometimes the generated file is still empty when passing to `DemoCards.makedemos` + sleep(0.01) + end + # insert attributes page + # TODO(johnnychen): make this part of the page template + attr_name = string(backend, ".jl") + open(joinpath(cardspath, attr_name), "w") do jl + pkg = Plots.PlotsBase.backend_instance(Symbol(lowercase(string(backend)))) + write(jl, """ + # --- + # title: Supported attribute values + # id: $(backend)_attributes + # hidden: true + # author: "$(author())" + # date: $(now()) + # --- + + # - Supported arguments: $(markdown_code_to_string(collect(Plots.PlotsBase.supported_attrs(pkg)))) + # - Supported values for linetype: $(markdown_symbols_to_string(Plots.PlotsBase.supported_seriestypes(pkg))) + # - Supported values for linestyle: $(markdown_symbols_to_string(Plots.PlotsBase.supported_styles(pkg))) + # - Supported values for marker: $(markdown_symbols_to_string(Plots.PlotsBase.supported_markers(pkg))) + """ + ) + end + open(joinpath(cardspath, "config.json"), "w") do config + sec_config["title"] = "" # avoid `# Generated` section in gallery + sec_config["description"] = "[Supported attributes](@ref $(backend)_attributes)" + push!(sec_config["order"], attr_name) + write(config, json(sec_config)) + end + needs_rng_fix +end + +# tables detailing the features that each backend supports +function make_support_df(allvals, func; default_backends) + vals = sort(collect(allvals)) # rows + bs = sort(collect(default_backends)) + df = DataFrames.DataFrame(keys=vals) + + for be ∈ bs # cols + be_supported_vals = fill("", length(vals)) + for (i, val) ∈ enumerate(vals) + be_supported_vals[i] = if func == Plots.PlotsBase.supported_seriestypes + stype = Plots.PlotsBase.seriestype_supported(Plots.PlotsBase.backend_instance(be), val) + stype ≡ :native ? "✅" : (stype ≡ :no ? "" : "🔼") + else + val ∈ func(Plots.PlotsBase.backend_instance(be)) ? "✅" : "" + end + end + df[!, be] = be_supported_vals + end + df +end + +function generate_supported_markdown(; default_backends) + supported_args = OrderedDict( + "Keyword Arguments" => (Plots.Commons._all_attrs, Plots.PlotsBase.supported_attrs), + "Markers" => (Plots.Commons._all_markers, Plots.PlotsBase.supported_markers), + "Line Styles" => (Plots.Commons._all_styles, Plots.PlotsBase.supported_styles), + "Scales" => (Plots.Commons._all_scales, Plots.PlotsBase.supported_scales) + ) + open(joinpath(GEN_DIR, "supported.md"), "w") do md + write(md, """ + ```@meta + EditURL = "$(edit_url())" + ``` + + ## [Series Types](@id supported) + + Key: + + - ✅ the series type is natively supported by the backend. + - 🔼 the series type is supported through series recipes. + + ```@raw html + $(to_html(make_support_df(Plots.PlotsBase.all_seriestypes(), Plots.PlotsBase.supported_seriestypes; default_backends))) + ``` + """ + ) + for (header, args) ∈ supported_args + write(md, """ + + ## $header + + ```@raw html + $(to_html(make_support_df(args...; default_backends))) + ``` + """ + ) + end + write(md, '\n' * autogenerated()) + end +end + +function make_attr_df(ktype::Symbol, defs::KW) + n = length(defs) + df = DataFrame( + Attribute = fill("", n), + Aliases = fill("", n), + Default = fill("", n), + Type = fill("", n), + Description = fill("", n), + ) + for (i, (k, def)) ∈ enumerate(defs) + type, desc = get(Plots.PlotsBase._arg_desc, k, (Any, "")) + + aliases = sort(collect(keys(filter(p -> p.second == k, Plots.Commons._keyAliases)))) + df.Attribute[i] = string(k) + df.Aliases[i] = join(aliases, ", ") + df.Default[i] = show_default(def) + df.Type[i] = string(type) + df.Description[i] = string(desc) + end + sort!(df, [:Attribute]) + df +end + +surround_backticks(args...) = '`' * string(args...) * '`' +show_default(x) = surround_backticks(x) +show_default(x::Symbol) = surround_backticks(":$x") + +function generate_attr_markdown(c) + attribute_texts = Dict( + :Series => "These attributes apply to individual series (lines, scatters, heatmaps, etc)", + :Plot => "These attributes apply to the full Plot. (A Plot contains a tree-like layout of Subplots)", + :Subplot => "These attributes apply to settings for individual Subplots.", + :Axis => """ + These attributes apply by default to all Axes in a Subplot (for example the `subplot[:xaxis]`). + !!! info + You can also specific the x, y, or z axis for each of these attributes by prefixing the attribute name with x, y, or z + (for example `xmirror` only sets the mirror attribute for the x axis). + """, + ) + attribute_defaults = Dict( + :Series => Plots.Commons._series_defaults, + :Plot => Plots.Commons._plot_defaults, + :Subplot => Plots.Commons._subplot_defaults, + :Axis => Plots.Commons._axis_defaults, + ) + + df = make_attr_df(c, attribute_defaults[c]) + cstr = lowercase(string(c)) + ATTRIBUTE_SEARCH[cstr] = collect(zip(df.Attribute, df.Aliases)) + + open(joinpath(GEN_DIR, "attributes_$cstr.md"), "w") do md + write(md, """ + ```@meta + EditURL = "$(edit_url())" + ``` + ### $c + + $(attribute_texts[c]) + + ```@raw html + $(to_html(df)) + ``` + + $(autogenerated()) + """ + ) + end +end + +generate_attr_markdown() = + foreach(c -> generate_attr_markdown(c), (:Series, :Plot, :Subplot, :Axis)) + +function generate_graph_attr_markdown() + df = DataFrame( + Attribute = [ + "dim", + "T", + "curves", + "curvature_scalar", + "root", + "node_weights", + "names", + "fontsize", + "nodeshape", + "nodesize", + "nodecolor", + "x, y, z", + "method", + "func", + "shorten", + "axis_buffer", + "layout_kw", + "edgewidth", + "edgelabel", + "edgelabel_offset", + "self_edge_size", + "edge_label_box", + ], + Aliases = [ + "", + "", + "", + "curvaturescalar, curvature", + "", + "nodeweights", + "", + "", + "node_shape", + "node_size", + "marker_color", + "x", + "", + "", + "shorten_edge", + "axisbuffer", + "", + "edge_width, ew", + "edge_label, el", + "edgelabeloffset, elo", + "selfedgesize, ses", + "edgelabelbox, edgelabel_box, elb", + ], + Default = [ + "2", + "Float64", + "true", + "0.05", + ":top", + "nothing", + "[]", + "7", + ":hexagon", + "0.1", + "1", + "nothing", + ":stress", + "get(_graph_funcs, method, by_axis_local_stress_graph)", + "0.0", + "0.2", + "Dict{Symbol,Any}()", + "(s, d, w) -> 1", + "nothing", + "0.0", + "0.1", + "true", + ], + Description = [ + "The number of dimensions in the visualization.", + "The data type for the coordinates of the graph nodes.", + "Whether or not edges are curved. If `curves == true`, then the edge going from node \$s\$ to node \$d\$ will be defined by a cubic spline passing through three points: (i) node \$s\$, (ii) a point `p` that is distance `curvature_scalar` from the average of node \$s\$ and node \$d\$ and (iii) node \$d\$.", + "A scalar that defines how much edges curve, see `curves` for more explanation.", + "For displaying trees, choose from `:top`, `:bottom`, `:left`, `:right`. If you choose `:top`, then the tree will be plotted from the top down.", + "The weight of the nodes given by a list of numbers. If `node_weights != nothing`, then the size of the nodes will be scaled by the `node_weights` vector.", + "Names of the nodes given by a list of objects that can be parsed into strings. If the list is smaller than the number of nodes, then GraphRecipes will cycle around the list.", + "Font size for the node labels and the edge labels.", + "Shape of the nodes, choose from `:hexagon`, `:circle`, `:ellipse`, `:rect` or `:rectangle`.", + "The size of nodes in the plot coordinates. Note that if `names` is not empty, then nodes will be scaled to fit the labels inside them.", + "The color of the nodes. If `nodecolor` is an integer, then it will be taken from the current color pallette. Otherwise, the user can pass any color that would be recognised by the Plots `color` attribute.", + "The coordinates of the nodes.", + "The method that GraphRecipes uses to produce an optimal layout, choose from `:spectral`, `:sfdp`, `:circular`, `:shell`, `:stress`, `:spring`, `:tree`, `:buchheim`, `:arcdiagram` or `:chorddiagram`. See [NetworkLayout](https://github.com/JuliaGraphs/NetworkLayout.jl) for further details.", + "A layout algorithm that can be passed in by the user.", + "An amount to shorten edges by.", + "Increase the `xlims` and `ylims`/`zlims` of the plot. Can be useful if part of the graph sits outside of the default view.", + "A list of keywords to be passed to the layout algorithm, see [NetworkLayout](https://github.com/JuliaGraphs/NetworkLayout.jl) for a list of keyword arguments for each algorithm.", + "The width of the edge going from \$s\$ to node \$d\$ with weight \$w\$.", + "A dictionary of `(s, d) => label`, where `s` is an integer for the source node, `d` is an integer for the destiny node and `label` is the desired label for the given edge. Alternatively the user can pass a vector or a matrix describing the edge labels. If you use a vector or matrix, then either `missing`, `false`, `nothing`, `NaN` or `\"\"` values will not be displayed. In the case of multigraphs, triples can be used to define edges.", + "The distance between edge labels and edges.", + "The size of self edges.", + "A box around edge labels that avoids intersections between edge labels and the edges that they are labeling.", + ] + ) + open(joinpath(GEN_DIR, "graph_attributes.md"), "w") do md + write(md, """ + ```@meta + EditURL = "$(edit_url())" + ``` + # [Graph Attributes](@id graph_attributes) + + Where possible, GraphRecipes will adopt attributes from Plots.jl to format visualizations. + For example, the `linewidth` attribute from Plots.jl has the same effect in GraphRecipes. + In order to give the user control over the layout of the graph visualization, GraphRecipes + provides a number of keyword arguments (attributes). Here we describe those attributes + alongside their default values. + + ```@raw html + $(to_html(df)) + ``` + \n + ## Aliases + Certain keyword arguments have aliases, so GraphRecipes "does what you mean, not + what you say". + + So for example, `nodeshape=:rect` and `node_shape=:rect` are equivalent. To see the + available aliases, type `GraphRecipes.graph_aliases`. If you are unhappy with the provided + aliases, then you can add your own: + ```julia + using GraphRecipes, Plots + + push!(GraphRecipes.graph_aliases[:nodecolor],:nc) + + # These two calls produce the same plot, modulo some randomness in the layout. + plot(graphplot([0 1; 0 0], nodecolor=:red), graphplot([0 1; 0 0], nc=:red)) + ``` + + $(autogenerated()) + """ + ) + end +end + +function generate_colorschemes_markdown() + open(joinpath(GEN_DIR, "colorschemes.md"), "w") do md + write(md, """ + ```@meta + EditURL = "$(edit_url())" + ``` + """ + ) + for line ∈ readlines(joinpath(SRC_DIR, "colorschemes.md")) + write(md, line * '\n') + end + write(md, """ + ## misc + + These colorschemes are not defined or provide different colors in ColorSchemes.jl + They are kept for compatibility with Plots behavior before v1.1.0. + """ + ) + write(md, "```@raw html\n") + ks = [:default; sort(collect(keys(PlotUtils.MISC_COLORSCHEMES)))] + write(md, to_html(make_colorschemes_df(ks); allow_html_in_cells=true)) + write(md, "\n```\n\nThe following colorschemes are defined by ColorSchemes.jl.\n\n") + for cs ∈ ("cmocean", "scientific", "matplotlib", "colorbrewer", "gnuplot", "colorcet", "seaborn", "general") + ks = sort([k for (k, v) ∈ PlotUtils.ColorSchemes.colorschemes if occursin(cs, v.category)]) + write(md, "\n## $cs\n\n```@raw html\n") + write(md, to_html(make_colorschemes_df(ks); allow_html_in_cells=true)) + write(md, "\n```\n") + end + end +end + +function colors_svg(cs, w, h) + n = length(cs) + ws = min(w / n, h) + # NOTE: html tester, codebeautify.org/htmlviewer or htmledit.squarefree.com + html = replace(""" + + + + """, "\n" => " " + ) # NOTE: no linebreaks (because those break html code) + for (i, c) ∈ enumerate(cs) + html *= """""" + end + html *= "" +end + +function make_colorschemes_df(ks) + n = length(ks) + df = DataFrame( + Name = fill("", n), + Palette = fill("", n), + Gradient = fill("", n), + ) + len, w, h = 100, 60, 5 + for (i, k) ∈ enumerate(ks) + p = palette(k) + cg = cgrad(k)[range(0, 1, length = len)] + cp = length(p) ≤ len ? color_list(p) : cg + df.Name[i] = string(':', k) + df.Palette[i] = colors_svg(cp, w, h) + df.Gradient[i] = colors_svg(cg, w, h) + end + df +end + +# ---------------------------------------------------------------------- + +function to_html(df::AbstractDataFrame; table_style=Dict("font-size" => "12px"), kw...) + io = PipeBuffer() # NOTE: `DataFrames` exports `PrettyTables` + show( + IOContext(io, :limit => false, :compact => false), MIME"text/html"(), df; + show_row_number=false, summary=false, eltypes=false, table_style, + kw... + ) + read(io, String) +end + +function main() + get!(ENV, "MPLBACKEND", "agg") # set matplotlib gui backend + get!(ENV, "GKSwstype", "nul") # disable default GR ws + + mkpath(GEN_DIR) + + # initialize all backends + gr() + pythonplot() + plotlyjs() + pgfplotsx() + unicodeplots() + gaston() + + # NOTE: for a faster representative test build use `PLOTDOCS_BACKENDS='GR' PLOTDOCS_EXAMPLES='1'` + default_backends = "GR PythonPlot PlotlyJS PGFPlotsX UnicodePlots Gaston" + backends = get(ENV, "PLOTDOCS_BACKENDS", default_backends) + backends = backends == "ALL" ? default_backends : backends + @info "selected backends: $backends" + backends = Symbol.(lowercase.(split(backends))) + + slice = parse.(Int, split(get(ENV, "PLOTDOCS_EXAMPLES", ""))) + slice = length(slice) == 0 ? nothing : slice + @info "selected examples: $slice" + + work = basename(WORK_DIR) + + @info "generate markdown" + generate_attr_markdown() + generate_supported_markdown(; default_backends = backends) + generate_graph_attr_markdown() + generate_colorschemes_markdown() + + for (pkg, dest) ∈ ( + (PlotThemes, "plotthemes.md"), + # (StatsPlots, "statsplots.md"), #TODO: uncomment after having compatible StatsPlots + ) + cp(pkgdir(pkg, "README.md"), joinpath(GEN_DIR, dest); force = true) + end + + @info "gallery" + gallery = Pair{String,String}[] + gallery_assets, gallery_callbacks, user_gallery = map(_ -> [], 1:3) + needs_rng_fix = Dict{String,Any}() + + for name ∈ backends + pname = string(Plots.PlotsBase.backend_package_name(name)) + needs_rng_fix[pname] = generate_cards(joinpath(@__DIR__, "gallery"), name, slice) + let (path, cb, assets) = makedemos(joinpath("gallery", string(name)); src = "$work/gallery") + push!(gallery, pname => joinpath("gallery", path)) + push!(gallery_callbacks, cb) + push!(gallery_assets, assets) + end + end + user_gallery, cb, assets = makedemos(joinpath("user_gallery"); src = work) + push!(gallery_callbacks, cb) + push!(gallery_assets, assets) + unique!(gallery_assets) + + pages = [ + "Home" => "index.md", + "Getting Started" => [ + "Installation" => "install.md", + "Basics" => "basics.md", + "Tutorial" => "tutorial.md", + "Series Types" => [ + "Contour Plots" => "series_types/contour.md", + "Histograms" => "series_types/histogram.md", + ], + ], + "Manual" => [ + "Input Data" => "input_data.md", + "Output" => "output.md", + "Attributes" => "attributes.md", + "Series Attributes" => "generated/attributes_series.md", + "Plot Attributes" => "generated/attributes_plot.md", + "Subplot Attributes" => "generated/attributes_subplot.md", + "Axis Attributes" => "generated/attributes_axis.md", + "Layouts" => "layouts.md", + "Recipes" => [ + "Overview" => "recipes.md", + "RecipesBase" => [ + "Home" => "RecipesBase/index.md", + "Recipes Syntax" => "RecipesBase/syntax.md", + "Recipes Types" => "RecipesBase/types.md", + "Internals" => "RecipesBase/internals.md", + "Public API" => "RecipesBase/api.md", + ], + "RecipesPipeline" => [ + "Home" => "RecipesPipeline/index.md", + "Public API" => "RecipesPipeline/api.md", + ], + ], + "Colors" => "colors.md", + "ColorSchemes" => "generated/colorschemes.md", + "Animations" => "animations.md", + "Themes" => "generated/plotthemes.md", + "Backends" => "backends.md", + "Supported Attributes" => "generated/supported.md", + ], + "Learning" => "learning.md", + "Contributing" => "contributing.md", + "Ecosystem" => [ + # "StatsPlots" => "generated/statsplots.md", #TODO: uncomment once StatsPlots is ready + # "GraphRecipes" => [ + # "Introduction" => "GraphRecipes/introduction.md", + # "Examples" => "GraphRecipes/examples.md", + # "Attributes" => "generated/graph_attributes.md", + # ], #TODO: uncomment once GraphRecipes is ready + "UnitfulExt" => [ + "Introduction" => "UnitfulExt/unitfulext.md", + "Examples" => [ + "Simple" => "generated/unitfulext_examples.md", + "Plots" => "generated/unitfulext_plots.md", + ] + ], + "Overview" => "ecosystem.md", + ], + "Advanced Topics" => ["Plot objects" => "plot_objects.md","Plotting pipeline" => "pipeline.md"], + "Gallery" => gallery, + "User Gallery" => user_gallery, + "API" => "api.md", + ] + + # those will be built pages - to skip some pages, comment them above + selected_pages = [] + collect_pages!(p::Pair) = if p.second isa AbstractVector + collect_pages!(p.second) + else + push!(selected_pages, basename(p.second)) + end + collect_pages!(v::AbstractVector) = foreach(collect_pages!, v) + + collect_pages!(pages) + unique!(selected_pages) + # @show selected_pages length(gallery) length(user_gallery) + + # FIXME: github.com/JuliaDocs/DemoCards.jl/pull/134 + # delete src/democards/bulmagridtheme.css when released + n = 0 + for (root, dirs, files) ∈ walkdir(SRC_DIR) + foreach(dir -> mkpath(joinpath(WORK_DIR, dir)), dirs) + for file ∈ files + _, ext = splitext(file) + (ext == ".md" && file ∉ selected_pages) && continue + cp(joinpath(root, file), joinpath(replace(root, SRC_DIR => WORK_DIR), file); force = true) + n += 1 + end + end + @info "copied $n source file(s) to scratch directory `$work`" + + @info "UnitfulExt" + src_unitfulext = "src/UnitfulExt" + unitfulext = joinpath(@__DIR__, src_unitfulext) + notebooks = joinpath(unitfulext, "notebooks") + + execute = true # set to true for executing notebooks and documenter + nb = false # set to true to generate the notebooks + for (root, _, files) ∈ walkdir(unitfulext), file ∈ files + last(splitext(file)) == ".jl" || continue + ipath = joinpath(root, file) + opath = replace(ipath, src_unitfulext => "$work/generated") |> splitdir |> first + Literate.markdown(ipath, opath; documenter = execute) + nb && Literate.notebook(ipath, notebooks; execute) + end + + ansicolor = get(ENV, "PLOTDOCS_ANSICOLOR", "true") == "true" + @info "makedocs" ansicolor + failed = false + try + @time makedocs(; + source = work, + format = Documenter.HTML(; + size_threshold = nothing, + prettyurls = get(ENV, "CI", nothing) == "true", + assets = ["assets/favicon.ico", gallery_assets...], + collapselevel = 2, + ansicolor, + ), + sitename = "Plots", + authors = "Thomas Breloff", + warnonly = true, + pagesonly = true, + pages, + ) + catch e + failed = true + e isa InterruptException || rethrow() + end + + foreach(gallery_callbacks) do cb + cb() # URL redirection for DemoCards-generated gallery + end + + failed && return # don't deploy and post-process on failure + + # postprocess gallery html files to remove `rng` in user displayed code + # non-exhaustive list of examples to be fixed: + # [1, 4, 5, 7:12, 14:21, 25:27, 29:30, 33:34, 36, 38:39, 41, 43, 45:46, 48, 52, 54, 62] + for name ∈ split(backends) + prefix = joinpath(@__DIR__, "build", "gallery", lowercase(name), "generated") + must_fix = needs_rng_fix[name] + for file ∈ glob("*/index.html", prefix) + (m = match(r"-ref(\d+)", file)) ≡ nothing && continue + idx = parse(Int, first(m.captures)) + get(must_fix, idx, false) || continue + lines = readlines(file; keep=true) + open(file, "w") do io + count, in_code, sub = 0, false, "" + for line ∈ lines + trailing = if (m = match(r""".*""", line)) ≢ nothing + in_code = true + m.match + else + line + end + if in_code && occursin("rng", line) + line = replace(line, r"rng\s*?,\s*" => "") + count += 1 + end + occursin("", trailing) && (in_code = false) + write(io, line) + end + count > 0 && @info "replaced $count `rng` occurrence(s) in $file" + @assert count > 0 "idx=$idx - count=$count - file=$file" + end + end + end + + # postprocess temporary work dir + src = basename(SRC_DIR) + for file ∈ glob("*/index.html", joinpath(@__DIR__, "build")) + lines = readlines(file; keep=true) + any((occursin("blob/master/docs", line) for line ∈ lines)) || continue + open(file, "w") do io + for line ∈ lines + write(io, replace(line, "blob/master/docs/$work" => "blob/master/docs/$src")) + end + end + end + + @info "deploydocs" + deploydocs( + repo = "github.com/JuliaPlots/PlotDocs.jl.git", + versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], + push_preview = true, + forcepush = true, + ) +end + +main() diff --git a/docs/src/GraphRecipes/examples.md b/docs/src/GraphRecipes/examples.md new file mode 100644 index 000000000..8cc99d38f --- /dev/null +++ b/docs/src/GraphRecipes/examples.md @@ -0,0 +1,183 @@ +```@setup graphexamples +using Plots, GraphRecipes, Graphs, LinearAlgebra, SparseArrays, AbstractTrees; gr() +Plots.reset_defaults() +``` +# [Examples](@id graph_examples) +### Undirected graph +Plot an undirected graph with labeled nodes and individual node sizes/colors. +```@example graphexamples +using GraphRecipes +using Plots + +const n = 15 +const A = Float64[ rand() < 0.5 ? 0 : rand() for i=1:n, j=1:n] +for i=1:n + A[i, 1:i-1] = A[1:i-1, i] + A[i, i] = 0 +end + +graphplot(A, + markersize = 0.2, + node_weights = 1:n, + markercolor = range(colorant"yellow", stop=colorant"red", length=n), + names = 1:n, + fontsize = 10, + linecolor = :darkgrey + ) +``` + +Now plot the graph in three dimensions. +```@example graphexamples +graphplot(A, + node_weights = 1:n, + markercolor = :darkgray, + dim = 3, + markersize = 5, + linecolor = :darkgrey, + linealpha = 0.5 + ) + +``` + +### Graphs.jl +You can visualize a `Graphs.AbstractGraph` by passing it to `graphplot`. +```julia +using GraphRecipes, Plots +using Graphs + +g = wheel_graph(10) +graphplot(g, curves=false) +``` + +![](https://user-images.githubusercontent.com/8610352/74631053-de196b80-51c0-11ea-8cba-ddbdc2c6312f.png) +#### Directed Graphs +If you pass `graphplot` a `Graphs.DiGraph` or an asymmetric adjacency matrix, then `graphplot` will use arrows to indicate the direction of the edges. Note that using the `arrow` attribute with the `pythonplot` backend will allow you to control the aesthetics of the arrows. +```julia +using GraphRecipes, Plots +g = [0 1 1; + 0 0 1; + 0 1 0] + +graphplot(g, names=1:3, curvature_scalar=0.1) +``` + +![](https://user-images.githubusercontent.com/8610352/74631107-04d7a200-51c1-11ea-87c1-be9cbf1b02eb.png) +#### Edge Labels +Edge labels can be passed via the `edgelabel` keyword argument. You can pass edge labels +as a dictionary of `(si::Int, di::Int) => label`, where `si`, `di` are the indices of the source and destiny nodes for the edge being labeled. Alternatively, you can pass a matrix or a vector of labels. `graphplot` will try to convert any label you pass it into a string unless you pass one of `missing`, `NaN`, `nothing`, `false` or `""`, in which case, `graphplot` will skip the label. + +```@example graphexamples +using GraphRecipes, Plots +using Graphs + +n = 8 +g = wheel_digraph(n) +edgelabel_dict = Dict() +edgelabel_mat = Array{String}(undef, n, n) +for i in 1:n + for j in 1:n + edgelabel_mat[i, j] = edgelabel_dict[(i, j)] = string("edge ", i, " to ", j) + end +end +edgelabel_vec = edgelabel_mat[:] + +graphplot(g, names=1:n, edgelabel=edgelabel_dict, curves=false, nodeshape=:rect) # Or edgelabel=edgelabel_mat, or edgelabel=edgelabel_vec. +``` + +#### Self edges +```@example graphexamples +using Graphs, Plots, GraphRecipes + +g = [1 1 1; + 0 0 1; + 0 0 1] + +graphplot(DiGraph(g), self_edge_size=0.2) +``` + +#### Multigraphs +```@example graphexamples +graphplot([[1,1,2,2],[1,1,1],[1]], names="node_".*string.(1:3), nodeshape=:circle, self_edge_size=0.25) +``` + +#### Arc and chord diagrams + +```@example graphexamples +using LinearAlgebra +using SparseArrays +using GraphRecipes +using Plots + +adjmat = Symmetric(sparse(rand(0:1,8,8))) + +plot( + graphplot(adjmat, + method=:chorddiagram, + names=[text(string(i), 8) for i in 1:8], + linecolor=:black, + fillcolor=:lightgray), + + graphplot(adjmat, + method=:arcdiagram, + markersize=0.5, + linecolor=:black, + markercolor=:black) + ) + +``` + + +#### Julia code -- AST + +```@example graphexamples +using GraphRecipes +using Plots +default(size=(1000, 1000)) + +code = :( +function mysum(list) + out = 0 + for value in list + out += value + end + out +end +) + +plot(code, fontsize=12, shorten=0.01, axis_buffer=0.15, nodeshape=:rect) + +``` + +#### Julia Type Trees + +```@example graphexamples +using GraphRecipes +using Plots +default(size=(1000, 1000)) + +plot(AbstractFloat, method=:tree, fontsize=10, nodeshape=:ellipse) + +``` + + +#### `AbstractTrees` Trees + +```@example graphexamples +using AbstractTrees + +AbstractTrees.children(d::Dict) = [p for p in d] +AbstractTrees.children(p::Pair) = AbstractTrees.children(p[2]) +function AbstractTrees.printnode(io::IO, p::Pair) + str = isempty(AbstractTrees.children(p[2])) ? string(p[1], ": ", p[2]) : string(p[1], ": ") + print(io, str) +end + +d = Dict(:a => 2,:d => Dict(:b => 4,:c => "Hello"),:e => 5.0) + +using GraphRecipes +using Plots +default(size=(1000, 1000)) + +plot(TreePlot(d), method=:tree, fontsize=10, nodeshape=:ellipse) + +``` diff --git a/docs/src/GraphRecipes/introduction.md b/docs/src/GraphRecipes/introduction.md new file mode 100644 index 000000000..1a5587ef4 --- /dev/null +++ b/docs/src/GraphRecipes/introduction.md @@ -0,0 +1,25 @@ +```@setup graphintro +using Plots, GraphRecipes; gr() +Plots.reset_defaults() +``` +# GraphRecipes +[GraphRecipes](https://github.com/JuliaPlots/GraphRecipes.jl) is a collection of recipes for visualizing graphs. Users specify a graph through an adjacency matrix, an adjacency list, or an `AbstractGraph` via [Graphs](https://github.com/JuliaGraphs/Graphs.jl). GraphRecipes will then use a layout algorithm to produce a visualization of the graph that the user passed. + +## Installation +GraphRecipes can be installed with the package manager: +```julia +] add GraphRecipes +``` + +## Usage +The main user interface is through the fuction `graphplot`: +```@example graphintro +using GraphRecipes, Plots + +g = [0 1 1; + 1 0 1; + 1 1 0] +graphplot(g) +``` + +See [Examples](@ref graph_examples) for example usages and [Attributes](@ref graph_attributes) for an explanation of keyword arguments to the `graphplot` function. diff --git a/docs/src/RecipesBase/api.md b/docs/src/RecipesBase/api.md new file mode 100644 index 000000000..8d25af306 --- /dev/null +++ b/docs/src/RecipesBase/api.md @@ -0,0 +1,3 @@ +```@autodocs +Modules = [RecipesBase] +``` diff --git a/docs/src/RecipesBase/index.md b/docs/src/RecipesBase/index.md new file mode 100644 index 000000000..ea56d13d8 --- /dev/null +++ b/docs/src/RecipesBase/index.md @@ -0,0 +1,15 @@ +# RecipesBase + +**Author: Thomas Breloff (@tbreloff)** + +RecipesBase is a lightweight Package without dependencies that allows to define custom visualizations with the [`@recipe`](@ref) macro. + +Package developers and users can define recipes to tell [Plots.jl](https://github.com/JuliaPlots/Plots.jl) how to plot custom types without depending on it. +Furthermore, recipes can be used for complex visualizations and new series types. +Plots, for example, uses recipes internally to define histograms or bar plots. +[StatsPlots.jl](https://github.com/JuliaPlots/StatsPlots.jl) and [GraphRecipes.jl](https://github.com/JuliaPlots/GraphRecipes.jl) extend Plots functionality for statistical plotting and visualization of graphs. + +RecipesBase exports the [`@recipe`](@ref) macro which provides a nice syntax for defining plot recipes. +Under the hood [`@recipe`](@ref) defines a new method for `RecipesBase.apply_recipe` which is called recursively in Plots at different stages of the argument processing pipeline. +This way other packages can communicate with Plots, i.e. define custom plotting recipes, only depending on RecipesBase. +Furthermore, the convenience macros [`@series`](@ref), [`@userplot`](@ref) and [`@shorthands`](@ref) are exported by RecipesBase. diff --git a/docs/src/RecipesBase/internals.md b/docs/src/RecipesBase/internals.md new file mode 100644 index 000000000..057d36607 --- /dev/null +++ b/docs/src/RecipesBase/internals.md @@ -0,0 +1,149 @@ +## RecipesBase + +The [`@recipe`](@ref) macro defines a new method for `RecipesBase.apply_recipe`. +```julia +@recipe function f(args...; kwargs...) +``` +defines +```julia +RecipesBase.apply_recipe(plotattributes, args...; kwargs...) +``` +returning a `Vector{RecipeData}` where `RecipeData` holds the `plotattributes` Dict and the arguments returned in [`@recipe`](@ref) or in [`@series`](@ref). +```julia +struct RecipeData + plotattributes::AbstractDict{Symbol,Any} + args::Tuple +end +``` +This function sets and overwrites entries in `plotattributes` and possibly adds new series. +- `attr --> val` translates to `haskey(plotattributes, :attr) || plotattributes[:attr] = val` +- `attr := val` sets `plotattributes[:attr] = val`. +- [`@series`](@ref) allows to add new series within [`@recipe`](@ref). It copies `plotattributes` from [`@recipe`](@ref), applies the replacements defined in its code block and returns corresponding new `RecipeData` object. + !!! info + [`@series`](@ref) have to be defined as a code block with `begin` and `end` statements. + ```julia + @series begin + ... + end + ``` + +So `RecipesBase.apply_recipe(plotattributes, args...; kwargs...)` returns a `Vector{RecipeData}`. +Plots can then recursively apply it again on the `plotattributes` and `args` of the elements of this vector, dispatching on a different signature. + + +## Plots + +The standard plotting commands +```julia +plot(args...; plotattributes...) +plot!(args...; plotattributes...) +``` +and shorthands like `scatter` or `bar` call the core internal plotting function `Plots._plot!`. +```julia +Plots._plot!(plt::Plot, plotattributes::AbstractDict{Symbol, Any}, args::Tuple) +``` + +In the following we will go through the major steps of the preprocessing pipeline implemented in `Plots._plot!`. + +#### Preprocess `plotattributes` +Before `Plots._plot!` is called and after each recipe is applied, `preprocessArgs!` preprocesses the `plotattributes` Dict. +It replaces aliases, expands magic arguments, and converts some attribute types. +- `lc = nothing` is replaced by `linecolor = RGBA(0, 0, 0, 0)`. +- `marker = (:red, :circle, 8)` expands to `markercolor = :red`, `markershape = :circle` and `markersize = 8`. + +#### Process User Recipes + +In the first step, `_process_userrecipe` is called. + +```julia +kw_list = _process_userrecipes(plt, plotattributes, args) +``` +It converts the user-provided `plotattributes` to a vector of `RecipeData`. +It recursively applies `RecipesBase.apply_recipe` on the fields of the first element of the `RecipeData` vector and prepends the resulting `RecipeData` vector to it. +If the `args` of an element are empty, it extracts `plotattributes` and adds it to a Vector of Dicts `kw_list`. +When all `RecipeData` elements are fully processed, `kw_list` is returned. + +#### Process Type Recipes + +After user recipes are processed, at some point in the recursion above args is of the form `(y, )`, `(x, y)` or `(x, y, z)`. +Plots defines recipes for these signatures. +The two argument version, for example, looks like this. + +```julia +@recipe function f(x, y) + did_replace = false + newx = _apply_type_recipe(plotattributes, x) + x === newx || (did_replace = true) + newy = _apply_type_recipe(plotattributes, y) + y === newy || (did_replace = true) + if did_replace + newx, newy + else + SliceIt, x, y, nothing + end +end +``` + +It recursively calls `_apply_type_recipe` on each argument until none of the arguments is replaced. +`_apply_type_recipe` applies the type recipe with the corresponding signature and for vectors it tries to apply the recipe element-wise. +When no argument is changed by `_apply_type_recipe`, the fallback `SliceIt` recipe is applied, which adds the data to `plotattributes` and returns `RecipeData` with empty args. + +#### Process Plot Recipes + +At this stage all arguments have been processed to something Plots supports. +In `_plot!` we have a `Vector{Dict}` `kw_list` with an entry for each series and already populated `:x`, `:y` and `:z` keys. +Now `_process_plotrecipe` is called until all plot recipes are processed. + +```julia +still_to_process = kw_list +kw_list = KW[] +while !isempty(still_to_process) + next_kw = popfirst!(still_to_process) + _process_plotrecipe(plt, next_kw, kw_list, still_to_process) +end +``` + +If no series type is set in the Dict, `_process_plotrecipe` pushes it to `kw_list` and returns. +Otherwise it tries to call `RecipesBase.apply_recipe` with the plot recipe signature. +If there is a method for this signature and the seriestype has changed by applying the recipe, the new `plotattributes` are appended to `still_to_process`. +If there is no method for the current plot recipe signature, we append the current Dict to `kw_list` and rely on series recipe processing. + +After all plot recipes have been applied, the plot and subplots are set-up. +```julia +_plot_setup(plt, plotattributes, kw_list) +_subplot_setup(plt, plotattributes, kw_list) +``` + +#### Process Series Recipes + +We are almost finished. +Now the series defaults are populated and `_process_seriesrecipe` is called for each series . + +```julia +for kw in kw_list + # merge defaults + series_attr = Attr(kw, _series_defaults) + _process_seriesrecipe(plt, series_attr) +end +``` + +If the series type is natively supported by the backend, we finalize processing and pass the series along to the backend. +Otherwise, the series recipe for the current series type is applied and `_process_seriesrecipe` is called again for the `plotattributes` in each returned `RecipeData` object. +Here we have to check again that the series type changed. +Due to this recursive processing, complex series types can be built up by simple blocks. +For example if we add an `@show st` in `_process_seriesrecipe` and plot a histogram, we go through the following series types: + +```julia +plot(histogram(randn(1000))) +``` +```julia +st = :histogram +st = :barhist +st = :barbins +st = :bar +st = :shape +``` +```@example +using Plots # hide +plot(histogram(randn(1000))) #hide +``` diff --git a/docs/src/RecipesBase/syntax.md b/docs/src/RecipesBase/syntax.md new file mode 100644 index 000000000..29741c335 --- /dev/null +++ b/docs/src/RecipesBase/syntax.md @@ -0,0 +1,121 @@ +```@setup syntax +using Plots, Random +Random.seed!(100) +default(legend = :topleft, markerstrokecolor = :auto, markersize = 6) +``` + +# Recipes Syntax + +The syntax in the [`@recipe`](@ref) macro is best explained using an example. +Suppose, we have a custom type storing the results of a simulation `x` and `y` and a measure `ε` for the maximum error in `y`. + +```@example syntax +struct Result + x::Vector{Float64} + y::Vector{Float64} + ε::Vector{Float64} +end +``` + +If we want to plot the `x` and `y` values of such a result with an error band given by `ε`, we could run something like +```@example syntax +res = Result(1:10, cumsum(rand(10)), cumsum(rand(10)) / 5) + +using Plots + +# plot the error band as invisible line with fillrange +plot( + res.x, + res.y .+ res.ε, + xlabel = "x", + ylabel = "y", + fill = (res.y .- res.ε, :lightgray, 0.5), + linecolor = nothing, + primary = false, # no legend entry +) + +# add the data to the plots +plot!(res.x, res.y, marker = :diamond) +``` + +Instead of typing this plot command over and over for different results we can define a **user recipe** to tell Plots what to do with input of the type `Result`. +Here is an example for such a user recipe with the additional feature to highlight datapoints with a maximal error above a certain threshold `ε_max`. + +```@example syntax +@recipe function f(r::Result; ε_max = 0.5) + # set a default value for an attribute with `-->` + xlabel --> "x" + yguide --> "y" + markershape --> :diamond + # add a series for an error band + @series begin + # force an argument with `:=` + seriestype := :path + # ignore series in legend and color cycling + primary := false + linecolor := nothing + fillcolor := :lightgray + fillalpha := 0.5 + fillrange := r.y .- r.ε + # ensure no markers are shown for the error band + markershape := :none + # return series data + r.x, r.y .+ r.ε + end + # get the seriescolor passed by the user + c = get(plotattributes, :seriescolor, :auto) + # highlight big errors, otherwise use the user-defined color + markercolor := ifelse.(r.ε .> ε_max, :red, c) + # return data + r.x, r.y +end +``` + +Let's walk through this recipe step by step. +First, the function signature in the recipe definition determines the recipe type, in this case a user recipe. +The function name `f` in is irrelevant and can be replaced by any other function name. +[`@recipe`](@ref) does not use it. +In the recipe body we can set default values for [Plots attributes](https://docs.juliaplots.org/latest/attributes/). +``` +attr --> val +``` +This will set `attr` to `val` unless it is specified otherwise by the user in the plot command. +``` +plot(args...; kw..., attr = otherval) +``` +Similarly we can force an attribute value with `:=`. +``` +attr := val +``` +This overwrites whatever the user passed to `plot` for `attr` and sets it to `val`. +!!! tip + It is strongly recommended to avoid using attribute aliases in recipes as this might lead to unexpected behavior in some cases. + In the recipe above `xlabel` is used as aliases for `xguide`. + When the recipe is used Plots will show a warning and hint to the default attribute name. + They can also be found in the attribute tables under https://docs.juliaplots.org/latest/attributes/. + +We use the [`@series`](@ref) macro to add a new series for the error band to the plot. +Within an [`@series`](@ref) block we can use the same syntax as above to force or set default values for attributes. + +In [`@recipe`](@ref) we have access to `plotattributes`. This is an `AbstractDict` storing the attributes that have been already processed at the current stage in the Plots pipeline. +For user recipes, which are called early in the pipeline, this mostly contains the keyword arguments provided by the user in the `plot` command. +In our example we want to highlight data points with an error above a certain threshold by changing the marker color. +For all other data points we set the marker color to whatever is the default or has been provided as keyword argument. +We can do this by getting the `seriescolor` from `plotattributes` and defaulting to `auto` if it has not been specified by the user. + +Finally, in both, [`@recipe`](@ref)s and [`@series`](@ref) blocks we return the data we wish to pass on to Plots (or the next recipe). + +!!! compat + With RecipesBase 1.0 the `return` statement is allowed in [`@recipe`](@ref) and [`@series`](@ref). + +With the recipe above we can now plot `Result`s with just + +```@example syntax +plot(res) +``` + +or + +```@example syntax +scatter(res, ε_max = 0.7, color = :green, marker = :star) +``` diff --git a/docs/src/RecipesBase/types.md b/docs/src/RecipesBase/types.md new file mode 100644 index 000000000..a69ed9b19 --- /dev/null +++ b/docs/src/RecipesBase/types.md @@ -0,0 +1,401 @@ +```@setup types +using Plots, Random +Random.seed!(100) +default(legend = :topleft, markerstrokecolor = :auto, markersize = 6) +``` + +# Recipe Types + +## Overview + +There are four main types of recipes which are determined by the signature of the [`@recipe`](@ref) macro. + +### User Recipes + +```julia +@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...) +``` + +!!! tip + [`@userplot`](@ref) provides a convenient way to create a custom type to dispatch on and defines custom plotting functions. + ```julia + @userplot MyPlot + @recipe function f(mp::MyPlot; ...) + ... + end + ``` + Now we can plot with: + ```julia + myplot(args...; kw...) + myplot!(args...; kw...) + ``` + +### Type Recipes + +```julia +@recipe function f(::Type{T}, val::T) where T +``` + +!!! compat + With RecipesBase 1.0 type recipes are aware of the current axis (`:x`, `:y`, `:z`). + ```julia + @recipe function f(::Type{MyType}, val::MyType) + guide --> "My Guide" + ... + end + ``` + This only sets the guide for the axes with `MyType`. + For more complex type recipes the current axis letter can be accessed in [`@recipe`](@ref) with `plotattributes[:letter]`. + +!!! compat + With RecipesBase 1.0 type recipes of the form + ```julia + @recipe function f(::Type{T}, val::T) where T <: AbstractArray{MyType} + ``` + for `AbstractArray`s of custom types are supported too. + +!!! info + User recipes and type recipes must return either + - an `AbstractArray{<:V}` where `V` is a *valid type*, + - two functions, or + - nothing + + A *valid type* is either a Plots *datapoint* or a type that can be handled by another user recipe or type recipe. + Plots *datapoints* are all subtypes of `Union{AbstractString, Missing}` and `Union{Number, Missing}`. + + If two functions are returned the former should tell Plots how to convert from `T` to a *datapoint* and the latter how to convert from *datapoint* to string for tick label formatting. + +### Plot Recipes + +```julia +@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...) +``` + +### Series Recipes + +```julia +@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...) +``` + +!!! tip + The [`@shorthands`](@ref) macro provides a convenient way to define plotting functions for custom plot recipes or series recipes. + ```julia + @shorthands myseriestype + @recipe function f(::Type{Val{:myseriestype}}, x, y, z; ...) + ... + end + ``` + This allows to plot with: + ```julia + myseriestype(args...; kw...) + myseriestype!(args...; kw...) + ``` + +!!! warning + Plot recipes and series recipes have to set the `seriestype` attribute. + +## User Recipes +User recipes are called early in the processing pipeline and allow designing custom visualizations. +```julia +@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...) +``` + +We have already seen an example for a user recipe in the syntax section above. +User recipes can also be used to define a custom visualization without necessarily wishing to plot a custom type. +For this purpose we can create a type to dispatch on. +The [`@userplot`](@ref) macro is a convenient way to do this. +```julia +@userplot MyPlot +``` +expands to +```julia +mutable struct MyPlot + args +end +export myplot, myplot! +myplot(args...; kw...) = plot(MyPlot(args); kw...) +myplot!(args...; kw...) = plot!(MyPlot(args); kw...) +``` + +To check `args` type, define a struct with type parameters. + +```julia +@userplot struct MyPlot{T<:Tuple{AbstractVector}} + args::T +end +``` + +We can use this to define a user recipe for a pie plot. +```@example types +# defines mutable struct `UserPie` and sets shorthands `userpie` and `userpie!` +@userplot UserPie +@recipe function f(up::UserPie) + y = up.args[end] # extract y from the args + # if we are passed two args, we use the first as labels + labels = length(up.args) == 2 ? up.args[1] : eachindex(y) + framestyle --> :none + aspect_ratio --> true + s = sum(y) + θ = 0 + # add a shape for each piece of pie + for i in 1:length(y) + # determine the angle until we stop + θ_new = θ + 2π * y[i] / s + # calculate the coordinates + coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + @series begin + seriestype := :shape + label --> string(labels[i]) + coords + end + θ = θ_new + end + # we already added all shapes in @series so we don't want to return a series + # here. (Technically we are returning an empty series which is not added to + # the legend.) + primary := false + () +end +``` + +Now we can just use the recipe like this: + +```@example types +userpie('A':'D', rand(4)) +``` + +## Type Recipes +Type recipes define one-to-one mappings from custom types to something Plots supports +```julia +@recipe function f(::Type{T}, val::T) where T +``` + +Suppose we have a custom wrapper for vectors. + +```@example types +struct MyWrapper + v::Vector +end +``` +We can tell Plots to just use the wrapped vector for plotting in a type recipe. +```@example types +@recipe f(::Type{MyWrapper}, mw::MyWrapper) = mw.v +``` +Now Plots knows what to do when it sees a `MyWrapper`. +```@example types +mw = MyWrapper(cumsum(rand(10))) +plot(mw) +``` +Due to the recursive application of type recipes they even compose automatically. +```@example types +struct MyOtherWrapper + w +end + +@recipe f(::Type{MyOtherWrapper}, mow::MyOtherWrapper) = mow.w + +mow = MyOtherWrapper(mw) +plot(mow) +``` +If we want an element-wise conversion of custom types we can define a conversion function to a type that Plots supports (`Real`, `AbstractString`) and a formatter for the tick labels. +Consider the following simple time type. +```@example types +struct MyTime + h::Int + m::Int +end + +# show e.g. `MyTime(1, 30)` as "01:30" +time_string(mt) = join((lpad(string(c), 2, "0") for c in (mt.h, mt.m)), ":") +# map a `MyTime` object to the number of minutes that have passed since midnight. +# this is the actual data Plots will use. +minutes_since_midnight(mt) = 60 * mt.h + mt.m +# convert the minutes passed since midnight to a nice string showing `MyTime` +formatter(n) = time_string(MyTime(divrem(n, 60)...)) + +# define the recipe (it must return two functions) +@recipe f(::Type{MyTime}, mt::MyTime) = (minutes_since_midnight, formatter) +``` +Now we can plot vectors of `MyTime` automatically with the correct tick labelling. +`DateTime`s and `Char`s are implemented with such a type recipe in Plots for example. + +```@example types +times = MyTime.(0:23, rand(0:59, 24)) +vals = log.(1:24) + +plot(times, vals) +``` +Again everything composes nicely. +```@example types +plot(MyWrapper(vals), MyOtherWrapper(times)) +``` + +## Plot Recipes +Plot recipes are called after all input data is processed by type recipes but before the plot and subplots are set-up. They allow to build series with custom layouts and set plot-wide attributes. +```julia +@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...) +``` + +Plot recipes define a new series type. +They are applied after type recipes. +Hence, standard Plots types can be assumed for input data `:x`, `:y` and `:z` in `plotattributes`. +Plot recipes can access plot and subplot attributes before they are processed, for example to build layouts. +Both, plot recipes and series recipes must change the series type. +Otherwise we get a warning that we would run into a StackOverflow error. + +We can define a seriestype `:yscaleplot`, that automatically shows data with a linear y scale in one subplot and with a logarithmic yscale in another one. +```@example types +@recipe function f(::Type{Val{:yscaleplot}}, plt::AbstractPlot) + x, y = plotattributes[:x], plotattributes[:y] + layout := (1, 2) + for (i, scale) in enumerate((:linear, :log)) + @series begin + title --> string(scale, " scale") + seriestype := :path + subplot := i + yscale := scale + end + end +end +``` +We can call it with `plot(...; ..., seriestype = :yscaleplot)` or we can define a shorthand with the [`@shorthands`](@ref) macro. +```julia +@shorthands myseries +``` +expands to +```julia +export myseries, myseries! +myseries(args...; kw...) = plot(args...; kw..., seriestype = :myseries) +myseries!(args...; kw...) = plot!(args...; kw..., seriestype = :myseries) +``` +So let's try the `yscaleplot` plot recipe. +```@example types +@shorthands yscaleplot + +yscaleplot((1:10).^2) +``` +Magically the composition with type recipes works again. +```@example types +yscaleplot(MyWrapper(times), MyOtherWrapper((1:24).^2)) +``` +## Series Recipes +Series recipes are applied recursively until the current backend supports a series type. They are used for example to convert the input data of a bar plot to the coordinates of the shapes that define the bars. +```julia +@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...) +``` + +If we want to call the `userpie` recipe with a custom type we run into errors. +```julia +userpie(MyWrapper(rand(4))) +``` +```julia +ERROR: MethodError: no method matching keys(::MyWrapper) +Stacktrace: + [1] eachindex(::MyWrapper) at ./abstractarray.jl:209 +``` +Furthermore, if we want to show multiple pie charts in different subplots, we don't get what we expect either +```@example types +userpie(rand(4, 2), layout = 2) +``` +We could overcome these issues by implementing the required `AbstractArray` methods for `MyWrapper` (instead of the type recipe) and by more carefully dealing with different series in the `userpie` recipe. +However, the simpler approach is writing the pie recipe as a series recipe and relying on Plots' processing pipeline. +```@example types +@recipe function f(::Type{Val{:seriespie}}, x, y, z) + framestyle --> :none + aspect_ratio --> true + s = sum(y) + θ = 0 + for i in eachindex(y) + θ_new = θ + 2π * y[i] / s + coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + @series begin + seriestype := :shape + label --> string(x[i]) + x := first.(coords) + y := last.(coords) + end + θ = θ_new + end +end +@shorthands seriespie +``` +Here we use the already processed values `x` and `y` to calculate the shape coordinates for each pie piece, update `x` and `y` with these coordinates and set the series type to `:shape`. +```@example types +seriespie(rand(4)) +``` +This automatically works together with type recipes ... +```@example types +seriespie(MyWrapper(rand(4))) +``` +... or with layouts +```@example types +seriespie(rand(4, 2), layout = 2) +``` + +## Remarks + +Plot recipes and series recipes are actually very similar. +In fact, a pie recipe could be also implemented as a plot recipe by acessing the data through `plotattributes`. + +```@example types +@recipe function f(::Type{Val{:plotpie}}, plt::AbstractPlot) + y = plotattributes[:y] + labels = plotattributes[:x] + framestyle --> :none + aspect_ratio --> true + s = sum(y) + θ = 0 + for i in 1:length(y) + θ_new = θ + 2π * y[i] / s + coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + @series begin + seriestype := :shape + label --> string(labels[i]) + x := first.(coords) + y := last.(coords) + end + θ = θ_new + end +end +@shorthands plotpie + +plotpie(rand(4, 2), layout = (1, 2)) +``` +The series recipe syntax is just a little nicer in this case. + +!!! info + Here's subtle difference between these recipe types: + Plot recipes are applied in any case while series are only applied if the backend does not support the series type natively. + +Let's try it the other way around and implement our `yscaleplot` recipe as a series recipe. + +```@example types +@recipe function f(::Type{Val{:yscaleseries}}, x, y, z) + layout := (1, 2) + for (i, scale) in enumerate((:linear, :log)) + @series begin + title --> string(scale, " scale") + seriestype := :path + subplot := i + yscale := scale + end + end +end +@shorthands yscaleseries +``` +That looks a little nicer than the plot recipe version as well. +Let's try to plot. +```julia +yscaleseries((1:10).^2) +``` +```julia +MethodError: Cannot `convert` an object of type Int64 to an object of type Plots.Subplot{Plots.GRBackend} +Closest candidates are: + convert(::Type{T}, !Matched::T) where T at essentials.jl:168 + Plots.Subplot{Plots.GRBackend}(::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any) where T<:RecipesBase.AbstractBackend at /home/daniel/.julia/packages/Plots/rNwM4/src/types.jl:88 +``` + +That is because the plot and subplots have already been built before the series recipe is applied. + +!!! tip + For everything that modifies plot-wide attributes plot recipes have to be used, otherwise series recipes are recommended. diff --git a/docs/src/RecipesPipeline/api.md b/docs/src/RecipesPipeline/api.md new file mode 100644 index 000000000..6ec819cfc --- /dev/null +++ b/docs/src/RecipesPipeline/api.md @@ -0,0 +1,3 @@ +```@autodocs +Modules = [RecipesPipeline] +``` diff --git a/docs/src/RecipesPipeline/index.md b/docs/src/RecipesPipeline/index.md new file mode 100644 index 000000000..638bbcc6e --- /dev/null +++ b/docs/src/RecipesPipeline/index.md @@ -0,0 +1,5 @@ +# RecipesPipeline + +## An implementation of the recipe pipeline from Plots + +This package was factored out of `Plots.jl` to allow any other plotting package to use the recipe pipeline. In short, the extremely lightweight `RecipesBase` package can be depended on by any package to define "recipes": plot specifications of user-defined types, as well as custom plot types. `RecipePipeline` contains the machinery to translate these recipes to full specifications for a plot. diff --git a/docs/src/UnitfulExt/unitfulext.md b/docs/src/UnitfulExt/unitfulext.md new file mode 100644 index 000000000..66c13d1ad --- /dev/null +++ b/docs/src/UnitfulExt/unitfulext.md @@ -0,0 +1,25 @@ +*for plotting data with units seamlessly in Julia* + +`Plots` provides `Unitful` recipes for plotting figures when using data with [Unitful.jl](https://github.com/PainterQubits/Unitful.jl) units. + +!!! note + Since julia `1.9`, the module formerly known as `UnitfulRecipes` has been moved to a weak dependency called `UnitfulExt`. + +--- + +### Documentation + +The goal is that if you can plot something with [Plots.jl](https://github.com/JuliaPlots/Plots.jl) then you should be able to plot the same thing with units. + +Essentially, `Unitful` recipes strips the units of your data and appends them to the corresponding axis labels. + +Pictures speak louder than words, so we wrote some examples (accessible through the links on the left) for you to get an idea of what this package does or to simply try it out for yourself! + +!!! note "You can run the examples!" + These examples are available as Jupyter notebooks (through [nbviewer](https://nbviewer.jupyter.org/) or [binder](https://mybinder.org/))! + +--- + +### Ommissions, bugs, and contributing + +Please do not hesitate to raise an [issue](https://github.com/JuliaPlots/Plots.jl/issues) or submit a [PR](https://github.com/JuliaPlots/Plots.jl/pulls) if you would like a new recipe to be added. diff --git a/docs/src/UnitfulExt/unitfulext_examples.jl b/docs/src/UnitfulExt/unitfulext_examples.jl new file mode 100644 index 000000000..dc891dd42 --- /dev/null +++ b/docs/src/UnitfulExt/unitfulext_examples.jl @@ -0,0 +1,215 @@ +#--------------------------------------------------------- +# # [Simple Examples](@id 1_Examples) +#--------------------------------------------------------- + +#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/1_Examples.ipynb) +#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/1_Examples.ipynb) + +#md # !!! note +#md # These examples are available as Jupyter notebooks. +#md # You can execute them online with [binder](https://mybinder.org/) or just view them with [nbviewer](https://nbviewer.jupyter.org/) by clicking on the badges above! + +# These examples show what `Unitful` recipes are all about. + +# First we need to tell Julia we are using Unitful and Plots + +using Unitful, Plots + +# ## Simplest plot + +# This is the most basic example + +y = randn(10)*u"kg" +plot(y) + +# Add some more plots, and it will be aware of the units you used previously (note `y2` is about 10 times smaller than `y1`) + +y2 = 100randn(10)*u"g" +plot!(y2) + + +# `Unitful` recipes will not allow you to plot with different unit-dimensions, so +# ```julia +# plot!(rand(10)*u"m") +# ``` +# won't work here. +# +# But you can add inset subplots with different axes that have different dimensions + +plot!(rand(10)*u"m", inset=bbox(0.5, 0.5, 0.3, 0.3), subplot=2) + +# ## Axis label + +# If you specify an axis label, the unit will be appended to it. + +plot(y, ylabel="mass") + +# Unless you want it untouched, in which case you can use a "protected" string using the `@P_str` macro. + +plot(y, ylabel=P"mass in kilograms") + +# Just like with the `label` keyword for legends, no axis label is added if you specify the axis label to be an empty string. + +plot(y, ylabel="") + +# ### Unit formatting + +# If you prefer some other formatting over the round parentheses, you can +# supply a keyword `unitformat`, which can be a number of different things: + +# `unitformat` can be a boolean or `nothing`: + +plot([plot(y, ylab="mass", title=repr(s), unitformat=s) for s in (nothing, true, false)]...) + +# `unitformat` can be one of a number of predefined symbols, defined in + +URsymbols = if isdefined(Base, :get_extension) + getproperty(Base.get_extension(Plots, :UnitfulExt), :UNIT_FORMATS) +else + Plots.UnitfulExt.UNIT_FORMATS +end |> keys + +# which correspond to these unit formats: + +plot([plot(y, ylab="mass", title=repr(s), unitformat=s) for s in URsymbols]...) + +# `unitformat` can also be a `Char`, a `String`, or a `Tuple` (of `Char`s or +# `String`s), which will be inserted around the label and unit depending on the +# length of the tuple: + +URtuples = [", in ", (", in (", ")"), ("[", "] = (", ")"), ':', ('$', '$'), (':', ':', ':')] +plot([plot(y, ylab="mass", title=repr(s), unitformat=s) for s in URtuples]...) + +# For *extreme* customizability, you can also supply a function that turns two +# arguments (label, unit) into a string: + +formatter(l, u) = string("\$\\frac{\\textrm{", l, "}}{\\mathrm{", u, "}}\$") +plot(y, ylab="mass", unitformat=formatter) + +# ## Axis unit + +# You can use the axis-specific keyword arguments to convert units on the fly + +plot(y, yunit=u"g") + +# ## Axis limits and ticks + +# Setting the axis limits and ticks can be done with units + +x = (1:length(y)) * u"μs" +plot(x, y, ylims=(-1000u"g",2000u"g"), xticks = x[[1,end]]) + +# or without + +plot(x, y, ylims=(-1,2), xticks=1:3:length(x)) + +# ## Multiple series + +# You can plot multiple series as 2D arrays + +x, y = rand(10,3)*u"m", rand(10,3)*u"g" +plot(x, y) + +# Or vectors of vectors (of potentially different lengths) + +x, y = [rand(10), rand(15), rand(20)]*u"m", [rand(10), rand(15), rand(20)]*u"g" +plot(x, y) + +# ## 3D + +# It works in 3D + +x, y = rand(10)*u"km", rand(10)*u"hr" +z = x ./ y +plot(x, y, z) + +# ## Heatmaps + +# For which colorbar limits (`clims`) can have units + +heatmap((1:5)u"μs", 1:4, rand(5,4)u"m", clims=(0u"m", 2u"m")) + +# ## Scatter plots + +# You can do scatter plots + +scatter(x, y, zcolor=z, clims=(5,20).*unit(eltype(z))) + +# and 3D scatter plots too + +scatter(x, y, z, zcolor=z) + + +# ## Contour plots + +# for contours plots + +x, y = (1:0.01:2)*u"m", (1:0.02:2)*u"s" +z = x' ./ y +contour(x, y, z) + +# and filled contours, again with optional `clims` units + +contourf(x, y, z, clims=(0u"m/s", 3u"m/s")) + + +# ## Error bars + +# For example, you can use the `yerror` keyword argument with units, +# which will be converted to the units of `y` and plot your errorbars: + +using Unitful: GeV, MeV, c +x = (1.0:0.1:10) * GeV/c +y = @. (2 + sin(x / (GeV/c))) * 0.4GeV/c^2 # a sine to make it pretty +yerror = 10.9MeV/c^2 * exp.(randn(length(x))) # some noise for pretty again +plot(x, y; yerror, title="My unitful data with yerror bars", lab="") + + +# ## Ribbon + +# You can use units with the `ribbon` feature: + +x = 1:10 +plot(x, -x.^2 .* 1u"m", ribbon=500u"cm") + + +# ## Functions +# +# In order to plot a unitful function on a unitful axis, supply as a second argument a +# vector of unitful sample points, or the unit for the independent axis: + +model(x) = 1u"V"*exp(-((x-0.5u"s")/0.7u"s")^2) +t = randn(10)u"s" # Sample points +U = model.(t) + randn(10)u"dV" .|> u"V" # Noisy acquicisions +plot(t, U; xlabel="t", ylabel="U", st=:scatter, label="Samples") +plot!(model, t; st=:scatter, label="Noise removed") +plot!(model, u"s"; label="True function") + +# ## Initializing empty plot +# +# A plot can be initialized with unitful axes but without datapoints by +# simply supplying the unit: + +plot(u"m", u"s") +plot!([2u"ft"], [1u"minute"], st=:scatter) + +# ## Aspect ratio +# +# Unlike in a normal unitless plot, the aspect ratio of a unitful plot is in turn a unitful +# number $r$, such that $r\cdot \hat{y}$ would take as much space on the $x$ axis as +# $\hat{y}$ does on the $y$ axis. +# +# By default, `aspect_ratio` is set to `:auto`, which lets you ignore this. +# +# Another special value is `:equal`, which (possibly unintuitively) corresponds to $r=1$. +# Consider a rectangle drawn in a plot with $\mathrm{m}$ on the $x$ axis and +# $\mathrm{km}$ on the $y$ axis. If the rectangle is +# $100\;\mathrm{m} \times 0.1\;\mathrm{km}$, `aspect_ratio=:equal` will make it appear +# square. + +plot( + plot(randn(10)u"m", randn(10)u"dm"; aspect_ratio=:equal, title=":equal"), + plot(randn(10)u"m", randn(10)u"s"; aspect_ratio=2u"m/s", + title="\$2\\;\\mathrm{m}/\\mathrm{s}\$"), + plot(randn(10)u"m", randn(10); aspect_ratio=5u"m", title="\$5\\;\\mathrm{m}\$") + ) diff --git a/docs/src/UnitfulExt/unitfulext_plots.jl b/docs/src/UnitfulExt/unitfulext_plots.jl new file mode 100644 index 000000000..67a1ce3a3 --- /dev/null +++ b/docs/src/UnitfulExt/unitfulext_plots.jl @@ -0,0 +1,196 @@ +#--------------------------------------------------------- +# # [Plots.jl examples](@id 2_Plots) +#--------------------------------------------------------- + +#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/examples/2_Plots.ipynb) +#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/examples/2_Plots.ipynb) + +#md # !!! note +#md # These examples are available as Jupyter notebooks. +#md # You can execute them online with [binder](https://mybinder.org/) or just view them with [nbviewer](https://nbviewer.jupyter.org/) by clicking on the badges above! + +# These examples were slightly modified from some of [the examples in the Plots.jl documentation](https://github.com/JuliaPlots/Plots.jl/blob/master/src/examples.jl) and can be used as both a tutorial or as a series of test for `Unitful` recipes. +# (they are essentially the same except we have added some units to the data). + +# First we need to tell Julia we are using Unitful and Plots + +using Unitful, Plots + +# ## Lines + +plot(Plots.fakedata(50, 5) * u"m", w=3) + +# ## Parametric plots + +plot(t -> sin(t)*u"s", t -> sin(2t)*u"m", 0, 2π, line=4, leg=false, fill=(0, :orange)) + +# ## Colors + +y = rand(100)*u"km" +plot((0:10:100)*u"hr", rand(11, 4)*u"km", lab="lines", w=3, palette=:grays, fill=0, α=0.6) +scatter!(y, zcolor=abs.(y .- 0.5u"km"), m=(:heat, 0.8, Plots.stroke(1, :green)), ms=10 * abs.(y .- 0.5u"km") .+ 4u"km", lab="grad") + +# ## Global + +# Note that a few changes had to be made for this to work. + +using Statistics +y = rand(20, 3)*u"W" +x = (1:size(y,1))*u"Hz" +plot(x, y, xlabel="XLABEL", xlims=(-5, 30), xflip=true, xticks=0:2:20, background_color=RGB(0.2, 0.2, 0.2), leg=false) +hline!(mean(y, dims=1) + rand(1, 3)*u"W", line=(4, :dash, 0.6, [:lightgreen :green :darkgreen])) +vline!([5, 10]*u"Hz") +title!("TITLE") +yaxis!("YLABEL", :log10) + +# ## Arguments + +ys = Vector[rand(10), rand(20)] .* u"km" +plot(ys, color=[:black :orange], line=(:dot, 4), marker=([:hex :d], 12, 0.8, Plots.stroke(3, :gray))) + +# ## Build plot in pieces + +plot(rand(100) / 3 * u"km", reg=true, fill=(0, :green)) +scatter!(rand(100) * u"km", markersize=6, c=:orange) + +# ## Histogram2D + +histogram2d(randn(10000) * u"cm", randn(10000) * u"cm", nbins=20) + +# ## Line types + +# ``` +# linetypes = [:path :steppre :steppost :sticks :scatter] +# n = length(linetypes) +# x = Vector[sort(rand(20)) for i = 1:n] * u"km" +# y = rand(20, n) * u"ms" +# plot(x, y, line=(linetypes, 3), lab=map(string, linetypes), ms=15) +# ``` + +# ## Line styles + +styles = intersect([:solid, :dash, :dot, :dashdot, :dashdotdot], Plots.supported_styles()) +styles = reshape(styles, 1, length(styles)) +n = length(styles) +y = cumsum(randn(20, n), dims=1) * u"km" +plot(y, line=(5, styles), label=map(string, styles), legendtitle="linestyle") + +# ## Ribbons + +# Ribbons can be added to lines via the `ribbon` keyword; +# you can pass: +# * an array (for symmetric ribbons) +# * a function +# * a number +# (Tuple of arrays for upper and lower bounds are currently unsupported.) +# + +x = y = (0:10)*u"m" +plot( + plot(x,y; ribbon = (0:0.5:5)*u"m", label = "Vector"), + plot(x,y; ribbon = sqrt, label = "Function"), + plot(x,y; ribbon = 1u"m", label = "Constant"), + link=:all +) + +# ## Fillrange + +# The fillrange keyword defines a second line and fills between it and the y data. +# Note: ribbons are fillranges. + +x = y = (0:10)*u"m" +plot( + plot(x,y; fillrange = (0:0.5:5)*u"m", label = "Vector"), + plot(x,y; fillrange = sin, label = "Function"), + plot(x,y; fillrange = 0u"m", label = "Constant"), + link = :all +) + +# ## Marker types + +markers = intersect(Plots._shape_keys, Plots.supported_markers()) +markers = reshape(markers, 1, length(markers)) +n = length(markers) +x = (range(0, stop=10, length=n + 2))[2:end - 1] * u"km" +y = repeat(reshape(reverse(x), 1, :), n, 1) +scatter(x, y, m=(8, :auto), lab=map(string, markers), bg=:linen, xlim=(0, 10), ylim=(0, 10)) + +# ## Bar + +bar(randn(99) * u"km") + +# ## Histogram + +histogram(randn(1000) * u"km", bins=:scott, weights=repeat(1:5, outer=200)) + +# ## Subplots + +l = @layout([a{0.1h};b [c;d e]]) +plot(randn(100, 5) * u"km", layout=l, t=[:line :histogram :scatter :steppre :bar], leg=false, ticks=nothing, border=:none) + +# ## Adding to subplots + +plot(Plots.fakedata(100, 10) * u"km", layout=4, palette=[:grays :blues :heat :lightrainbow], bg_inside=[:orange :pink :darkblue :black]) + +# ## Contour plots + +x = (1:0.05:10) * u"m" +y = (1:0.01:2) * u"s" +f(x,y) = x^2 / y +z = f.(x',y) +p1 = contour(x, y, f, fill=true) +p2 = contour(x, y, z) +p3 = contourf(x, y, z) +plot(p1, p2, p3) + +# ## 3D + +n = 100 +ts = range(0, stop=8π, length=n) * u"rad" +x = @. ts * cos(ts) +y = @. 0.1ts * sin(ts) +z = ts +plot(x, y, z, zcolor=reverse(z), m=(10, 0.8, :blues, Plots.stroke(0)), leg=false, cbar=true, w=5, xlabel="x", ylabel="y", zlabel="z") +plot!(zeros(n), zeros(n), z, w=5) + +# ## Groups and Subplots + +group = rand(map((i->"group $(i)"), 1:4), 100) +plot(rand(100)*u"km", layout=@layout([a b;c]), group=group, linetype=[:bar :scatter :steppre], linecolor=:match) + +# ## Heatmap, categorical axes, and aspect_ratio + +xs = [string("x", i) for i = 1:10] +ys = [string("y", i) for i = 1:4] +z = float((1:4) * reshape(1:10, 1, :)) * u"km" +heatmap(xs, ys, z, aspect_ratio=1) + +# ## Magic grid argument + +x = rand(10) * u"km" +p1 = plot(x, title="Default looks") +p2 = plot(x, grid=(:y, :olivedrab, :dot, 1, 0.9), title="Modified y grid") +p3 = plot(deepcopy(p2), title="Add x grid") +xgrid!(p3, :on, :cadetblue, 2, :dashdot, 0.4) +plot(p1, p2, p3, layout=(1, 3), label="", fillrange=0, fillalpha=0.3) + +# ## Framestyle + +# Suggestion: we might want to not add the unit label when the axis is not shown? + +scatter(fill(randn(10), 6) * u"m", fill(randn(10), 6) * u"s", framestyle=[:box :semi :origin :zerolines :grid :none], title=[":box" ":semi" ":origin" ":zerolines" ":grid" ":none"], color=permutedims(1:6), layout=6, label="", markerstrokewidth=0, ticks=-2:2) + +# ## Lines and markers with varying colors + +# note that marker_z as a function did not work so it is modified here + +t = range(0, stop=1, length=100) * u"s" +θ = 6π * u"rad/s" * t +x = @. t * cos(θ) +y = @. t * sin(θ) +z = x + y +p1 = plot(x, y, line_z=t, linewidth=3, legend=false) +p2 = scatter(x, y, marker_z=z, color=:bluesreds, legend=false) +plot(p1, p2) + + diff --git a/docs/src/animations.md b/docs/src/animations.md new file mode 100644 index 000000000..dd7c907fc --- /dev/null +++ b/docs/src/animations.md @@ -0,0 +1,76 @@ +```@setup animations +using Plots; gr() +Plots.reset_defaults() +``` + +### [Animations](@id animations) + +Animations are created in 3 steps: + +- Initialize an `Animation` object. +- Save each frame of the animation with `frame(anim)`. +- Convert the frames to an animated gif with `gif(anim, filename, fps=15)` + +!!! tip + The convenience macros `@gif` and `@animate` simplify this code immensely. See the [home page](@ref simple-is-beautiful) for examples of the short version, or the [gr example](@ref gr_demo_2) for the long version. + +--- + +### Convenience macros + +There are two macros for varying levels of convenience in creating animations: `@animate` and `@gif`. The main difference is that `@animate` will return an `Animation` object for later processing, and `@gif` will create an animated gif file (and display it when returned to an IJulia cell). + +Use `@gif` for simple, one-off animations that you want to view immediately. Use `@animate` for anything more complex. Constructing `Animation` objects can be done when you need full control of the life-cycle of the animation (usually unnecessary though). + +Examples: + +```@example animations +using Plots + +@userplot CirclePlot +@recipe function f(cp::CirclePlot) + x, y, i = cp.args + n = length(x) + inds = circshift(1:n, 1 - i) + linewidth --> range(0, 10, length = n) + seriesalpha --> range(0, 1, length = n) + aspect_ratio --> 1 + label --> false + x[inds], y[inds] +end + +n = 150 +t = range(0, 2π, length = n) +x = sin.(t) +y = cos.(t) + +anim = @animate for i ∈ 1:n + circleplot(x, y, i) +end +gif(anim, "anim_fps15.gif", fps = 15) +``` + +```@example animations +gif(anim, "anim_fps30.gif", fps = 30) +``` + +The `every` flag will only save a frame "every N iterations": + +```@example animations +@gif for i ∈ 1:n + circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) +end every 5 +``` + +The `when` flag will only save a frame "when the expression is true" + +```@example animations +n = 400 +t = range(0, 2π, length = n) +x = 16sin.(t).^3 +y = 13cos.(t) .- 5cos.(2t) .- 2cos.(3t) .- cos.(4t) + +@gif for i ∈ 1:n + circleplot(x, y, i, line_z = 1:n, cbar = false, c = :reds, framestyle = :none) +end when i > 40 && mod1(i, 10) == 5 +``` diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 000000000..2137dfcbc --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,70 @@ +# [References](@id api) + +## Contents +```@contents +Pages = ["api.md"] +Depth = 4 +``` + +## Index + +```@index +Pages = ["api.md"] +``` + +## Public Interface + +### Plot specification +```@docs +plot +bbox +grid +@layout +default +theme +with +``` + +```@autodocs +Modules = [Plots] +Pages = ["components.jl"] +Order = [:function] +``` + +```@autodocs +Modules = [Plots] +Pages = ["shorthands.jl"] +``` + +### Animations +```@docs +animate +frame +gif +mov +mp4 +webm +@animate +@gif +``` + +### Retriever + +```@docs +current +Plots.xlims +Plots.ylims +Plots.zlims +backend_object +plotattr +``` + +### Output +```@docs +display +``` + +```@autodocs +Modules = [Plots] +Pages = ["output.jl"] +``` diff --git a/docs/src/assets/axis_logo.png b/docs/src/assets/axis_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..7a5e516eb55fa60450d9757f0909f95f8f6448f2 GIT binary patch literal 73378 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/axis_logo.svg b/docs/src/assets/axis_logo.svg new file mode 100755 index 000000000..37b4c6e57 --- /dev/null +++ b/docs/src/assets/axis_logo.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/assets/axis_logo_600x400.png b/docs/src/assets/axis_logo_600x400.png new file mode 100644 index 0000000000000000000000000000000000000000..2d212c3d25b318a9864a5b5f0c48b37060ecce95 GIT binary patch literal 16253 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/favicon.ico b/docs/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..73f818ed2d0bbc30b3a397c513f4ac7a07a9bc08 GIT binary patch literal 2533 zcmVg3EY?CgK;z2|)Q zJnnzbSq5E*+_fRa~pt?r++yN!#uR=!dO3W9Jd<4Apl-4v7*k1 z-U{F?hGFsuGX;R-xL*P|?9Av_i04-f!+gqc9H$0Q4M6q^Dd&tTIsl^x0Ph3%oiih> zJUX2Y$B!S!tXZ=V8XC%Hs7rY{mIbTVLnxI3LScXAK0p8{2N20;bZKcR)~#EIRjXEE zZ!>=>j{DbAcZgPAjD@|j}5yqYUtq4rM)p}$W;*AtQi1R!|xR`?{RfMIOO&EZ~lam99L}Is?#b`luT{}8826+2JKLxw@F89R7z_p+IdTL$cI<#gqoM1JA3q+aPoKt^F=K4&5}`^j z+{dj8)i7vH@Kj0>za#>q5(D_i_FP`GrWFk*j-xUEB+TvY5X$B7OI?D1H`YNkY#6UK zcmeR}(IdL!`T6-ao=r^F6UxjP&$$96Wds{{H?{=-TfZ zasKz$VKK8dCB!lzmh4T2U#NQnrXzFU1;Bv=2e5ztembYwY=%fAqMsupBdHZwgCVhi z!t86%Jk;9OSB3jx{=1Xl;rn8px10u`t*s5KSFfg~PjvoA7;=p1$k+JkA>#3x;8YpV$uX`wxvcb)f~{-tL50#2_WX z3u!ZzPw|58Hu6<)?0iiqhLXnt+cC7_R&OAoJWev3iLLM>!GSmE# zI^73C!L#G;XZejdb+Hvj)04C-mkO|Xl^S!VI8M1k1^`$VZN=B|;PcO*uf7M7LV@A) z7Q#Pm8I1=`8Z&b88u0baj%NUg87!Tl#QHQpy3|>|c zACETRY!SKXdPZ-pRAK2%rNhuPv;a5|ulffAjutkfq^uLf;^!p$AY=Y85FonJof-oU zop^-uYCXu4$+4bzJ3|eH=L;4C%mxcR7YKw5B%WdjdpQ?$$E3ZJMg3zFHTfWw1Wym2 zc5E!mp|Qn+syaO+VgaJVWIaH79@cKN0G-Bw+yAaZLuD((QXxX(R0v5>LnLv$OAhUL z01R#cfVHr=akdIK3#wq~G=UK?@EPHO>6x(zh#o!w@drNCEdc1-jrd3QHM;v&B8Qoz z6;X&^95HY$1~7wL05Eo%aQ>rfcu>}22f&m!#vyk8s2<`sb~fN@O%XV7h!2>Ik&0jl zTy>q)Z8f(5K-R4Ist#WtErn5Mrtb{?VLq6a83SdIx1C6p&E?o%^bT%6E~R|qRB?Fw zXYXOGe=P5J4QC9)Z2+)l7S$yUsIO>-L@q+`1T_VL!~-i?SThb?&&Kg@|7!b=?0iP- z25gwP8DgRRy`wVJ|0(3y1j>4-RF zC2dEHDxqJ&OK;ZEEFcrg`!(KKvklIlbPG3G`r#+#FOz(?lz zrNmi*UCrkb0JO#qeDS{{_}h(7Y`#kbog9>cZ7Clh(EBH@mfJQTxt%6KRoK7kFkAwF znKj{JRUr;t`w)%V1}dCXBt>e}3T&9T1>RDhK?9(ytPGbfT|#1FBBG+Aco%A20szsj zsk0Gh%fGg(%~kdQ#H=ci4ZhM=Gzr$Fx) z0HiQVZiSaGUyfb7cG)e|YG{FpHNa1<0z==<2cNRDv$1pMPLR?)aRt(}v1rjEdTSjU z8w-yELhM(L*kP>tL_6rsidTYmg`CJ)=ESMrBaDW zlO|!_ym?4TNkLp3X?(M%UI8E#pT3B+$Pf^GeSM);lNO_)B~rB_@(>H?O2isS@irnN z0(0if!M=U_oCAOqLHnYLiV8Z}j2ScRs=`ABLt$Yd?NKn9OjHEYs}K?rLSw>|DO2eG z@bK_nH>+PUz*-m~F<|%Z-9x1}U6<^7W@aXJNn(-2EoaZ3jj2ZuxF>EixFtoYJ?rxgn vxQPIM4dC@x@COv6gW?o`PZ)+Nwu1J5|Avso=6&^b00000NkvXXu0mjfO`NXA literal 0 HcmV?d00001 diff --git a/docs/src/assets/hdf5_samplestruct.png b/docs/src/assets/hdf5_samplestruct.png new file mode 100644 index 0000000000000000000000000000000000000000..00316cdaf568a26ad404276b8e2ea36cf69e276d GIT binary patch literal 22391 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 b<3H9tKr{$Cj2)+dq9TskI9XR%p1Ay9xhL~E literal 0 HcmV?d00001 diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..7a5e516eb55fa60450d9757f0909f95f8f6448f2 GIT binary patch literal 73378 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/old_batman_logo.png b/docs/src/assets/old_batman_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1eb3b10502cc0803301fde65d2bfab11e6d17607 GIT binary patch literal 6702 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 -cos(s) * log(s) + g = t -> sin(t) * log(t) + [f g] + end + + @series begin + subplot := 2 + (n > 2) + RecipesBase.recipetype(:groupedbar, d) + end + + if n > 2 + @series begin + subplot := 2 + line_z := t + label := false + seriescolor := :viridis + seriestype := surface + t, t, (x, y) -> x * sin(x) - y * cos(y) + end + + @series begin + subplot := 4 + seriestype := contourf + t, t, (x, y) -> x * sin(x) - y * cos(y) + end + end +end +``` + +# [Backends](@id backends) + +Backends are the lifeblood of Plots, and the diversity between features, approaches, and strengths/weaknesses was +one of the primary reasons that I started this package. + +For those who haven't had the pleasure of hacking on 15 different plotting APIs: first, consider yourself lucky. +However, you will probably have a hard time choosing the right backend for your task at hand. +This document is meant to be a guide and introduction to make that choice. + +# At a glance + +My favorites: `GR` for speed, `Plotly(JS)` for interactivity, `UnicodePlots` for REPL/SSH and `PythonPlot` otherwise. + +| If you require... | then use... | +| :------------------------ | :------------------------------------------ | +| features | GR, PythonPlot, Plotly(JS), Gaston | +| speed | GR, UnicodePlots, InspectDR, Gaston | +| interactivity | PythonPlot, Plotly(JS), InspectDR | +| beauty | GR, Plotly(JS), PGFPlots/ PGFPlotsX | +| REPL plotting | UnicodePlots | +| 3D plots | GR, PythonPlot, Plotly(JS), Gaston | +| a GUI window | GR, PythonPlot, PlotlyJS, Gaston, InspectDR | +| a small footprint | UnicodePlots, Plotly | +| backend stability | PythonPlot, Gaston | +| plot+data -> `.hdf5` file | HDF5 | + +Of course this list is rather subjective and nothing in life is that simple. Likely there are subtle tradeoffs between backends, long hidden bugs, and more excitement. Don't be shy to try out something new ! + +--- + +## [GR](https://github.com/jheinen/GR.jl) + +The default backend. Very fast with lots of plot types. Still actively developed and improving daily. + +```@example backends +gr(); backendplot() #hide +``` + +Pros: + +- Speed +- 2D and 3D +- Standalone or inline + +Cons: + +- Limited interactivity + +Primary author: Josef Heinen (@jheinen) + +### Fine tuning +It is possible to use more features of `GR` via the [`extra_kwargs`](@ref extra_kwargs) mechanism. + +```@example backends +using Plots; gr() + +x = range(-3, 3, length=30) +surface( + x, x, (x, y)->exp(-x^2 - y^2), c=:viridis, legend=:none, + nx=50, ny=50, display_option=Plots.GR.OPTION_SHADED_MESH, # <-- series[:extra_kwargs] +) +``` + +#### Supported `:subplot` `:extra_kwargs` + +| Keyword | Description | +| :------------- | :---------------------------------- | +| legend_hfactor | Vertical spacing factor for legends | +| legend_wfactor | Multiplicative factor influencing the legend width | + +#### Supported `:series` `:extra_kwargs` + +| Series Type | Keyword | Description | +| :----------------------- | :------------- | :----------------------------------------------------------------------------------------------- | +| `:surface` | nx | Number of interpolation points in the x direction | +| `:surface` | ny | Number of interpolation points in the y direction | +| `:surface`, `:wireframe` | display_option | see [GR doc](https://gr-framework.org/julia-gr.html#GR.surface-e3e6f234cc6cd4713b8727c874a5f331) | + + +## [Plotly / PlotlyJS](https://github.com/spencerlyon2/PlotlyJS.jl) + +These are treated as separate backends, though they share much of the code and use the Plotly JavaScript API. +`plotly()` is the only dependency-free plotting option, as the required JavaScript is bundled with Plots. +It can create inline plots in IJulia, or open standalone browser windows when run from the Julia REPL. + +`plotlyjs()` is the preferred option, and taps into the great functionality of Spencer Lyon's PlotlyJS.jl. +Inline IJulia plots can be updated from any cell... something that makes this backend stand out. +From the Julia REPL, it taps into Blink.jl and Electron to plot within a standalone GUI window... also very cool. +Also, PlotlyJS supports saving the output to more formats than Plotly, such as EPS and PDF, and thus is the recommended version of Plotly for developing publication-quality figures. + +```@example backends +plotlyjs(); backendplot(n = 2) #hide +png("backends_plotlyjs.png") #hide +``` +![](backends_plotlyjs.png) + +Pros: + +- [Tons of functionality](https://plot.ly/javascript/) +- 2D and 3D +- Mature library +- Interactivity (even when inline) +- Standalone or inline + +Cons: + +- No custom shapes +- JSON may limit performance + +Primary PlotlyJS.jl author: Spencer Lyon (@spencerlyon2) + +### MathJax + +Plotly needs to load MathJax to render LaTeX strings, therefore passing extra keywords with `extra_kwargs = :plot` is implemented. +With that it is possible to pass a header to the extra `include_mathjax` keyword. +It has the following options: + +- `include_mathjax = ""` (default): no mathjax header +- `include_mathjax = "cdn"` include the standard online version of the header +- `include_mathjax = ""` include a user-defined file + +These can also be passed using the `extra_plot_kwargs` keyword. + +```@example backends +using LaTeXStrings +plotlyjs() +plot( + 1:4, + [[1,4,9,16]*10000, [0.5, 2, 4.5, 8]], + labels = [L"\alpha_{1c} = 352 \pm 11 \text{ km s}^{-1}"; + L"\beta_{1c} = 25 \pm 11 \text{ km s}^{-1}"] |> permutedims, + xlabel = L"\sqrt{(n_\text{c}(t|{T_\text{early}}))}", + ylabel = L"d, r \text{ (solar radius)}", + yformatter = :plain, + extra_plot_kwargs = KW( + :include_mathjax => "cdn", + :yaxis => KW(:automargin => true), + :xaxis => KW(:domain => "auto") + ), +) +Plots.html("plotly_mathjax") #hide +``` +```@raw html + +``` + +### Fine tuning +It is possible to add additional arguments to the plotly series and layout dictionaries via the [`extra_kwargs`](@ref extra_kwargs) mechanism. +Arbitrary arguments are supported but one needs to be careful since no checks are performed and thus it is possible to unintentionally overwrite existing entries. + +For example adding [customdata](https://plotly.com/javascript/reference/scatter/#scatter-customdata) can be done the following way `scatter(1:3, customdata=["a", "b", "c"])`. +One can also pass multiple extra arguments to plotly. +``` +pl = scatter( + 1:3, + rand(3), + extra_kwargs = KW( + :series => KW(:customdata => ["a", "b", "c"]), + :plot => KW(:legend => KW(:itemsizing => "constant")) + ) +) +``` + +## [PythonPlot](https://github.com/stevengj/PythonPlot.jl) + +A Julia wrapper around the popular python package `Matplotlib`. It uses `PythonCall.jl` to pass data with minimal overhead. + +```@example backends +pythonplot(); backendplot() #hide +``` + +Pros: + +- Tons of functionality +- 2D and 3D +- Mature library +- Standalone or inline +- Well supported in Plots + +Cons: + +- Uses Python +- Dependencies frequently cause setup issues + +Primary author: Steven G Johnson (@stevengj) + +### Fine tuning +It is possible to use more features of `matplotlib` via the [`extra_kwargs`](@ref extra_kwargs) mechanism. +For example, for a 3D plot, the following example should generate a colorbar at a proper location; without the `extra_kwargs` below, the colorbar is displayed too far right to see its ticks and numbers. The four coordinates in the example below, i.e., `[0.9, 0.05, 0.05, 0.9]` specify the colorbar location `[ left, bottom, width, height ]`. Note that for 2D plots, this fine tuning is not necessary. + +```@example backends +using Plots; pythonplot() + +x = y = collect(range(-π, π; length = 100)) +fn(x, y) = 3 * exp(-(3x^2 + y^2)/5) * (sin(x+2y))+0.1randn(1)[1] +surface(x, y, fn, c=:viridis, extra_kwargs=Dict(:subplot=>Dict("3d_colorbar_axis" => [0.9, 0.05, 0.05, 0.9]))) +``` + +#### Supported `:subplot` `:extra_kwargs` + +| Keyword | Description | +| :--------------- | :------------------------------------------------------------------------------- | +| 3d_colorbar_axis | Specifying the colorbar location `[ left, bottom, width, height ]` for a 3D plot | + + +## [PGFPlotsX](https://github.com/KristofferC/PGFPlotsX.jl) + +LaTeX plotting, based on `PGF/TikZ`. + +```@example backends +pgfplotsx(); backendplot() #hide +``` + +Successor backend of PGFPlots backend. + +Has more features and is still in development otherwise the same. + +!!! tip + To add save a standalone .tex file including a preamble use attribute `tex_output_standalone = true` in your `plot` command. + +Pros: + +- Nice looking plots +- Lots of functionality (though the code is still WIP) + +Cons: + +- Tricky to install +- Heavy-weight dependencies + +Authors: + +- PGFPlots: Christian Feuersanger +- PGFPlotsX.jl: Kristoffer Carlsson (@KristofferC89), Tamas K. Papp (@tpapp) +- Plots <--> PGFPlotsX link code: Simon Christ (@BeastyBlacksmith), based on the code of Patrick Kofod Mogensen (@pkofod) + +### LaTeX workflow + +To use the native LaTeX output of the `pgfplotsx` backend you can save your plot as a `.tex` or `.tikz` file. +```julia +using Plots; pgfplotsx() +pl = plot(1:5) +pl2 = plot((1:5).^2, tex_output_standalone = true) +savefig(pl, "myline.tikz") # produces a tikzpicture environment that can be included in other documents +savefig(pl2, "myparabola.tex") # produces a standalone document that compiles by itself including preamble +``` +Saving as `.tikz` file has the advantage, that you can use `\includegraphics` to rescale your plot without changing the size of the fonts. +The default LaTeX output is intended to be included as a figure in another document and will not compile by itself. +If you include these figures in another LaTeX document you need to have the correct preamble. +The preamble of a plot can be shown using `Plots.pgfx_preamble(pl)` or copied from the standalone output. + +#### Fine tuning + +It is possible to use more features of `PGFPlotsX` via the [`extra_kwargs`](@ref extra_kwargs) mechanism. +By default it interprets every extra keyword as an option to the `plot` command. +Setting `extra_kwargs = :subplot` will treat them as an option to the `axis` command and `extra_kwargs = :plot` will be treated as an option to the `tikzpicture` environment. + +For example changing the colormap to one that is native to pgfplots can be achieved with the following. +Like this it is possible to keep the preamble of latex documents clean. + +```@example backends +using Plots; pgfplotsx() +surface(range(-3,3, length=30), range(-3,3, length=30), + (x, y) -> exp(-x^2-y^2), + label="", + colormap_name = "viridis", + extra_kwargs =:subplot) +``` + +Further more additional commands or strings can be added via the special `add` keyword. +This adds a square to a normal line plot: + +```@example backends +plot(1:5, add = raw"\draw (1,2) rectangle (2,3);", extra_kwargs = :subplot) +``` + +## [UnicodePlots](https://github.com/JuliaPlots/UnicodePlots.jl) + +Simple and lightweight. Plot directly in your terminal. You won't produce anything publication quality, but for a quick look at your data it is awesome. Allows plotting over a headless node (SSH). + +```@example backends +import FileIO, FreeType #hide +unicodeplots(); backendplot() #hide +``` + +Pros: + +- Minimal dependencies +- REPL plotting +- Lightweight +- Fast + +Cons: + +- Limited precision, density + +Primary author: Christof Stocker (@Evizero) + +### Fine tuning +It is possible to use more features of `UnicodePlots` via the [`extra_kwargs`](@ref extra_kwargs) mechanism. + +```@example backends +using Plots; unicodeplots() + +extra_kwargs = Dict(:subplot=>(; border = :bold, blend = false)) +p = plot(1:4, 1:4, c = :yellow; extra_kwargs) +plot!(p, 2:3, 2:3, c = :red) +``` + +#### Supported `:subplot` `:extra_kwargs` + +| Keyword | Description | +| :--------- | :--------------------------------------------------------------------------------------------------------- | +| width | Plot width | +| height | Plot height | +| projection | 3D projection (`:orthographic`, `perspective`) | +| zoom | 3D zoom level | +| up | 3D up vector (azimuth and elevation are controlled using `Plots.jl`'s `camera`) | +| canvas | Canvas type (see [Low-level Interface](https://github.com/JuliaPlots/UnicodePlots.jl#low-level-interface)) | +| border | Border type (`:solid`, `:bold`, `:dashed`, `:dotted`, `:ascii`, `:none`) | +| blend | Toggle canvas color blending (`true` / `false`) | + +#### Supported `:series` `:extra_kwargs` + +| Series Type | Keyword | Description | +| :--------------- | :------- | :------------------------------------------------------------------------------ | +| `all` | colormap | Colormap (see [Options](https://github.com/JuliaPlots/UnicodePlots.jl#options)) | +| `heatmap`, `spy` | fix_ar | Toggle fixing terminal aspect ratio (`true` / `false`) | +| `surfaceplot` | zscale | `z` axis scaling | +| `surfaceplot` | lines | Use `lineplot` instead of `scatterplot` (monotonic data) | + +## [Gaston](https://github.com/mbaz/Gaston.jl) + +`Gaston` is a direct interface to [gnuplot](https://gnuplot.info), a cross platform command line driven plotting utility. The integration of `Gaston` in `Plots` is recent (2021), but a lot of features are supported. + +```@example backends +gaston(); backendplot() #hide +``` + +## [InspectDR](https://github.com/ma-laforge/InspectDR.jl) + +Fast plotting with a responsive GUI (optional). Target: quickly identify design/simulation issues & glitches in order to shorten design iterations. + +Pros: + +- Relatively short load times / time to first plot. +- Interactive mouse/keybindings. + - Fast & simple way to pan/zoom into data. +- Drag & drop Δ-markers (measure/display Δx, Δy & slope). +- Designed with larger datasets in mind. + - Responsive even with moderate (>200k points) datasets. + - Confirmed to handle 2GB datsets with reasonable speed on older desktop running Windows 7 (drag+pan of data area highly discouraged). + +Cons: + +- Mostly limited to 2D line/scatter plots + +Primary author: MA Laforge (@ma-laforge) + +## [HDF5](https://github.com/JuliaIO/HDF5.jl) (HDF5-Plots) + +Write plot + data to a *single* `HDF5` file using a human-readable structure that can easily be reverse-engineered. + +![](assets/hdf5_samplestruct.png) + +**Write to .hdf5 file** +```julia +hdf5() # Select HDF5-Plots "backend" +p = plot(...) # Construct plot as usual +Plots.hdf5plot_write(p, "plotsave.hdf5") +``` + +**Read from .hdf5 file** +```julia +pythonplot() # Must first select some backend +pread = Plots.hdf5plot_read("plotsave.hdf5") +display(pread) +``` + +Pros: + +- Open, standard file format for complex datasets. +- Human readable (using [HDF5view](https://support.hdfgroup.org/products/java/hdfview/)). +- Save plot + data to a single binary file. +- (Re)-render plots at a later time using your favourite backend(s). + +Cons: + +- Currently missing support for `SeriesAnnotations` & `GridLayout`. + - (Please open an "issue" if you have a need). +- Not yet designed for backwards compatibility (no proper versioning). + - Therefore not truly adequate for archival purposes at the moment. +- Currently implemented as a "backend" to avoid adding dependencies to `Plots.jl`. + +Primary author: MA Laforge (@ma-laforge) + +--- + +# Deprecated backends + +### [PyPlot](https://github.com/stevengj/PyPlot.jl) + +`matplotlib` based backend, using `PyCall.jl` and `PyPlot.jl`. Superseded by `PythonCall.jl` and `PythonPlot.jl`. +Whilst still supported in `Plots 1.X`, users are advised to transition to the `pythonplot` backend. + +### [PGFPlots](https://github.com/sisl/PGFPlots.jl) + +LaTeX plotting, based on PGF/TikZ. + +!!! tip + To add save a standalone .tex file including a preamble use attribute `tex_output_standalone = true` in your `plot` command. + +Pros: + +- Nice looking plots +- Lots of functionality (though the code is still WIP) + +Cons: + +- Tricky to install +- Heavy-weight dependencies + +Authors: + +- PGFPlots: Christian Feuersanger +- PGFPlots.jl: Mykel Kochenderfer (@mykelk), Louis Dressel (@dressel), and others +- Plots <--> PGFPlots link code: Patrick Kofod Mogensen (@pkofod) + + +### [Gadfly](https://github.com/dcjones/Gadfly.jl) + +A Julia implementation inspired by the "Grammar of Graphics". + +Pros: + +- Clean look +- Lots of features +- Flexible when combined with Compose.jl (inset plots, etc.) + +Cons: + +- Does not support 3D +- Slow time-to-first-plot +- Lots of dependencies +- No interactivity + +Primary author: Daniel C Jones + +### [Immerse](https://github.com/JuliaGraphics/Immerse.jl) + +Built on top of Gadfly, Immerse adds some interactivity and a standalone GUI window, including zoom/pan and a cool "point lasso" tool to save Julia vectors with the selected data points. + +Pros: + +- Same as Gadfly +- Interactivity +- Standalone or inline +- Lasso functionality + +Cons: + +- Same as Gadfly + +Primary author: Tim Holy + +### [Qwt](https://github.com/tbreloff/Qwt.jl) + +My package which wraps PyQwt. Similar to PyPlot, it uses PyCall to convert calls to python. Though Qwt.jl was the "first draft" of Plots, the functionality is superceded by other backends, and it's not worth my time to maintain. + +Primary author: Thomas Breloff + +### [Bokeh](https://github.com/bokeh/Bokeh.jl) + +Unfinished, but very similar to PlotlyJS... use that instead. + +### [Winston](https://github.com/nolta/Winston.jl) + +Functionality incomplete... I never finished wrapping it, and I don't think it offers anything beyond other backends. However, the plots are clean looking and it's relatively fast. + +--- diff --git a/docs/src/basics.md b/docs/src/basics.md new file mode 100644 index 000000000..c5bff4bba --- /dev/null +++ b/docs/src/basics.md @@ -0,0 +1,64 @@ +### Basic Concepts + +Use `plot` to create a new plot object, and `plot!` to add to an existing one: + +```julia +plot(args...; kw...) # creates a new Plot, and set it to be the `current` +plot!(args...; kw...) # modifies Plot `current()` +plot!(plt, args...; kw...) # modifies Plot `plt` +``` + +The graphic is not shown implicitly, only when "displayed". This will happen automatically when returned to a REPL prompt or to an IJulia cell. There are [many other options](@ref output) as well. + +Input arguments can take [many forms](@ref input-data). Some valid examples: + +```julia +plot() # empty Plot object +plot(4) # initialize with 4 empty series +plot(rand(10)) # 1 series... x = 1:10 +plot(rand(10,5)) # 5 series... x = 1:10 +plot(rand(10), rand(10)) # 1 series +plot(rand(10,5), rand(10)) # 5 series... y is the same for all +plot(sin, rand(10)) # y = sin.(x) +plot(rand(10), sin) # same... y = sin.(x) +plot([sin,cos], 0:0.1:π) # 2 series, sin.(x) and cos.(x) +plot([sin,cos], 0, π) # sin and cos on the range [0, π] +plot(1:10, Any[rand(10), sin]) # 2 series: rand(10) and map(sin,x) +@df dataset("Ecdat", "Airline") plot(:Cost) # the :Cost column from a DataFrame... must import StatsPlots +``` + +[Keyword arguments](@ref attributes) allow for customization of the plot, subplots, axes, and series. They follow consistent rules as much as possible, and you'll avoid common pitfalls if you read this section carefully: + +- Many arguments have aliases which are [replaced during preprocessing](@ref step-1-replace-aliases). `c` is the same as `color`, `m` is the same as `marker`, etc. You can choose a verbosity that you are comfortable with. +- There are some [special arguments](@ref step-2-handle-magic-arguments) which magically set many related things at once. +- If the argument is a "matrix-type", then [each column will map to a series](@ref columns-are-series), cycling through columns if there are fewer columns than series. In this sense, a vector is treated just like an "nx1 matrix". +- Many arguments accept many different types... for example the color (also markercolor, fillcolor, etc) argument will accept strings or symbols with a color name, or any Colors.Colorant, or a ColorScheme, or a symbol representing a ColorGradient, or an AbstractVector of colors/symbols/etc... + +--- + +### Useful Tips + +!!! tip + A common error is to pass a Vector when you intend for each item to apply to only one series. Instead of an n-length Vector, pass a 1xn Matrix. + +!!! tip + You can update certain plot settings after plot creation: + ```julia + plot!(title = "New Title", xlabel = "New xlabel", ylabel = "New ylabel") + plot!(xlims = (0, 5.5), ylims = (-2.2, 6), xticks = 0:0.5:10, yticks = [0,1,5,10]) + + # or using magic: + plot!(xaxis = ("mylabel", :log10, :flip)) + xaxis!("mylabel", :log10, :flip) + ``` + +!!! tip + With [supported backends](@ref supported), you can pass a `Plots.Shape` object for the marker/markershape arguments. `Shape` takes a vector of 2-tuples in the constructor, defining the points of the polygon's shape in a unit-scaled coordinate space. To make a square, for example, you could do: `Shape([(1,1),(1,-1),(-1,-1),(-1,1)])` + +!!! tip + You can see the default value for a given argument with `default(arg::Symbol)`, and set the default value with `default(arg::Symbol, value)` or `default(; kw...)`. For example set the default window size and whether we should show a legend with `default(size=(600,400), leg=false)`. + +!!! tip + Call `gui()` to display the plot in a window. Interactivity depends on backend. Plotting at the REPL (without semicolon) implicitly calls `gui()`. + +--- diff --git a/docs/src/colors.md b/docs/src/colors.md new file mode 100644 index 000000000..112f094dc --- /dev/null +++ b/docs/src/colors.md @@ -0,0 +1,85 @@ +## Colors + +There are many color attributes, for lines, fills, markers, backgrounds, and foregrounds. Many colors follow a hierarchy... `linecolor` gets its value from `seriescolor`, for example, unless you override the value. This allows for you to simply set precisely what you want, without lots of boilerplate. + +Color attributes will accept many different types: + +- `Symbol`s or `String`s will be passed to `Colors.parse(Colorant, c)`, so `:red` is equivalent to `colorant"red"` +- `false` or `nothing` will be converted to an invisible `RGBA(0,0,0,0)` +- Any `Colors.Colorant`, with or without alpha/opacity +- Any `Plots.ColorScheme`, which includes `ColorVector`, `ColorGradient`, etc +- An integer, which picks the corresponding color from the `seriescolor` + +In addition, there is an extensive facility for selecting and generating color maps/gradients. + +- A valid Symbol: `:inferno` (the default), `:heat`, `:blues`, etc +- A list of colors (or anything that can be converted to a color) +- A pre-built `ColorGradient`, which can be constructed with the `cgrad` helper function. See [this short tutorial](https://github.com/tbreloff/ExamplePlots.jl/blob/master/notebooks/cgrad.ipynb) for example usage. + +### Color names +The supported color names is the union of [X11's](https://en.wikipedia.org/wiki/X11_color_names) and SVG's. +They are defined in the [Colors.jl](https://github.com/JuliaGraphics/Colors.jl/blob/master/src/names_data.jl) +,like `blue`, `blue2`, `blue3`, ...etc. + +--- + +#### Series Colors + +For series, there are a few attributes to know: + +- **seriescolor**: Not used directly, but defines the base color for the series +- **linecolor**: Color of paths +- **fillcolor**: Color of area fill +- **markercolor**: Color of the interior of markers and shapes +- **markerstrokecolor**: Color of the border/stroke of markers and shapes + +`seriescolor` defaults to `:auto`, and gets assigned a color from the `color_palette` based on its index in the subplot. By default, the other colors `:match`. (See the table below) + +!!! tip + In general, color gradients can be set by `*color`, and the corresponding color values to look up in the gradients by `*_z`. + +This color... | matches this color... +--- | --- +linecolor | seriescolor +fillcolor | seriescolor +markercolor | seriescolor +markerstrokecolor | foreground_color_subplot + +!!! note + each of these attributes have a corresponding alpha override: `seriesalpha`, `linealpha`, `fillalpha`, `markeralpha`, and `markerstrokealpha`. They are optional, and you can still give alpha information as part of an `Colors.RGBA`. + +!!! note + In some contexts, and when the user hasn't set a value, the `linecolor` or `markerstrokecolor` may be overridden. + +--- + +#### Foreground/Background + +Foreground and background colors work similarly: + + +This color... | matches this color... +--- | --- +background\_color\_outside | background\_color +background\_color\_subplot | background\_color +background\_color\_legend | background\_color\_subplot +background\_color\_inside | background\_color\_subplot +foreground\_color\_subplot | foreground\_color +foreground\_color\_legend | foreground\_color\_subplot +foreground\_color\_grid | foreground\_color\_subplot +foreground\_color\_title | foreground\_color\_subplot +foreground\_color\_axis | foreground\_color\_subplot +foreground\_color\_border | foreground\_color\_subplot +foreground\_color\_guide | foreground\_color\_subplot +foreground\_color\_text | foreground\_color\_subplot + + +--- + +#### Misc + +- the `linecolor` under the default theme is not CSS-defined, but close to `:steelblue`. +- `line_z` and `marker_z` parameters will map data values into a `ColorGradient` value +- `color_palette` determines the colors assigned when `seriescolor == :auto`: + - If passed a vector of colors, it will force cycling of those colors + - If passed a gradient, it will infinitely draw unique colors from that gradient, attempting to spread them out diff --git a/docs/src/colorschemes.md b/docs/src/colorschemes.md new file mode 100644 index 000000000..008905896 --- /dev/null +++ b/docs/src/colorschemes.md @@ -0,0 +1,84 @@ +```@setup colors +using Plots; gr() +Plots.reset_defaults() +``` + +# Colorschemes + +Plots supports all colorschemes from [ColorSchemes.jl](https://juliagraphics.github.io/ColorSchemes.jl/stable/basics/#Pre-defined-schemes-1). +They can be used as a gradient or as a palette and are passed as a symbol holding their name to `cgrad` or `palette`. + +```@example colors +plot( + [x -> sin(x - a) for a in range(0, π / 2, length = 5)], 0, 2π; + palette = :Dark2_5, +) +``` + +```@example colors +function f(x, y) + r = sqrt(x^2 + y^2) + return cos(r) / (1 + r) +end +x = range(0, 2π, length = 30) +heatmap(x, x, f, c = :thermal) +``` + +### ColorPalette + +Plots chooses colors for series automatically from the palette passed to the `color_palette` attribute. +The attribute accepts symbols of colorscheme names or `ColorPalette` objects. +Color palettes can be constructed with `palette(cs, [n])` where `cs` can be a `Symbol`, a vector of colors, a `ColorScheme`, `ColorPalette` or `ColorGradient`. +The optional argument `n` decides how many colors to choose from `cs`. + +```@example colors +palette(:tab10) +``` + +```@example colors +palette([:purple, :green], 7) +``` + +### ColorGradient + +For `heatmap`, `surface`, `contour` or `line_z`, `marker_z` and `line_z` Plots.jl chooses colors from a `ColorGradient`. +If not specified, the default `ColorGradient` `:inferno` is used. +A different gradient can be selected by passing a symbol for a colorscheme name to the `seriescolor` attribute. +For more detailed configuration, the color attributes also accept a `ColorGradient` object. +Color gradients can be constructed with +```julia +cgrad(cs, [z], alpha = nothing, rev = false, scale = nothing, categorical = nothing) +``` +where `cs` can be a `Symbol`, a vector of colors, a `ColorScheme`, `ColorPalette` or `ColorGradient`. + +```@example colors +cgrad(:acton) +``` +You can pass a vector of values between 0 and 1 as second argument to specify positions of color transitions. +```@example colors +cgrad([:orange, :blue], [0.1, 0.3, 0.8]) +``` +With `rev = true` the colorscheme colors are reversed. +```@example colors +cgrad(:thermal, rev = true) +``` +Setting `categorical = true` returns a `CategoricalColorGradient` that only chooses from a discrete set of colors without interpolating continuously. +The optional second argument determines how many colors to choose from the colorscheme. +They are distributed uniformly along the colorscheme colors. +```@example colors +cgrad(:matter, 5, categorical = true) +``` +Categorical gradients also accept a vector for positions of color transitions and can be reversed. +```@example colors +cgrad(:matter, [0.1, 0.3, 0.8], rev = true, categorical = true) +``` +The distribution of color selection can be scaled with the `scale` keyword argument which accepts `:log`, `:log10`, `:ln`, `:log2`, `:exp` or a function to be applied on the color position values between 0 and 1. +```@example colors +cgrad(:roma, scale = :log) +``` +Categorical gradients can also be scaled. +```@example colors +cgrad(:roma, 10, categorical = true, scale = :exp) +``` + +# Pre-defined ColorSchemes diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 000000000..f8bb2978f --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,317 @@ +```@setup contributing +using Plots; gr() +Plots.reset_defaults() +``` + +This is a guide to contributing to Plots and the surrounding ecosystem. Plots is a complex and far-reaching suite of software components, and as such will be most effective when the community contributes their own expertise, knowledge, perspective, and effort. The document is roughly broken up into the following categories, and after reading this introduction you should feel comfortable skipping to the section(s) that interest you the most: + +- [The JuliaPlots Organization](#The-JuliaPlots-Organization): Packages and dependencies +- [Choosing a Project](#Choosing-a-Project): Fix bugs, add features, create recipes +- [Key Design Principles](#Key-Design-Principles): Design goals and considerations +- [Code Organization](#Code-Organization): Where to look when implementing new features +- [Git-fu (or... the mechanics of contributing)](#Git-fu-(or...-the-mechanics-of-contributing)): Git (how to commit/push), Github (how to submit a PR), Testing (VisualRegressionTests, Travis) + +When in doubt, use this handy dandy logic designed by a [legendary open source guru](https://github.com/tbreloff)... + +![](https://cloud.githubusercontent.com/assets/933338/23193321/4cd1d578-f876-11e6-92dc-222b52598054.png) + +--- + +## The JuliaPlots Organization + +[JuliaPlots](https://github.com/JuliaPlots) is the home for all things Plots. It was founded by [Tom Breloff](https://www.breloff.com), and extended through many contributions from [members](https://github.com/orgs/JuliaPlots/people) and others. The first step in contributing will be to understand which package(s) are appropriate destinations for your code. + + +### Plots + +This is the core package for: + +- Definitions of `plot`/`plot!` +- The [core processing pipeline](@ref pipeline) +- Base [recipes](@ref recipes) for `path`, `scatter`, `bar`, and many others +- Generic [output](@ref output) methods +- Generic [layout](@ref layouts) methods +- Generic [animation](@ref animations) methods +- Generic types: Plot, Subplot, Axis, Series, ... +- Conveniences: `getindex`/`setindex`, `push!`/`append!`, `unzip`, `cycle`, ... + +This package depends on RecipesBase, PlotUtils, and PlotThemes. When contributing new functionality/features, you should make best efforts to find a more appropriate home (StatsPlots, PlotUtils, etc) than contributing to core Plots. In general, the push has been to reduce the size and scope of Plots, when possible, and move features to other packages. + +### Backends + +Backend code (such as code linking Plots with GR) lives in the `Plots/src/backends` directory. As such, backend code should be contributed to core Plots. GR and Plotly are the only backends installed by default. All other backend code is loaded conditionally using [Requires.jl](https://github.com/JuliaPackaging/Requires.jl) in `Plots/src/init.jl`. + +### PlotDocs + +PlotDocs is the home of this documentation. The documentation is built using [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl). + +### RecipesBase + +Seldom updated, but essential. This is the package that you would depend on to create third-party recipes. It contains the bare minimum to define new recipes. + +### PlotUtils + +Components that could be used for other (non-Plots) packages. Anything that is sufficiently generic and useful could be contributed here. + +- Color (conversions, construction, conveniences) +- Color gradients/maps +- Tick computation + +### PlotThemes + +Visual themes (i.e. attribute defaults) such as "dark", "orange", etc. + +### StatsPlots + +An extension of Plots: Statistical plotting and tabular data. Complex histograms and densities, correlation plots, and support for DataFrames. Anything related to stats or special handling for table-like data should live here. + +### GraphRecipes + +An extension of StatsPlots: Graphs, maps, and more. + +--- + +## Choosing a Project + +For people new to Plots, the first step should be to read (and reread) the documentation. Code up some examples, play with the attributes, and try out multiple backends. It's really hard to contribute to a project that you don't know how to use. + +### Beginner Project Ideas + +- **Create a new recipe**: Preferably something you care about. Maybe you want custom overlays of heatmaps and scatters? Maybe you have an input format that isn't currently supported? Make a recipe for it so you can just `plot(thing)`. +- **Fix bugs**: There are many "bugs" which are specific to one backend, or incorrectly implement features that are infrequently used. Some ideas can be found in the [issues marked easy](https://github.com/JuliaPlots/Plots.jl/issues?q=is%3Aissue+is%3Aopen+label%3A%22easy+-+up+for+grabs%22). +- **Add recipes to external packages**: By depending on RecipesBase, a package can define a recipe for their custom types. Submit a PR to a package you care about that adds a recipe for that package. For example, see [this PR to add OHLC plots for TimeSeries.jl](https://github.com/JuliaStats/TimeSeries.jl/pull/303). + +### Intermediate Project Ideas + +- **Improve your favorite backend**: There are many missing features and other improvements that can be made to individual backends. Most issues specific to a backend have a [special tag](https://github.com/JuliaPlots/Plots.jl/issues?q=is%3Aissue+is%3Aopen+label%3APlotly). +- **Help with documentation**: This could come in the form of improved descriptions, additional examples, or full tutorials. Please contribute improvements to [PlotDocs](https://github.com/JuliaPlots/PlotDocs.jl). +- **Expand StatsPlots functionality**: qqplot, DataStreams, or anything else you can think of. + +### Advanced Project Ideas + +- **ColorBar redesign**: Colorbars [need serious love](https://github.com/JuliaPlots/Plots.jl/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20colorbar)... this would likely require a new Colorbar type that links with the appropriate Series object(s) and is independent during subplot layout. We want to allow many series (possibly from multiple subplots) to use the same clims and to share a colorbar, or have multiple colorbars that can be flexibly positioned. +- **PlotSpec redesign**: This [long standing redesign proposal](https://github.com/JuliaPlots/Plots.jl/issues/390) could allow generic serialization/deserialization of Plot data and attributes, as well as some improvements/optimizations when mutating plots. For example, we could lazily compute attribute values, and intelligently flag them as "dirty" when they change, allowing backends to skip much of the wasted processing and unnecessary rebuilding that currently occurs. +- **Improve graph recipes**: Lots to do here: clean up visuals, improve edge drawing, implement [layout algorithms](https://github.com/JuliaGraphs/NetworkLayout.jl), and much more. + +--- + +## Key Design Principles + +Flexible and generic... these are the core principles underlying Plots development, and also tend to cause confusion when users laser-focus on their specific use case. + +I (Tom) have painstakingly designed the core logic to support nearly any use case that exists or may exist. I don't pretend to know how you want to use Plots, or what type of data you might pass in, or what sort of recipe you may want to apply. As such, I try to avoid unnecessary restriction of types, or forced conversions, or many other pitfalls of limited visualization frameworks. The result is a highly modular framework which is limited by your imagination. + +When contributing new features to Plots (or the surrounding ecosystem), you should strive for this mentality as well. New features should be left as generic as possible, while avoiding obvious feature clash. + +As an example, you may want a new recipe that shows a histogram when passed Float64 numbers, but shows counts of every unique value for strings. So you make a recipe that works perfectly for your purpose: + +```@example contributing +using Plots, StatsBase +gr(size = (300, 300), leg = false) + +@userplot MyCount +@recipe function f(mc::MyCount) + # get the array from the args field + arr = mc.args[1] + + T = typeof(arr) + if T.parameters[1] == Float64 + seriestype := :histogram + arr + else + seriestype := :bar + cm = countmap(arr) + x = sort!(collect(keys(cm))) + y = [cm[xi] for xi ∈ x] + x, y + end +end +``` + +The recipe defined above is a "user recipe", which builds a histogram for arrays of Float64, and otherwise shows a "countmap" of sorted unique values and their observed counts. You only care about Float64 and String, and so you're results are fine: + +```@example contributing +mycount(rand(500)) +``` + +```@example contributing +mycount(rand(["A","B","C"],100)) +``` + +But you didn't consider the person that, in the future, might want to pass integers to this recipe: + +```@example contributing +mycount(rand(1:500, 500)) +``` + +This user expected integers to be treated as numbers and output a histogram, but instead they were treated like strings. A simple solution would have been to replace `if T.parameters[1] == Float64` with `if T.parameters[1] <: Number`. However, should we even depend on `T` having it's first parameter be the element type? (No) So even better would be `if eltype(arr) <: Number`, which now allows any container with any numeric type to trigger the "histogram" logic. + +This simple example outlines a common theme when developing Plots (or really any other Julia package). Try to create the most generic implementation you can think of while maintaining correctness. You don't know what crazy types someone else will use to try to access your functionality. + +--- + +## Code Organization + +Generally speaking, similar functionality is kept within the same file. Within the `src` directory, much of the files should be self explanatory (for example, you'll find animation methods/macros in the `animation.jl` file), but some could use a summary of contents: + +- `Plots.jl`: imports, exports, shorthands, and initialization +- `args.jl`: defaults, aliases, and attribute processing +- `components.jl`: shapes, fonts, and other assorted goodies +- `pipeline.jl`: code which builds the plots and subplots through recursive application of recipes +- `recipes.jl`: primarily core series recipes +- `series.jl`: core input data handling and processing +- `utils.jl`: lots of functionality that didn't have a home... `getindex`/`setindex!` for `Plot`/`Subplot`/`Axis`/`Series`, `push!`/`append!` for adding data to a series, `cycle`/`unzip` and similar utility functions, `Segments`/`SegmentsIterator`, etc. + +These files should probably be reorganized, but until then... + +### Creating new backends + +Model new backends on `Plots/src/backends/template.jl`. Implement the callbacks that are appropriate, especially `_display` and `_show` for GUI and image output respectively. + +### Style/Design Guidelines + +- Make every effort to minimize external dependencies and exports. Requiring new dependencies is the most likely way to make your PR "unmergeable". +- Be careful adding method signatures on existing methods with Base types (Array, etc) as you may override key functionality. This is especially true with recipes. Consider wrapping inputs in a new type (like in "user recipes"). +- Terse code is ok, as is verbose code. What's important is understanding and context. Will someone reading your code know what you mean? If not, consider writing comments to describe your reason for the design, or describe the hack you just implemented in clear prose. Sometimes [it's ok that your comments are longer than your code](https://github.com/JuliaPlots/Plots.jl/blob/master/src/pipeline.jl#L62-L67). +- Pick your project for yourself, but write code for others. It should be generic and useful beyond your needs, and you should **never break functionality** because you can't figure out how to implement something well. Spend more time on it... there's always a better way. + +--- + +## Git-fu (or... the mechanics of contributing) + +Many people have trouble with Git. More have trouble with Github. I think much of the confusion happens when you run commands without understanding what they do. We're all guilty of it, but recovering usually means "starting over". In this section, I'll try to keep a simple, practical approach to making PRs. It's worked well for me, though YMMV. + +### Guidelines + +Here are some guidelines for the development workflow (Note: Even if you've made 20 PRs to Plots in the past, please read this as it may be different than past guidelines): + +- **Commit to a branch that belongs to you.** Typically that means you should give your branches names that are unique to you, and that might include information on the feature you're developing. For example, I might choose to `git checkout -b tb-fonts` when starting work on fonts. +- **Open a PR against master.** `master` is the "bleeding edge". (Note: I used to recommend PRing to `dev`) +- **Only merge others changes when absolutely necessary.** You should prefer to use `git rebase origin/master` instead of `git merge origin/master`. A rebase replays your recent commits on top of the most recent `master`, avoiding complicated and messy merge commits and generally avoiding confusion. If you follow the first rule, then you likely won't get yourself in trouble. Rebase horror stories generally result when many people are working on the same branch. I find [this resource](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is great for understanding the important parts of `git rebase`. + +--- + +### Development Workflow + +My suggestions for a smooth development workflow: + +#### Fork the repo + +Navigate to the repo site (https://github.com/JuliaPlots/Plots.jl) and click the "Fork" button. You might get a choice of which account or organization to place the fork. I'll assume going forward that you forked to Github username `user123`. + +#### Set up the git remote + +Navigate to the local repo. Note: I'm assuming that you do development in your Julia directory, and using Mac/Linux. Adjust as needed. + +``` +cd ~/.julia/v0.5/Plots +git remote add forked git@github.com:user123/Plots.jl.git +``` + +After running these commands, `git remote -v` should show two remotes: `origin` (the main repo) and `forked` (your fork). A remote is simply a reference/pointer to the github site hosting the repo, and a fork is simply any other git repo with a special link to the originating repo. + +#### Create a new branch + +If you're just starting work on a new feature: + +``` +git fetch origin +git checkout master +git merge --ff-only origin/master +git checkout -b user123-myfeature +git push -u forked user123-myfeature +``` + +The first three lines are meant to ensure you start from the main repo's master branch. The `--ff-only` flag ensures you will only "fast forward" to newer commits, and avoids creating a new merge commit when you didn't mean to. The `git checkout` line both creates a new branch (the `-b`) pointing to the current commit and makes that branch current. The `git push` line adds this branch to your Github fork, and sets up the local branch to "track" (`-u`) the remote branch for subsequent `git push` and `git pull` calls. + +#### or... Reuse an old branch + +If you have an ongoing development branch (say, `user123-dev`) which you'd prefer to use (and which has previously been merged into master!) then you can get that up to date with: + +``` +git fetch origin +git checkout user123-dev +git merge --ff-only origin/master +git push forked user123-dev +``` + +We update our local copy of origin, checkout the dev branch, then attempt to "fast-forward" to the current master. If successful, we push the branch back to our forked repo. + +#### Write code, and format + +Power up your favorite editor (maybe [Juno](https://junolab.org/)?) and make some code changes to the repo. + +Format your changes (code style consistency) using: +```bash +$ julia -e 'using JuliaFormatter; format(["src", "test"])' +``` + +#### Commit + +After applying changes, you'll want to "commit" or save a snapshot of all the changes you made. After committing, you can "push" those changes to your forked repo on Github: + +``` +git add src/my_new_file.jl +git commit -am "my commit message" +git push forked user123-dev +``` + +The first line is optional, and is used when adding new files to the repo. The `-a` means "commit all my changes", and the `-m` lets you write a note about the commit (you should always do this, and hopefully make it descriptive). + +#### Submit a PR + +You're almost there! Browse to your fork (https://github.com/user123/Plots.jl). Most likely there will be a section just above the code that asks if you'd like to create a PR from the `user123-dev` branch. If not, you can click the "New pull request" button. + +Make sure the "base" branch is JuliaPlots `master` and the "compare" branch is `user123-dev`. Add an informative title and description, and link to relevant issues or discussions, then click "Create pull request". You may get some questions about it, and possibly suggestions of how to fix it to be "merge-ready". Then hopefully it gets merged... thanks for the contribution!! + +#### Cleanup + +After all of this, you will likely want to go back to using `master` (or possibly using a tagged release, once your feature is tagged). To clean up: + +``` +git fetch origin +git checkout master +git merge --ff-only origin/master +git branch -d user123-dev +``` + +This catches your local master branch up to the remote master branch, then deletes the dev branch. If you want to return to tagged releases, run `Pkg.free("Plots")` from the Julia REPL. + +--- + +### Tags + +New tags should represent "stable releases"... those that you are happy to distribute to end-users. Effort should be made to ensure tests pass before creating a new tag, and ideally new tests would be added which test your new functionality. This is, of course, a much trickier problem for visualization libraries as compared to other software. See the [testing section](#testing) below. + +Only JuliaPlots members may create a new tag. To create a new tag, we'll create a new release on Github and use [attobot](https://github.com/attobot/attobot) to generate the PR to METADATA. Create a new release at https://github.com/JuliaPlots/Plots.jl/releases/new (of course replacing the repo name with the package you're tagging). + +The version number (vMAJOR.MINOR.PATCH) should be incremented using [semver](https://semver.org/), which generally means that breaking changes should increment the major number, backwards compatible changes should increment the minor number, and bug fixes should increment the patch number. For "v0.x.y" versions, this requirement is relaxed. The minor version can be incremented for breaking changes. + +--- + +### Testing + +#### VisualRegressionTests + +Testing in Plots is done with the help of [VisualRegressionTests](https://github.com/JuliaPlots/VisualRegressionTests.jl). Reference images are stored in [PlotReferenceImages](https://github.com/JuliaPlots/PlotReferenceImages.jl). Sometimes the reference images need to be updated (if features change, or if the underlying backend changes). VisualRegressionTests makes it somewhat painless to update the reference images: + +From the Julia REPL, run `Pkg.test(name="Plots")`. This will try to plot the tests, and then compare the results to the stored reference images. If the test output is sufficiently different than the reference output (using Tim Holy's excellent algorithm for the comparison), then a GTK window will pop up with a side-by-side comparison. You can choose to replace the reference image, or not, depending on whether a real error was discovered. + +After the reference images have been updated, navigate to PlotReferenceImages and push the changes to Github: + +``` +cd ~/.julia/v0.5/PlotReferenceImages +git add Plots/* +git commit -am "a useful message" +git push +``` + +If there are mis-matches due to bugs, **don't update the reference image**. + +#### CI + +On a `git push` the tests will be run automatically as part of our continuous integration setup. +This runs the same tests as above, downloading and comparing to the reference images, though with a larger tolerance for differences. +When these error, it may be due to timeouts, stale reference images, or a host of other reasons. +Check the logs to determine the reason. +If the tests are broken because of a new commit, consider rolling back. diff --git a/docs/src/democards/bulmagridtheme.css b/docs/src/democards/bulmagridtheme.css new file mode 100644 index 000000000..3f4e1c567 --- /dev/null +++ b/docs/src/democards/bulmagridtheme.css @@ -0,0 +1,12 @@ +.bulma-grid-card { + display: flex; + flex-direction: column; +} + +.bulma-grid-card-cover { + order: -1; +} + +.bulma-grid-card-cover img { + width: 100%; +} \ No newline at end of file diff --git a/docs/src/ecosystem.md b/docs/src/ecosystem.md new file mode 100644 index 000000000..2ba4636f4 --- /dev/null +++ b/docs/src/ecosystem.md @@ -0,0 +1,135 @@ +```@setup ecosystem +using StatsPlots, Plots, RDatasets, Distributions; gr() +Plots.reset_defaults() + +iris = dataset("datasets", "iris") +singers = dataset("lattice","singer") +dist = Gamma(2) +a = [randn(100); randn(100) .+ 3; randn(100) ./ 2 .+ 3] +``` + +Plots is great on its own, but the real power comes from the ecosystem surrounding it. The design of Plots (and more specifically [RecipesBase](https://github.com/JuliaPlots/RecipesBase.jl)) is to bind together disparate functionality into a cohesive and consistent user experience. Some packages may choose to implement recipes to visualize their custom types. Others may extend the functionality of Plots for Base types. On this page I'll attempt to collect and display some of the many things you can do using the ecosystem which has developed around the Plots core. + +--- + +# [JuliaPlots](@id ecosystem) + +The [JuliaPlots](https://github.com/JuliaPlots) organization builds and maintains much of the most commonly used functionality external to core Plots, as well as RecipesBase, PlotUtils, the documentation, and more. + +# Community packages + +## [AtariAlgos](https://github.com/tbreloff/AtariAlgos.jl) + +`AtariAlgos.jl` wraps the ArcadeLearningEnvironment as an implementation of an AbstractEnvironment from the Reinforce interface. This allows it to be used as a plug-and-play module with general reinforcement learning agents. + +Games can also be "plotted" using Plots.jl, allowing it to be a component of more complex visualizations for tracking learning progress and more, as well as making it easy to create animations. + +![](https://cloud.githubusercontent.com/assets/933338/17670982/8923a2f6-62e2-11e6-943f-bd0a2a7b5c1f.gif) + +## [Reinforce](https://github.com/tbreloff/Reinforce.jl) + +`Reinforce.jl` is an interface for Reinforcement Learning. It is intended to connect modular environments, policies, and solvers with a simple interface. + +![](https://cloud.githubusercontent.com/assets/933338/17703784/f3e18414-63a0-11e6-9f9e-f531278216f9.gif) + + +## [JuliaML](https://github.com/JuliaML) + +Tools, models, and math related to machine learning in Julia. + +![](https://cloud.githubusercontent.com/assets/933338/18800737/93b71b42-81ac-11e6-9c7a-0cddf6d083ab.png) + +## [Augmentor](https://github.com/Evizero/Augmentor.jl) + +`Augmentor.jl` is an image-augmentation library designed to render the process of artificial dataset enlargement more convenient, less error prone, and easier to reproduce. This is achieved using probabilistic transformation pipelines. + +![](https://cloud.githubusercontent.com/assets/10854026/17645973/3894d2b0-61b6-11e6-8b10-1cb5139bfb6d.gif) + +## [DifferentialEquations](https://github.com/ChrisRackauckas/DifferentialEquations.jl) + +`DifferentialEquations.jl` is a package for solving numerically solving differential equations in Julia by Chris Rackauckas. The purpose of this package is to supply efficient Julia implementations of solvers for various differential equations. Equations within the realm of this package include ordinary differential equations (ODEs), stochastic ordinary differential equations (SODEs or SDEs), stochastic partial differential equations (SPDEs), partial differential equations (with both finite difference and finite element methods), differential algebraic equations, and differential delay equations. It includes well-optimized implementations classic algorithms and ones from recent research, including algorithms optimized for high-precision and HPC applications. + +All of the solvers return solution objects which are set up with plot recipes to give informative default plots. + +![diffeq](https://cloud.githubusercontent.com/assets/1814174/17526562/9daa2d1e-5e1c-11e6-9f21-fda6f49f6833.png) + +## [PhyloTrees](https://github.com/jangevaare/PhyloTrees.jl) + +The `PhyloTrees.jl` package provides a type representation of phylogenetic trees. Simulation, inference, and visualization functionality is also provided for phylogenetic trees. A plot recipe allows the structure of phylogenetic trees to be drawn by whichever plotting backend is preferred by the user. + +![](https://cloud.githubusercontent.com/assets/5422422/17630286/a25374fc-608c-11e6-9160-32466b094f0b.png) + +## [EEG](https://github.com/codles/EEG.jl) + +Process EEG files and visualize brain activity. + +![](https://cloud.githubusercontent.com/assets/748691/17362167/210f9c28-5974-11e6-8a05-62fa399d32d1.png) + +![](https://cloud.githubusercontent.com/assets/748691/17363374/523373a0-597a-11e6-94d9-826381617756.png) + +## [ImplicitEquations](https://github.com/jverzani/ImplicitEquations.jl) + +In a paper, Tupper presents a method for graphing two-dimensional implicit equations and inequalities. This package gives an implementation of the paper's basic algorithms to allow the Julia user to naturally represent and easily render graphs of implicit functions and equations. + +![](https://camo.githubusercontent.com/950ef704a0601ed9429addb35e6b7246ca5da149/687474703a2f2f692e696d6775722e636f6d2f4c4368547a43312e706e67) + + + +## [ControlSystems](https://github.com/JuliaControl/ControlSystems.jl) + +A control systems design toolbox for Julia. This toolbox works similar to that of other major computer-aided control systems design (CACSD) toolboxes. Systems can be created in either a transfer function or a state space representation. These systems can then be combined into larger architectures, simulated in both time and frequency domain, and analyzed for stability/performance properties. + +![](https://juliacontrol.github.io/ControlSystems.jl/latest/plots/pidgofplot2.svg) + +## [ValueHistories](https://github.com/JuliaML/ValueHistories.jl) + +Utility package for efficient tracking of optimization histories, training curves or other information of arbitrary types and at arbitrarily spaced sampling times + +![](https://cloud.githubusercontent.com/assets/10854026/17512899/58461c20-5e2a-11e6-94d4-b4699c63ab1a.png) + + +## [ApproxFun](https://github.com/ApproxFun/ApproxFun.jl) + +`ApproxFun.jl` is a package for approximating functions. It is heavily influenced by the Matlab package Chebfun and the Mathematica package RHPackage. + +![](https://raw.githubusercontent.com/ApproxFun/ApproxFun.jl/master/images/extrema.png) + + +## [AverageShiftedHistograms](https://github.com/joshday/AverageShiftedHistograms.jl) + +Density estimation using Average Shifted Histograms. + +![](https://cloud.githubusercontent.com/assets/933338/17702262/3bfc9a96-639b-11e6-8976-aa8bb8fabfc8.gif) + +## [MLPlots](https://github.com/JuliaML/MLPlots.jl) + +Common plotting recipes for statistics and machine learning. + +![](https://cloud.githubusercontent.com/assets/933338/17702652/bca0158c-639c-11e6-8e36-4bfc7b36727e.png) + +![](https://cloud.githubusercontent.com/assets/933338/17702662/cdc08752-639c-11e6-8c3c-e186456630e2.png) + + +## [LazySets](https://github.com/JuliaReach/LazySets.jl) + +`LazySets.jl` is a Julia package for calculus with convex sets. The principle behind LazySets is to wrap set computations into specialized types, delaying the evaluation of the result of an expression until it is necessary. Combining lazy operations in high dimensions and explicit computations in low dimensions, the library can be applied to solve complex, high-dimensional problems. + +Reachability plot of a [two-mode hybrid system](https://juliareach.github.io/LazySets.jl/dev/man/reach_zonotopes_hybrid/#Example): + +![](https://raw.githubusercontent.com/JuliaReach/JuliaReach-website/master/src/images/hybrid2d.png) + +--- + +And many more: + +- `Losses.jl` +- `IterativeSolvers.jl` +- `SymPy.jl` +- `OnlineStats.jl` +- `Robotlib.jl` +- `JWAS.jl` +- `QuantEcon.jl` +- `Reinforce.jl` +- `Optim.jl` +- `Transformations.jl` / `Flow.jl` +- ... diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..1f021ee5a --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,143 @@ +```@setup index +using Plots; gr() +Plots.reset_defaults() +``` + +# Plots - powerful convenience for visualization in Julia + +**Author: Thomas Breloff (@tbreloff)** + +To get started, [see the tutorial](@ref tutorial). + +Almost everything in Plots is done by specifying plot [attributes](@ref attributes). + +Tap into the extensive visualization functionality enabled by the [Plots ecosystem](@ref ecosystem), and easily build your own complex graphics components with [recipes](@ref recipes). + + +## Intro to Plots in Julia + +Data visualization has a complicated history. Plotting software makes trade-offs between features and simplicity, speed and beauty, and a static and dynamic interface. Some packages make a display and never change it, while others make updates in real-time. + +Plots is a visualization interface and toolset. It sits above other backends, like GR, PythonPlot, PGFPlotsX, or Plotly, connecting commands with implementation. If one backend does not support your desired features or make the right trade-offs, you can just switch to another backend with one command. No need to change your code. No need to learn a new syntax. Plots might be the last plotting package you ever learn. + +The goals with the package are: + +- **Powerful**. Do more with less. Complex visualizations become easy. +- **Intuitive**. Start generating plots without reading volumes of documentation. Commands should "just work." +- **Concise**. Less code means fewer mistakes and more efficient development and analysis. +- **Flexible**. Produce your favorite plots from your favorite package, only quicker and simpler. +- **Consistent**. Don't commit to one graphics package. Use the same code and access the strengths of all [backends](@ref backends). +- **Lightweight**. Very few dependencies, since backends are loaded and initialized dynamically. +- **Smart**. It's not quite AGI, but Plots should figure out what you **want** it to do... not just what you **tell** it. + + +Use the [preprocessing pipeline](@ref pipeline) in Plots to describe your visualization completely before it calls the backend code. This preprocessing maintains modularity and allows for efficient separation of front end code, algorithms, and backend graphics. + +Please add wishlist items, bugs, or any other comments/questions to the [issues list](https://github.com/tbreloff/Plots.jl/issues), and [join the conversation on zulip](https://julialang.zulipchat.com/#streams/236493/plots.jl). + +Nevertheless, extreme configurability is not a goal of Plots. If you require a rather specific plotting feature, feel free to [request it](https://github.com/JuliaPlots/Plots.jl/issues?q=is%3Aissue+is%3Aopen+label%3Aextension). However, do understand that Plots has to implement the feature across all backends which might be challenging due some backends' limitations. + +--- + +### [Simple is Beautiful](@id simple-is-beautiful) + +Lorenz Attractor + +```@example index +using Plots +# define the Lorenz attractor +Base.@kwdef mutable struct Lorenz + dt::Float64 = 0.02 + σ::Float64 = 10 + ρ::Float64 = 28 + β::Float64 = 8/3 + x::Float64 = 1 + y::Float64 = 1 + z::Float64 = 1 +end + +function step!(l::Lorenz) + dx = l.σ * (l.y - l.x) + dy = l.x * (l.ρ - l.z) - l.y + dz = l.x * l.y - l.β * l.z + l.x += l.dt * dx + l.y += l.dt * dy + l.z += l.dt * dz +end + +attractor = Lorenz() + + +# initialize a 3D plot with 1 empty series +plt = plot3d( + 1, + xlim = (-30, 30), + ylim = (-30, 30), + zlim = (0, 60), + title = "Lorenz Attractor", + legend = false, + marker = 2, +) + +# build an animated gif by pushing new points to the plot, saving every 10th frame +@gif for i=1:1500 + step!(attractor) + push!(plt, attractor.x, attractor.y, attractor.z) +end every 10 +``` + +Make some waves + +```@example index +using Plots +default(legend = false) +x = y = range(-5, 5, length = 40) +zs = zeros(0, 40) +n = 100 + +@gif for i in range(0, stop = 2π, length = n) + f(x, y) = sin(x + 10sin(i)) + cos(y) + + # create a plot with 3 subplots and a custom layout + l = @layout [a{0.7w} b; c{0.2h}] + p = plot(x, y, f, st = [:surface, :contourf], layout = l) + + # induce a slight oscillating camera angle sweep, in degrees (azimuth, altitude) + plot!(p[1], camera = (10 * (1 + cos(i)), 40)) + + # add a tracking line + fixed_x = zeros(40) + z = map(f, fixed_x, y) + plot!(p[1], fixed_x, y, z, line = (:black, 5, 0.2)) + vline!(p[2], [0], line = (:black, 5)) + + # add to and show the tracked values over time + global zs = vcat(zs, z') + plot!(p[3], zs, alpha = 0.2, palette = cgrad(:blues).colors) +end +``` + + +Iris Dataset + +```@example index +# load a dataset +using RDatasets +iris = dataset("datasets", "iris"); + +# load the StatsPlots recipes (for DataFrames) available via: +# Pkg.add("StatsPlots") +using StatsPlots + +# Scatter plot with some custom settings +@df iris scatter( + :SepalLength, + :SepalWidth, + group = :Species, + title = "My awesome plot", + xlabel = "Length", + ylabel = "Width", + m = (0.5, [:cross :hex :star7], 12), + bg = RGB(0.2, 0.2, 0.2) +) +``` diff --git a/docs/src/input_data.md b/docs/src/input_data.md new file mode 100644 index 000000000..026d9d062 --- /dev/null +++ b/docs/src/input_data.md @@ -0,0 +1,263 @@ +```@setup input_data +using Plots; gr() +Plots.reset_defaults() +``` + +# [Input Data](@id input-data) + +Part of the power of Plots lies is in the many combinations of allowed input data. +You shouldn't spend your time transforming and massaging your data into a specific format. +Let Plots do that for you. + +There are a few rules to remember, and you'll be a power user in no time. + +## Inputs are arguments, not keywords + +The `plot` function has several methods: +`plot(y)`: treats the input as values for the `y`-axis and yields a unit-range as `x`-values. +`plot(x, y)`: creates a 2D plot +`plot(x, y, z)`: creates a 3D plot + +The reason lies in the flexibility of Julia's multiple dispatch, where every combination of input types +can have unique behavior, when desired. + +## [Columns are series](@id columns-are-series) + +In most cases, passing a (`n` × `m`) matrix of values (numbers, etc) will create `m` series, each with `n` data points. This follows a consistent rule… vectors apply to a series, matrices apply to many series. This rule carries into keyword arguments. `scatter(rand(10,4), markershape = [:circle, :rect])` will create 4 series, each assigned the markershape vector [:circle,:rect]. However, `scatter(rand(10,4), markershape = [:circle :rect])` will create 4 series, with series 1 and 3 having markers shaped as `:circle` and series 2 and 4 having markers shaped as `:rect` (i.e. as squares). The difference is that in the first example, it is a length-2 column vector, and in the second example it is a (1 × 2) row vector (a Matrix). + +The flexibility and power of this can be illustrated by the following piece of code: +```@example input_data +using Plots + +# 10 data points in 4 series +xs = range(0, 2π, length = 10) +data = [sin.(xs) cos.(xs) 2sin.(xs) 2cos.(xs)] + +# We put labels in a row vector: applies to each series +labels = ["Apples" "Oranges" "Hats" "Shoes"] + +# Marker shapes in a column vector: applies to data points +markershapes = [:circle, :star5] + +# Marker colors in a matrix: applies to series and data points +markercolors = [ + :green :orange :black :purple + :red :yellow :brown :white +] + +plot( + xs, + data, + label = labels, + shape = markershapes, + color = markercolors, + markersize = 10 +) +``` +This example plots the four series with different labels, marker shapes, and marker colors by combining row and column vectors to decorate the data. + +The following example illustrates how Plots.jl handles: an array of matrices, an array of arrays of arrays and an array of tuples of arrays. +```@example input_data +x1, x2 = [1, 0], [2, 3] # vectors +y1, y2 = [4, 5], [6, 7] # vectors +m1, m2 = [x1 y1], [x2 y2] # 2x2 matrices + +plot([m1, m2]) # array of matrices -> 4 series, plots each matrix column, x assumed to be integer count +plot([[x1,y1], [x2,y2]]) # array of array of arrays -> 4 series, plots each individual array, x assumed to be integer count +plot([(x1,y1), (x2,y2)]) # array of tuples of arrays -> 2 series, plots each tuple as new series +``` + +## Unconnected Data within same groups + +As shown in the examples, you can plot a single polygon by using a single call to `plot` using the `:path` line type. You can use several calls to `plot` to draw several polygons. + +Now, let's say you're plotting `n` polygons grouped into `g` groups, with `n` > `g`. While you can use `plot` to draw separate polygons with each call, you cannot group two separate plots back into a single group. You'll end up with `n` groups in the legend, rather than `g` groups. + +To adress this, you can use `NaN` as a path separator. A call to `plot` would then draw one path with disjoints The following code draws `n=4` rectangles in `g=2` groups. + +```@example input_data +using Plots +plotlyjs() + +function rectangle_from_coords(xb,yb,xt,yt) + [ + xb yb + xt yb + xt yt + xb yt + xb yb + NaN NaN + ] +end + +some_rects=[ + rectangle_from_coords(1, 1, 5, 5) + rectangle_from_coords(10, 10, 15, 15) +] +other_rects=[ + rectangle_from_coords(1, 10, 5, 15) + rectangle_from_coords(10, 1, 15, 5) +] + +plot(some_rects[:,1], some_rects[:,2], label = "some group") +plot!(other_rects[:,1], other_rects[:,2], label = "other group") +png("input_data_1") # hide +``` +![](input_data_1.png) + +## DataFrames support + +Using the [StatsPlots](https://github.com/JuliaPlots/StatsPlots.jl) extension package, you can pass a `DataFrame` as the first argument (similar to Gadfly or R's ggplot2). For data fields or certain attributes (such as `group`) a symbol will be replaced with the corresponding column(s) of the `DataFrame`. Additionally, the column name might be used as the An example: + +```@example input_data +using StatsPlots, RDatasets +gr() +iris = dataset("datasets", "iris") +@df iris scatter( + :SepalLength, + :SepalWidth, + group = :Species, + m = (0.5, [:+ :h :star7], 12), + bg = RGB(0.2, 0.2, 0.2) +) +``` + +## Functions + +Functions can typically be used in place of input data, and they will be mapped as needed. 2D and 3D parametric plots can also be created, and ranges can be given as vectors or min/max. For example, here are alternative methods to create the same plot: + +```@example input_data +using Plots +tmin = 0 +tmax = 4π +tvec = range(tmin, tmax, length = 100) + +plot(sin.(tvec), cos.(tvec)) +``` +```@example input_data +plot(sin, cos, tvec) +``` +```@example input_data +plot(sin, cos, tmin, tmax) +``` + +Vectors of functions are allowed as well (one series per function). + +## Images + +Images can be directly added to plots by using the [Images.jl](https://github.com/timholy/Images.jl) library. For example, one can import a raster image and plot it with Plots via the commands: + +```julia +using Plots, Images +img = load("image.png") +plot(img) +``` + +PDF graphics can also be added to Plots.jl plots using `load("image.pdf")`. Note that Images.jl requires that the PDF color scheme is RGB. + +## Shapes + +*Save Gotham* + +```@example input_data +using Plots + +function make_batman() + p = [(0, 0), (0.5, 0.2), (1, 0), (1, 2), (0.3, 1.2), (0.2, 2), (0, 1.7)] + s = [(0.2, 1), (0.4, 1), (2, 0), (0.5, -0.6), (0, 0), (0, -0.15)] + m = [(p[i] .+ p[i + 1]) ./ 2 .+ s[i] for i in 1:length(p) - 1] + + pts = similar(m, 0) + for (i, mi) in enumerate(m) + append!( + pts, + map(BezierCurve([p[i], m[i], p[i + 1]]), range(0, 1, length = 30)) + ) + end + x, y = Plots.unzip(Tuple.(pts)) + Shape(vcat(x, -reverse(x)), vcat(y, reverse(y))) +end + +# background and limits +plt = plot( + bg = :black, + xlim = (0.1, 0.9), + ylim = (0.2, 1.5), + framestyle = :none, + size = (400, 400), + legend = false, +) +``` + +```@example input_data +# create an ellipse in the sky +pts = Plots.partialcircle(0, 2π, 100, 0.1) +x, y = Plots.unzip(pts) +x = 1.5x .+ 0.7 +y .+= 1.3 +pts = collect(zip(x, y)) + +# beam +beam = Shape([(0.3, 0.0), pts[95], pts[50], (0.3, 0.0)]) +plot!(beam, fillcolor = plot_color(:yellow, 0.3)) +``` + +```@example input_data +# spotlight +plot!(Shape(x, y), c = :yellow) +``` + +```@example input_data +# buildings +rect(w, h, x, y) = Shape(x .+ [0, w, w, 0, 0], y .+ [0, 0, h, h, 0]) +gray(pct) = RGB(pct, pct, pct) +function windowrange(dim, denom) + range(0, 1, length = max(3, round(Int, dim/denom)))[2:end - 1] +end + +for k in 1:50 + local w, h, x, y = 0.1rand() + 0.05, 0.8rand() + 0.3, rand(), 0.0 + shape = rect(w, h, x, y) + graypct = 0.3rand() + 0.3 + plot!(shape, c = gray(graypct)) + + # windows + I = windowrange(w, 0.015) + J = windowrange(h, 0.04) + local pts = vec([(Float64(x + w * i), Float64(y + h * j)) for i in I, j in J]) + windowcolors = Symbol[rand() < 0.2 ? :yellow : :black for i in 1:length(pts)] + scatter!(pts, marker = (stroke(0), :rect, windowcolors)) +end +plt +``` + +```@example input_data +# Holy plotting, Batman! +batman = Plots.scale(make_batman(), 0.07, 0.07, (0, 0)) +batman = translate(batman, 0.7, 1.23) +plot!(batman, fillcolor = :black) +``` + +## [Extra keywords](@id extra_kwargs) + +There are some features that are very specific to a certain backend or not yet implemented in Plots. +For these cases it is possible to forward extra keywords to the backend. +Every keyword that is not a Plots keyword will then be collected in a `extra_kwargs` dictionary. + +This dictionary has three layers: `:plot`, `:subplot` and `:series` (default). +To which layer the keywords get collected can be specified by the `extra_kwargs` keyword. +If arguments should be passed at multiple layers in the same call or the keyword is already a valid Plots keyword, the `extra_kwargs` dictionary has to be constructed at the call site. +```julia +plot(1:5, series_keyword = 5) +# results in extra_kwargs = Dict( :series => Dict( series_keyword => 5 ) ) +plot(1:5, colormap_width = 6, extra_kwargs = :subplot) +# results in extra_kwargs = Dict( :subplot => Dict( colormap_width = 6 ) ) +plot(1:5, extra_kwargs = Dict( :series => Dict( series_keyword => 5 ), :subplot => Dict( colormap_width => 6 ) ) ) +``` + +Refer to the [tracking issue](https://github.com/JuliaPlots/Plots.jl/issues/2648) to see for which backends this feature is implemented. +Which extra keywords the backend actually handles should be documented in the backend documentation. + +!!! warning + Using the extra keywords machinery will make your code backend dependent. + Only use it for final tweaks. It is clearly a bad idea to use it in recipes. diff --git a/docs/src/install.md b/docs/src/install.md new file mode 100644 index 000000000..3f40bd978 --- /dev/null +++ b/docs/src/install.md @@ -0,0 +1,78 @@ + +### Install + +First, add the package: + +```julia +import Pkg +Pkg.add("Plots") + +# if you want the latest features: +Pkg.pkg"add Plots#master" +``` + +The GR [backend](@ref backends) is included by default, but you can install additional plotting packages if you need a different backend. + +Tier 1 support backends (in alphabetical order): +```julia +Pkg.add("GR") +# You do not need to add this package because it is the default backend and +# therefore it is automatically installed with Plots.jl. Note that you might +# need to install additional system packages if you are on Linux, see +# https://gr-framework.org/julia.html#installation + +Pkg.add("PGFPlotsX") +# You need to have LaTeX installed on your system + +Pkg.add("PlotlyJS"); Pkg.add("PlotlyBase") +# Note that you only need to add this if you need Electron windows and +# additional output formats, otherwise `plotly()` comes shipped with Plots.jl. +# In order to have a good experience with Jupyter, refer to Plotly-specific +# Jupyter installation (https://github.com/plotly/plotly.py#installation) + +Pkg.add("PythonPlot") +# Depends only on PythonPlot package + +Pkg.add("UnicodePlots") +``` + +Tier 2 support backends: +```julia +Pkg.add("InspectDR") +Pkg.add("Gaston") +``` + +Learn more about backends [here](https://docs.juliaplots.org/latest/backends/). + +Finally, you may wish to add some extensions from the [Plots ecosystem](@ref ecosystem): + +```julia +Pkg.add("StatsPlots") +Pkg.add("GraphRecipes") +``` + +--- + +### Initialize + +```julia +using Plots # or StatsPlots +# using GraphRecipes # if you wish to use GraphRecipes package too +``` + +Optionally, [choose a backend](@ref backends) and/or override default settings at the same time: + +```julia +gr(size = (300, 300), legend = false) # provide optional defaults +pgfplotsx() +plotly(ticks=:native) # plotlyjs for richer saving options +pythonplot() # backends are selected with lowercase names +unicodeplots() # plot in terminal +``` + +!!! tip + Plots will use the GR backend by default. You can override this choice by setting an environment variable in your `~/.julia/config/startup.jl` file (if the file does not exist, create it). To do this, add e.g. the following line of code: `ENV["PLOTS_DEFAULT_BACKEND"] = "PlotlyJS"`. + +!!! tip + You can override standard default values in your `~/.julia/config/startup.jl` file, for example `PLOTS_DEFAULTS = Dict(:markersize => 10, :legend => false, :warn_on_unsupported => false)`. +--- diff --git a/docs/src/layouts.md b/docs/src/layouts.md new file mode 100644 index 000000000..5e3d94fe4 --- /dev/null +++ b/docs/src/layouts.md @@ -0,0 +1,120 @@ +```@setup layouts +using Plots; gr() +Plots.reset_defaults() +``` + +# [Layouts](@id layouts) + +As of v0.7.0, Plots has taken control of subplot positioning, allowing complex, nested grids of subplots and components. Care has been taken to keep the framework flexible and generic, so that backends need only support the ability to precisely define the absolute position of a subplot, and they get the full power of nesting, plot area alignment, and more. Just set the `layout` keyword in a call to `plot(...)` + +It's helpful at this point to review terminology: + +- **Plot**: The whole figure/window +- **Subplot**: One subplot, containing a title, axes, colorbar, legend, and plot area. +- **Axis**: One axis of a subplot, containing axis guide (label), tick labels, and tick marks. +- **Plot Area**: The part of a subplot where the data is shown... contains the series, grid lines, etc. +- **Series**: One distinct visualization of data. (For example: a line or a set of markers) + +--- + +#### Simple Layouts + +Pass an integer to `layout` to allow it to automatically compute a grid size for that many subplots: + +```@example layouts +# create a 2x2 grid, and map each of the 4 series to one of the subplots +plot(rand(100, 4), layout = 4) +``` + +Pass a tuple to `layout` to create a grid of that size: + +```@example layouts +# create a 4x1 grid, and map each of the 4 series to one of the subplots +plot(rand(100, 4), layout = (4, 1)) +``` + +More complex grid layouts can be created with the `grid(...)` constructor: + +```@example layouts +plot(rand(100, 4), layout = grid(4, 1, heights=[0.1 ,0.4, 0.4, 0.1])) +``` + +Titles and labels can be easily added: + +```@example layouts +plot(rand(100,4), layout = 4, label=["a" "b" "c" "d"], + title=["1" "2" "3" "4"]) +``` + +--- + +#### Advanced Layouts + +The `@layout` macro is the easiest way to define complex layouts, using Julia's [multidimensional Array construction](https://docs.julialang.org/en/v1/manual/arrays/#man-array-concatenation) as the basis for a custom layout syntax. Precise sizing can be achieved with curly brackets, otherwise the free space is equally split between the **plot areas** of subplots. + +The symbols themselves (`a` and `b` in the example below) can be any valid identifier and don't have any special meaning. + +```@example layouts +l = @layout [ + a{0.3w} [grid(3,3) + b{0.2h} ] +] +plot( + rand(10, 11), + layout = l, legend = false, seriestype = [:bar :scatter :path], + title = ["($i)" for j in 1:1, i in 1:11], titleloc = :right, titlefont = font(8) +) +``` + +--- + +Create inset (floating) subplots using the `inset_subplots` attribute. `inset_subplots` takes a list of (parent_layout, BoundingBox) tuples, where the bounding box is relative to the parent. + +Use `px`/`mm`/`inch` for absolute coords, `w`/`h` for percentage relative to the parent. Origin is top-left. `h_anchor`/`v_anchor` define what the `x`/`y` inputs of the bounding box refer to. + +```@example layouts_2 +# boxplot is defined in StatsPlots +using StatsPlots, StatsPlots.PlotMeasures +gr(leg = false, bg = :lightgrey) + +# Create a filled contour and boxplot side by side. +plot(contourf(randn(10, 20)), boxplot(rand(1:4, 1000), randn(1000))) + +# Add a histogram inset on the heatmap. +# We set the (optional) position relative to bottom-right of the 1st subplot. +# The call is `bbox(x, y, width, height, origin...)`, where numbers are treated as +# "percent of parent". +histogram!( + randn(1000), + inset = (1, bbox(0.05, 0.05, 0.5, 0.25, :bottom, :right)), + ticks = nothing, + subplot = 3, + bg_inside = nothing +) + +# Add sticks floating in the window (inset relative to the window, as opposed to being +# relative to a subplot) +sticks!( + randn(100), + inset = bbox(0, -0.2, 200px, 100px, :center), + ticks = nothing, + subplot = 4 +) +``` + +### Adding Subplots incrementally +You can also combine multiple plots to a single plot. To do this, simply pass the variables holding the previous plots to the `plot` function: + +```julia +l = @layout [a ; b c] +p1 = plot(...) +p2 = plot(...) +p3 = plot(...) +plot(p1, p2, p3, layout = l) +``` + +### Ignore plots in layout +You can use the `_` character to ignore plots in the layout (blank plots): +```julia +plot((plot() for i in 1:7)..., layout=@layout([_ ° _; ° ° °; ° ° °])) +``` diff --git a/docs/src/learning.md b/docs/src/learning.md new file mode 100644 index 000000000..8de15b9d7 --- /dev/null +++ b/docs/src/learning.md @@ -0,0 +1,33 @@ +# Tutorials + +- [Start with the tutorial](@ref tutorial) +- [Section from Chris Rackauckas' awesome earlier tutorial](https://ucidatascienceinitiative.github.io/IntroToJulia/Html/PlotsJL) +- [Machine Learning and Visualization in Julia](https://www.breloff.com/JuliaML-and-Plots/) +- [Quant Econ tutorial](https://lectures.quantecon.org/jl/julia_plots.html#plotsjl-jl) +- [Plotting section of a Julia wiki](https://en.wikibooks.org/wiki/Introducing_Julia/Plotting) +- [How do Recipes actually work?](https://daschw.github.io/recipes/) + +# Demos, Examples and Notebooks + +- [Visualizing Graphs in Julia using Plots and PlotRecipes](https://www.breloff.com/Graphs/) +- [ExamplePlots](https://github.com/JuliaPlots/ExamplePlots.jl) +- [Some notebooks](https://github.com/tbreloff/notebooks) + +# Reference sheets + +- [A one-page Plots.jl cheatsheet](https://github.com/sswatson/cheatsheets/blob/master/plotsjl-cheatsheet.pdf) + +# Video tutorials + +### Plots with Plots - JuliaCon 2016 + +```@raw html + +``` +### Ecosystem and Pipeline + +https://www.breloff.com/plots-video/ + +```@raw html + +``` diff --git a/docs/src/output.md b/docs/src/output.md new file mode 100644 index 000000000..e50c8c444 --- /dev/null +++ b/docs/src/output.md @@ -0,0 +1,75 @@ + +# [Output](@id output) + + +**A Plot is only displayed when returned** (a semicolon will suppress the return), or if explicitly displayed with `display(plt)`, `gui()`, or by adding `show = true` to your plot command. + + +!!! tip + You can have MATLAB-like interactive behavior by setting the default value: default(show = true) + +### Standalone window + +Calling `gui(plt)` will open a standalone window. `gui()`, like `plot!(...)`, applies to the "current" Plot. Returning a Plot object to the REPL is like calling `gui(plt)`. + + +### Jupyter / IJulia + +Plots are shown inline when returned to a cell. The default output format is `svg` for backends that support it. +This can be changed by the `html_output_format` attribute, with alias `fmt`: + +```julia +plot(rand(10), fmt = :png) +``` + +### Juno / Atom + +Plots are shown in the Atom PlotPane when possible, either when returned to the console or to an inline code block. At any time, the plot can be opened in a standalone window using the `gui()` command. +The PlotPane can be disabled in Juno's settings. + +### savefig / format + +Plots support 2 different versions per save-command. +Command `savefig` chooses file type automatically based on the file extension. + +```julia +savefig(filename_string) # save the most recent fig as filename_string (such as "output.png") +savefig(plot_ref, filename_string) # save the fig referenced by plot_ref as filename_string (such as "output.png") +``` + +In addition, `Plots` exports the convenience function `png(filename::AbstractString)`. +Other functions such as `Plots.pdf` or `Plots.svg` remain unexported, since they might +conflict with exports from other packages. +In this case the string fn containing the filename does not need a file extension. + +```julia +png(filename_string) # save the current fig as png with filename filename_string (such as "output.png") +png(plot_ref, filename_string) # save the fig referenced by plot_ref as png with filename filename_string (such as "output.png") +``` + +#### File formats supported by most graphical backends + + - png (default output format for `savefig`, if no file extension is given) + - svg + - PDF + +When not using `savefig`, the default output format depends on the environment (e.g., when using IJulia/Jupyter). + +#### Supported output file formats + +Note: not all backends support every output file format ! +A simple table showing which format is supported by which backend + +| format | backends | +| :----- | :------------------------------------------------------------------- | +| eps | inspectdr, plotlyjs, pythonplot | +| html | plotly, plotlyjs | +| json | plotly, plotlyjs | +| pdf | gr, plotlyjs, pythonplot, pgfplotsx, inspectdr, gaston | +| png | gr, plotlyjs, pythonplot, pgfplotsx, inspectdr, gaston, unicodeplots | +| ps | gr, pythonplot | +| svg | gr, inspectdr, pgfplotsx, plotlyjs, pythonplot, gaston | +| tex | pgfplotsx, pythonplot | +| text | hdf5, unicodeplots | + +Supported file formats can be written to an IO stream via, for example, `png(myplot, pipebuffer::IO)`, so the image file can be passed via a PipeBuffer to other functions, eg. `Cairo.read_from_png(pipebuffer::IO)`. diff --git a/docs/src/pipeline.md b/docs/src/pipeline.md new file mode 100644 index 000000000..679056824 --- /dev/null +++ b/docs/src/pipeline.md @@ -0,0 +1,168 @@ +```@setup pipeline +using Plots; gr() +Plots.reset_defaults() +``` + +# [Processing Pipeline](@id pipeline) + +Plotting commands will send inputs through a series of preprocessing steps, in order to convert, simplify, and generalize. The idea is that end-users need incredible flexibility in what (and how) they are able to make calls. They may want total control over plot attributes, or none at all. There may be 8 attributes that are constant, but one that varies by data series. We need to be able to easily layer complex plots on top of each other, and easily define what they should look like. Input data might come in any form. + +I'll go through the steps that occur after a call to `plot()` or `plot!()`, and hint at the power and flexibility that arises. + +### An example command + +Suppose we have data: + +```@example pipeline; continued = true +n = 100 +x, y = range(0, 1, length = n), randn(n, 3) +``` + +and we'd like to visualize `x` against each column of `y`. Here's a sample command in Plots: + +```@example pipeline +using Plots; pythonplot(size = (400, 300)) +plot( + x, y, + line = (0.5, [4 1 0], [:path :scatter :histogram]), + normalize = true, + bins = 30, + marker = (10, 0.5, [:none :+ :none]), + color = [:steelblue :orangered :green], + fill = 0.5, + orientation = [:v :v :h], + title = "My title", +) +``` + +In this example, we have an input matrix, and we'd like to plot three series on top of each other, one for each column of data. +We create a row vector (1x3 matrix) of symbols to assign different visualization types for each series, set the orientation of the histogram, and set +alpha values. + +For comparison's sake, this is somewhat similar to the following calls in PythonPlot: + +```@example pipeline +import PythonPlot +fig = PythonPlot.gcf() +fig.set_size_inches(4, 3, forward = true) +fig.set_dpi(100) +PythonPlot.clf() + +n = 100 +x, y = range(0, 1, length = n), randn(n, 3) + +PythonPlot.plot(x, y[:,1], alpha = 0.5, "steelblue", linewidth = 4) +PythonPlot.scatter(x, y[:,2], alpha = 0.5, marker = "+", s = 100, c="orangered") +PythonPlot.hist( + y[:,3], + orientation = "horizontal", + alpha = 0.5, + density = true, + bins=30, + color="green", + linewidth = 0 +) + +ax = PythonPlot.gca() +ax.xaxis.grid(true) +ax.yaxis.grid(true) +PythonPlot.title("My title") +PythonPlot.legend(["y1","y2"]) +PythonPlot.savefig("pythonplot.svg") +nothing #hide +``` +![](pythonplot.svg) + +--- + + + +### [Step 1: Preprocess Attributes](@id step-1-replace-aliases) + +See [replacing aliases](@ref aliases) and [magic arguments](@ref magic-arguments) for details. + +Afterwards, there are some arguments which are simplified and compressed, such as converting the boolean setting `colorbar = false` to the internal description `colorbar = :none` as to allow complex behavior without complex interface, replacing `nothing` with the invisible `RGBA(0,0,0,0)`, and similar. + +--- + + + +### [Step 2: Process input data: User Recipes, Grouping, and more](@id step-2-handle-magic-arguments) + +Plots will rarely ask you to pre-process your own inputs. You have a Julia array? Great. DataFrame? No problem. Surface function? You got it. + +During this step, Plots will translate your input data (within the context of the plot type and other inputs) into a list of sliced and/or expanded representations, +where each item represents the data for one plot series. Under the hood, it makes heavy use of [multiple dispatch](https://docs.julialang.org/en/release-0.4/manual/methods/) and [recipes](@ref recipes). + +Inputs are recursively processed until a matching recipe is found. This means you can make modular and hierarchical recipes which are processed just like anything built into Plots. + +```@example pipeline +Plots.reset_defaults() # hide +mutable struct MyVecWrapper + v::Vector{Float64} +end +mv = MyVecWrapper(rand(10)) + +@recipe function f(mv::MyVecWrapper) + markershape --> :circle + markersize --> 8 + mv.v +end + +plot( + plot(mv.v), + plot(mv) +) +``` + +Note that if dispatch does not find a recipe for the full combination of inputs, it will then try to apply [type recipes](@ref type-recipes) to each individual argument. + +This hook gave us a nice way to swap out the input data and add custom visualization attributes for a user type. Things like error bars, regression lines, ribbons, and group filtering are also handled during this recursive pass. + +Groups: When you'd like to split a data series into multiple plot series, you can use the `group` keyword. Attributes can be applied to the resulting series as if your data had been already separated into distinct input data. The `group` variable determines how to split the data and also assigns the legend label. + +In this example, we split the data points into 3 groups randomly, and give them different marker shapes (`[:s :o :x]` are aliases for `:star5`, `:octagon`, and `:xcross`). The other attibutes (`:markersize` and `:markeralpha`) are shared. + +```@example pipeline +scatter(rand(100), group = rand(1:3, 100), marker = (10,0.3, [:s :o :x])) +``` + +--- + + + +### Step 3: Initialize and update Plot and Subplots + +Attributes which apply to Plot, Subplot, or Axis objects are pulled out and processed. Backend methods for initializing the figure/window are triggered, and the [layout](@ref layouts) is built. + + +--- + + + +### Step 4: Series Recipes + +This part is somewhat magical. Following the first three steps, we have a list of keyword dictionaries (type `KW`) which contain both data and attributes. Now we will recursively apply [series recipes](@ref series-recipes), first checking to see if a backend supports a series type natively, and if not, applying a series recipe and re-processing. + +The result is that one can create generic recipes (converting a histogram to a bar plot, for example), which will reduce the series to the highest-level type(s) that a backend supports. Since recipes are so simple to create, we can do complex visualizations in backends which support very little natively. + +--- + + + +### Step 5: Preparing for output + +Much of the heavy processing is offloaded until it's needed. Plots will try to avoid expensive graphical updates until you actually choose to [display](@ref output) the plot. Just before display, we will compute the layout specifics and bounding boxes of the subplots and other plot components, then trigger the callback to the backend code to draw/update the plot. + +--- + + + + +### Step 6: Display it + +Open/refresh a GUI window, write to a file, or display inline in IJulia. Remember that, in IJulia or the REPL, **a Plot is only displayed when returned** (a semicolon will suppress the return), or if explicitly displayed with `display()`, `gui()`, or by adding `show = true` to your plot command. + +!!! tip + You can have MATLAB-like interactive behavior by setting the default value: default(show = true) +--- diff --git a/docs/src/plot_objects.md b/docs/src/plot_objects.md new file mode 100644 index 000000000..5e10a6292 --- /dev/null +++ b/docs/src/plot_objects.md @@ -0,0 +1,11 @@ +# [Plot objects](@id object) + +The [`plot`](@ref) function returns a bespoke julia type called [`Plot`](@ref). +The plot object. +It holds all the attributes of the plot itself its [`Subplot`](@ref)s and [`Series`](@ref). + +```@docs +Plot +Subplot +Series +``` \ No newline at end of file diff --git a/docs/src/recipes.md b/docs/src/recipes.md new file mode 100644 index 000000000..366a321e0 --- /dev/null +++ b/docs/src/recipes.md @@ -0,0 +1,493 @@ +```@setup recipes +using Plots; gr() +Plots.reset_defaults() +``` + + +# [Recipes](@id recipes) + +Recipes are a way of defining visualizations in your own packages and code, without having to depend on Plots. The functionality relies on [RecipesBase](https://github.com/JuliaPlots/RecipesBase.jl), a super lightweight but powerful package which allows users to create advanced plotting logic without Plots. The `@recipe` macro in RecipesBase will add a method definition for `RecipesBase.apply_recipe`. Plots adds to and calls this same function, and so your package and Plots can communicate without ever knowing about the other. Magic! + +Visualizing custom user types has always been a confusing problem. Should a package developer add a dependency on a plotting package (forcing the significant baggage that comes with that dependency)? Should they attempt conditional dependencies? Should they submit a PR to graphics packages to define their custom visualizations? It seems that every option had many cons for each pro, and the decision was tough. With recipes, these issues go away. One tiny package (RecipesBase) gives simple hooks into the visualization pipeline, allowing users and package developers to focus solely on the specifics of their visualization. Pick the shapes/lines/colors that will represent your data well, decide on custom defaults, and convert the inputs (if you need to). Everything else is handled by Plots. There are many examples of recipes both within Plots and in many external packages, including [GraphRecipes](https://github.com/JuliaPlots/GraphRecipes.jl). + + +### Visualizing User Types + +Examples are always best. Lets explore the implementation of [creating visualization recipes for Distributions](https://github.com/tbreloff/ExamplePlots.jl/tree/master/notebooks/usertype_recipes.ipynb). + +### Custom treatment of input combinations + +Want to do something special whenever the first input is a time series? Maybe you want to preprocess your data depending on keyword flags? This is all possible by making recipes with unique dispatch signatures. You can offload and use the pre and post processing of Plots, and just add the bits that are specific to you. + +### Type Recipes: Easy drop-in replacement of data types + +Many times a data type is a simple wrapper of a Function or Array. For example: + +```julia +mutable struct MyVec + v::Vector{Int} +end +``` + +If `MyVec` was a subtype of AbstractVector, there would not be anything to do... it should "just work". However this isn't always desireable, and it would be nice if you could call `plot(10:20, myvec)` without having to personally define every possible combination of inputs. It this case, you'll want to use a special type of recipe signature: + +```julia +@recipe f(::Type{MyVec}, myvec::MyVec) = myvec.v +``` + +Afterwards, all plot commands which work for vectors will also work for your datatype. + + +### Series Recipes + +Lets quickly discuss a mainstay of data visualization: the histogram. Hadley Wickham has explored the nature of histograms as part of his [Layered Grammar of Graphics](https://vita.had.co.nz/papers/layered-grammar.pdf). In it, he discusses how a histogram is really nothing more than a bar graph which has its data pre-binned. This is true, and it can be taken further. A bar-graph is really an extension of a step-graph, in which zeros are interwoven among the x-values. A step-graph is really nothing more than a path (line) which can travel only horizontally or vertically. Of course, a similar decomposition could be had by treating the bars as filled polygons. + +The point to be had is that a graphics package need only be able to draw lines and polygons, and they can support drawing a histogram. The path from data to histogram is normally very complicated, but we can avoid the complexity and define a recipe to convert it to its subcomponents. In a few lines of readable code, we can implement a key statistical visualization. See the [tutorial on series recipes](https://github.com/tbreloff/ExamplePlots.jl/tree/master/notebooks/series_recipes.ipynb) for a better understanding of how you might use them. + + + +## Recipe Types + +Above we described `Type recipes` and `Series Recipes`. In total there are four main types of recipes in Plots (listed in the order they are processed): + +- User Recipes +- Type Recipes +- Plot Recipes +- Series Recipes + +**The recipe type is determined completely by the dispatch signature.** Each recipe type is called from a different part of the [plotting pipeline](https://docs.juliaplots.org/latest/pipeline/), so you will choose a type of recipe to match how much processing you want completed before your recipe is applied. + +These are the dispatch signatures for each type (note that most of these can accept positional or keyword args, denoted by `...`): + +### User Recipes +```julia +@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...) end +``` +- Process a unique set of types early in the pipeline. Good for user-defined types or special combinations of Base types. +- The `@userplot` macro is a nice convenience which both defines a new type (to ensure correct dispatch) and exports shorthands. +- See `graphplot` for an example. + +### [Type Recipes](@id type-recipes) +```julia +@recipe function f(::Type{T}, val::T) where{T} end +``` +- For user-defined types which wrap or have a one-to-one mapping to something supported by Plots, simply define a conversion method. +- Note: this is effectively saying "when you see type T, replace it with ..." +- See `SymPy` for an example. + +### Plot Recipes +```julia +@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...) end +``` +- These are called after input data has been processed, but **before the plot is created**. +- Build layouts, add subplots, and other plot-wide attributes. +- See `marginalhist` in [StatsPlots](https://github.com/JuliaPlots/StatsPlots.jl) for an example. + +### [Series Recipes](@id series-recipes) +```julia +@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...) end +``` +- These are the last calls to happen. Each backend will support a short list of series types (`path`, `shape`, `histogram`, etc). If a series type is natively supported, processing is passed (delegated) to the backend. If a series type is **not** natively supported by the backend, we attempt to call a "series recipe". +- Note: If there's no series recipe defined, and the backend doesn't support it, you'll see an error like: `ERROR: The backend must not support the series type Val{:hi}, and there isn't a series recipe defined.` +- Note: You must have the `x, y, z` included in the signature, or it won't be processed as a series type!! + +## Recipe Syntax/Rules + +Lets decompose what's happening inside the recipe macro, starting with a simple recipe: + +```@example recipes +mutable struct MyType end + +@recipe function f(::MyType, n::Integer = 10; add_marker = false) + linecolor --> :blue + seriestype := :path + markershape --> (add_marker ? :circle : :none) + delete!(plotattributes, :add_marker) + rand(n) +end +``` + +We create a new type `MyType`, which is empty, and used purely for dispatch. Our goal here is to create a random path of `n` points. + +There are a few important things to know, after which recipes boil down to updating an attribute dictionary and returning input data: + +- A recipe signature `f(args...; kw...)` is converted into a definition of `apply_recipe(plotattributes::KW, args...)` where: + - `plotattributes` is an attribute dictionary of type `typealias KW Dict{Symbol,Any}` + - Your `args` must be distinct enough that dispatch will call your definition (and without masking an existing definition). Using a custom data type will ensure proper dispatch. + - The function `f` is unused/meaningless... call it whatever you want. +- The special operator `-->` turns `linecolor --> :blue` into `get!(plotattributes, :linecolor, :blue)`, setting the attribute only when it doesn't already exist. (Tip: Wrap the right hand side in parentheses for complex expressions.) +- The special operator `:=` turns `seriestype := :path` into `plotattributes[:seriestype] = :path`, forcing that attribute value. (Tip: Wrap the right hand side in parentheses for complex expressions.) +- One cannot use aliases (such as `colour` or `alpha`) in a recipe, only the full attribute name. +- The return value of the recipe is the `args` of a `RecipeData` object, which also has a reference to the attribute dictionary. +- A recipe returns a Vector{RecipeData}. We'll see how to add to this list later with the `@series` macro. + +!!! compat "RecipesBase 0.9" + Use of the `return` keyword in a recipe requires at least RecipesBase 0.9. + +Breaking down the example: + +In the example above, we use `MyType` for dispatch, with optional positional argument `n::Integer`: + +```julia +@recipe function f(::MyType, n::Integer = 10; add_marker = false) +``` + +With a call to `plot(MyType())` or similar, this recipe will be invoked. If `linecolor` has not been set, it is set to `:blue`: + +```julia + linecolor --> :blue +``` + +The `seriestype` is forced to be `:path`: + +```julia + seriestype := :path +``` + +The `markershape` is a little more complex; it checks the `add_marker` custom keyword, but only if `markershape` was not already set. (Note: the `add_marker` key is redundant, as the user can just set the marker shape directly... I use it only for demonstration): + +```julia + markershape --> (add_marker ? :circle : :none) +``` + +then return the data to be plotted. +```julia + rand(n) +end +``` + +Some example usages of our (mostly useless) recipe: + +```@example recipes +mt = MyType() +plot( + plot(mt), + plot(mt, 100, linecolor = :red), + plot(mt, marker = (:star,20), add_marker = false), + plot(mt, add_marker = true) +) +``` + +--- + +### User Recipes + +The example above is an example of a "user recipe", in which you define the full signature for dispatch. User recipes (like others) can be stacked and modular. The following is valid: + +```julia +@recipe f(mt::MyType, n::Integer = 10) = (mt, rand(n)) +@recipe f(mt::MyType, v::AbstractVector) = (seriestype := histogram; v) +``` + +Here a call to `plot(MyType())` will apply these recipes in order; first mapping `mt` to `(mt, rand(10))` and then subsequently setting the seriestype to `:histogram`. + +```@example recipes +plot(MyType()) +``` + +--- + +### Type Recipes + +For some custom data types, they are essentially light wrappers around built-in containers. For example you may have a type: + +```julia +mutable struct MyWrapper + v::Vector +end +``` + +In this case, you'd like your `MyWrapper` objects to be treated just like Vectors, but do not wish to subtype AbstractArray. No worries! Just define a type recipe to do the conversion: + +```julia +@recipe f(::Type{MyWrapper}, mw::MyWrapper) = mw.v +``` + +This signature is called on each input when dispatch did not find a suitable recipe for the full `args...`. So `plot(rand(10), MyWrapper(rand(10)))` will "just work". + +--- + +### Series Recipes + +This is where the magic happens. You can create your own custom visualizations for arbitrary data. Quickly define violin plots, error bars, and even standard types like histograms and step plots. A histogram is a bar plot: + +```julia +@recipe function f(::Type{Val{:histogram}}, x, y, z) + edges, counts = my_hist(y, plotattributes[:bins], + normed = plotattributes[:normalize], + weights = plotattributes[:weights]) + x := edges + y := counts + seriestype := :bar + () +end +``` + +while a 2D histogram is really a heatmap: + +```julia +@recipe function f(::Type{Val{:histogram2d}}, x, y, z) + xedges, yedges, counts = my_hist_2d(x, y, plotattributes[:bins], + normed = plotattributes[:normalize], + weights = plotattributes[:weights]) + x := centers(xedges) + y := centers(yedges) + z := Surface(counts) + seriestype := :heatmap + () +end +``` + +The argument `y` is always populated, the argument `x` is populated with a call like `plot(x,y, seriestype =: histogram2d)` and correspondingly for `z`, `plot(x,y,z, seriestype =: histogram2d)` + +See below where I go through a series recipe for creating boxplots. Many of these "standard" recipes are defined in Plots, though they can be defined anywhere **without requiring the package to be dependent on Plots**. + + +--- + + +# Case studies + +### Marginal Histograms + +Here we show a user recipe version of the `marginalhist` plot recipe for [StatsPlots](https://github.com/JuliaPlots/StatsPlots.jl). This is a nice example because, although easy to understand, it utilizes some great Plots features. + +Marginal histograms are a visualization comparing two variables. The main plot is a 2D histogram, where each rectangle is a (possibly normalized and weighted) count of data points in that bucket. Above the main plot is a smaller histogram of the first variable, and to the right of the main plot is a histogram of the second variable. The full recipe: + +```@example recipes +@userplot MarginalHist + +@recipe function f(h::MarginalHist) + if length(h.args) != 2 || !(typeof(h.args[1]) <: AbstractVector) || + !(typeof(h.args[2]) <: AbstractVector) + error("Marginal Histograms should be given two vectors. Got: $(typeof(h.args))") + end + x, y = h.args + + # set up the subplots + legend := false + link := :both + framestyle := [:none :axes :none] + grid := false + layout := @layout [tophist _ + hist2d{0.9w,0.9h} righthist] + + # main histogram2d + @series begin + seriestype := :histogram2d + subplot := 2 + x, y + end + + # these are common to both marginal histograms + fillcolor := :black + fillalpha := 0.3 + linealpha := 0.3 + seriestype := :histogram + + # upper histogram + @series begin + subplot := 1 + x + end + + # right histogram + @series begin + orientation := :h + subplot := 3 + y + end +end +``` + +Usage: + + +```@example recipes +using Distributions +n = 1000 +x = rand(Gamma(2), n) +y = -0.5x + randn(n) +marginalhist(x, y, fc = :plasma, bins = 40) +``` + + +--- + +Now I'll go through each section in detail: + +The `@userplot` macro is a nice convenience for creating a new wrapper for input arguments that can be distinct during dispatch. It also creates lowercase convenience methods (`marginalhist` and `marginalhist!`) and exports them. + +```julia +@userplot MarginalHist +``` + +thus create a type `MarginalHist` for dispatch. An object of type `MarginalHist` has the field `args` which is the tuple of arguments the plot function is invoked with, which can be either `marginalhist(x,y,...)` or `plot(x,y, seriestype = :marginalhist)`. The first syntax is a shorthand created by the `@userplot` macro. + +We dispatch only on the generated type, as the real inputs are wrapped inside it: + +```julia +@recipe function f(h::MarginalHist) +``` + +Some error checking. Note that we're extracting the real inputs (like in a call to `marginalhist(randn(100), randn(100))`) into `x` and `y`: + +```julia + if length(h.args) != 2 || !(typeof(h.args[1]) <: AbstractVector) || + !(typeof(h.args[2]) <: AbstractVector) + error("Marginal Histograms should be given two vectors. Got: $(typeof(h.args))") + end + x, y = h.args +``` + +Next we build the subplot layout and define some attributes. A few things to note: + +- The layout creates three subplots (`_` is left blank) +- Attributes are mapped to each subplot when passed in as a matrix (row-vector) +- The attribute `link := :both` means that the y-axes of each row (and x-axes of + each column) will share data extrema. Other values include `:x`, `:y`, + `:all`, and `:none`. + +```julia + # set up the subplots + legend := false + link := :both + framestyle := [:none :axes :none] + grid := false + layout := @layout [tophist _ + hist2d{0.9w,0.9h} righthist] +``` + +Define the series of the main plot. The `@series` macro makes a local copy of the attribute dictionary `plotattributes` using a "let block". The copied dictionary and the returned args are added to the `Vector{RecipeData}` which is returned from the recipe. This block is similar to calling `histogram2d!(x, y; subplot = 2, plotattributes...)` (but you wouldn't actually want to do that). + +Note: this `@series` block gets a "snapshot" of the attributes, so it contains anything that was set before this block, but nothing from after it. `@series` blocks can be standalone, as these are, or they can be in a loop. + +```julia + # main histogram2d + @series begin + seriestype := :histogram2d + subplot := 2 + x, y + end +``` + +Next we move on to the marginal plots. We first set attributes which are shared by both: + +```julia + # these are common to both marginal histograms + fillcolor := :black + fillalpha := 0.3 + linealpha := 0.3 + seriestype := :histogram +``` + +Now we create two more series, one for each histogram. + +```julia + # upper histogram + @series begin + subplot := 1 + x + end + + # right histogram + @series begin + orientation := :h + subplot := 3 + y + end +end +``` + +It's important to note: normally we would return arguments from a recipe, and those arguments would be added to a `RecipeData` object and pushed onto our `Vector{RecipeData}`. However, when creating series using the `@series` macro, you have the option of returning `nothing`, which will bypass that last step. + +One can also have multiple series in a single subplot and repeat the same for multiple subplots if needed. This would require one to supply the correct subplot id/number. + +```julia +mutable struct SeriesRange + range::UnitRange{Int64} +end +@recipe function f(m::SeriesRange) + range = m.range + layout := length(range) + for i in range + @series begin + subplot := i + seriestype := scatter + rand(10) + end + @series begin + subplot := i + rand(10) + end + end +end +``` +--- + +### Documenting plot functions + +A documentation string added above the recipe definition will have no effect, just like the function name is meaningless. Since everything in Julia can be associated with a doc-string, the documentation can be added to the name of the plot function like this +```julia +""" +My docstring +""" +my_plotfunc +``` +This can be put anywhere in the code and will appear on the call `?my_plotfunc`. + +--- + +### Troubleshooting + +It can sometimes be helpful when debugging recipes to see the order of dispatch inside the `apply_recipe` calls. Turn on debugging info with: + +```julia +RecipesBase.debug() +``` + +You can also pass a `Bool` to the `debug` method to turn it on/off. + +Here are some common errors, and what to look out for: + +#### convertToAnyVector + +``` +ERROR: In convertToAnyVector, could not handle the argument types: <> + [inlined code] from ~/.julia/v0.4/Plots/src/series_new.jl:87 + in apply_recipe at ~/.julia/v0.4/RecipesBase/src/RecipesBase.jl:237 + in _plot! at ~/.julia/v0.4/Plots/src/plot.jl:312 + in plot at ~/.julia/v0.4/Plots/src/plot.jl:52 +``` + +This error occurs when the input types could not be handled by a recipe. The type `<>` cannot be processed. Remember, there may be recursive calls to multiple recipes for a complicated plot. + + +#### MethodError: `start` has no method matching start(::Void) + +``` +ERROR: MethodError: `start` has no method matching start(::Void) + in collect at ./array.jl:260 + in collect at ./array.jl:272 + in plotly_series at ~/.julia/v0.4/Plots/src/backends/plotly.jl:345 + in _series_added at ~/.julia/v0.4/Plots/src/backends/plotlyjs.jl:36 + in _apply_series_recipe at ~/.julia/v0.4/Plots/src/plot.jl:224 + in _plot! at ~/.julia/v0.4/Plots/src/plot.jl:537 +``` + +This error is commonly encountered when a series type expects data for `x`, `y`, or `z`, but instead was passed `nothing` (which is of type `Void`). Check that you have a `z` value defined for 3D plots, and likewise that you have valid values for `x` and `y`. This could also apply to attributes like `fillrange`, `marker_z`, or `line_z` if they are expected to have non-void values. + +#### MethodError: Cannot `convert` an object of type Float64 to an object of type RecipeData + +``` +ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type RecipeData +Closest candidates are: + convert(::Type{T}, ::T) where T at essentials.jl:171 + RecipeData(::Any, ::Any) at ~/.julia/packages/RecipesBase/G4s6f/src/RecipesBase.jl:57 +``` +!!! compat "RecipesBase 0.9" + Use of the `return` keyword in recipes requires RecipesBase 0.9 + +This error is encountered if you use the `return` keyword in a recipe, which is not supported in RecipesBase up to v0.8. + + diff --git a/docs/src/series_types/contour.md b/docs/src/series_types/contour.md new file mode 100644 index 000000000..94e887831 --- /dev/null +++ b/docs/src/series_types/contour.md @@ -0,0 +1,146 @@ +```@setup contour +using Plots +Plots.reset_defaults() +``` + +# [Contour Plots](@id contour) + +The easiest way to get started with contour plots is to use the PythonPlot backend. PythonPlot requires the `PythonPlot.jl` +package which can be installed by typing `]` and then `add PythonPlot` into the REPL. The first time you call `pythonplot()`, +Julia may install matplotlib for you. All of the plots generated on this page use PythonPlot, although the code will work +for the default GR backend as well. + +Let's define some ranges and a function `f(x, y)` to plot. Notice the `'` in the line defining `z`. +This is the adjoint operator and makes `x` a row vector. You can check the shape of `x'` by typing `size(x')`. In the +tutorial, we mentioned that the `@.` macro evaluates whatever is to the right of it in an element-wise manner. More +precisely, the dot `.` is shorthand for broadcasting; since `x'` is of size `(1, 100)` and y is of size `(50, )`, +`z = @. f(x', y)` will broadcast the function `f` over `x'` and `y` and yield a matrix of size `(50, 100)`. + +```@example contour +using Plots; pythonplot() + +f(x, y) = (3x + y^2) * abs(sin(x) + cos(y)) + +x = range(0, 5, length=100) +y = range(0, 3, length=50) +z = @. f(x', y) +contour(x, y, z) +``` + +Much like with `plot!` and `scatter!`, the `contour` function also has a mutating version `contour!` which can be +used to modify the plot after it has been generated. + +With the `pythonplot` backend, `contour` can also take in a row vector for `x`, so alternatively, you can define `x` as +a row vector as shown below and PythonPlot will know how to plot it correctly. Beware that this will NOT work for other +backends such as the default GR backend, which require `x` and `y` to both be column vectors. + +```julia +x = range(0, 5, length=100)' +y = range(0, 3, length=50) +z = @. f(x, y) +contour(x, y, z) +``` + +## Common Attributes + +Let's make this plot more presentable with the following attributes: + +1. The number of levels can be changed with `levels`. +2. Besides the title and axes labels, we can also add contour labels via the attribute `contour_labels`, which has the alias `clabels`. We'll use the LaTeXStrings.jl package to write the function expression in the title. (To install this package, type `]` and then `add LaTeXStrings` into the REPL.) +3. The colormap can be changed using `seriescolor`, which has the alias `color`, or even `c`. The default colormap is `:inferno`, from matplotlib. A full list of colormaps can be found in the ColorSchemes section of the manual. +4. The colorbar location can be changed with the attribute `colorbar`, alias `cbar`. We can remove it by setting `cbar=false`. +5. The widths of the isocontours can be changed using `linewidth`, or `lw`. + +Note that `levels`, `color`, and `contour_labels` need to be specified in `contour`. + +```@example contour +using LaTeXStrings + +f(x, y) = (3x + y^2) * abs(sin(x) + cos(y)) + +x = range(0, 5, length=100) +y = range(0, 3, length=50) +z = @. f(x', y) + +contour(x, y, z, levels=10, color=:turbo, clabels=true, cbar=false, lw=1) +title!(L"Plot of $(3x + y^2)|\sin(x) + \cos(y)|$") +xlabel!(L"x") +ylabel!(L"y") +``` + +If only black lines are desired, you can set the `color` attribute like so: + +```julia +contour(x, y, z, color=[:black]) +``` + +and for alternating black and red lines of a specific hex value, you could type `color=[:black, "#E52B50"]`, and so on. + +To get a full list of the available values that an attribute can take, type `plotattr("attribute")` into the REPL. For +example, `plotattr("cbar")` shows that it can take either symbols from a predefined list (e.g. `:left` and `:top`), +which move the colorbar from its default location; or a boolean `true` or `false`, the latter of which hides the +colorbar. + +## Filled Contours + +We can also specify that the contours should be filled in. One way to do this is by using the attribute `fill`: + +```julia +contour(x, y, z, fill=true) +``` + +Another way is to use the function `contourf`, along with its mutating version `contourf!`: + +```@example contour +contourf(x, y, z, levels=20, color=:turbo) +title!(L"(3x + y^2)|\sin(x) + \cos(y)|") +xlabel!(L"x") +ylabel!(L"y") +``` + +If you are using the GR backend to plot filled contours, there will be black lines separating the filled regions. If +these lines are undesirable, you can set the line width to 0: `lw=0`. + +## Logarithmic Contour Plots + +Much like with line and scatter plots, the X and Y axes can be made logarithmic through the `xscale` and `yscale` +attributes. If both axes need to be logarithmic, then you can set `scale=:log10`. + +It will be easier for the backend to generate the plot if the attributes are specified in the `contourf` command +directly instead of using their mutating versions. + +```@example contour +g(x, y) = log(x*y) + +x = 10 .^ range(0, 6, length=100) +y = 10 .^ range(0, 6, length=100) +z = @. g(x', y) +contourf(x, y, z, color=:plasma, scale=:log10, + title=L"\log(xy)", xlabel=L"x", ylabel=L"y") +``` + +It is often desired that the colorbar be logarithmic. The process to get this working correctly is a bit more involved +and will require some manual tweaking. First, we define a function `h(x, y) = exp(x^2 + y^2)`, which we will plot the +logarithm of. Then we adjust the `levels` and `colorbar_ticks` attributes. + +The `colorbar_ticks` attribute can take in a tuple of two vectors `(tickvalues, ticklabels)`. Since `h(x, y)` varies +from `10^0` to `10^8` over the prescribed domain, tickvalues will be a vector `tv = 0:8`. We can format +the labels with superscripts by using LaTeXStrings again. Note that the string interpolation operator changes from `$` +to `%$` when working within `L"..."` to avoid clashing with `$` as normally used in LaTeX. + +```@example contour +h(x, y) = exp(x^2 + y^2) + +x = range(-3, 3, length=100) +y = range(-3, 3, length=100) +z = @. h(x', y) + +tv = 0:8 +tl = [L"10^{%$i}" for i in tv] +contourf(x, y, log10.(z), color=:turbo, levels=8, + colorbar_ticks=(tv, tl), aspect_ratio=:equal, + title=L"\exp(x^{2} + y^{2})", xlabel=L"x", ylabel=L"y") +``` + +If you want the fill boundaries to correspond to the orders of magnitude, `levels=8`. Depending on the data, this +number may require some tweaking. If you want a smoother plot, then you can set `levels` to a much larger number. diff --git a/docs/src/series_types/histogram.md b/docs/src/series_types/histogram.md new file mode 100644 index 000000000..df771f150 --- /dev/null +++ b/docs/src/series_types/histogram.md @@ -0,0 +1,125 @@ +```@setup histogram +using Plots; gr() +Plots.reset_defaults() +``` + +# [Histograms](@id histogram) + +One-dimensional histograms are accessed through the function `histogram` and its mutating variant `histogram!`. We +will use the default GR backend on this page. + +The most basic plot of a histogram is that of a vector of random numbers sampled from the unit normal distribution. + +```@example histogram +using Plots + +x = randn(10^3) +histogram(x) +``` + +The default number of bins is determined by the +[Freedman-Diaconis rule](https://en.wikipedia.org/wiki/Histogram#Freedman%E2%80%93Diaconis'_choice). You can select +other bin algorithms using the attribute `bins`, which can take on values like `:sqrt`, or `:scott` for +[Scott's rule](https://en.wikipedia.org/wiki/Histogram#Scott's_normal_reference_rule). Alternatively, you can pass +in a range to more precisely control the number of bins and their minimum and maximum. For example, to plot 20 bins +from -5 to +5, type + +```julia +range(-5, 5, length=21) +``` + +where we have to add 1 to the length because the length counts the number of bin boundaries. Finally, you can also pass +in an integer, like `bins=15`, but this will only be an approximation and the actual number of bins may vary. + +## Normalization + +It is often desirable to normalize the histogram in some way. To do this, the `normalize` attribute is used, and +we want `normalize=:pdf` (or `:true`) to normalize the total area of the bins to 1. Since we sampled from the normal +distribution, we may as well plot it too. Of course, other common attributes like the title, axis labels, and colors +can be changed as well. + +```@example histogram +p(x) = 1/sqrt(2pi) * exp(-x^2/2) +b_range = range(-5, 5, length=21) + +histogram(x, label="Experimental", bins=b_range, normalize=:pdf, color=:gray) +plot!(p, label="Analytical", lw=3, color=:red) +xlims!(-5, 5) +ylims!(0, 0.4) +title!("Normal distribution, 1000 samples") +xlabel!("x") +ylabel!("P(x)") +``` + +`normalize` can take on other values, including: + +* `:probability`, which sums all the bin heights to 1 +* `:density`, which makes the area of each bin equal to the counts + +## Weighted Histograms + +Another common feature is to weight the values in `x`. Say that `x` consists of data sampled from a uniform +distribution and we wanted to weight the values according to an exponential function. We would pass in a vector of +weights of the same length as `x`. To check that the weighting is done correctly, we plot the exponential function +multiplied by a normalization factor. + +```@example histogram +f_exp(x) = exp(x)/(exp(1)-1) + +x = rand(10^4) +w = exp.(x) + +histogram(x, label="Experimental", bins=:scott, weights=w, normalize=:pdf, color=:gray) +plot!(f_exp, label="Analytical", lw=3, color=:red) +plot!(legend=:topleft) +xlims!(0, 1.0) +ylims!(0, 1.6) +title!("Uniform distribution, weighted by exp(x)") +xlabel!("x") +ylabel!("P(x)") +``` + +## Other Variations + +* Histogram scatter plots can be made via `scatterhist` and `scatterhist!`, where points substitute in for bars. +* Histogram step plots can be made via `stephist` and `stephist!`, where an outline substitutes in for bars. + +```@example histogram +p1 = histogram(x, title="Bar") +p2 = scatterhist(x, title="Scatter") +p3 = stephist(x, title="Step") +plot(p1, p2, p3, layout=(1, 3), legend=false) +``` + +Note that the Y axis of the histogram scatter plot will not start from 0 by default. + +## 2D Histograms + +Two-dimensional histograms are accessed through the function `histogram2d` and its mutating variant `histogram2d!`. +To plot them, two vectors `x` and `y` of the same length are needed. + +The histogram is plotted in 2D as a heatmap instead of as 3D bars. The default colormap is `:inferno`, as with contour +plots and heatmaps. Bins without any count are not plotted at all by default. + +```@example histogram +x = randn(10^4) +y = randn(10^4) +histogram2d(x, y) +``` + +Things like custom bin numbers, weights, and normalization work in 2D, along with changing things like the +colormap. However, the bin numbers need to be passed in via tuples; if only one number is passed in for +the bins, for example, it is assumed that both axes will set the same number of bins. Additionally, the weights +only accept a single vector for the `x` values. + +Not plotting the bins at all may not be visually appealing, especially if a colormap is used with dark colors on the +low end. To rectify this, use the attribute `show_empty_bins=true`. + +```@example histogram +w = exp.(x) +histogram2d(x, y, bins=(40, 20), show_empty_bins=true, + normalize=:pdf, weights=w, color=:plasma) +title!("Normalized 2D Histogram") +xlabel!("x") +ylabel!("y") +``` diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md new file mode 100644 index 000000000..894c68a92 --- /dev/null +++ b/docs/src/tutorial.md @@ -0,0 +1,538 @@ +```@setup tutorial +using Plots; gr() +Plots.reset_defaults() +``` + +# [Tutorial](@id tutorial) + +This is a guide for getting you up and running with Plots.jl. Its main goal is +to introduce you to the terminology used in the package, how to use Plots.jl in +common use cases, and put you in a position to easily understand the rest of +the manual. It is recommended that the code examples be followed inside +the REPL or an interactive notebook. + +## Basic Plotting: Line Plots + +After you have installed Plots.jl via `Pkg.add("Plots")`, the first step is to +initialize the package. Depending on your computer, this will take a few seconds: + +```@example tutorial +using Plots +``` + +To start, let's plot some trigonometric functions. For the `x` coordinates, we can +create a range from 0 to 10 of, say, 100 elements. For the `y` coordinates, we +can create a vector by evaluating `sin(x)` in an element-wise fashion. To do this +in Julia, we insert a dot right after the function call. Finally, we use `plot()` +to plot the line. + +```@example tutorial +x = range(0, 10, length=100) +y = sin.(x) +plot(x, y) +``` + +The plot is displayed in a plot pane, a stand-alone window or the browser, +depending on the environment and backend (see [below](@ref plotting-backends)). + +If this is your first plot of the session and it takes a while to show up, +this is normal; this latency is called the "time to first plot" problem (or `TTFP`), +and subsequent plots will be fast. Because of the way Julia works under +the hood, this is a difficult problem to solve, but much progress has been made +in the past few years to reduce this compilation time. + +In Plots.jl, every column is a **series**, a set of related points which +form lines, surfaces, or other plotting primitives. We can plot multiple +lines by plotting a matrix of values where each column is interpreted as a +separate line. Below, `[y1 y2]` forms a 100x2 matrix (100 elements, 2 columns). + +```@example tutorial +x = range(0, 10, length=100) +y1 = sin.(x) +y2 = cos.(x) +plot(x, [y1 y2]) +``` + +Additionally, we can add more lines by mutating the plot object. This is done +by the `plot!` command, where the `!` denotes that the command is modifying +the current plot. +You'll notice that we also use an `@.` macro. This is a convenience macro +that inserts dots for every function call to the right of the macro, ensuring +that the entire expression is to be evaluated in an element-wise manner. +If we inputted the dots manually, we would need three of them for the sine, +exponent, and subtraction, and the resulting code would be less readable. + +```@example tutorial +y3 = @. sin(x)^2 - 1/2 # equivalent to y3 = sin.(x).^2 .- 1/2 +plot!(x, y3) +``` + +Note that we could have done the same as above using an explicit plot variable, +which we call `p`: + +```@example tutorial +x = range(0, 10, length=100) +y1 = sin.(x) +y2 = cos.(x) +p = plot(x, [y1 y2]) + +y3 = @. sin(x)^2 - 1/2 +plot!(p, x, y3) +``` + +In cases where the plot variable is omitted, Plots.jl uses the global +`Plots.CURRENT_PLOT` automatically. + +### Saving Figures + +Saving plots is done by the `savefig` command. For example: + +```julia +savefig("myplot.png") # saves the CURRENT_PLOT as a .png +savefig(p, "myplot.pdf") # saves the plot from p as a .pdf vector graphic +``` + +There also exist convenience functions `png`, `Plots.pdf` and other +unexported helpers. With these, the extension is omitted from the filename. +The following is equivalent to the above code: + +```julia +png("myplot") +Plots.pdf(p, "myplot") +``` + +More information about outputting figures can be found in the +[Output](@ref output) section of the Manual. + +## Plot Attributes + +In the previous section we made plots... we're done, right? No! We need to style +our plots. In Plots.jl, the modifiers to plots are called **attributes**, which +are documented at the [attributes page](@ref attributes). Plots.jl follows two +simple rules with data and attributes: + +* Positional arguments correspond to input data +* Keyword arguments correspond to attributes + +So something like `plot(x, y, z)` is three-dimensional data for 3D plots with no +attributes, while `plot(x, y, attribute=value)` is two-dimensional data with +one attribute assigned to some value. + +As an example, we can change the line width using `linewidth` (or its alias `lw`), +change the legend's labels using `label`, and add a title with `title`. Notice how +`["sin(x)" "cos(x)"]` has the same number of columns as the data. +Additionally, since the line width is being attributed to `[y1 y2]`, both lines +will be affected by the assigned value. Let's apply all of this to our previous +plot: + +```@example tutorial +x = range(0, 10, length=100) +y1 = sin.(x) +y2 = cos.(x) +plot(x, [y1 y2], title="Trigonometric functions", label=["sin(x)" "cos(x)"], linewidth=3) +``` + +Every attribute can also be applied by mutating the plot with a +modifier function. Some attributes have their own dedicated modifier functions, +while others can be accessed through `plot!(attribute=value)`. +For example, the `xlabel` attribute adds a label for the +x-axis. We can specify it in the plot command with `xlabel=...`, +or we can use the modifier function below to add it after the plot has already +been generated. It's up to you to decide which is better for code readability. + +```julia +xlabel!("x") +``` + +Every modifier function is the name of the attribute followed by `!`. This will +implicitly use the global `Plots.CURRENT_PLOT`. We can apply it to +other plot objects via `attribute!(p, value)`, where `p` is the name +of the plot object that wants to be modified. + +Let's use keywords and modifier functions interchangeably to perform some +common modifications to our example, listed below. You'll notice that for the +attributes `ls` and `legend`, the values include a colon `:`. +The colon denotes a symbol in Julia. They are commonly used for values of +attributes in Plots.jl, along with strings and numbers. + +* Labels for the individual lines, seen in the legend +* Line widths (we'll use the alias `lw` instead of `linewidth`) +* Line styles (we'll use the alias `ls` instead of `linestyle`) +* Legend position (outside the plot, as the default would clutter the plot) +* Legend columns (3, to better use the horizontal space) +* X-limits to go from `0` to `2pi` +* Plot title and axis labels + +```@example tutorial +x = range(0, 10, length=100) +y1 = sin.(x) +y2 = cos.(x) +y3 = @. sin(x)^2 - 1/2 + +plot(x, [y1 y2], label=["sin(x)" "cos(x)"], lw=[2 1]) +plot!(x, y3, label="sin(x)^2 - 1/2", lw=3, ls=:dot) +plot!(legend=:outerbottom, legendcolumns=3) +xlims!(0, 2pi) +title!("Trigonometric functions") +xlabel!("x") +ylabel!("y") +``` + +Note that `y3` is being plotted as a dotted line. This is distinct from a +scatter plot of the data. + +### Logarithmic Scale Plots + +Sometimes data needs to be plotted across orders of magnitude. The attributes +`xscale` and `yscale` can be set to `:log10` in this case. They can also be +set to `:identity` to keep them linear-scale. +Care should be taken to ensure that the data and limits are positive. + +```@example tutorial +x = 10 .^ range(0, 4, length=100) +y = @. 1/(1+x) + +plot(x, y, label="1/(1+x)") +plot!(xscale=:log10, yscale=:log10, minorgrid=true) +xlims!(1e+0, 1e+4) +ylims!(1e-5, 1e+0) +title!("Log-log plot") +xlabel!("x") +ylabel!("y") +``` + +More information about attributes can be found in the +[Attributes](@ref attributes) section of the Manual. + +### LaTeX Equation Strings + +Plots.jl works with LaTeXStrings.jl, a package that allows the user to type +LaTeX equations in string literals. To install this, type in +`Pkg.add("LaTeXStrings")`. The easiest way to use it is to prepend `L` to a +LaTeX-formatted string. If the string is a mix between normal text and LaTeX +equations, insert dollar signs `$` as needed. + +```@example tutorial +using LaTeXStrings + +x = 10 .^ range(0, 4, length=100) +y = @. 1/(1+x) + +plot(x, y, label=L"\frac{1}{1+x}") +plot!(xscale=:log10, yscale=:log10, minorgrid=true) +xlims!(1e+0, 1e+4) +ylims!(1e-5, 1e+0) +title!(L"Log-log plot of $\frac{1}{1+x}$") +xlabel!(L"x") +ylabel!(L"y") +``` + +## Changing Series Type: Scatter Plots + +At this point you know about line plots, but don't you want to plot your data +in other ways? In Plots.jl, these other ways of plotting a series is called a +**series type**. A line is one series type. However, a scatter plot is another +series type which is commonly used. + +Let's start with the sine function again, but this time, we'll define a vector +called `y_noisy` that adds some randomness. +We can change the series type using the `seriestype` attribute. + +```@example tutorial +x = range(0, 10, length=100) +y = sin.(x) +y_noisy = @. sin(x) + 0.1*randn() + +plot(x, y, label="sin(x)") +plot!(x, y_noisy, seriestype=:scatter, label="data") +``` + +For each built-in series type, there is a shorthand function for directly +calling that series type which matches its name. It handles +attributes just the same as the `plot` command, and it has a mutating form which +ends in `!`. For example, we can write the last line as: + +```julia +scatter!(x, y_noisy, label="data") +``` + +The series types which are available are dependent on the backend, and are +documented on the [Supported Attributes page](@ref supported). As we will describe +later, other libraries can add new series types using **recipes**. + +Scatter plots will have some common attributes related to the markers. Here +is an example of the same plot, but with some attributes fleshed out to make +the plot more presentable. Many aliases are used for brevity, and the list +below is by no means exhaustive. + +* `lc` for `linecolor` +* `lw` for `linewidth` +* `mc` for `markercolor` +* `ms` for `markersize` +* `ma` for `markeralpha` + +```@example tutorial +x = range(0, 10, length=100) +y = sin.(x) +y_noisy = @. sin(x) + 0.1*randn() + +plot(x, y, label="sin(x)", lc=:black, lw=2) +scatter!(x, y_noisy, label="data", mc=:red, ms=2, ma=0.5) +plot!(legend=:bottomleft) +title!("Sine with noise") +xlabel!("x") +ylabel!("y") +``` + +## [Plotting Backends](@id plotting-backends) + +Plots.jl is a plotting metapackage: it's an interface over many different plotting libraries. +What Plots.jl is actually doing is interpreting your commands and then +generating the plots using another plotting library, called the **backend**. +The nice thing about this is that you can use many different plotting libraries +all with the Plots.jl syntax, and we'll see in a little bit that Plots.jl +adds new features to each of these libraries! + +When we started plotting above, our plot used the default backend GR. +However, let's say we want a different plotting backend which will plot into +a nice GUI or into the plot pane of VS Code. To do this, we'll need a backend +which is compatible with these features. Some common backends for this are +PythonPlot and Plotly. For example, to install PythonPlot, simply type the command +`Pkg.add("PythonPlot")` into the REPL; to install Plotly, type +`Pkg.add("PlotlyJS")`. + +We can specifically choose the backend we are plotting into by using the name +of the backend in all lowercase as a function. Let's plot the example from +above using Plotly and then GR: + +```@example tutorial +plotlyjs() # set the backend to Plotly + +x = range(0, 10, length=100) +y = sin.(x) +y_noisy = @. sin(x) + 0.1*randn() + +# this plots into a standalone window via Plotly +plot(x, y, label="sin(x)", lc=:black, lw=2) +scatter!(x, y_noisy, label="data", mc=:red, ms=2, ma=0.5) +plot!(legend=:bottomleft) +title!("Sine with noise, plotted with Plotly") +xlabel!("x") +ylabel!("y") +png("plotlyjs_tutorial") #hide +``` +![](plotlyjs_tutorial.png) + +```@example tutorial +gr() # set the backend to GR + +# this plots using GR +plot(x, y, label="sin(x)", lc=:black, lw=2) +scatter!(x, y_noisy, label="data", mc=:red, ms=2, ma=0.5) +plot!(legend=:bottomleft) +title!("Sine with noise, plotted with GR") +xlabel!("x") +ylabel!("y") +``` + +Each plotting backend has a very different feel. Some have interactivity, some +are faster and can deal with huge numbers of datapoints, and some can do +3D plots. Some backends like GR can save to vector graphics and PDFs, while +others like Plotly can only save to PNGs. + +For more information on backends, see the [backends page](@ref backends). +For examples of plots from the various backends, see the Examples section. + +## Plotting in Scripts + +At the start of the tutorial, we recommended following along the code examples +in an interactive session for the following reason: try adding those same +plotting commands to a script. Now call the script... and the plot doesn't +show up? This is because Julia in interactive use through the REPL calls `display` on every +variable that is returned by a command without a semicolon `;`. In each case +above, the interactive usage was automatically calling `display` on the returned +plot objects. + +In a script, Julia does not do automatic displays, which is why `;` is not +necessary. However, if we would like to display our plots in a script, this +means we just need to add the `display` call. For example: + +```julia +display(plot(x, y)) +``` + +Alternatively, we could call `gui()` at the end to do the same thing. +Finally, if we have a plot object `p`, we can type `display(p)` to +display the plot. + +## Combining Multiple Plots as Subplots + +We can combine multiple plots together as subplots using **layouts**. +There are many methods for doing this, and we will show two simple methods +for generating simple layouts. More advanced layouts are shown in the +[Layouts page](@ref layouts). + +The first method is to define a layout which will split a series. The `layout` +command takes in a 2-tuple `layout=(N, M)` which builds an NxM grid of plots, +and it will automatically split a series to be in each plot. For example, if we +type `layout=(3, 1)` on a plot with three series, then we will get three rows of +plots, each with one series in it. + +Let's define some functions and plot them in separate plots. Since there's only +one series in each plot, we'll also remove the legend in each of the plots +using `legend=false`. + +```@example tutorial +x = range(0, 10, length=100) +y1 = @. exp(-0.1x) * cos(4x) +y2 = @. exp(-0.3x) * cos(4x) +y3 = @. exp(-0.5x) * cos(4x) +plot(x, [y1 y2 y3], layout=(3, 1), legend=false) +``` + +We can also use layouts on plots of plot objects. For example, we can generate +four separate plots and make a single plot that combines them into a 2x2 grid. + +```@example tutorial +x = range(0, 10, length=100) +y1 = @. exp(-0.1x) * cos(4x) +y2 = @. exp(-0.3x) * cos(4x) +y3 = @. exp(-0.1x) +y4 = @. exp(-0.3x) +y = [y1 y2 y3 y4] + +p1 = plot(x, y) +p2 = plot(x, y, title="Title 2", lw=3) +p3 = scatter(x, y, ms=2, ma=0.5, xlabel="xlabel 3") +p4 = scatter(x, y, title="Title 4", ms=2, ma=0.2) +plot(p1, p2, p3, p4, layout=(2,2), legend=false) +``` + +Note that the attributes in the individual plots are applied to those +individual plots, while the attribute `legend=false` in the final `plot` +call is applied to all of the subplots. + +## Plot Recipes and Recipe Libraries + +You now know all of the basic terminology of Plots.jl and can roam the +documentation freely to become a plotting master. However, there is one +thing left: **recipes**. Plotting recipes are extensions to the Plots.jl +framework. They add: + +1. New `plot` commands via **user recipes**. +2. Default interpretations of Julia types as plotting data via **type recipes**. +3. New functions for generating plots via **plot recipes**. +4. New series types via **series recipes**. + +Writing your own recipes is an advanced topic described on the +[recipes page](@ref recipes). Instead, we will introduce the ways that one uses +a recipe. + +Recipes are included in many recipe libraries. Two fundamental recipe libraries +are [PlotRecipes.jl](https://github.com/JuliaPlots/PlotRecipes.jl) and +[StatsPlots.jl](https://github.com/JuliaPlots/StatsPlots.jl). Let's look into +StatsPlots.jl. StatsPlots.jl adds a bunch of recipes, but the ones we'll focus +on are: + +1. It adds a type recipe for `Distribution`s. +2. It adds a plot recipe for marginal histograms. +3. It adds a bunch of new statistical plot series. + +Besides recipes, StatsPlots.jl also provides a specialized macro `@df` from plotting +directly from data tables. + +### Using User Recipes + +A user recipe says how to interpret plotting commands on a new data type. +In this case, StatsPlots.jl has a macro `@df` which allows you to plot +a `DataFrame` directly by using the column names. Let's build a `DataFrame` +with columns `a`, `b`, and `c`, and tell Plots.jl to use `a` as the `x` axis +and plot the series defined by columns `b` and `c`: + +```@example tutorial +# Pkg.add("StatsPlots") +# required for the dataframe user recipe +using StatsPlots + +# now let's create the dataframe +using DataFrames +df = DataFrame(a=1:10, b=10*rand(10), c=10*rand(10)) + +# plot the dataframe by declaring the points by the column names +# x = :a, y = [:b :c] (notice that y has two columns!) +@df df plot(:a, [:b :c]) +``` + +There's not much you have to do here: all of the commands from before +(attributes, series types, etc.) will still work on this data: + +```@example tutorial +# x = :a, y = :b +@df df scatter(:a, :b, title="My DataFrame Scatter Plot!") +``` + +### Using a Type Recipe + +In addition, StatsPlots.jl extends Distributions.jl by adding a type recipe +for its distribution types, so they can be directly interpreted as plotting +data: + +```@example tutorial +using Distributions +plot(Normal(3, 5), lw=3) +``` + +Type recipes are a very convenient way to plot a specialized type which +requires no more intervention! + +### Using Plot Recipes + +StatsPlots.jl adds the `marginhist` multiplot via a plot recipe. For our data, +we will pull in the famous `iris` dataset from RDatasets: + +```@example tutorial +# Pkg.add("RDatasets") +using RDatasets, StatsPlots +iris = dataset("datasets", "iris") +@df iris marginalhist(:PetalLength, :PetalWidth) +``` + +Here, `iris` is a DataFrame; using the `@df` macro on `DataFrame`s described above, +we give `marginalhist(x, y)` the data from the `PetalLength` and the `PetalWidth` +columns. + +Notice that this is more than a series since it generates multiple series +(i.e. there are multiple plots due to the hists on the top and right). +Thus a plot recipe is not just a series, but also something like a new +`plot` command. + +### Using Series Recipes + +StatsPlots.jl also introduces new series recipes. The key is that you don't have +to do anything differently. After `using StatsPlots`, you can simply use those +new series recipes as though they were built into the plotting libraries. Let's +use the Violin plot on some random data: + +```@example tutorial +y = rand(100, 4) +violin(["Series 1" "Series 2" "Series 3" "Series 4"], y, legend=false) +``` + +We can add a `boxplot` on top using the same mutation commands as before: + +```@example tutorial +boxplot!(["Series 1" "Series 2" "Series 3" "Series 4"], y, legend=false) +``` + +## Additional Addons To Try + +Given the easy extendability of Plots.jl, there are many other things you can +try. Here's a short list of very usable addons to check out: + +- [PlotThemes.jl](https://github.com/JuliaPlots/PlotThemes.jl) allows you to + change the color scheme of your plots. For example, `theme(:dark)` adds a + dark theme. +- [StatsPlots.jl](https://github.com/JuliaPlots/StatsPlots.jl) adds functionality + for visualizations of statistical analysis +- The [ecosystem page](@ref ecosystem) shows many other packages which have recipes + and extend Plots.jl's functionality. diff --git a/docs/user_gallery/config.json b/docs/user_gallery/config.json new file mode 100644 index 000000000..5a97b6048 --- /dev/null +++ b/docs/user_gallery/config.json @@ -0,0 +1,9 @@ +{ + "theme": "bulmagrid", + "properties":{ + "notebook": "false" + }, + "order": [ + "misc" + ] +} diff --git a/docs/user_gallery/index.md b/docs/user_gallery/index.md new file mode 100644 index 000000000..ee8443046 --- /dev/null +++ b/docs/user_gallery/index.md @@ -0,0 +1,5 @@ +# User Gallery + +This is a collection of user-contributed demo examples. Contributions are welcome! + +{{{democards}}} diff --git a/docs/user_gallery/misc/config.json b/docs/user_gallery/misc/config.json new file mode 100644 index 000000000..1df675c31 --- /dev/null +++ b/docs/user_gallery/misc/config.json @@ -0,0 +1,3 @@ +{ + "title": "Miscellaneous" +} diff --git a/docs/user_gallery/misc/double_pendulum.jl b/docs/user_gallery/misc/double_pendulum.jl new file mode 100644 index 000000000..449c0ff8a --- /dev/null +++ b/docs/user_gallery/misc/double_pendulum.jl @@ -0,0 +1,102 @@ +# --- +# title: Double Pendulum Problem +# description: "" +# cover: assets/Pendulum.gif +# author: "[Felix Michaelis](https://www.instagram.com/dietzlix/)" +# date: 2022-09-07 +# --- + +# This animation illustrates the double pendulum problem. + +# Double pendulum formula translated from the [matplotlib gallery](https://matplotlib.org/stable/gallery/animation/double_pendulum.html#sphx-glr-gallery-animation-double-pendulum-py). + + +using OrdinaryDiffEq + +G = 9.8 # acceleration due to gravity, in m/s^2 +L1 = 1.0 # length of pendulum 1 in m +L2 = 1.0 # length of pendulum 2 in m +L = L1 + L2 # maximal length of the combined pendulum +M1 = 1.0 # mass of pendulum 1 in kg +M2 = 1.0 # mass of pendulum 2 in kg +t_stop = 5 # how many seconds to simulate + +function pendulum!(du, u, p, t) + (; M1, M2, L1, L2, G) = p + + du[1] = u[2] + + delta = u[3] - u[1] + den1 = (M1 + M2) * L1 - M2 * L1 * cos(delta) * cos(delta) + du[2] = ( + ( + M2 * L1 * u[2] * u[2] * sin(delta) * cos(delta) + + M2 * G * sin(u[3]) * cos(delta) + + M2 * L2 * u[4] * u[4] * sin(delta) - (M1 + M2) * G * sin(u[1]) + ) / den1 + ) + + du[3] = u[4] + + den2 = (L2 / L1) * den1 + du[4] = ( + ( + -M2 * L2 * u[4] * u[4] * sin(delta) * cos(delta) + + (M1 + M2) * G * sin(u[1]) * cos(delta) - + (M1 + M2) * L1 * u[2] * u[2] * sin(delta) - (M1 + M2) * G * sin(u[3]) + ) / den2 + ) + nothing +end + +# `th1` and `th2` are the initial angles (degrees) +# +# `w10` and `w20` are the initial angular velocities (degrees per second) +th1 = 120.0 +w1 = 0.0 +th2 = -10.0 +w2 = 0.0 + +p = (; M1, M2, L1, L2, G) +prob = ODEProblem(pendulum!, deg2rad.([th1, w1, th2, w2]), (0.0, t_stop), p) +sol = solve(prob, Tsit5()) + +x1 = +L1 * sin.(sol[1, :]) +y1 = -L1 * cos.(sol[1, :]) + +x2 = +L2 * sin.(sol[3, :]) + x1 +y2 = -L2 * cos.(sol[3, :]) + y1 + +using Plots +gr() +anim = @animate for i in eachindex(x2) + + x = [0, x1[i], x2[i]] + y = [0, y1[i], y2[i]] + + plot(x, y, legend = false) + plot!(xlims = (-2, 2), xticks = -2:0.5:2) + plot!(ylims = (-2, 1), yticks = -2:0.5:1) + scatter!(x, y) + + x = x2[1:i] + y = y2[1:i] + + plot!(x, y, linecolor = :orange) + plot!(xlims = (-2, 2), xticks = -2:0.5:2) + plot!(ylims = (-2, 1), yticks = -2:0.5:1) + scatter!( + x, + y, + color = :orange, + markersize = 2, + markerstrokewidth = 0, + markerstrokecolor = :orange, + ) + annotate!(-1.25, 0.5, "time= $(rpad(round(sol.t[i]; digits=2),4,"0")) s") +end +gif(anim, fps = 10) + +# save cover image #src +mkpath("assets") #src +gif(anim, "assets/Pendulum.gif", fps = 10) #src diff --git a/docs/user_gallery/misc/gr_lorenz_attractor.jl b/docs/user_gallery/misc/gr_lorenz_attractor.jl new file mode 100644 index 000000000..7b209d049 --- /dev/null +++ b/docs/user_gallery/misc/gr_lorenz_attractor.jl @@ -0,0 +1,56 @@ +# --- +# title: Lorenz Attractor +# description: Simple is beautiful +# cover: assets/lorenz_attractor.gif +# author: "[Thomas Breloff](https://github.com/tbreloff)" +# date: 2021-08-11 +# --- + +using Plots +gr() + +## define the Lorenz attractor +Base.@kwdef mutable struct Lorenz + dt::Float64 = 0.02 + σ::Float64 = 10 + ρ::Float64 = 28 + β::Float64 = 8 / 3 + x::Float64 = 1 + y::Float64 = 1 + z::Float64 = 1 +end + +function step!(l::Lorenz) + dx = l.σ * (l.y - l.x) + dy = l.x * (l.ρ - l.z) - l.y + dz = l.x * l.y - l.β * l.z + l.x += l.dt * dx + l.y += l.dt * dy + l.z += l.dt * dz +end + +attractor = Lorenz() + + +## initialize a 3D plot with 1 empty series +plt = plot3d( + 1, + xlim = (-30, 30), + ylim = (-30, 30), + zlim = (0, 60), + title = "Lorenz Attractor", + legend = false, + marker = 2, +) + +## build an animated gif by pushing new points to the plot, saving every 10th frame +## equivalently, you can use `@gif` to replace `@animate` and thus no need to explicitly call `gif(anim)`. +anim = @animate for i = 1:1_500 + step!(attractor) + push!(plt, attractor.x, attractor.y, attractor.z) +end every 10 +gif(anim) + +# save cover image #src +mkpath("assets") #src +gif(anim, "assets/lorenz_attractor.gif") #src From 4bae77b0244a483b6df7ea79c54c19d9df0ec6b1 Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Tue, 10 Sep 2024 07:44:58 +0100 Subject: [PATCH 25/85] CI fixes v2 (#4980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CI fixes (#4974) * Update tests for Latexify 0.16.5 Behaviour was changed such that ã becomes \tilde{a}, instead of \textnormal{\~{a}}. * Test Julia 1.6 on x86 macOS instead of ARM See: https://discourse.julialang.org/t/how-to-fix-github-actions-ci-failures-with-julia-1-6-or-1-7-on-macos-latest-and-macos-14/117019 * Bump GR compat and Plots version number * update plotly show methods for PlotlyKaleido v2 * remove Pkg * add it back --------- Co-authored-by: Simon Christ * Don't need to edit CI for 1.6 as no longer supported * Don't test GR/50 on macOS * Reorganise failing tests - Disabled reference tests 25 and 30, which require StatsPlots support, for all backends (in PlotsBase/src/examples.jl) - Separate tests that are skipped and tests that are known to be broken due to upstream issues - Re-enable reference test 41 as upstream issue has been fixed (https://github.com/JuliaLang/julia/issues/47261) - Disable reference test 50 because of upstream issue (https://github.com/jheinen/GR.jl/issues/550) --------- Co-authored-by: Simon Christ --- PlotsBase/src/examples.jl | 8 +++++--- PlotsBase/test/runtests.jl | 8 ++------ PlotsBase/test/test_backends.jl | 2 +- PlotsBase/test/test_pgfplotsx.jl | 2 +- PlotsBase/test/test_reference.jl | 10 ++++++++-- Project.toml | 16 +++++++++++++++- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/PlotsBase/src/examples.jl b/PlotsBase/src/examples.jl index 86c02710e..d11b4a342 100644 --- a/PlotsBase/src/examples.jl +++ b/PlotsBase/src/examples.jl @@ -1258,12 +1258,10 @@ _animation_examples = [02, 31] _backend_skips = Dict( :none => Int[], :pythonplot => Int[], - :gr => [25, 30], # TODO: add back when StatsPlots is available + :gr => Int[], :plotlyjs => [ 21, 24, - 25, - 30, 49, 50, 51, @@ -1314,6 +1312,10 @@ _backend_skips = Dict( ], ) _backend_skips[:plotly] = _backend_skips[:plotlyjs] +# 25 and 30 require StatsPlots, which doesn't support Plots v2 yet +for backend in keys(_backend_skips) + append!(_backend_skips[backend], [25, 30]) +end # --------------------------------------------------------------------------------- # replace `f(args...)` with `f(rng, args...)` for `f ∈ (rand, randn)` diff --git a/PlotsBase/test/runtests.jl b/PlotsBase/test/runtests.jl index 85b987e21..c154cc0ca 100644 --- a/PlotsBase/test/runtests.jl +++ b/PlotsBase/test/runtests.jl @@ -54,12 +54,8 @@ is_ci() || @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 ref_name(i) = "ref" * lpad(i, 3, '0') -const blacklist = if VERSION.major == 1 && VERSION.minor ≥ 9 - [ - 25, - 30, # FIXME: remove, when StatsPlots supports Plots v2 - 41, - ] # FIXME: github.com/JuliaLang/julia/issues/47261 +const broken_examples = if Sys.isapple() + [50] # FIXME: https://github.com/jheinen/GR.jl/issues/550 else [] end diff --git a/PlotsBase/test/test_backends.jl b/PlotsBase/test/test_backends.jl index 6c83ac87b..b30fc6b99 100644 --- a/PlotsBase/test/test_backends.jl +++ b/PlotsBase/test/test_backends.jl @@ -66,7 +66,7 @@ is_pkgeval() || @testset "Backends" begin @test filesize(fn) > 1_000 end Sys.islinux() && for be ∈ TEST_BACKENDS - skip = vcat(PlotsBase._backend_skips[be], blacklist) + skip = vcat(PlotsBase._backend_skips[be], broken_examples) PlotsBase.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage closeall() end diff --git a/PlotsBase/test/test_pgfplotsx.jl b/PlotsBase/test/test_pgfplotsx.jl index af51bde12..0d5f86068 100644 --- a/PlotsBase/test/test_pgfplotsx.jl +++ b/PlotsBase/test/test_pgfplotsx.jl @@ -436,7 +436,7 @@ with(:pgfplotsx) do @test PlotsBase.pgfx_sanitize_string("A string, with 2 punctuation chars.") == "A string, with 2 punctuation chars." @test PlotsBase.pgfx_sanitize_string("Interpolação polinomial") == - raw"Interpola$\textnormal{\c{c}}$$\textnormal{\~{a}}$o polinomial" + raw"Interpola$\textnormal{\c{c}}$$\tilde{a}$o polinomial" @test PlotsBase.pgfx_sanitize_string("∫∞ ∂x") == raw"$\int$$\infty$ $\partial$x" # special LaTeX characters diff --git a/PlotsBase/test/test_reference.jl b/PlotsBase/test/test_reference.jl index a03389b30..4e3133af0 100644 --- a/PlotsBase/test/test_reference.jl +++ b/PlotsBase/test/test_reference.jl @@ -105,6 +105,7 @@ end function image_comparison_facts( pkg::Symbol; skip = [], # skip these examples (int index) + broken = [], # known broken examples (int index) only = nothing, # limit to these examples (int index) debug = false, # print debug information ? sigma = [1, 1], # number of pixels to "blur" @@ -112,7 +113,11 @@ function image_comparison_facts( ) for i ∈ setdiff(1:length(PlotsBase._examples), skip) if only ≡ nothing || i in only - @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) + if i ∈ broken + @test_broken success(image_comparison_tests(pkg, i; debug, sigma, tol)) + else + @test success(image_comparison_tests(pkg, i; debug, sigma, tol)) + end end end end @@ -141,7 +146,8 @@ end image_comparison_facts( :gr, tol = PLOTS_IMG_TOL, - skip = vcat(PlotsBase._backend_skips[:gr], blacklist), + skip = vcat(PlotsBase._backend_skips[:gr]), + broken = broken_examples, ) end end diff --git a/Project.toml b/Project.toml index cb4539f3b..36e36ba3f 100644 --- a/Project.toml +++ b/Project.toml @@ -9,8 +9,22 @@ PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +[weakdeps] +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[extensions] +FileIOExt = "FileIO" +GeometryBasicsExt = "GeometryBasics" +IJuliaExt = "IJulia" +ImageInTerminalExt = "ImageInTerminal" +UnitfulExt = "Unitful" + [compat] -GR = "0, 1" +GR = "0.73, 1" PlotsBase = "0.1" PrecompileTools = "1" Reexport = "1" From 0d6f0b449a49e9c96a2ab38cdaae9899fe7ef070 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:43:21 +0200 Subject: [PATCH 26/85] CompatHelper: add new compat entry for IJulia in [weakdeps] at version 1, (keep existing compat) (#4986) Co-authored-by: CompatHelper Julia --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 36e36ba3f..6570a2d4b 100644 --- a/Project.toml +++ b/Project.toml @@ -25,6 +25,7 @@ UnitfulExt = "Unitful" [compat] GR = "0.73, 1" +IJulia = "1" PlotsBase = "0.1" PrecompileTools = "1" Reexport = "1" From bec14d15f7699281317252c421e61840d7d88b5a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:43:45 +0200 Subject: [PATCH 27/85] CompatHelper: add new compat entry for FileIO in [weakdeps] at version 1, (keep existing compat) (#4985) Co-authored-by: CompatHelper Julia --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 6570a2d4b..ac931c75b 100644 --- a/Project.toml +++ b/Project.toml @@ -24,6 +24,7 @@ ImageInTerminalExt = "ImageInTerminal" UnitfulExt = "Unitful" [compat] +FileIO = "1" GR = "0.73, 1" IJulia = "1" PlotsBase = "0.1" From 2696246e0faea5f6d2d94d4656647011b4f43cb9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:44:43 +0200 Subject: [PATCH 28/85] CompatHelper: add new compat entry for GeometryBasics in [weakdeps] at version 0.4, (keep existing compat) (#4984) Co-authored-by: CompatHelper Julia Co-authored-by: Simon Christ --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index ac931c75b..cae6e5279 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,7 @@ UnitfulExt = "Unitful" [compat] FileIO = "1" GR = "0.73, 1" +GeometryBasics = "0.4" IJulia = "1" PlotsBase = "0.1" PrecompileTools = "1" From 9c256c9625af52bd591c47c5c75f830003783b53 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:44:59 +0200 Subject: [PATCH 29/85] CompatHelper: add new compat entry for Unitful in [weakdeps] at version 1, (keep existing compat) (#4983) Co-authored-by: CompatHelper Julia --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index cae6e5279..fc89c25d6 100644 --- a/Project.toml +++ b/Project.toml @@ -31,6 +31,7 @@ IJulia = "1" PlotsBase = "0.1" PrecompileTools = "1" Reexport = "1" +Unitful = "1" julia = "1.9" [extras] From 5a611ce6085d36544be481ec104d7e5e22d3009e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:45:28 +0200 Subject: [PATCH 30/85] CompatHelper: add new compat entry for ImageInTerminal in [weakdeps] at version 0.5, (keep existing compat) (#4982) Co-authored-by: CompatHelper Julia Co-authored-by: Simon Christ --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index fc89c25d6..1fb3803c9 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,7 @@ UnitfulExt = "Unitful" [compat] FileIO = "1" GR = "0.73, 1" +ImageInTerminal = "0.5" GeometryBasics = "0.4" IJulia = "1" PlotsBase = "0.1" From f000fc77ff3f22613e18a2e77f288d4e612b8532 Mon Sep 17 00:00:00 2001 From: Patrick Jaap Date: Wed, 2 Oct 2024 10:32:24 +0200 Subject: [PATCH 31/85] GR backend: use textext for :log2 and :ln axis scaling (#4991) (#4993) This fixes #4871 Only the case of :log10 has been treated before. --- .zenodo.json | 4 ++++ PlotsBase/ext/GRExt.jl | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 42fbc2c8a..eb7341d1f 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -775,6 +775,10 @@ "affiliation": "The Alan Turing Institute", "name": "Penelope Yong", "type": "Other" + }, + { + "name": "Patrick Jaap", + "type": "Other" } ], "upload_type": "software" diff --git a/PlotsBase/ext/GRExt.jl b/PlotsBase/ext/GRExt.jl index 2e4490803..c354d17ce 100644 --- a/PlotsBase/ext/GRExt.jl +++ b/PlotsBase/ext/GRExt.jl @@ -431,7 +431,7 @@ end gr_inqtext(x, y, s) = gr_inqtext(x, y, string(s)) gr_inqtext(x, y, s::AbstractString) = - if (occursin('\\', s) || occursin("10^{", s)) && + if (occursin('\\', s) || occursin(r"10\^{|2\^{|e\^{", s)) && match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.inqtextext(x, y, s) else @@ -440,7 +440,7 @@ gr_inqtext(x, y, s::AbstractString) = gr_text(x, y, s) = gr_text(x, y, string(s)) gr_text(x, y, s::AbstractString) = - if (occursin('\\', s) || occursin("10^{", s)) && + if (occursin('\\', s) || occursin(r"10\^{|2\^{|e\^{", s)) && match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.textext(x, y, s) else From 17b6ca0c94cd0f3d1058d4303846b62e162e4818 Mon Sep 17 00:00:00 2001 From: Lukas Weber <49278367+lukas-weber@users.noreply.github.com> Date: Wed, 2 Oct 2024 06:54:29 -0400 Subject: [PATCH 32/85] RecipesPipeline: GroupBy: apply idxfilter to errorbars (#4967) * RecipesPipeline: GroupBy: apply idxfilter to errorbars too Fixes #4917 * modifiy .zenodo.json --------- Co-authored-by: Simon Christ --- .zenodo.json | 6 ++++++ RecipesPipeline/src/group.jl | 2 +- RecipesPipeline/test/test_group.jl | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index eb7341d1f..8362504a4 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -771,6 +771,12 @@ "orcid": "0000-0002-1589-2916", "type": "Other" }, + { + "affiliation": "Flatiron Institute", + "name": "Lukas Weber", + "orcid": "0000-0003-4949-5529", + "type": "Other" + }, { "affiliation": "The Alan Turing Institute", "name": "Penelope Yong", diff --git a/RecipesPipeline/src/group.jl b/RecipesPipeline/src/group.jl index ede1641c8..33d70f725 100644 --- a/RecipesPipeline/src/group.jl +++ b/RecipesPipeline/src/group.jl @@ -50,7 +50,7 @@ filter_data(v::AVec, idxfilter::AVec{Int}) = v[idxfilter] filter_data(v, idxfilter) = v function filter_data!(plotattributes::AKW, idxfilter) - for s in (:x, :y, :z) + for s in (:x, :y, :z, :xerror, :yerror, :zerror) plotattributes[s] = filter_data(get(plotattributes, s, nothing), idxfilter) end end diff --git a/RecipesPipeline/test/test_group.jl b/RecipesPipeline/test/test_group.jl index 41f4c862a..98bc58b3e 100644 --- a/RecipesPipeline/test/test_group.jl +++ b/RecipesPipeline/test/test_group.jl @@ -46,4 +46,16 @@ lp = map(i -> "xx" * "$(i % 599)", 1:2_000) RecipesPipeline.GroupBy @test RecipesPipeline._extract_group_attributes(Dict(:A => [1], :B => [2])) isa RecipesPipeline.GroupBy + + @testset "_filter_input_data!" begin + filtered_keys = [:x, :y, :z, :xerror, :yerror, :zerror] + orig_akw = Dict{Symbol,Any}(k => rand(10) for k in filtered_keys) + orig_akw[:idxfilter] = [1,4,10] + + akw = deepcopy(orig_akw) + RecipesPipeline._filter_input_data!(akw) + for k in filtered_keys + @test akw[k] == orig_akw[k][orig_akw[:idxfilter]] + end + end end From 7b37fd5bef43649591737688a90df0900eeef0f5 Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" <744411+ViralBShah@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:04:13 -0700 Subject: [PATCH 33/85] Update ci.yml for aarch64 mac --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ab7f3dd0..8aeca5096 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,12 @@ jobs: - '1.9' # minimal declared julia compat in `Project.toml` experimental: - false - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] arch: [x64] include: + - os: macOS-latest + arch: aarch64 + version: '1' - os: ubuntu-latest experimental: true version: 'pre' # upcoming julia version (`alpha`, `beta` or `rc`) From efcbdbab3f177e5685934dec7860c21e989ad481 Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" <744411+ViralBShah@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:07:16 -0700 Subject: [PATCH 34/85] Update ci.yml --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aeca5096..48b8bb8ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,14 @@ jobs: os: [ubuntu-latest, windows-latest] arch: [x64] include: - - os: macOS-latest - arch: aarch64 - version: '1' - os: ubuntu-latest experimental: true version: 'pre' # upcoming julia version (`alpha`, `beta` or `rc`) - + - os: macOS-latest + arch: aarch64 + version: '1' + experimental: false + steps: - uses: actions/checkout@v4 From 049bba8f1420f559d073069aa5ee293bcc30fc46 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Tue, 8 Oct 2024 21:34:02 +0200 Subject: [PATCH 35/85] restrict to LTS 1.10 (#4997) --- .github/workflows/ci.yml | 2 +- PlotsBase/Project.toml | 2 +- Project.toml | 2 +- RecipesBase/Project.toml | 2 +- RecipesPipeline/Project.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48b8bb8ea..237641963 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: matrix: version: - '1' # latest stable - - '1.9' # minimal declared julia compat in `Project.toml` + - '1.10' # minimal declared julia compat in `Project.toml` experimental: - false os: [ubuntu-latest, windows-latest] diff --git a/PlotsBase/Project.toml b/PlotsBase/Project.toml index f2df17c9a..6cb9826e9 100644 --- a/PlotsBase/Project.toml +++ b/PlotsBase/Project.toml @@ -109,7 +109,7 @@ UnicodePlots = "3" UnitfulLatexify = "1" Unzip = "0.1 - 0.2" UUIDs = "1" -julia = "1.9" +julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/Project.toml b/Project.toml index 1fb3803c9..976ad1d92 100644 --- a/Project.toml +++ b/Project.toml @@ -33,7 +33,7 @@ PlotsBase = "0.1" PrecompileTools = "1" Reexport = "1" Unitful = "1" -julia = "1.9" +julia = "1.10" [extras] PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" diff --git a/RecipesBase/Project.toml b/RecipesBase/Project.toml index 284647765..550642662 100644 --- a/RecipesBase/Project.toml +++ b/RecipesBase/Project.toml @@ -8,7 +8,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" [compat] PrecompileTools = "1" -julia = "1.6" +julia = "1.10" [extras] StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" diff --git a/RecipesPipeline/Project.toml b/RecipesPipeline/Project.toml index bba45bff5..9ff3bf196 100644 --- a/RecipesPipeline/Project.toml +++ b/RecipesPipeline/Project.toml @@ -15,7 +15,7 @@ NaNMath = "0.3, 1" PlotUtils = "0.6.5, 1" RecipesBase = "1.3.1" PrecompileTools = "1" -julia = "1.9" +julia = "1.10" [extras] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" From 19e30739815a9a0453e510b0f8beaaf05fa1abdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:30:36 +0200 Subject: [PATCH 36/85] Bump peter-evans/create-pull-request from 6 to 7 (#4981) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/format_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml index 96e41e6e7..19db47078 100644 --- a/.github/workflows/format_pr.yml +++ b/.github/workflows/format_pr.yml @@ -20,7 +20,7 @@ jobs: - name: Create Pull Request if: ${{ failure() }} id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "Format .jl files [skip ci]" From f137efb6c7e97032a9b8913d2159441a3878cf14 Mon Sep 17 00:00:00 2001 From: leckerbeon Date: Thu, 10 Oct 2024 01:35:29 +0200 Subject: [PATCH 37/85] Adding uparrow as a predefined marker and creating unfilled markershapes (#4977) * :uparrow and :downarrow markershapes are now defined as well as markercolor=nothing creates an unfilled open arrow shape * Changes to update markerstrokecolor * Added myself to .zenodo.json * add special :arrow markershape for sticks * improve arrowshape * update tests * fix custom markershapes * remove extensions from Plots project file --------- Co-authored-by: Simon Christ Co-authored-by: Simon Christ --- .zenodo.json | 4 ++++ PlotsBase/ext/GRExt.jl | 8 ++++++++ PlotsBase/src/Commons/Commons.jl | 1 + PlotsBase/src/Commons/attrs.jl | 4 +++- PlotsBase/src/Shapes.jl | 5 +++++ PlotsBase/src/recipes.jl | 5 +++++ PlotsBase/src/utils.jl | 4 ++-- PlotsBase/test/test_animations.jl | 2 +- PlotsBase/test/test_reference.jl | 4 ++-- Project.toml | 19 ------------------- 10 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 8362504a4..fec2e5ead 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -782,6 +782,10 @@ "name": "Penelope Yong", "type": "Other" }, + { + "name": "Leon Becker", + "type": "Other" + }, { "name": "Patrick Jaap", "type": "Other" diff --git a/PlotsBase/ext/GRExt.jl b/PlotsBase/ext/GRExt.jl index c354d17ce..86b9b0056 100644 --- a/PlotsBase/ext/GRExt.jl +++ b/PlotsBase/ext/GRExt.jl @@ -227,6 +227,7 @@ const gr_markertypes = ( vline = -30, hline = -31, ) +const gr_marker_keys = keys(gr_markertypes) const gr_haligns = ( left = GR.TEXT_HALIGN_LEFT, hcenter = GR.TEXT_HALIGN_CENTER, @@ -822,6 +823,9 @@ alignment(symb) = end # -------------------------------------------------------------------------------------- +function gr_get_markershape(s::Symbol) + s in gr_marker_keys ? s : Shape(s) +end function gr_set_gradient(c) grad = _as_gradient(c) @@ -1263,6 +1267,7 @@ function gr_add_legend(sp, leg, viewport_area) if (msh = series[:markershape]) ≢ :none msz = max(first(series[:markersize]), 0) + msh = gr_get_markershape.(msh) msw = max(first(series[:markerstrokewidth]), 0) mfac = 0.8 * lfps / (msz + 0.5 * msw + 1e-20) gr_draw_marker( @@ -2047,6 +2052,9 @@ function gr_draw_markers( ms = get_thickness_scaling(series) * _cycle(msize, i) msw = get_thickness_scaling(series) * _cycle(strokewidth, i) shape = _cycle(shapes, i) + if !(shape isa Shape) + shape = gr_get_markershape.(shape) + end for j ∈ rng gr_draw_marker( series, diff --git a/PlotsBase/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl index 573953f9a..50dd564bf 100644 --- a/PlotsBase/src/Commons/Commons.jl +++ b/PlotsBase/src/Commons/Commons.jl @@ -61,6 +61,7 @@ using ..ColorTypes: alpha using ..RecipesBase using ..Statistics using ..NaNMath +using ..Unzip using ..Printf const width = Measures.width diff --git a/PlotsBase/src/Commons/attrs.jl b/PlotsBase/src/Commons/attrs.jl index 48da09c12..6b16d2c02 100644 --- a/PlotsBase/src/Commons/attrs.jl +++ b/PlotsBase/src/Commons/attrs.jl @@ -144,7 +144,6 @@ const _styleAliases = Dict{Symbol,Symbol}( const _shape_keys = Symbol[ :circle, :rect, - :star5, :diamond, :hexagon, :cross, @@ -157,6 +156,7 @@ const _shape_keys = Symbol[ :heptagon, :octagon, :star4, + :star5, :star6, :star7, :star8, @@ -164,6 +164,8 @@ const _shape_keys = Symbol[ :hline, :+, :x, + :uparrow, + :downarrow, ] const _all_markers = vcat(:none, :auto, _shape_keys) # sort(collect(keys(_shapes)))) diff --git a/PlotsBase/src/Shapes.jl b/PlotsBase/src/Shapes.jl index 1feef553e..7d5bd92aa 100644 --- a/PlotsBase/src/Shapes.jl +++ b/PlotsBase/src/Shapes.jl @@ -47,6 +47,9 @@ function Shape(x::AVec{X}, y::AVec{Y}) where {X,Y} return Shape(convert(Vector{X}, x), convert(Vector{Y}, y)) end +# make it broadcast like a scalar +Base.Broadcast.broadcastable(shape::Shape) = Ref(shape) + get_xs(shape::Shape) = shape.x get_ys(shape::Shape) = shape.y vertices(shape::Shape) = collect(zip(shape.x, shape.y)) @@ -135,6 +138,8 @@ const _shapes = KW( :star6 => makestar(6), :star7 => makestar(7), :star8 => makestar(8), + :uparrow => Shape([(-1.3,-1), (0, 1.5), (0,-1.5), (0, 1.5), (1.3,-1)]), + :downarrow => Shape([(-1.3, 1), (0, -1.5), (0,1.5), (0, -1.5),(1.3, 1)]), ) Shape(k::Symbol) = deepcopy(_shapes[k]) diff --git a/PlotsBase/src/recipes.jl b/PlotsBase/src/recipes.jl index 597977e0a..3e2db37c2 100644 --- a/PlotsBase/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -332,6 +332,11 @@ end if plotattributes[:markershape] ≢ :none primary := false @series begin + markershape := if plotattributes[:markershape] === :arrow + [isless(yi, 0.0) ? :downarrow : :uparrow for yi in y] + else + plotattributes[:markershape] + end seriestype := :scatter x := x y := y diff --git a/PlotsBase/src/utils.jl b/PlotsBase/src/utils.jl index fb266145e..ec24b35ae 100644 --- a/PlotsBase/src/utils.jl +++ b/PlotsBase/src/utils.jl @@ -108,9 +108,9 @@ function _update_series_attributes!(plotattributes::AKW, plt::Plot, sp::Subplot) elseif plotattributes[:markerstrokecolor] ≡ :auto get_series_color(plotattributes[:markercolor], sp, plotIndex, stype) else - get_series_color(plotattributes[:markerstrokecolor], sp, plotIndex, stype) + get_series_color(something(plotattributes[:markerstrokecolor], plotattributes[:seriescolor]), sp, plotIndex, stype) end - + # if marker_z, fill_z or line_z are set, ensure we have a gradient if plotattributes[:marker_z] ≢ nothing Commons.ensure_gradient!(plotattributes, :markercolor, :markeralpha) diff --git a/PlotsBase/test/test_animations.jl b/PlotsBase/test/test_animations.jl index ca22cf6db..a988ac3c9 100644 --- a/PlotsBase/test/test_animations.jl +++ b/PlotsBase/test/test_animations.jl @@ -54,7 +54,7 @@ end circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines) end when i % 5 == 0 fps = 10 - @test_throws LoadError macroexpand( + @test_throws ErrorException macroexpand( @__MODULE__, quote @gif for i ∈ 1:n diff --git a/PlotsBase/test/test_reference.jl b/PlotsBase/test/test_reference.jl index 4e3133af0..650add787 100644 --- a/PlotsBase/test/test_reference.jl +++ b/PlotsBase/test/test_reference.jl @@ -18,7 +18,7 @@ reference_dir(args...) = else joinpath(homedir(), ".julia", "dev", "PlotReferenceImages.jl", args...) end -reference_path(backend, version) = reference_dir("Plots", string(backend), string(version)) +reference_path(backend, version) = reference_dir("PlotsBase", string(backend), string(version)) function checkout_reference_dir(dn::AbstractString) mkpath(dn) @@ -54,7 +54,7 @@ end function reference_file(backend, version, i) # NOTE: keep ref[...].png naming consistent with `PlotDocs` - refdir = reference_dir("Plots", string(backend)) + refdir = mkpath(reference_dir("PlotsBase", string(backend))) fn = ref_name(i) * ".png" reffn = joinpath(refdir, string(version), fn) for ver ∈ sort(VersionNumber.(readdir(refdir)), rev = true) diff --git a/Project.toml b/Project.toml index 976ad1d92..e8d70b3e9 100644 --- a/Project.toml +++ b/Project.toml @@ -9,30 +9,11 @@ PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -[weakdeps] -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" -ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[extensions] -FileIOExt = "FileIO" -GeometryBasicsExt = "GeometryBasics" -IJuliaExt = "IJulia" -ImageInTerminalExt = "ImageInTerminal" -UnitfulExt = "Unitful" - [compat] -FileIO = "1" GR = "0.73, 1" -ImageInTerminal = "0.5" -GeometryBasics = "0.4" -IJulia = "1" PlotsBase = "0.1" PrecompileTools = "1" Reexport = "1" -Unitful = "1" julia = "1.10" [extras] From fa6341142abbec07bed7cc7701a66c684e0bc54e Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sat, 12 Oct 2024 13:52:55 +0200 Subject: [PATCH 38/85] delete` .github/workflows/invalidations.yml` See https://github.com/julia-actions/julia-invalidations/issues/19 --- .github/workflows/invalidations.yml | 41 ----------------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/invalidations.yml diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml deleted file mode 100644 index cee213dcf..000000000 --- a/.github/workflows/invalidations.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: invalidations -on: - workflow_dispatch: - pull_request: - push: - branches: [v2] - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: julia-actions/setup-julia@latest - with: - version: '1' - - uses: actions/checkout@v4 - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-invalidations@v1 - id: invs_pr - - - uses: actions/checkout@v4 - with: - ref: 'master' - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-invalidations@v1 - id: invs_master - - - name: Report invalidation counts - run: | - echo "Invalidations on master: ${{ steps.invs_master.outputs.total }} (${{ steps.invs_master.outputs.deps }} via deps)" - echo "This branch: ${{ steps.invs_pr.outputs.total }} (${{ steps.invs_pr.outputs.deps }} via deps)" - shell: bash - - name: PR doesn't increase number of invalidations - run: | - if (( ${{ steps.invs_pr.outputs.total }} > ${{ steps.invs_master.outputs.total }} )); then - exit 1 - fi - shell: bash From c1cfa308681879f54a9755012669872af507cafd Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 16:04:43 +0200 Subject: [PATCH 39/85] fix docs on v2 (#4999) * fix docs on v2 * fix test * unexport center, too generic * update --- PlotsBase/src/PlotsBase.jl | 5 -- PlotsBase/test/test_components.jl | 2 +- docs/make.jl | 88 +++++++++++++------------ docs/src/GraphRecipes/examples.md | 2 +- docs/src/GraphRecipes/introduction.md | 2 +- docs/src/RecipesBase/types.md | 6 +- docs/src/UnitfulExt/unitfulext_plots.jl | 8 +-- docs/src/animations.md | 2 +- docs/src/backends.md | 12 ++-- docs/src/colorschemes.md | 2 +- docs/src/contributing.md | 2 +- docs/src/democards/bulmagridtheme.css | 12 ---- docs/src/ecosystem.md | 2 +- docs/src/index.md | 2 +- docs/src/input_data.md | 8 +-- docs/src/layouts.md | 2 +- docs/src/pipeline.md | 4 +- docs/src/recipes.md | 2 +- docs/src/series_types/contour.md | 2 +- docs/src/series_types/histogram.md | 2 +- docs/src/tutorial.md | 2 +- 21 files changed, 80 insertions(+), 89 deletions(-) delete mode 100644 docs/src/democards/bulmagridtheme.css diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl index 14dbc92f9..d4f7995dc 100644 --- a/PlotsBase/src/PlotsBase.jl +++ b/PlotsBase/src/PlotsBase.jl @@ -108,11 +108,6 @@ export test_examples, coords, - translate, - translate!, - rotate, - rotate!, - center, plotattr, scalefontsizes, resetfontsizes diff --git a/PlotsBase/test/test_components.jl b/PlotsBase/test/test_components.jl index 5999a27ab..de53ade64 100644 --- a/PlotsBase/test/test_components.jl +++ b/PlotsBase/test/test_components.jl @@ -38,7 +38,7 @@ @test PlotsBase.translate(square, 0, 1).x == squareUp.x @test PlotsBase.translate(square, 0, 1).y == squareUp.y - @test PlotsBase.center(translate!(square, 1)) == (1.5, 1.5) + @test PlotsBase.center(PlotsBase.translate!(square, 1)) == (1.5, 1.5) end @testset "Rotate" begin diff --git a/docs/make.jl b/docs/make.jl index 01f3d114f..b86de99eb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -93,7 +93,7 @@ ref_name(i) = "ref" * lpad(i, 3, '0') function generate_cards( prefix::AbstractString, backend::Symbol, slice; - skip = get(Plots.PlotsBase._backend_skips, backend, Int[]) + skip = get(PlotsBase._backend_skips, backend, Int[]) ) @show backend # create folder: for each backend we generate a DemoSection "generated" under "gallery" @@ -102,7 +102,7 @@ function generate_cards( needs_rng_fix = Dict{Int,Bool}() - for (i, example) ∈ enumerate(Plots.PlotsBase._examples) + for (i, example) ∈ enumerate(PlotsBase._examples) (slice ≢ nothing && i ∉ slice) && continue # write out the header, description, code block, and image link jlname = "$backend-$(ref_name(i)).jl" @@ -114,7 +114,7 @@ function generate_cards( # DemoCards YAML frontmatter # https://johnnychen94.github.io/DemoCards.jl/stable/quickstart/usage_example/julia_demos/1.julia_demo/#juliademocard_example - asset = if i ∈ Plots.PlotsBase._animation_examples + asset = if i ∈ PlotsBase._animation_examples "anim_$(backend)_$(ref_name(i)).gif" else "$(backend)_$(ref_name(i)).png" @@ -151,14 +151,14 @@ function generate_cards( # DemoCards use Literate.jl syntax with extra leading `#` as markdown lines write(jl, "# $(replace(example.desc, "\n" => "\n # "))\n") isnothing(example.imports) || pretty_print_expr(jl, example.imports) - needs_rng_fix[i] = (exprs_rng = Plots.PlotsBase.replace_rand(example.exprs)) != example.exprs + needs_rng_fix[i] = (exprs_rng = PlotsBase.replace_rand(example.exprs)) != example.exprs pretty_print_expr(jl, exprs_rng) # NOTE: the supported `Literate.jl` syntax is `#src` and `#hide` NOT `# src` !! # from the docs: """ # #src and #hide are quite similar. The only difference is that #src lines are filtered out before execution (if execute=true) and #hide lines are filtered out after execution. # """ - asset = if i ∈ Plots.PlotsBase._animation_examples + asset = if i ∈ PlotsBase._animation_examples "gif(anim, \"assets/anim_$(backend)_$(ref_name(i)).gif\")\n" # NOTE: must not be hidden, for appearance in the rendered `html` else "png(\"assets/$(backend)_$(ref_name(i)).png\") #src\n" @@ -192,7 +192,7 @@ function generate_cards( # TODO(johnnychen): make this part of the page template attr_name = string(backend, ".jl") open(joinpath(cardspath, attr_name), "w") do jl - pkg = Plots.PlotsBase.backend_instance(Symbol(lowercase(string(backend)))) + pkg = PlotsBase.backend_instance(Symbol(lowercase(string(backend)))) write(jl, """ # --- # title: Supported attribute values @@ -202,10 +202,10 @@ function generate_cards( # date: $(now()) # --- - # - Supported arguments: $(markdown_code_to_string(collect(Plots.PlotsBase.supported_attrs(pkg)))) - # - Supported values for linetype: $(markdown_symbols_to_string(Plots.PlotsBase.supported_seriestypes(pkg))) - # - Supported values for linestyle: $(markdown_symbols_to_string(Plots.PlotsBase.supported_styles(pkg))) - # - Supported values for marker: $(markdown_symbols_to_string(Plots.PlotsBase.supported_markers(pkg))) + # - Supported arguments: $(markdown_code_to_string(collect(PlotsBase.supported_attrs(pkg)))) + # - Supported values for linetype: $(markdown_symbols_to_string(PlotsBase.supported_seriestypes(pkg))) + # - Supported values for linestyle: $(markdown_symbols_to_string(PlotsBase.supported_styles(pkg))) + # - Supported values for marker: $(markdown_symbols_to_string(PlotsBase.supported_markers(pkg))) """ ) end @@ -227,11 +227,11 @@ function make_support_df(allvals, func; default_backends) for be ∈ bs # cols be_supported_vals = fill("", length(vals)) for (i, val) ∈ enumerate(vals) - be_supported_vals[i] = if func == Plots.PlotsBase.supported_seriestypes - stype = Plots.PlotsBase.seriestype_supported(Plots.PlotsBase.backend_instance(be), val) + be_supported_vals[i] = if func == PlotsBase.supported_seriestypes + stype = PlotsBase.seriestype_supported(PlotsBase.backend_instance(be), val) stype ≡ :native ? "✅" : (stype ≡ :no ? "" : "🔼") else - val ∈ func(Plots.PlotsBase.backend_instance(be)) ? "✅" : "" + val ∈ func(PlotsBase.backend_instance(be)) ? "✅" : "" end end df[!, be] = be_supported_vals @@ -241,10 +241,10 @@ end function generate_supported_markdown(; default_backends) supported_args = OrderedDict( - "Keyword Arguments" => (Plots.Commons._all_attrs, Plots.PlotsBase.supported_attrs), - "Markers" => (Plots.Commons._all_markers, Plots.PlotsBase.supported_markers), - "Line Styles" => (Plots.Commons._all_styles, Plots.PlotsBase.supported_styles), - "Scales" => (Plots.Commons._all_scales, Plots.PlotsBase.supported_scales) + "Keyword Arguments" => (Plots.Commons._all_attrs, PlotsBase.supported_attrs), + "Markers" => (Plots.Commons._all_markers, PlotsBase.supported_markers), + "Line Styles" => (Plots.Commons._all_styles, PlotsBase.supported_styles), + "Scales" => (Plots.Commons._all_scales, PlotsBase.supported_scales) ) open(joinpath(GEN_DIR, "supported.md"), "w") do md write(md, """ @@ -260,7 +260,7 @@ function generate_supported_markdown(; default_backends) - 🔼 the series type is supported through series recipes. ```@raw html - $(to_html(make_support_df(Plots.PlotsBase.all_seriestypes(), Plots.PlotsBase.supported_seriestypes; default_backends))) + $(to_html(make_support_df(PlotsBase.all_seriestypes(), PlotsBase.supported_seriestypes; default_backends))) ``` """ ) @@ -289,7 +289,7 @@ function make_attr_df(ktype::Symbol, defs::KW) Description = fill("", n), ) for (i, (k, def)) ∈ enumerate(defs) - type, desc = get(Plots.PlotsBase._arg_desc, k, (Any, "")) + type, desc = get(PlotsBase._arg_desc, k, (Any, "")) aliases = sort(collect(keys(filter(p -> p.second == k, Plots.Commons._keyAliases)))) df.Attribute[i] = string(k) @@ -585,12 +585,17 @@ function main() unicodeplots() gaston() - # NOTE: for a faster representative test build use `PLOTDOCS_BACKENDS='GR' PLOTDOCS_EXAMPLES='1'` - default_backends = "GR PythonPlot PlotlyJS PGFPlotsX UnicodePlots Gaston" - backends = get(ENV, "PLOTDOCS_BACKENDS", default_backends) - backends = backends == "ALL" ? default_backends : backends + # NOTE: for a faster representative test build use `PLOTDOCS_PACKAGES='GR' PLOTDOCS_EXAMPLES='1'` + default_packages = "GR,PythonPlot,PlotlyJS,PGFPlotsX,UnicodePlots,Gaston" + packages = get(ENV, "PLOTDOCS_PACKAGES", default_packages) + packages = let val = packages == "ALL" ? default_packages : packages + Symbol.(filter(!isempty, strip.(split(val, ",")))) + end + packages_backends = NamedTuple(p => Symbol(lowercase(string(p))) for p ∈ packages) + backends = values(packages_backends) |> collect + + @info "selected packages: $packages" @info "selected backends: $backends" - backends = Symbol.(lowercase.(split(backends))) slice = parse.(Int, split(get(ENV, "PLOTDOCS_EXAMPLES", ""))) slice = length(slice) == 0 ? nothing : slice @@ -606,7 +611,7 @@ function main() for (pkg, dest) ∈ ( (PlotThemes, "plotthemes.md"), - # (StatsPlots, "statsplots.md"), #TODO: uncomment after having compatible StatsPlots + # (StatsPlots, "statsplots.md"), # TODO: uncomment after having compatible StatsPlots ) cp(pkgdir(pkg, "README.md"), joinpath(GEN_DIR, dest); force = true) end @@ -614,13 +619,13 @@ function main() @info "gallery" gallery = Pair{String,String}[] gallery_assets, gallery_callbacks, user_gallery = map(_ -> [], 1:3) - needs_rng_fix = Dict{String,Any}() + needs_rng_fix = Dict{Symbol,Any}() - for name ∈ backends - pname = string(Plots.PlotsBase.backend_package_name(name)) - needs_rng_fix[pname] = generate_cards(joinpath(@__DIR__, "gallery"), name, slice) - let (path, cb, assets) = makedemos(joinpath("gallery", string(name)); src = "$work/gallery") - push!(gallery, pname => joinpath("gallery", path)) + for pkg ∈ packages + be = packages_backends[pkg] + needs_rng_fix[pkg] = generate_cards(joinpath(@__DIR__, "gallery"), be, slice) + let (path, cb, assets) = makedemos(joinpath("gallery", string(be)); src = "$work/gallery") + push!(gallery, string(pkg) => joinpath("gallery", path)) push!(gallery_callbacks, cb) push!(gallery_assets, assets) end @@ -635,21 +640,21 @@ function main() "Getting Started" => [ "Installation" => "install.md", "Basics" => "basics.md", - "Tutorial" => "tutorial.md", + # "Tutorial" => "tutorial.md", # TODO: uncomment once StatsPlots is ready "Series Types" => [ "Contour Plots" => "series_types/contour.md", "Histograms" => "series_types/histogram.md", ], ], "Manual" => [ - "Input Data" => "input_data.md", + # "Input Data" => "input_data.md", # TODO: uncomment once StatsPlots is ready "Output" => "output.md", "Attributes" => "attributes.md", "Series Attributes" => "generated/attributes_series.md", "Plot Attributes" => "generated/attributes_plot.md", "Subplot Attributes" => "generated/attributes_subplot.md", "Axis Attributes" => "generated/attributes_axis.md", - "Layouts" => "layouts.md", + # "Layouts" => "layouts.md", # TODO: uncomment once StatsPlots is ready "Recipes" => [ "Overview" => "recipes.md", "RecipesBase" => [ @@ -674,12 +679,12 @@ function main() "Learning" => "learning.md", "Contributing" => "contributing.md", "Ecosystem" => [ - # "StatsPlots" => "generated/statsplots.md", #TODO: uncomment once StatsPlots is ready + # "StatsPlots" => "generated/statsplots.md", # TODO: uncomment once StatsPlots is ready # "GraphRecipes" => [ # "Introduction" => "GraphRecipes/introduction.md", # "Examples" => "GraphRecipes/examples.md", # "Attributes" => "generated/graph_attributes.md", - # ], #TODO: uncomment once GraphRecipes is ready + # ], # TODO: uncomment once GraphRecipes is ready "UnitfulExt" => [ "Introduction" => "UnitfulExt/unitfulext.md", "Examples" => [ @@ -687,7 +692,7 @@ function main() "Plots" => "generated/unitfulext_plots.md", ] ], - "Overview" => "ecosystem.md", + # "Overview" => "ecosystem.md", # TODO: uncomment once StatsPlots is ready ], "Advanced Topics" => ["Plot objects" => "plot_objects.md","Plotting pipeline" => "pipeline.md"], "Gallery" => gallery, @@ -708,8 +713,6 @@ function main() unique!(selected_pages) # @show selected_pages length(gallery) length(user_gallery) - # FIXME: github.com/JuliaDocs/DemoCards.jl/pull/134 - # delete src/democards/bulmagridtheme.css when released n = 0 for (root, dirs, files) ∈ walkdir(SRC_DIR) foreach(dir -> mkpath(joinpath(WORK_DIR, dir)), dirs) @@ -770,9 +773,10 @@ function main() # postprocess gallery html files to remove `rng` in user displayed code # non-exhaustive list of examples to be fixed: # [1, 4, 5, 7:12, 14:21, 25:27, 29:30, 33:34, 36, 38:39, 41, 43, 45:46, 48, 52, 54, 62] - for name ∈ split(backends) - prefix = joinpath(@__DIR__, "build", "gallery", lowercase(name), "generated") - must_fix = needs_rng_fix[name] + for pkg ∈ packages + be = packages_backends[pkg] + prefix = joinpath(@__DIR__, "build", "gallery", string(be), "generated") + must_fix = needs_rng_fix[pkg] for file ∈ glob("*/index.html", prefix) (m = match(r"-ref(\d+)", file)) ≡ nothing && continue idx = parse(Int, first(m.captures)) diff --git a/docs/src/GraphRecipes/examples.md b/docs/src/GraphRecipes/examples.md index 8cc99d38f..ba9ad0c51 100644 --- a/docs/src/GraphRecipes/examples.md +++ b/docs/src/GraphRecipes/examples.md @@ -1,6 +1,6 @@ ```@setup graphexamples using Plots, GraphRecipes, Graphs, LinearAlgebra, SparseArrays, AbstractTrees; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Examples](@id graph_examples) ### Undirected graph diff --git a/docs/src/GraphRecipes/introduction.md b/docs/src/GraphRecipes/introduction.md index 1a5587ef4..9ec455f11 100644 --- a/docs/src/GraphRecipes/introduction.md +++ b/docs/src/GraphRecipes/introduction.md @@ -1,6 +1,6 @@ ```@setup graphintro using Plots, GraphRecipes; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # GraphRecipes [GraphRecipes](https://github.com/JuliaPlots/GraphRecipes.jl) is a collection of recipes for visualizing graphs. Users specify a graph through an adjacency matrix, an adjacency list, or an `AbstractGraph` via [Graphs](https://github.com/JuliaGraphs/Graphs.jl). GraphRecipes will then use a layout algorithm to produce a visualization of the graph that the user passed. diff --git a/docs/src/RecipesBase/types.md b/docs/src/RecipesBase/types.md index a69ed9b19..f21615837 100644 --- a/docs/src/RecipesBase/types.md +++ b/docs/src/RecipesBase/types.md @@ -142,7 +142,7 @@ We can use this to define a user recipe for a pie plot. # determine the angle until we stop θ_new = θ + 2π * y[i] / s # calculate the coordinates - coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + coords = [(0.0, 0.0); PlotsBase.partialcircle(θ, θ_new, 50)] @series begin seriestype := :shape label --> string(labels[i]) @@ -307,7 +307,7 @@ However, the simpler approach is writing the pie recipe as a series recipe and r θ = 0 for i in eachindex(y) θ_new = θ + 2π * y[i] / s - coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + coords = [(0.0, 0.0); PlotsBase.partialcircle(θ, θ_new, 50)] @series begin seriestype := :shape label --> string(x[i]) @@ -347,7 +347,7 @@ In fact, a pie recipe could be also implemented as a plot recipe by acessing the θ = 0 for i in 1:length(y) θ_new = θ + 2π * y[i] / s - coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)] + coords = [(0.0, 0.0); PlotsBase.partialcircle(θ, θ_new, 50)] @series begin seriestype := :shape label --> string(labels[i]) diff --git a/docs/src/UnitfulExt/unitfulext_plots.jl b/docs/src/UnitfulExt/unitfulext_plots.jl index 67a1ce3a3..e33c30d10 100644 --- a/docs/src/UnitfulExt/unitfulext_plots.jl +++ b/docs/src/UnitfulExt/unitfulext_plots.jl @@ -18,7 +18,7 @@ using Unitful, Plots # ## Lines -plot(Plots.fakedata(50, 5) * u"m", w=3) +plot(PlotsBase.fakedata(50, 5) * u"m", w=3) # ## Parametric plots @@ -69,7 +69,7 @@ histogram2d(randn(10000) * u"cm", randn(10000) * u"cm", nbins=20) # ## Line styles -styles = intersect([:solid, :dash, :dot, :dashdot, :dashdotdot], Plots.supported_styles()) +styles = intersect([:solid, :dash, :dot, :dashdot, :dashdotdot], PlotsBase.supported_styles()) styles = reshape(styles, 1, length(styles)) n = length(styles) y = cumsum(randn(20, n), dims=1) * u"km" @@ -108,7 +108,7 @@ plot( # ## Marker types -markers = intersect(Plots._shape_keys, Plots.supported_markers()) +markers = intersect(Plots._shape_keys, PlotsBase.supported_markers()) markers = reshape(markers, 1, length(markers)) n = length(markers) x = (range(0, stop=10, length=n + 2))[2:end - 1] * u"km" @@ -130,7 +130,7 @@ plot(randn(100, 5) * u"km", layout=l, t=[:line :histogram :scatter :steppre :bar # ## Adding to subplots -plot(Plots.fakedata(100, 10) * u"km", layout=4, palette=[:grays :blues :heat :lightrainbow], bg_inside=[:orange :pink :darkblue :black]) +plot(PlotsBase.fakedata(100, 10) * u"km", layout=4, palette=[:grays :blues :heat :lightrainbow], bg_inside=[:orange :pink :darkblue :black]) # ## Contour plots diff --git a/docs/src/animations.md b/docs/src/animations.md index dd7c907fc..3ec758a3c 100644 --- a/docs/src/animations.md +++ b/docs/src/animations.md @@ -1,6 +1,6 @@ ```@setup animations using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` ### [Animations](@id animations) diff --git a/docs/src/backends.md b/docs/src/backends.md index 5be18c97b..9c8ac3ef3 100644 --- a/docs/src/backends.md +++ b/docs/src/backends.md @@ -1,6 +1,7 @@ ```@setup backends -using StatsPlots, RecipesBase, Statistics; gr() -Plots.reset_defaults() +# using StatsPlots # NOTE: restore when StatsPlots compatible +using Plots, RecipesBase, Statistics; gr() +Plots.Commons.reset_defaults() @userplot BackendPlot @@ -17,10 +18,13 @@ Plots.reset_defaults() [f g] end + #= + # NOTE: restore when StatsPlots compatible @series begin subplot := 2 + (n > 2) RecipesBase.recipetype(:groupedbar, d) end + =# if n > 2 @series begin @@ -133,7 +137,7 @@ Also, PlotlyJS supports saving the output to more formats than Plotly, such as E ```@example backends plotlyjs(); backendplot(n = 2) #hide -png("backends_plotlyjs.png") #hide +png("backends_plotlyjs.png") #hide=# ``` ![](backends_plotlyjs.png) @@ -181,7 +185,7 @@ plot( :xaxis => KW(:domain => "auto") ), ) -Plots.html("plotly_mathjax") #hide +PlotsBase.html("plotly_mathjax") #hide ``` ```@raw html diff --git a/docs/src/colorschemes.md b/docs/src/colorschemes.md index 008905896..f9127858e 100644 --- a/docs/src/colorschemes.md +++ b/docs/src/colorschemes.md @@ -1,6 +1,6 @@ ```@setup colors using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # Colorschemes diff --git a/docs/src/contributing.md b/docs/src/contributing.md index f8bb2978f..2c0954a6a 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -1,6 +1,6 @@ ```@setup contributing using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` This is a guide to contributing to Plots and the surrounding ecosystem. Plots is a complex and far-reaching suite of software components, and as such will be most effective when the community contributes their own expertise, knowledge, perspective, and effort. The document is roughly broken up into the following categories, and after reading this introduction you should feel comfortable skipping to the section(s) that interest you the most: diff --git a/docs/src/democards/bulmagridtheme.css b/docs/src/democards/bulmagridtheme.css deleted file mode 100644 index 3f4e1c567..000000000 --- a/docs/src/democards/bulmagridtheme.css +++ /dev/null @@ -1,12 +0,0 @@ -.bulma-grid-card { - display: flex; - flex-direction: column; -} - -.bulma-grid-card-cover { - order: -1; -} - -.bulma-grid-card-cover img { - width: 100%; -} \ No newline at end of file diff --git a/docs/src/ecosystem.md b/docs/src/ecosystem.md index 2ba4636f4..d8b432bb2 100644 --- a/docs/src/ecosystem.md +++ b/docs/src/ecosystem.md @@ -1,6 +1,6 @@ ```@setup ecosystem using StatsPlots, Plots, RDatasets, Distributions; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() iris = dataset("datasets", "iris") singers = dataset("lattice","singer") diff --git a/docs/src/index.md b/docs/src/index.md index 1f021ee5a..52e03362d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,6 +1,6 @@ ```@setup index using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # Plots - powerful convenience for visualization in Julia diff --git a/docs/src/input_data.md b/docs/src/input_data.md index 026d9d062..08dc77466 100644 --- a/docs/src/input_data.md +++ b/docs/src/input_data.md @@ -1,6 +1,6 @@ ```@setup input_data using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Input Data](@id input-data) @@ -191,7 +191,7 @@ plt = plot( ```@example input_data # create an ellipse in the sky -pts = Plots.partialcircle(0, 2π, 100, 0.1) +pts = PlotsBase.partialcircle(0, 2π, 100, 0.1) x, y = Plots.unzip(pts) x = 1.5x .+ 0.7 y .+= 1.3 @@ -233,8 +233,8 @@ plt ```@example input_data # Holy plotting, Batman! -batman = Plots.scale(make_batman(), 0.07, 0.07, (0, 0)) -batman = translate(batman, 0.7, 1.23) +batman = PlotsBase.scale(make_batman(), 0.07, 0.07, (0, 0)) +batman = PlotsBase.translate(batman, 0.7, 1.23) plot!(batman, fillcolor = :black) ``` diff --git a/docs/src/layouts.md b/docs/src/layouts.md index 5e3d94fe4..74f8584a6 100644 --- a/docs/src/layouts.md +++ b/docs/src/layouts.md @@ -1,6 +1,6 @@ ```@setup layouts using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Layouts](@id layouts) diff --git a/docs/src/pipeline.md b/docs/src/pipeline.md index 679056824..3d67dc859 100644 --- a/docs/src/pipeline.md +++ b/docs/src/pipeline.md @@ -1,6 +1,6 @@ ```@setup pipeline using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Processing Pipeline](@id pipeline) @@ -97,7 +97,7 @@ where each item represents the data for one plot series. Under the hood, it mak Inputs are recursively processed until a matching recipe is found. This means you can make modular and hierarchical recipes which are processed just like anything built into Plots. ```@example pipeline -Plots.reset_defaults() # hide +Plots.Commons.reset_defaults() # hide mutable struct MyVecWrapper v::Vector{Float64} end diff --git a/docs/src/recipes.md b/docs/src/recipes.md index 366a321e0..a3510b6aa 100644 --- a/docs/src/recipes.md +++ b/docs/src/recipes.md @@ -1,6 +1,6 @@ ```@setup recipes using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` diff --git a/docs/src/series_types/contour.md b/docs/src/series_types/contour.md index 94e887831..de478ee5d 100644 --- a/docs/src/series_types/contour.md +++ b/docs/src/series_types/contour.md @@ -1,6 +1,6 @@ ```@setup contour using Plots -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Contour Plots](@id contour) diff --git a/docs/src/series_types/histogram.md b/docs/src/series_types/histogram.md index df771f150..f844ee996 100644 --- a/docs/src/series_types/histogram.md +++ b/docs/src/series_types/histogram.md @@ -1,6 +1,6 @@ ```@setup histogram using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Histograms](@id histogram) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 894c68a92..b7278225b 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -1,6 +1,6 @@ ```@setup tutorial using Plots; gr() -Plots.reset_defaults() +Plots.Commons.reset_defaults() ``` # [Tutorial](@id tutorial) From 46c6361049fcdfe0d089c18fc223d12d0cde0f48 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 20:29:22 +0200 Subject: [PATCH 40/85] merge `GraphRecipes` & `StatsPlots` into monorepo - fix docs build (#5000) --- .github/workflows/ci.yml | 21 +- .github/workflows/format_check.yml | 2 +- GraphRecipes/LICENSE.md | 22 + GraphRecipes/Project.toml | 45 + GraphRecipes/README.md | 44 + GraphRecipes/assets/arc_chord_diagrams.png | Bin 0 -> 34608 bytes GraphRecipes/assets/ast_example.png | Bin 0 -> 45291 bytes .../assets/custom_nodeshapes_single.png | Bin 0 -> 21978 bytes .../assets/custom_nodeshapes_various.png | Bin 0 -> 23113 bytes GraphRecipes/assets/directed.png | Bin 0 -> 14652 bytes GraphRecipes/assets/edgelabel.png | Bin 0 -> 31203 bytes .../assets/funky_edge_and_marker_args.png | Bin 0 -> 37508 bytes GraphRecipes/assets/julia_dict_tree.png | Bin 0 -> 52434 bytes GraphRecipes/assets/julia_type_tree.png | Bin 0 -> 46476 bytes GraphRecipes/assets/light_graphs.png | Bin 0 -> 22124 bytes GraphRecipes/assets/marker_properties.png | Bin 0 -> 24869 bytes GraphRecipes/assets/multigraphs.png | Bin 0 -> 23838 bytes GraphRecipes/assets/random_3d_graph.png | Bin 0 -> 24664 bytes GraphRecipes/assets/random_labelled_graph.png | Bin 0 -> 46220 bytes GraphRecipes/assets/readme_julia_logo_pun.png | Bin 0 -> 18004 bytes GraphRecipes/assets/selfedges.png | Bin 0 -> 14323 bytes GraphRecipes/src/GraphRecipes.jl | 24 + GraphRecipes/src/graph_layouts.jl | 496 +++++++ GraphRecipes/src/graphs.jl | 1160 +++++++++++++++++ GraphRecipes/src/misc.jl | 115 ++ GraphRecipes/src/trees.jl | 60 + GraphRecipes/src/utils.jl | 378 ++++++ GraphRecipes/test/functions.jl | 283 ++++ GraphRecipes/test/parse_readme.jl | 21 + GraphRecipes/test/pkg_deps.jl | 116 ++ GraphRecipes/test/runtests.jl | 187 +++ StatsPlots/LICENSE.md | 22 + StatsPlots/Project.toml | 51 + StatsPlots/README.md | 516 ++++++++ StatsPlots/src/StatsPlots.jl | 55 + StatsPlots/src/andrews.jl | 63 + StatsPlots/src/bar.jl | 97 ++ StatsPlots/src/boxplot.jl | 259 ++++ StatsPlots/src/cornerplot.jl | 120 ++ StatsPlots/src/corrplot.jl | 121 ++ StatsPlots/src/covellipse.jl | 40 + StatsPlots/src/dendrogram.jl | 54 + StatsPlots/src/df.jl | 226 ++++ StatsPlots/src/distributions.jl | 105 ++ StatsPlots/src/dotplot.jl | 116 ++ StatsPlots/src/ecdf.jl | 26 + StatsPlots/src/errorline.jl | 272 ++++ StatsPlots/src/hist.jl | 252 ++++ StatsPlots/src/interact.jl | 110 ++ StatsPlots/src/marginalhist.jl | 75 ++ StatsPlots/src/marginalkde.jl | 75 ++ StatsPlots/src/marginalscatter.jl | 74 ++ StatsPlots/src/ordinations.jl | 24 + StatsPlots/src/violin.jl | 215 +++ StatsPlots/test/runtests.jl | 494 +++++++ ci/downstream.jl | 23 +- docs/Project.toml | 1 + docs/ci_build.sh | 23 +- docs/make.jl | 24 +- docs/src/UnitfulExt/unitfulext_examples.jl | 2 +- docs/src/UnitfulExt/unitfulext_plots.jl | 2 +- docs/src/backends.md | 5 +- docs/src/input_data.md | 2 +- docs/src/layouts.md | 4 +- 64 files changed, 6469 insertions(+), 53 deletions(-) create mode 100644 GraphRecipes/LICENSE.md create mode 100644 GraphRecipes/Project.toml create mode 100644 GraphRecipes/README.md create mode 100644 GraphRecipes/assets/arc_chord_diagrams.png create mode 100644 GraphRecipes/assets/ast_example.png create mode 100644 GraphRecipes/assets/custom_nodeshapes_single.png create mode 100644 GraphRecipes/assets/custom_nodeshapes_various.png create mode 100644 GraphRecipes/assets/directed.png create mode 100644 GraphRecipes/assets/edgelabel.png create mode 100644 GraphRecipes/assets/funky_edge_and_marker_args.png create mode 100644 GraphRecipes/assets/julia_dict_tree.png create mode 100644 GraphRecipes/assets/julia_type_tree.png create mode 100644 GraphRecipes/assets/light_graphs.png create mode 100644 GraphRecipes/assets/marker_properties.png create mode 100644 GraphRecipes/assets/multigraphs.png create mode 100644 GraphRecipes/assets/random_3d_graph.png create mode 100644 GraphRecipes/assets/random_labelled_graph.png create mode 100644 GraphRecipes/assets/readme_julia_logo_pun.png create mode 100644 GraphRecipes/assets/selfedges.png create mode 100644 GraphRecipes/src/GraphRecipes.jl create mode 100644 GraphRecipes/src/graph_layouts.jl create mode 100644 GraphRecipes/src/graphs.jl create mode 100644 GraphRecipes/src/misc.jl create mode 100644 GraphRecipes/src/trees.jl create mode 100644 GraphRecipes/src/utils.jl create mode 100644 GraphRecipes/test/functions.jl create mode 100644 GraphRecipes/test/parse_readme.jl create mode 100644 GraphRecipes/test/pkg_deps.jl create mode 100644 GraphRecipes/test/runtests.jl create mode 100644 StatsPlots/LICENSE.md create mode 100644 StatsPlots/Project.toml create mode 100644 StatsPlots/README.md create mode 100644 StatsPlots/src/StatsPlots.jl create mode 100644 StatsPlots/src/andrews.jl create mode 100644 StatsPlots/src/bar.jl create mode 100644 StatsPlots/src/boxplot.jl create mode 100644 StatsPlots/src/cornerplot.jl create mode 100644 StatsPlots/src/corrplot.jl create mode 100644 StatsPlots/src/covellipse.jl create mode 100644 StatsPlots/src/dendrogram.jl create mode 100644 StatsPlots/src/df.jl create mode 100644 StatsPlots/src/distributions.jl create mode 100644 StatsPlots/src/dotplot.jl create mode 100644 StatsPlots/src/ecdf.jl create mode 100644 StatsPlots/src/errorline.jl create mode 100644 StatsPlots/src/hist.jl create mode 100644 StatsPlots/src/interact.jl create mode 100644 StatsPlots/src/marginalhist.jl create mode 100644 StatsPlots/src/marginalkde.jl create mode 100644 StatsPlots/src/marginalscatter.jl create mode 100644 StatsPlots/src/ordinations.jl create mode 100644 StatsPlots/src/violin.jl create mode 100644 StatsPlots/test/runtests.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 237641963..d7a621134 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,20 +64,27 @@ jobs: - uses: julia-actions/cache@v2 - - name: Develop RecipesBase, RecipesPipeline, PlotsBase, Plots + - name: Develop all Plots packages env: JULIA_PKG_PRECOMPILE_AUTO: 0 shell: julia --color=yes {0} run: | using Pkg - foreach(path -> Pkg.develop(; path), ("./RecipesBase", "./RecipesPipeline", "./PlotsBase", ".")) + Pkg.develop([ + (; path="./RecipesBase"), + (; path="./RecipesPipeline"), + (; path="./PlotsBase"), + (; path="."), + (; path="./GraphRecipes"), + (; path="./StatsPlots"), + ]) - name: Install conda based matplotlib env: JULIA_PKG_PRECOMPILE_AUTO: 0 run: julia --color=yes ci/matplotlib.jl - - name: Test RecipesBase, RecipesPipeline, PlotsBase, Plots + - name: Test all Plots packages timeout-minutes: 60 run: | cmd=(julia --color=yes) @@ -87,13 +94,9 @@ jobs: echo ${cmd[@]} ${cmd[@]} -e ' using Pkg - foreach(name -> Pkg.test(name; coverage=true), ("RecipesBase", "RecipesPipeline", "PlotsBase", "Plots")) + Pkg.test(["GraphRecipes", "StatsPlots"]; coverage=true) + Pkg.test(["RecipesBase", "RecipesPipeline", "PlotsBase", "Plots"]; coverage=true) ' - - name: Test downstream packages - if: startsWith(matrix.os, 'ubuntu') - run: | - xvfb-run julia --color=yes ci/downstream.jl GraphRecipes || true - xvfb-run julia --color=yes ci/downstream.jl StatsPlots || true - uses: julia-actions/julia-processcoverage@latest if: startsWith(matrix.os, 'ubuntu') diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 10de85ed1..e97bcf6c4 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -25,7 +25,7 @@ jobs: - name: Format Julia files run: | using JuliaFormatter - format(["RecipesBase", "RecipesPipeline", "PlotsBase", "src", "test"]) + format(["RecipesBase", "RecipesPipeline", "PlotsBase", "src", "test", "GraphRecipes", "StatsPlots"]) shell: julia --color=yes --compile=min -O0 {0} - name: suggester / JuliaFormatter diff --git a/GraphRecipes/LICENSE.md b/GraphRecipes/LICENSE.md new file mode 100644 index 000000000..a15a28ae2 --- /dev/null +++ b/GraphRecipes/LICENSE.md @@ -0,0 +1,22 @@ +The GraphRecipes.jl package is licensed under the MIT "Expat" License: + +> Copyright (c) 2016: Thomas Breloff. +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/GraphRecipes/Project.toml b/GraphRecipes/Project.toml new file mode 100644 index 000000000..4c3ddc4ad --- /dev/null +++ b/GraphRecipes/Project.toml @@ -0,0 +1,45 @@ +name = "GraphRecipes" +uuid = "bd48cda9-67a9-57be-86fa-5b3c104eda73" +version = "1.0" + +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +GeometryTypes = "4d00f742-c7ba-57c2-abde-4428a4b178cb" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[compat] +AbstractTrees = "0.4" +GeometryTypes = "0.8" +Graphs = "1.7" +Interpolations = "0.13 - 0.15" +NaNMath = "1" +NetworkLayout = "0.4" +PlotUtils = "0.6.2, 1" +RecipesBase = "1" +Statistics = "1" +julia = "1.10" + +[extras] +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" + +[targets] +test = ["Gtk", "ImageMagick", "LinearAlgebra", "Logging", "Markdown", "Plots", "Random", "SparseArrays", "StableRNGs", "Test", "VisualRegressionTests"] diff --git a/GraphRecipes/README.md b/GraphRecipes/README.md new file mode 100644 index 000000000..7ea354e8a --- /dev/null +++ b/GraphRecipes/README.md @@ -0,0 +1,44 @@ +[gh-ci-img]: https://github.com/JuliaPlots/GraphRecipes.jl/workflows/ci/badge.svg?branch=master +[gh-ci-url]: https://github.com/JuliaPlots/GraphRecipes.jl/actions?query=workflow%3Aci + +# GraphRecipes +The repository formerly known as PlotRecipes + +[![Build Status][gh-ci-img]][gh-ci-url] +[![Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://docs.juliaplots.org/stable/GraphRecipes/introduction) +[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://julialang.zulipchat.com/#narrow/stream/236493-plots) + +## Summary +In this repository, a graph is a network of connected nodes (although sometimes people use the same word to refer to a plot). If you want to do plotting, then use [Plots.jl](https://github.com/JuliaPlots/Plots.jl). + +For a given graph, there are many legitimate ways to display and visualize the graph. However, some graph layouts will convey the structure of the underlying graph much more clearly than other layouts. GraphRecipes provides many options for producing graph layouts including (un)directed graphs, tree graphs and arc/chord diagrams. For each layout type the `graphplot` function will try to create a default layout that optimizes visual clarity. However, the user can tweak the default layout through a large number of powerful keyword arguments, see the [documentation](https://docs.juliaplots.org/stable/GraphRecipes/introduction) for more details and some examples. + +## Installation +```julia +] add GraphRecipes +``` + +## An example +```julia +using GraphRecipes +using Plots + +g = [0 1 1; + 1 0 1; + 1 1 0] + +graphplot(g, + x=[0,-1/tan(π/3),1/tan(π/3)], y=[1,0,0], + nodeshape=:circle, nodesize=1.1, + axis_buffer=0.6, + curves=false, + color=:black, + nodecolor=[colorant"#389826",colorant"#CB3C33",colorant"#9558B2"], + linewidth=10) +``` +![](assets/readme_julia_logo_pun.png) + + +This repo maintains a collection of recipes for graph analysis, and is a reduced and refactored version of the previous PlotRecipes. It uses the powerful machinery of [Plots](https://github.com/JuliPlots/Plots.jl) and [RecipesBase](https://github.com/JuliaPlots/Plots.jl/tree/master/RecipesBase) to turn simple transformations into flexible visualizations. + +Original author: Thomas Breloff (@tbreloff) diff --git a/GraphRecipes/assets/arc_chord_diagrams.png b/GraphRecipes/assets/arc_chord_diagrams.png new file mode 100644 index 0000000000000000000000000000000000000000..76a8e65f850bc3bc545d60f612d2e847da8c190e GIT binary patch literal 34608 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 z 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 F@IQU9OT7R9 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/ast_example.png b/GraphRecipes/assets/ast_example.png new file mode 100644 index 0000000000000000000000000000000000000000..36498ed1e42c453d1585acefe8ea892357922a22 GIT binary patch literal 45291 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/custom_nodeshapes_single.png b/GraphRecipes/assets/custom_nodeshapes_single.png new file mode 100644 index 0000000000000000000000000000000000000000..1430d0e044ba1edd7eb5c788f7052b0e22122083 GIT binary patch literal 21978 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�gRd&-@l(39uA&Kor6w?H3|x( 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 Lv`UV$dC>m?eYYLF literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/custom_nodeshapes_various.png b/GraphRecipes/assets/custom_nodeshapes_various.png new file mode 100644 index 0000000000000000000000000000000000000000..7277664ab7a2cea0c3429020ec71a79f5711df7c GIT binary patch literal 23113 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 DxL!-E literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/edgelabel.png b/GraphRecipes/assets/edgelabel.png new file mode 100644 index 0000000000000000000000000000000000000000..02658ea8290ce3f982786e79afe64b67e5066f74 GIT binary patch literal 31203 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 UH4V}=Ty5d|2~*up$`u>`2OF$y8vpA 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 XrwUxWB5Z`X2$2w#ffl{e`TTzYjHCi5 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/julia_dict_tree.png b/GraphRecipes/assets/julia_dict_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..6cc7407a08f01fb333497b3907e7ec71a041358c GIT binary patch literal 52434 zcmeEu^F(|jY3ZRG>F&4(pZDHB;QjeI z^MQexJ^SpvYOnaNiBOc6#Xu!R1pol!?Hh@g!`Q$3gY{Ml+Ulq%bN1^#9c;= z-1OEX!v%A5aCH&Bki)-?4^1;q8EX_$_X|+vqS!Hl4d!(W=wJU^=B@V7^tY_>XsTEv zD2Iuf7QAxE87L1?v2nml2_pp>5e{C?6e`01UMMrMW_%v>+wJ`UUZ`!|Ey1tB!TEsf0n;k& z(mM_hp>(12va+ccIZ!K>23rANP2fvT5 z`qxj2@;^#Y6Gkyh(7^!NhMc4^>YH7TO!zUc@e#N#3h^UfzW~;_0Z5lJFD~Ne&oB~6 zc*-65hwAECl=`{Zh9Y^rPHqutZy-T!(fpPbv##DI)?AYKF?#d%uSI%$gAPWHb!dQX&f%1W!h81%Mu!etN{<1|zheAZvwMx|! z27ZW@eHe@^$mXoYQzE2{lT}JAYFjFTC&vt|iOEag_l5-wk zLY;ay#yd)FF(Qk9ht*wXI6X(-!ih=ZeQ4H1lzB5cn->~`@HwKEq$)mItEE-N`Y@r^`= zEL!Pz;)D2c)ZeEsB?Q7Dtafh;xNih-q^(U}pGwQ`$n5l*3is2&2D07|2xb#eqYfZO z>%#|n{tAOg;Zq}jHW~L6paXIOJp~fOWhNP91y+>ihjZi|sstxPUPq}p@J0kuckDW% z1QpxJha`MD@6!b)Xwbhu)fSb?M4;2&y>LtnpsolVXtzKx&a zUUd{8j;BTG-8<~R~Ow-QQa_@(Bl)E65yYTPj> z&GY|SSoAMx2>Buujd28Dl0c{_zrK)62k4AZn5j_%$>TyMk(_x8_}mi1m*FJv-58ox zfM~+vPD@;b*)*>lAOX~M%?sLn%zVjmp4|(+ux`38>ssw0hTwvj=pyTv1>5A4?ow(A z5~2f!)pMW`nzOCb^vrtX0cG_SdaXxeRh9CAv-(qcEO8krf610@Tt8kc3uPQ*yrQp| z126lX-;$;-=B-~Pgm&Yq7catbj4H0*WK~gw6bF(%u%!wFPr)d0?8FFFtEp#sZ1kFi zGZg3W(8H%&Izdd42`%1_)1NaCvjK*~a6kfv&m|;qv zggHh_r1kYPy!|E0XN1WyFL;BXfZ{0M?16i5=jRLW2opjTVTuV~u}r$RorYt37w_Wn zQA2#i3|I-zyjyoI{~B@q)la=Y?*Ktc`O!th{A*pY*#YfB!MvtvrI-8hA*ca7?# z^GC^3B|)b%byDeVWYA}#Sr1G}JCvTT@X*?=k2w;j5=!s`Hn_N-eG%zH-zX-BQ{zg# zS2i{hB$uKkMZ5VDE*qpTlf;Hu&lIViuTwsuMIoQtX;?d7YF<5T?bN7?88xt3(@f!> zFtFj=IJtB5{L7&}pG|y$YC_V;I{!uL==Ry&!$*mZqbnZ~%8Np}xBBJ$#OU6z%0Fke z¥E29B1S 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
g`531O(TVsl@%BQ&agV7ysb5(G*n!a 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/light_graphs.png b/GraphRecipes/assets/light_graphs.png new file mode 100644 index 0000000000000000000000000000000000000000..fee0e9c61b724304b00e24093a167042b0532cda GIT binary patch literal 22124 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/marker_properties.png b/GraphRecipes/assets/marker_properties.png new file mode 100644 index 0000000000000000000000000000000000000000..e5ac9e12488df878a303f43638e09175232422b6 GIT binary patch literal 24869 zcmb5W1yojB6fU}HP#OfJOGH7syGuzC0clVg1nHKP78L0c=|+$S0V$CZkyfOX5|EJQ z&fjzId-uNa#(3lX=ZtZV__Oz3Yt8uPH@~?fwKS9oE>T}X5QIQgML`Ea(De}n?Kv(w z{N(I@$H=zzBhRTYV|{(G*Uy4p z+ioZ6&!U-al!*#(UW0J#V zDh!V#W(s|Nw@cv}u^gcaS2DL@ZINcS6nyR$YRolzI$IlFyMA3Wl`sE6y^~I{02|v% z7m>7X>G&(cq$ijfuVitVa9*UPiCOlJT{iEQ=BCC6J0M9muX4si=DfLg~h{%%WLDMS1u`< zn3z0zRG@ovuGV&JG*5ZPE-5SP{6_>sQ**Ovix;o7w6wT*b3=pI=QpaJTi?#sO)u;Q z56g*AixPc_fkW80{AY4<^35Aosca%-baeE=I|sBNO{t?d<$Bh`Sr4gJ@{>)d1?|Xv z71>_+pCA3)NG>B4vF0IR4t*{!8gMFte9Dq+KI#q)#e6FB_~_5_+S;1lgYmCl(SuZ2 z?E(_cvK<9os8VoAgpX#W*?UuN)@la{ad@ZT}?Wkf|qA@3(9>bu&E*MSWpZ4e4ATQQIUW_ zYPPFuak|bvKR;hn`fM-RG{AN>r7;Y3Y7LfNNK8)=h36B7jCP|v_>H>bduavtZeBt9 ztZFDs{~R1(Bl$X0Ute@}b_R!pY|S=4*;!CLh>~G^78yx$DNISx#uK$-V(VYi6CICx zi`W9K1hS+gYs(c>h(l!VYu*H{}YeA;973fjy&TR;K7%tnF!5&;_CV>;AC@!QaOf-gn@ww!IV8bIiaMa zocQtu6B}EoJtA|Ll$`v_nx(1#{=IU&lR3}t*Vx!gt;lm8N5gNoq-% znS+Zva&mI?j-3a|Q3}lT@e>QTEvhH)XwPt!{aYITNNoZ)w%9MVOzA)=gjUM0;iwx9 zx@dgu@bFOi`X~~Voo$-TWo&0>2Ns@gT4;L=+l~;^(VaCgjE9>uE8sE>Y2jOLME|x4 z{Dnux<_3qow>l>7`P-`U<;$0x58mM;uegjqn($KL`-0_w3)|%iA4wUyv1!`^LvRXm8g&-kh#0 z%<_Q^Dsleac7A?-zrtwLM!^1SnL)i1ov*y5C5w2O*TRR$;=6_Di24{TM#5*0U6$C~ z-2C_N-(ZXPign@FtcNmIzqhv+GmH|NDMGNwFc0E=XaDkdkv%zrO+XD#lMg%a`SWL3 zL|sni(S6rQ<8h%ln%%f5C2=eE(KhTFbtV0z($jH5Eyyz!sGvYWB?esl|A4_5#f%Z9M*`DPp`#ung$&$bU^}KEeJk;$49l;S!FW6XP8G z=`t!{xM26Hl*Gc5HT*+bx8e*)lzysL+^UUS|3SbPrVVOE2x?!d{vG#-4Asndzu81lRcQZRmpqPS8u>r2$b+xS-*bhv2eo`uC=dxT&U=Sk z)X4nrH`P+2U=^U?S@{>@MdWFumHOrjW>&%j4H*CLh2ELAC+Cpk{}=x=a;bv(JdQBg zb;9sqC08WDd>UxN z_=WIJXkSgO;buti33iCawb?uuQ8bylhNny~#m*nnUGQbET^nuF(~`l$#`gC0eL`Ap zY2)BfR$6MWd&Aw3t_wk)OUSX3tFr#i%F5#6;xfW{cFr*PTuJj)N2}5x0X!LFrbhAw z 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

n59==Cx8tSgrdS&{uiL6~b*@5Sx6V>{&$_)Ni zJXE;B@5`-dlkj|F7lM7>LVde&M?v8O;3u?sIGLi7k_9o^%O3{@X5Ly8A)nF&qa~9` zn5^*N-e=|$+Q8>jwjD>U>vP&bT6YkAmKQ=8Q#~2%(`eLYGx{0=0FWJbGR2bSDq7_Y z%RRcn3E+EV(cL5@Bq&Rf%k^rBx52^1K1olYqn7Y`tf%*?y*<#}JXebyOmKH#czA2P zG`Yhp1-Abw22KMZj=@KoDTXTo6g1;+Ha>?0xlMFM5Y<1?w6j}_xypZ&g9FG@KY#zN z@9jj0bL;+Bh{`jz<0B(Hk3OZ8m2oDr0$Fq`d4PtR8mKOgm30DJf~L$-NZFR&Y>2vd(Qs1>?a# zG4(?GxK{U%=_P&0DJlQ1Pi(;>VgAq>!?|?%vhTr0@z`n=t(zA|MWt{qGk()eyF%t! zfM<{sEG#X#jhnu>?VN122mT17REnh@|B5yNr5zw-dU|@#-?MQ1$HTr!N$#7|_c$py z$S+A{JL_h6U;;Qu#zgO^c{qT~|AjccoNU@a*!u~xmYW#etB|1}-gBw~AX4HPqzRF? z*27K#yi828jKoH5evRe34+4%?ug5Cwe&Qb%cqofN);aw%!2R84y?lCly7#>79ne8F z_W}KzQ%;Ca#@Q3mOB@2{j4(1W-NL94Qv-ianl}2{(6jbv1N! zU+N=4Qg|(uqSxW~y|-Hb0wm)kD^q_DmX}mm__H(YGTQv7RDQ=ZhnYOU{_Fcb8JugZ zoYiqvMMby>9PI-m>q?-l^&ZT%`O_dxo?BU|sg01-LqkKMfIn*9YU8KEe{`7f4#iPe za_E&oMTXxcaz|TVOKbb=?{^FtKR*fI4RTk9N2XG;|FcH=>Fwj|ixH$#>`-U!?JbJX zihDR5ZvWV7zc>e4z5V=n9qTd${#CxEju4!cquO_uP}k1Q&2`Wm0ur_FoNA`Iz?HZ@ z(S=liDjRpkpHY?={b^1njY>_DDbO#nl~BZ(p8rX{Z{odtusNfa4?*-vXDHs()YP!_ zg@pQ8{Jy9o%1ysdx|37Kt_*OuFNwYCy{E4~sY^&mFOjnT__;ANZs+&!3@o3mVut05J447-&OK;9$H)PdbK=%{#=h`b*zvG+4gFom{4RU zsU6K-OU|N~bs=V{TxR-%jRwSV@H+4nB!Wk+roiTWicZ(50o@;o!aC zXB$`Q4A0g|E59|ku&}WebeAaLghUdH1}jEWczbyT&2d#+94^MYxQ&RW^2kj7s7U$$ zRSXT8kVGor5YfL>VclIB-2MBPLBgvFu!-<@KQdC%8h9w><~wwFY;sGu5)u*=w+t~6 zKxEA>i`WxCs0xdUp54MJ)X0o_p_pKRPc884bN=m}ogL_s02P*ykkIV0X;|-Md{Pdv z=B|#8iQkSQENpgm79w}c?vS{ss3^1nYBeT}I@IeELI9=V6A=kh;qU$&wzkfkj0STK zlhoe<_#_{W{{t}5J??_Qv_ek7lE?)$lMn8%&n#1(YXiMY$jr)W1<4KB9@CV*LP=Af zDIEblowl=s2S|#T`^`tP*FOZg+*L}|VE7a*1`y7bcB6mar}ccjvVBGD)zEa6d5B&q zR#2V&1i)N54e&o#TA+~Vcr7pyzq9>`4UcG{iiU=i%n4yIQBIspvOy|o)A02aIwIH` zt;z2FOJSW{;ds@w3I(62yDPMxK(3=z1H)`?2pD*EOQ@}(t*k7DjxK}mq1v50_+Y+E zEnqwmor^pF@Do6W>zY|hVi^ZzrtMjCv{xnw#YB4!Dfzg;zeYKY8b$!JBT{F3cPlJy z??Pz{wuqQP-)*DdN1v$qMA0n>akQ}t%m&bxLo%cT&!Jsq6fxEmOCt=d@%vHPPzMAa zH3;rDl%;N$j)E7f5G&B;iwm=;g&e5hk+@Q;o)H7aC4}Y67&p14O8N>R8#DElaM0_I z4Plos9uO|@?mg?M(58VROO(xnF3hCzVj-HlA#OSFWl~agUI-c6ooBJJ%VM;l#BjeH zpyr?)AV6l`IRYbS6tR0D7>8|Wv*4!a}2q-zZw<>f;R%fGJGVuw6sRZY->!fkeL z&e+(vF>WXh3kwT6Zs*%wL?~YzScXjhHUJ-m6%0Bod*dEbx-)+0RIhbJ(+i!wTdaFT z^k-|X`S13RuGiBG4?fP+3)DZM-jXQd;H+ea+B4vH>+JEdPeoU)C0IFjh&eEF8JJbx zyN_t;6w3J?(lJf&iqi*oTveGhrz!w=(bQy!RpK^R_aIzct)Tjdpdz%0Qd%Nhx-We% zvEgTm?@g~?GYbgl=Ig{> zrz^glMgNvFmY-{F@daf3!e*1MxuLD1!U^JyUVuF(j<)!nDK{E014z8WRN*&XQVNw& z6WME?6*OBQ^1i|tFN%l1<%y0I*?$Ur?#yD}wK?$e#$E7CXqnfoNSocecST%G1aMkI zgGgyBcMT1G^UINje-1Xgb|!rGzqA1C+}zMSiL)J^6erA(vUK)Glx2vxALo7odg$`U7 z(K0Y7w%+^ce*cb5dVV{}tzTO@zk?%3^#3b&U4hF3Z~#yk49_;r3EDZZRGT6}Dk^0D zMJ%&PMd9t#puZzwoYy0j=!=Ab{_jX7|BI3mDL~qCx$npB zvc2es#kQUPfrgl*&mO=^&fTXO!6uc-l>Os3-EZ7Yvu!Yxk<6J7yySs~%j_i?!6U!# z+S(l9`#9Mw!zeZewr={R+BKfXg=6v|@lEy((gq?Gxw&X$xz(qqr{BDPX3|SG5mE6J zy({SKTCUMOP1k=ePt2x~&4fFh6gUk3QWEOKywMPUZ9;S3lm1r2t8T3lUTe>wzWr|`IBTtqAFRL z{o(7ke4_@p#`6v77)Z{SfW^6ccMOV!hdrJyH+@>AE@ayh?IsJ)?i1V{J0Ha7G0=S^ zdiVT#3b?wo1ZM{8rWqe&b57PedqyUmU!Dk6tI7BG?6$u)YPPm@}i>m2DWVMH@QCR zOrK5r(>>~7>vkJ;nbXeHxUZ-8Xms*9OGnhmv`yOAkaJ!J3k2ibVv{vYPTRx;JLcQ4 zV|M(E_Coc=j(DWAEej`o2q7^;en3B>`=$@Bm4mm6-I69V>XeX6P-ydX;vD254 z?h`PgGa_So(Z6AKh%_rXB6^`;l@i}PBX;*QZ4M13F2r!_GvgsAmtg1MAlrT0*=n-m zE^b!GIjJC{=a%3{&8+@AW>AtS6z9@2X&qUj#Uw5|PoF~^YO-lXpbd;L!;l!h1 zN`Zs0PnXDY>lzyxRB(A2=s9A&`RFN!O!a}qs`^Xir_0TH&FcEPb=J>0NfKCjME^j6#=BGN#QV`6(3vXU=Ge4LDznGICe;Gt7JN{ zg*kJr(Ea=O&2l&@qx&_t8D}5*RYp^CJ5AL*?1{Ph?b|nAUfxo@axwp7f#yiOlJat( zPo;=H`gpZ5k!4ngg*d6m(Xk#etNaNospjI|s?f9lxN!AnuI|mNW`9;s*rb2ZCtb#$ zJl71p;TsFuqDSH23m+_ai51ADKQCS1`|;Nz$kxHZYB)Wg{U!^GUU9eg%KykDgDi}(&zU-xrA9d(u2VWb z9wTS}+37kTE_PJ+0;q#@@88cB zeX_Dy|D9gUja}TTXkxw6sPPY!nQ=72Ljwc$8WBPLN>Y}ixEhc*RQbbft(fW@24 zIEyuAYk_K+@6MeDtV0|mNS`xH#ATtEAu!|BD?MXl|I@=AD5nUic!G?j@<(kzu2T<` z#D|`qk5)B6ggbw05#{F}dx}#8TnR*c@JZRkK++GEx3sF+DNPhTXM=^-eur zd`mNWPp{$kzc$TW1uZ-Mu2xujh&1>GW1v4}d@mhz!!rCbcHu96vF|*I3{tv0Dr_$# z#V&vE>av^$l~;i}1`;Oc0+jprfRkIIqDN4MAfR9JhUP8wbTERThz8CQNzr66c;C}oP1Z>me$&m?#*$4)D^(fcG}K^7tM3k`&*>S%bS~ztMsa#J7`^aF**jQPiprV@m(FLlPqxis6 z77%omm2JST8P(co>+4gUj00l;_^4dJdLi<9*7o1Ofyb+bsB%@@)9`!CM-u@70RW{< z%~sgY+N;kv`xagZ&gI*y6eSE~`p_45m^FYl4lpn!w~2(=djzNiyDvpAIc}K5Y`D;K zAe6jJk9tQlUl?(j!u_(Ppw;15ppgljz&FBlPJ^0Io54^#vNB?xh$`l#J0Leg7%My! zC{5-K=8nXg+RDX^@w77ro;^JcaI&a>?!=@t{um7nZCLW}?2}D=xKc(;j2@rp>Q>Icl2GW8kucMCNf%VJuqsn-FQ5C-# zB#X`(id5)HKhK$$>=Moa_Jdjp8ivsBXbm_!&O7S@2Hhw1@)I6}TH_UJ_ z2JQr@s;g%LPxA0#J^;DO$}N;wAX5y8o;oNP-i_Vl<|aZy<;Q)LYCI&by*fq|uoi`y1oAK%{I-WYKI8?^cAFK%)> zH}2R%(C&@7+O?u6WIn?gZvJO?*KInDXLxy*i$cJl+~baF*LqtUN)!cjG)h6Cp32R_ zBA;vX@GTV_iP!dgN=gcoe7J(LGBhs=uj=R!+|rEOcvBp_+KQRr@AJ2MN1B^0d)B+T zry=a3q)b|gf_+9bUaMSBSxM>R@}Ke2`<2GcuZDkGKYR#_12FR&`m6kpKG~F-*E`M8 zOZgS)6mL8naDCFD&?nhyVtVghevux6fzJ~gqeehYN!$RXunOb*yKAfK>m1}#zn`l< z0JdpTZ3oz-@$vCnZPTUqxrJ^>9WA{G2ne8v>ac?5ZCOdl#!7X^-15zd>XZuIBfD}B zKN}k&@gg%Z(mf3;87+d@3HRzUQ#a4xrwa#)7P@q zci*aCR^opBoTSFP6RSrdW(Me&HCR8BG=#Hj* z;0S>PYHgo`4cje@Yiy4zYMs+D(Phoda)1N1w9GRse-!_X4OfnlxT-Mwfsb%q(ZA2b zfmfpf{);^X@3!`!3lA_7LEQ+>GXM%0hf>6r@PDzZlheLNb~aP- zmJz{x=6S;%6xjD%@>GnL3Eu8~HXgy}4q|~!HQ_nzm$g0Z6$Cs$?3ne{B*#gBa zz0l6l)bd?lGXBmuFVjvSmWiRf!>aljx>PSUEgrJD#d^41{J+XC?hja%$F#5f{K=0$ z1}DIh=YpAn>xiYA2#PuQ%=Yj+o(ByDPTNio4R#0_DiBa>vID}+U+3w<&ge2g((gv=6uZF#g0l|x_uKB<@OT9AFf%W}BNu9B2OMsjLmL@l z?c(BM?B!vUaBeL{qwx0a+m;;NdwY8nGa)-ir(iHu7*Q-rF;&w67nj0bLp4|0%SM!} zyWWzQe?_=uG>72|$>_kNpKK5a1!H#~T3LPhQtK*~n4En7uJcbYII)bSS;BDhOUq$! z)RcSe#)TE`Ueah*8T0*5TVw$V#P7C!6<9s2uP=>Vr@m);V)>>eH?u;!TE9%E7^;-u z;NUdVY$3;K$WtI50Oewa@&kW}!+}QwreJ@DpN!M^9)1K1dT@)GE?n}oJG24HWj>!h zSer6i?RQOOy!3PLL%1dED>07>77|E5tCG4G9u=??1ghj`# zi2h(?p8MOJ7Jo}ii-d?s%DVn}UR#?K2S;h~2FJ~tK6~`QB?2uhwAuaPLuf=qx_!VU zSz@L+xh2Iv*VlVJ7Q;>pF6y#upSadBtV)~kbY$ah@9b2c<;o7^+NKR!#TRfvw>C)V zujxY$IjQV+Xu*O7UJmO7x(*s~e?VJR39N0Fq%S9}NHS0biw%v9vCwsbJdTWvj64Q~ zuh5N0*4ASJ<>PkTMh%Z4p@JWL9~`_TB=l-iDmE@o$XOcd3?8bK3Z3RHaz_mrJ)dU! z^0G|Z(MIjfu~j#S+zuU{AgBV*`|LJw<%R@(C1f6l>FCJFRnYD%tLY0->fO6Xaz(+D zZ;w7R{r23g+^F%KN6wG0&xZ6@w0ir?_5Egz-yr>>cr(e6ohq6q-@X!+>dM!gNzlb}GieRN709`<5F(;@IMTOC3()i3N!gVJd0EL#PtV2`w#T)rj(nrM zVPb71B_Hid^=#6H1_vG2Ha`swIqh4;Jbz9sv{`LWQzmh9V{wrsNex`hOR&YniWT=g zf=z8B)p7kQDdOdq1^ML;+uQahV(Mv}PoV!y5j|U|p8gK_8H5oX))j*>%7Knq5 zO#y#*?_Qls=*FMHcckVICcWPP!N8_$e*%`Z3#4q|N~(31_sUg-or~*{_0;LXjPr&C zAHX!=+<~P;laY~mpK@V${$jRmZS5vWyFO=M^{n#z^E+-vxmh3#TwGtbudx35^{Y_! zl8&L_@h?dyC#MZ9hEibWR(w49f%ym7iMieb^pu9TG1=J+;u51J+uPgFsr~Zh3jhg0 zW8Gq1Fh~h{ZdTT%QMUd4{r1y8(Gd}#FDbsKtBc4$UpO}IbHIF=ww2`3(UC)HN=Al} zp5E7mQlK;}2R=ac@{a!j3{fmCFJq~-B}8gG5HT4d|Ke}Mc%rP35@3_BR|=5D=kA|@ z!9oAM>zTtpl@%2Q3HCvMKh^7l(aX2G?Wr_O-I~Q;^QJA+AP>S!s(>vJpf6DTfv~Xz z2+pCgbFo(_T^$(mE@#~CAzvz76X>wrJH>jXKxM;JhZj&`MMdm+3$-j@2f|L@+XmUX zfo{HkuWpw57BlnfVjYL-9es%J_wIRfPx;MAIE`@H)mh%AezpJ0{DTrdx#C>olfoOj za}6$e~-%hj*3>W70}8BG5R9VO|Z-dY!U z+FqNqbNFNWfmdmzv#7Mx&6`o>N4L{+0oukgn+_Vy2T8zYdzvt3374yed!wG0S7Czwho)4h} zAGnGjLN##6_kwwio}Qi+(`%}(AV!67PL$}kdhIx@l(9UoDj+lKS=_V3?6V5+taNAN zvtLfD2xEc@h}gf>mQ=!N=|xecT7Ayla^i@&6FNnAT@Gmg)nduLQZCcBGC0ruwK`~y zQkr<>u9n8L$$f>2L?c5aAt?zg32-z614BCq&&1~rDtEz+Xh72l^Ab~Pyti(}&b6%r zeeC~t&eQj!3F-5mv)@(X_tp;o^k4x+w1J(q!I}&YAn8*m!}tq^67H_7Lu=;j7Sm&k zSGPlpb{{*?PO(3g0R-j;AhS2Z?MB`%fvm^K0^PhHRy z#1e&t*Xw^UEvdj7nq{?LcD%<8J2JwtAi$JuzI2IAGfTkj*9hQakej*Ek;oCEdZ#oS zUrpsq@Ft$>M{-=q%E)X%IpIo&Tf=%eL-|<@Os!1CUa)oQ5z0JPK{vt5H)oIP?B|aA z^d|G+b8peZH~LE|dp?o~3*!~I&o%TK((~P75;=-c5Sh`CEAFTW%GOF;>C?vRyNN)u zHo^(!d}bEJv(9^`58zkA9Q@4hbBwajTnko#le zLcwUAAlteV@+czEfw{S3affRDZcOO9Oqqh_33^a;^yP`Cj7#&V4Q3+0geGJEtJ9rP z&mzYk5B<<@889h#w~YG&!1w% zRxTS>S3&zFZs!qqI)f?73eyES*J6*`;Lv63(yptLLc_0t5YqrFj>Hw!>*fbYEkUoH zmkLk$N{+u%Y?HnC%Vp22ihW|+*9oSs1~UQ}quJ})tBnD{#%nzqWqxK&O-)#d-c#{` zNtCGNyw8dcd9B2sx-Ro6-drWjr=YY{i__!vW_RXQ^Us%4DWO9h2*_AM;?1RfH;hf2 zJe&QdA7UTBPfTR5%MiOV!yy<$`DiIqq&2)GM*PHr$|T>XLW}*qEuRJ^ikE{6B!|l7 z>aH1-rNJL3!S!z+%NXLT9EYgskMS5kty!N~Lgq!Yr`SAfKCuV?` z6_&DnSEtsu2t$$gW;|ua%?GW2&kG8U^-8j#dvfZ?I?>+;p<$4ckzFHoF<9hKJ3}hu*Ck%xX>5>{w%A>12iCyn&r&}~K#U?npYx*v z)G#C`X#3nZG`xgB_=WOGO$XWWh-Ql4VYfJbLWm9WJNcz zOfB}))7@bJy_+j5*f90tvGuJ>k)6x#Fnw?$?1TJru-bSwWIs?bKqt?7FbxYq>FBD= zyP#9iKuBZ|k7t~hc~w(Wl4GYc|(ZB#t+!PnOp z2MY@=2&9|9g-x330DS@T7AHqPvl|J?rg$MUq@?H@7&Q9q+kp%=IXO8s^+uAKu;a8F zls`ifx$oYA;xb#13jY=1xDv`Te*mO|(zwxmodUU&A+iHSCe%Q_dqOY;*Xm1DJ(>6B z$Nr^pi3cZy!XqYO?y7ZZ+K?Csj7lv>c{ND43+~~y9H>3Lj zrSOugw}XR&jt*(|FisHEthXM#`zL?+oSFkFUe4qJkPID7G7L*ieyzj{rGLmn1yFVW zr}RZ*TU!?6GoVtk#oRyk_7;|(cuSb5sCcH{(JMZ_Ykqd}?(Nq$?eBjBHh+9_u5vJ(Y5NQZL28i6! z(h_a{&+-7Oye&*(j$2F+C}cq9!LMCMFrZ%}GJCkD04=2yhlPIE5(-WyYN>32&g4 zDWWJ``e$D}#0@6K#oU}7`D$tjXzn|BS2TAU0G&e-hCX^x?Vae2PE^%8n6?l4H4K@@ zkGFw7eUM3K$0?P~KuZf{M^Qu8Ok>Jy||c|S?J?l!>HGWBO`!8=0niiO=k9F`Tgk+gemFs^A*}j zvPcCG>F)3DmO|Vtr@MEbl4E|n;j+%0aYdA|D=v@A_{*mUI&0ZBBRvAYEU4x$hn?my zN<&6{YgGmuAqbm5k_OElkiz|3Uf%rHIx~4pI>mW+h|)@?asp<11=xWLJs}pR&Kpe%V+z$@a`d~-&R*wq1`ah-!JNS_}Iv37D`0J zGxzX~#2sT&AR%ak9BkH0|E~cU5O&%(0a07?_U*sOqw;{@^1sgmqZX)~fI7cMx=v8> zfZ=Jh2gy%ZZI=JR6l5Z_AgEI7zXuFBi9VteP!|?LeM)p=?dqb;&)Vdkt;h zuZAQj=c+KH3dRUV=*<7p0&LpmNTv-*E$zG?zsH%-aGRAhLB(oRWPNQd3q*`SdZ5dI z&I@FY-@uEa+gNIXrTwdaY8d2et5drMGFTL$mi9mxl-`d}l`5P1@!$4~Ikz21F%TI~ z$Fk1>z2|DQjRs>~MyZQI4Fxr9ZxQY$pFv^9$cveIM{6iL7(~ zK4_rLFNb1>JFNf|)37&Id{a5Hn#7n)OFNk8XxYQSpULG`of+V*TuV~Z%#vsXO*imX zO;7;~2_1k`TF6ck&y^GuJHNiQo&Z$>>H7%8Jm@kYFhZ$2&7dQ?A&DLG1MD8*vHQZl zVf}~N%HzjXK$yVDhXQl;BMK{EFG1bA28u0^mw;rEiRtN_@Z~Aq%VjLAs(;{);F-}B zx7H>qzeBPBu|8yP(0K8?EGQh%m+PfNFi1)9Iy-ic4pd5q$yXQoJd{xCxiL$}&s|}M zNeJ@vG#MDLC)Uy4AB$Gfaon!}lAua~IhWn8JL(GDyJXiPE=J6DBu5sSH&OBN+_!G2 ztEz&|aTXi`l>ZHJ14>Z0EXeE%l7bv+3c~&N58AEZuN0XYILN=L)j!e&z}3PwK@v!M z`O@!bIT`2=IIjKes*ME|D-yO0k?)NsEG#TDqV{r}6rLioR$wwva`W935JwH}yaT!k z{JF}22QCOrzDI8vC#@9wsCmp}e0+{`WJ8d7XuYv0$8oZ=&x^5J30%&EDh~AS21JV5 zTWu#!5Op0LZ^;rnx1Sxr=~5FDzjrs={6JQqYH`^hR0ec2k}ggrk4 zuv7L1noeMr41uS-T|7F)dtjCD|JD7?75cG`9Wm6#l$5bFzOv=*obi_$1tRhf3Z}A z$Ynw!7=^$o{(@8#WX#pdU10IoLG{7R9K4<*HgG`3WWTe}4P@h=y*(?cM?iq0%yM{VG;LBEcpi)-zT}7$Pl9-~H2c!XN^Vc_*fav#Qkq zF4@Y!W4gDCuxQR}+}zF(e56meF%TG+Uxqxs@r~l3+vKL49$?b3&!3gzXz>te3DSZJ z$H|FnLN?a}^mPQFFXLQT>VE-^@qbgAz;s$adK5>+3y&Ss!-LG7K|pq z90x`am;t;y;uLOy+H-Y~1b&J!PXXxO)7uLHniz8j1_rXlJ%@TGRBH2WQwF)3l+vFm zyoN9U<-jY(XVCD6w+4JveWf_Va`)hVSJXdU+7lgia)>29&?L4^^YHZi4h-?DTMssh zGVGpiQerii;2L1cwp^T_z|Sczg^6W40sFkwvP(!WXc=8)3KR6q3Xh&Zs}B(R+n<=Q z!yLD6Il^$O5zfAT8fmWoy-;-^t9GGaI6gi`e-Hc)gge`$l0kV25?4dWY2K$GRfAUj z2A64MWF&+s5ViggE9xEpB53D%#ry@#Srz2xS6ll4lAG@bdF|mg6YS%o$2BpieWXt| zEH|k#>wt|Jx2uz3JOq>6JC(5#dW>SARzwU6NSXpA5ZfDpOC%4T)>j)BzE zNYoCz?oG_jmUdn01M??DpaFsEmji%&Lich?M2Q&_(bODcQAhxKK`Xm-+s~~dR;eAin3&+8F3ZZwBHO5T zx0F;{Ztm0doD3?xP!touHw0lR9RDgXiB}J|s3Qxr1Jc%WIJ>_kYt{lJ6_b?ohx!hA z3~bxvq<*8(kh>PVRRMkvy;*4io(uuZ@H{Jv{>qhZcP)x9FLCKAzY!1;wt*Y`o5%on z1va_WXaDAetmw5eTvR#zCCs?J3~xQZRE)Ao z&~LpRmO25E^PyjxVL4>hLI~gQ;pq)Pv;O5N93=s*2oDPj1_DB2n5y9Q*wC*CsUgLg zG$`PQ*K|P9s#>i3k>`{6R?AxeHxgeaN~-K%+Na=Zr%yg%g_ATfHYPw|BtTU|1I8ei z#JJX74x_62fz$-q2PrkYGzcQH&suTe{;P(X?nS0 z=$D@lT%RVrawQ1{^dPw-B7x_pu=3qMe;NocG1-H48+K%|xE$}`@xvvOW z2*Mbqc1(izl0f6+rW9f7 z%@UlxnV_<_J1h;=LmDG)m>jRm!Uu+cLog27s~)k_U|qj1Yz5Ll6yAEc5KSEmvzRC; z1O%XeY!BCvh&i}LFe4zkwu^J$T&<#UJ5#S8Veoj1xov$<&6yQ0w6(N=(rkY6Qv<=p z#Z}SJFlltn=P!fUdN-Ej-8T5>1sFUu1o6B4_C}S#@$Krs{%A?Kit2&upd;(z5zEvl z*RvN$8`>!G1HlLsm@pKBfE;-6<41VU&&%bmTK6FX5}~VtcDja!s9Fdy185J`+%0bI zpJMFFp$5nIChINKxEgS>gVb>E>T3cF-jEx@%m{UCh;0V6Y1!L<~ zR{e>Ar;7|jN4^)@8d&fTfMVs#iCqa1`mlCZdw6| zXs$%5r9JX$@#X5YXRUnBsEfbYInstLzm=Aj0#ja*3ba&ItmK!b^gwJ0bJm>@(-0Zv z1jvG2qSg`bV3xV+;s?C;L>*`^sKnla7eNMomy=@#9brYoyF3(r z;kt<#o13@kl~OjaHb(}qdMn;?dW>d9b-&fg)pBvy+xsK@G8DJ)LK%2vR8{e6tZv;I*>-EV?mP{=yBf5Qc;A$Ac|o?-t1ya;IA-nzwzf^1{GQh1?AN7pv&lPH6JjmdjP}OY zw(yJ+VICe@O7Qj`m|cAgvp7(#a46Wr>y6&=z^n!2?YA=9X?Th-`p&? zvAJ0&J~F!clJlu(r3M6vKq?X)?CTTY-AWBnnNNv0k-RTPd+$X`%H^G!!-8Q*G#O`Z zVF3mc>5ZNn0FXjML#fUoz|u2+#>*p%25f8>ZKXO6zCXG$S!KH6w7KG9gzAYFd3jDl zJDYRG-S3bekFBAu(zh?K&iH;IqLd2r^EDEdZZ|VH#)+tx=bCM8Z0@(47U9itpD}?r zVziPSrEdX9n0ua!rLH6H*}wzlS*oRTv!Y z*Bs*T^vt!7os7g?Iei-Rlbd!}!|SORg3t7t2izyWXj|BT9XF@vp7za%Yt8VXM`-7k zmt)J#I_Wx)d&XRrpvY0b$_)gK?2?+yn(}g2fnSq#TLOG9NDZqBm%ddO=5Ey`NGJDx zv)tWApEo)(4_l18`%hT)e_A@iVG7;KX8F9GdoD6FidUHT05`Z|e^70K7^~Bw` z9>71s3^HMXdH`+)92G!m{DP7S&c5Tr&Nd-P+{=b>j6zyGrKlR2Sv70lKRvIrUT+>R z3ynIY!9nEQ?npyPYx&J>-)yXIu{!goe%g=f4A$R}I(01%c+(vDz~kCGFi;7E8W8f} zM@YWp@WGg111{%3FQ>w*x9;AxExH3{E{jtA4yZig@P^6`+-n{{Ha)IeULj#ZZk`Aa zpd_D}4`+|GYX1MAl3d+P7gVLSvB^^jC)qn(DP=t+OV$jzjM!kN7sx4ns!y<8U zm)~ugi zX6F3z@=TC@m#{NU2MaCQ+um^O&Z7qgx3jpz{oG+rPSEw3uwY0%@yV0fCr*?t7jOqtdiBU4m(mBnc(j{$@I{T&W(66{Dd@1a z_P-LPlao+WJr=|kMORfOVPierbXDPx_ZJ^m zLfJ&E_aRXj|2L>?8EPWT%v#GPxR7=>&m(L3D47FYMy- z8BSHYjN$Ins0{MuovYeiee^qZsWa{|7ED-)4hBsXm6PP{0S?Wn4Ud3tf%8sPRO09P zapeWmS8`V3P(6txYs@&rB8d^D>ahzZ#6Y5qKO+O~gEo_t-Ar?|$)kx?Rxv@_)@K>` zgQi{WaGiAgA8+3}0o4ckzstKPP%S{5Cbl%(gi*j57dMc!`$_Ee;6|aOOI=SHt)0{5 z17R$d#%Fp@7RBJ3(Oi8*Tpb^*Gq2kvw|L>N!9h>Z7smU*0R|)}6P8_3Pc#PRePew+ za4_>2I=dAT^}l@F6DUo$j*~DpqqTdGE%k=@CqM-r0Rge1O!Of+uKIP48jbAf3J#I4 zObJ%8UD1#2^McZf?&vhG_|f3SA-_g9!U1&`m3 z5!E}tehO-RsDK^VDy47UBlH~2BF8lVp+gs! zqv+y_dROoX7Yj8yyU-ta3ZfaFmpkQ2otNbBFd1y`NkKV=RgY@G;ju9ftp}x@=X)G>1W-rW^<%@&>%X}uW@imD zi`{bf+2*J};+DC;3i}taPMl)Ltq7mctZlyA=y}7`3}mr*?cFa1MiWWi(MoWx2)0H$ zX>RrCH&?pME#QxT{H&`TG63CkYK4!hcZ2SrS78rP+@HWs!EQ;#!~qROV84JIZs69X zK^>jvEy#=Yg3Zmb&SO) zv%OZ+@mi#?@?6Is%_fNr0R8)Vd-L^5VE4<<%@vlB@c}giO3EMIr(o4Wtp;Ty)nE%o z4cM5VvTuKOgtHb>#M*U#amjJr66aBe4aUQx>JtRD3?Z;WnFFc}YQm{|C=~fAOwN_l zi3iVobY%|w_6rUTIf+XMI_G*Dy;j0$?A(5hu?%H!^YQksO)U|0k{tfb%fcJ_k%jl7 z!k!+Rggi<~2*Nd_k^b@ZAVd>1-LW)}JYL*ZIGb7DkZk0Wt*k|^x*b(_N8)y7Sj`zB zMuASu6pDf>c;aw+yJ1dpa#P&o z#;RVt9&_fl^r)Y{*0D7rigsJ@C?gYzVC+%jJ->M4ilk(4&YYq{w?TDT-cs0!<+Rv| z3(=AQe1eqwbc#ZPl~vV$=Lt<;vb&Q;M;sS@t0hmpL_S3bAF7hBlS|!xbaWt+UF!Fj z+F9i>yWN-fO>cVK^&%1>-gE6T`-j6M)O(2#q#yWlq9E~hVyEFpH3liJj@$K;w-RT@ zAZfhhQEexy`cgyre?tYw4$)0LOVvhhHoq7*6 z?6f#|8AT@0;O7=-i^8;AsBhEac@~60D$I*&{mJtZE`X>t`AO$XH2%-!UJ3kpq?kHv z|1P{5oe`$G@lR9S}}C?DW_ztMBfQ|{g60h^*b_`~-RzI=sgSK+zK%_V8f zOjAB-wCk1rG~P>LsP7xMmwxl)-g3^Hz4hiuna`(1-zAooTrQvfBK|F*KF*||li)jQ zt|cDc>{vPXxXyNEbYIKCsZTjo_)?>Z>(b93w^Q@q6b~-y=G}Tfiu29!#n361?3+3l zwr{n`Sl4_xVI0Nwt9{pYZ@<32zP-KOVUZVl{|sGvb#+C|3lYUjLPh75m&Z=`OZ!~? z;!z~8(;%=ZF7CAvzQErX5N1t_46E=Wc5#xZ%SJ1IvuyL>%%IP1@wvdVuReS4`X(3W z=Ob%wBZ>?4-$39L3hUn!KSN~*d-c<|lEAS+e-i7WJJ9J7uuBTCbY*2_t4r1`SfpI~ zcb9j~3qMoC!x*-53hErkwnw@zCWg*rEwM8ZLMp^`6#gk+Ikd$|J2_Ei3O$=3V%>`Y^<#zGL@XO6o4S1*xTmpUG!|tJm`CyHm>*| zEoMaeyD>-gh}A*#Tw`yY`GxdRy1m1WDwbXBV79jS&VkoJMOo~|CzJ4v4K+N_1>g@TTJFePU9A(OiHggmEjnzkH&!%aiOUEs-?>$n62WY(8~QUdk@ z(k>mK_0OLCR1CF0bAwufca2Ufu9bi;_ z5cWYznMGCiYTWIJsFW9H^K}`JMuaP$P+&*de%O$DX-^e><(%_SkB8BpxN{W1Y#jR9c7N;GmCr(X?jp3DVu=k;qFTU(9-0QAYIVuwj3 z`h29$5HAB)7-g=tGOBd7>i+2d-h_|dtdB+d<-zrdcA0iDG4Xh{{rh|fI%~d*!xSqI z;KD3RDnW>XfFe*y9gxzqvV!)>!yukEdM0VyhngEuV1)(qr(y_=&YR1 zIEv?QAXHXHQIW$wVJoHiKL4=u+IrtiezM(UjYL_9c5~BId%SqI`M1{rly32tFJ3$? z!n7z!#odlXETsbfVQaexj#v<6!08DVwA*j=OF>OF81|WN<5(4Nc{e=6aw7biz7Xy( z#}KzTqO*D2-f+2MMCOYoaCA&RG{d(^W_H?9$%6aJLK3R!rnaye*t=x@z zd*~qlm|>C=r_2Nz6GHEuMe_(e2m?{YHAc&0W0+h6aI91Qde6I1x@VQ;~&lL^>T&Ir%~` zUo;gjAktvb&?3kh5n0-5#xZat!dpoov0$RP|NUb^R_E$m5DANTDd}Ll8j&uY{5+DO}nmF{|5B5i65wf!@QseGc>V%Wga7Mwk{}J{slWp6LJmx>T{%Pl`h9acxwYdDxsJ6R{re51^=_e8BgZo3tk@9< zb`}6vzF*LV7W%V)4^b%4&t3Z;M1@GZS^aSU{%SxO^=}0HGh1pX#vdwITR4^csJD}h zIo^X~oCb*X-;uC28f8Xq2|m*XjEEpAjnUI&b!q{m`Oin8CbW52{u!z9{dfYL_6TJB zmgb(Kbrf`S^a3Gk@(bs5M&U&^1R|O_$^wCq?F4)SPYWI)%sI>vOd3om+&T;g<{GYm lPz0oh@Xer(;6HCaU_`%o)S(dhcojx~P*&7X$R`?;{s*G}Czk*K literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/multigraphs.png b/GraphRecipes/assets/multigraphs.png new file mode 100644 index 0000000000000000000000000000000000000000..01b6bd396e9b38296bd23cb89a6fc29b9d8a6f44 GIT binary patch literal 23838 zcmcG$hdg=k>g<^E%GsIL`CBgS9nPsmR&L2?PSwc{Qa=1Ol-Efk5urVs)2PG9bH-0?a_y70XD7uIU8s2LQfBsE%$I1D%e%jg0$Scdp z&rex1?kjER*D7CKm6DQd3=TdY&aD52D%)Ia-cke)%?q$Q^Z+Uk5 zMG6cqbUW@Lq?4-;7vc*2)Gb=V#;ka<@7%p(WR;tks2I>hASkHD({4A=)6-MOO1^&j zCa}m}Chf#IyfIweY(2Wh@=MjHryj;Sn#%+BGhJO>7cb7ah!O_d_CA;^Wn^F|oF3Tv z?^Kw5!|U_XkLhG+G*Y>Lw@+g(_N(sIMz z6}s}gd!T1bOg*DZ-uBUir|k63uy3Jt+o3R4B7%950{5o9xP*kmuOXM|(dpK5ub}l4 zNqWS7%cI;YA2}LyPX!RSh8~o-U1W`r>GhW}seKd`#Yl3hC9B+ix^jS?YP%txoF|aq zxOBQXWH$1-{&zBp%7-$__`15D?y^M?ubL~XrPX+^ zC0bkbnh&a+{1xO9{eG~3r_8~@o6+~nj~^^kLc7tTO*YYxs6SEwDJ}U8{08*166F8&kEeh z!pDyv*Il9vxg0%syySZ91JSb5h=!m2bNpNX1@33aYsL0r^jB0<4N^*+7s{ec)Jxkk z%r2xPT`Y7GbvR*NtNv$n6eA!Io>07HOX1z{rmkA+?I|DO>pJ@|*!-`Npm>k(ntFBs z=9na&w&U^0$Ovy*Tk#2+vlnaUC(1Z3wDHw!vAECEcVQX)uim*Cb)u-KsOf0N_ntN0 zVnYLK*8I;`XP%96_FL3FUMBbt5NDH3d{{QwDf!g0$&ksVsCG5wOS8}ZpIjt_P5yY= z>$BK~@4Ih13rPmK&}P1WU0y4kRqM2JV)y#yHg|3^dKrBS>lYB>oye_|CcDgZhSSYU z!|^@okLL#o3S;KVLHUgloPI*N{2Fcz(qzSgY{emVZ=@HEYezw`V$A z)cOugafxd`Z*%KS%q)8H>>0!E#Gm9cpt~+8B9dQJLt=XzH?R*9q)RRi2soG5iWv{M z$HX0NW!TrS`0`2Ul^Yy^R&r?}zcwoq$9btAeUt|HF40^Gj1fF`?1H+dW`_$daQu^o zgXQmw>gpe)5!94qYYLyRKqtGPNiqpKKH8 zLd$kDXY^;5=}s~6p_fF2uSYPlr1bRk#46%XQ@HhVL+FiXm%O$&rMjfB0sOSd^5^H; zd6T7>MMQ3JScn%1wA8E!uuU-%2w&v|0)GUlt4%%;Ymoj=fIG9*myzSK3x+$+$*4&A zL@l`}%7RZ~&lFdZWtGv58|O4MsQ!0RPHi%gxruK_=w&46pVUb=*}Nb6VJU7_>l@}m zjW?6x%_F0uq8HMy#a`DdzF<@Rxv5^71hcS*oO|D0r^Vg}O!=B3nVw3QMF;*|i$yT5 z6Fs2aV>r9$rNoj&%X2m(@#&@Pa|n2OEPZ@-0eKixqxtXB2yMgD%|x;WENxxk`E&ei_db`EBKgj{HCin|6R3L@8yLnU*6a_caT^6gWqJ-Sbyq(kMBMHajxL=1G&uBiBG$2swQ_# z+tQDzcm$`n4NF<}qzmlX|A~vK(08A{sESvC_G~1b>P4N~#2s%W5{sI%C9Otta!$Cq zF4KkWArLzE9aE%qc5~q=p>wD*+&)BdT(zw=>8+-7!@kR-+<{*U7sp2r)YsoXG@}~f zO{yMr3C?d!5C@T2DrG@8`zXW+{T<)KN86@KEsx5FsNRekx{1e1Okb6LMLx2$8v8pwJ~%eU zxu%WzQr0H(vsUaF@O%H?Bk5OkH3waaef_lJZVLTodt7rlX`Md#^puyFh;ksUUC!C* zfhV=URvr&CYO6B*W}2|xRPu}3p|tnxY*UTmU9R)dArNHmD+UoAxNmXI@v%^>wn6z2|$_<#EZF5t-0<^j^CVGTM5?oXV3a^2Rs+p+9zuCAjU9q=;(;U zV@Y?NmKU>3k-@Bn9*Kn|P3}}5+!B5ZS@3z^% zo|;Di0XLL(@s6tQeHF{s)8b!VRWJ#@4!4QwPgC)#tXixUclS_&3|Vr`T>W1Q2YYpzZ3?GQc8ltSdVaX-}`?saLeE&TY-;fxJ!u6eJ@U# zn_bxneYoZUtkR9}KexKxs0NAjn+&x?m{`bKkGoFR#@@`Z_!vBbV_85y=+i;O_1#!NXZ z|Lx)T?7byIiZwV@w)FZZ)q^yxSW0T@L*vl_Z{Ga;?=ka0jZfdNU++>=M@L7mT)A@k zvlN#0EO#;kbL$)P3;PuH*WK=1y((^i#YnEL-Pv4Q ztgfm`PD!z4XC>5)gUG3#cq4nNW?uHoGX`8Jr!?}6aKLrWpVN!4uyt;Edv9VMg@lC8 z$jVk$RP1BDfynId@8^{FWn^Od-qB%HdfP-RMWy-Y0#1x3@M{m>C&EIb^P!J9lHM z>z#{>i#Qn=DJCrxbNX!j(a%ze#7%1h>jWjsL;7|1WxDTc#Y&%Y8g2gZk9YV>-$q$`zpTos}{raV@uI{z`*Y@`9)U%%dQ)X&z;-^mW z^4@kbb5!!!56Y?a@5qUra%Rm#`S~R!g@u)VJKG^l$B6grA+@Vy=TC&h`DgsH zUdy+dhLn|+wc_MrpFh8R=~7~R{GHL}moqcB?M6unCr@K>G*gn34SIi+oS;yeT=FUL za^xTL=qpsEF3Vn>YAoK|^gebh>(7@LI~%jL0nTATL3f;-3QJ1|YW?Jo962IY_-ACq z-P6-|bG5y@+im{$f0FFC1LqNl+`6nnAZFpmb7o>t9!3sYm{!)Qd8<0-&1A#0&_&87t-6;$HC5y@3Z~YGFHto zXZKY)jkPv}9n{Y@7@D4LX=qSs-BjWc5fZx9ons-zHYa)b097RrMMUw@>R(UEHn z^l4RMr#P>2a42tcieko+#n^wk!SZzXx%>Of#<&AxVq&md-+uj?{{8#<_3J!Gk4kG_ zxP5yj<-!Z{{rh|R`bvw7Sy@<4icoFcC@3gkV`Y_+mhLZi8jJJWrlY6t`1&<|Rh1YE zU3O}{4?ExaYWcFsr|vH0Bay`AW@$RQmWNhkBi^>L%GzrX5fLFlaMwMYu_*)R-%u{ZuIeN=S>V+Z3yf5sqy^Hkg*i!On>2!a6*0+ro?My#>C2MG~2MS zX;a;yBl$dlqqwy6#gix6y1ECG4|8(Hf&Jf8xZ~{1E@l-P9ev;>tEGj-m3Kzpk)Qtl z{fTGG&29VeLD9;LK;R9SCG!(2DJeP32~{G(;?@63<__KZ8}+`m&jXHu+hMLpg>^>+ zwl3wlJeHxLc$|@uk(kJHr!MvUH@UjH3X$B~ z({t+d>8Da86ck@lHD32uc`VKkt7>SpjEq=_$`BJ+SQ$SI&@eN1%Nzg9QmJk7O@2SNfB$~mG&z}Xq40vbx{8t#yPWrr z_wV0-|ITzeiHJZ+O+GXm0+N)}_pqSNLr1@@P1QkiC4C__<`bhE<>e~XWNy1={~BK< ztv&90?4?{k8H?AO&bw|H842+4adB}WT~=6pq9?wmz(p%5C55~D`1r)e#v+Tk&ipL5 zYGD&~^NM`(L`_rEcVqcoK)}brN{5y0&?A~}0AF|S-1$*ttE;6&vnC%>T>NiiWp>_u zGx_|peZ32SN7^$&i;d%xlb*b`OXpUQCL_Q$NZ|j46zO?p(9kf!duLiD&in^>_Yt zn?b7McQ2kkO&r(a7ZGv%^Z9w9Rg1Lek^l`2jj|6BA(TKq#F<`Gvke$D%pI^8^EtY@ zRGyfe{;oVyi6{Tg0vG*$?!a^DX-O`j@j*}0%Zj3s`$x{dVw-$+L~Hw>U<=TAez*~E zDQr|U{yj^-qM~ARbMw%lLyr#}U-j^WRa$ASIqyS+B09CYz?=eSTc@!80>6 zZE^Au!NGJ43uh4wyS+NcQuYdq}A- z#+*!ukN0n07B;Jiij3^3@^I4EXHIdFb)Qqv({skQbwBgAqlVxw&*H7xB=DoB=RsiL z>JFz=ZWeFg(`V19C@Jqq{tRUm!+!&)`L*79k2FPgbaaT^Xt?#Wti7?(nlHok?+@Ux zy^YQ4!k-Iwd#5HRYkapV($kxon$nAkq7uuov=FngQBfAxuU`VQ$*#f;X&Dty#zaL$ z^?L zzbdV7-poo$Vqjo6*M1d9ilkBkTx)Ol*j$~@TOcNEi_a;_vS@?UL?1WSe0$~-uJZY_ z=VYg#zP>)F0H7i@m7k7|j+OQ0+qZ9#!%ti-yTivIc=YJx!~~G|!uj(VnVHl*haP>@ zh?6tX*Dqb!&vx$IxrMeHABOI%udid(H8nL)ojS$O&wnSH0Xz0kG|?XQ^XDDi+}fI& z6v*hv_FlU-bpJjvzQ^?ym6Uw`^yx4k-xJ-seJevBNiSUZa@@G&R)5vY<`Y<4q!Of^ zYuBzJlo&Nuz#fql78VvvD_!lIG<4r9OioU!U)cS+X=mqK(^_B8CnzY0AUu5dRFl+I z!q+_lf#wSH^YdVUuTR@gZoR`rlahvi|Mz%9T*mdUft{V5yZdUBwb0R{1$lV}HQwbf zUNFBj@$~dObLLE9V&cp|t(L7w!|*)b?YX_V_VE#orKP1{+m}0bH*cC%13m!g)Ro*o86;$GZqMNna2R8+{p&KVsW zivve$`xhQg?cuQoM7eqMW`uV%o~x(OdTMGaPTD1;`G_a2BGPzYf#n5r^Z2kZsuZ!e zGH$nbwzu47eu9&v6$$eq@r8zmYiej{XlXgv*nDmO-&a>x^s{HrqN5Kjd~e211O0`I zW?Xt3Nv0W60x;0f(2(?$gRo$}*?Dx;2Y(i%CG0G$K!BMR&oSecuJoI#QXKfqhFKcPFA(LR*qMknoC(zHi%1TfF^u-H_ zQ2D;N%li5o8yi|NC$Ie%!UVkPRHvvTgAqT%Lm!MtH3Me-O9ErV902yTZ|1-lHY~I{ zZd4SMxI&VZnd$7}GCe&lBP}fuD{|zBs=E5*&vK_I*Ue|NWPz)z?pSi9_T$Ijfeu<& zScK-SZe^yVSZ=NVgAW}U8QICf3i+u>CNMZM;*53lm9OyGC_%nu+DYsA{=L|yori$| z^K@Xr%f;1IT3Q+l;JdZ{yo0sYdkracY-~&w4_OC;ZOW zNJuz3IvQ!&-@3)l!BLIWT2v%%)J9TAo0n5R=yu7>tgW%JyQimTHtqcR^N2!BoZg#A zH9p8U-^tF5fB*h5x=+dEZpB4= zbntz^?c2Ba_^hujj`dHMXUNUImGweU3399f0C3M*_j#U_Z%oY0fFqM?&*vTCOW9Rb z-U#OV_wOU)V22tsuH_UK>R!Ei=ExD!DK3A<-65~d4FwbxEpgHO2MTn;tme%DT~29d zHI!32b^@3W9Qgd@%gWoBvyspij>8Ry`S~yPMpA$??5o$cy3;HOJk%cRxsz zm|sQd=s$q9sHo_^^`aHBkDZ-*Mi+f%eo6s8ea(hgl$4bpWM)o=bI1t9oC+YUef##+)YM@AKszU$ z5n4VZ?+bidfY5}3H?45CMKUlrGRX<<329hPtdrF(3Hcgpb6>0gyA*gQpKd3N8AYH4Zd z>V5;`UGSP;Tx@J?OiM}eaCg@h|CN8E5ul5trg`mR^BK=28}Mne1mp(<>NSo>LRgN6 z=HKj3vIC!T-@TLdSzqF^hh&lS-E`krwr8^}!-OWBd-PV$=O{1lIp17|ftr`EUV#*I z%6dj^nj&mK&%;7O5Tc2a;{tXzHU|0El_BzBM5JlFe(1tZJ~;1AM09Uq;jSb-g*oAy zgTVXQOiY2AnoYdK)Gl-!6%_nc>lf!L0;QUgl7cl7n7nLkoRylYGI7NYG!4qv-rk<^ zADafi6rqE7HK^v0SNxW$k<>1B1sU^p*!>}RCs%ZJABTq@N~Qu#C@9oH+kG0~3cHkb z<=tI3H()?+b~az_u9CQV^(S^p!2SD8v&DCY6aj)j_DhGH!~rC&u`(-7k-U#+IJY%u zKA_+-Gd^C2*j3Uddw;ULtn6x)yW`wx(=D)4K+u{c_?~_ zc~xcQ$bqS)iEpJ20|A`~suU;A1 z*a#qvbUCVopsJXaMe+ZhVr+Eu03%~(f4`hN2)Zs3w40mTtA~irZ(qOW<>x2ZT+T;i zQ5`%jWRT~-X_}@P+gt1B=i{@Lcm0xvi%T)`4~RNkGlp+*bDW})YMWxdv~zv;{HE*X zJ~cI6jFXdvPH&1<4QD@p;X=}{EXMI2GgC%7Iy_svjqdB|zEWhs9HZip=4GUa>PNRI zkMX;J*<<=nm{zTV<+3!y=YaKMUjjx|kb&;gomW@C>*xq;$K1;5P?e>M%ERJf7b`1% z4T<&1nHgl0=%^@4O3M17I~L1!4i0f(ub-bEleqOmL33kr((dx*?{D6`(UGiM)`v_2 zor8Vl_k-vI@~N$@ExyAJr6eWiW)YN?t%l9l+uKXs)6v~6!OcAc*G;^7Pgid*=#?Fm z{O~Y#8@4oFnVv3y@W0#Z&{y&fL@d`Jci{i^0yMuA`xqwW4Sf#b8XOeFRon5c78wu& zD+147lQ>F9r)K7bVD>ClLIDkr>BkmB52;di{>ySk8n);BhEJCB_?bM?xV z=w{szj9~BHy*7>uzXx02%7UzIVmfu+zUTtw;Nj&Ba3QaYACyr%c?pQw+}s>-NS3YO zu$Y(=j8ZT#s8k!a^J;1W{QM2w-78S^BP&M0kvCFKS4~Vl*}{lJ`9kvZu@BA77lEkA z(DDwP1$fqwlFxhmU+rdOt=;Gb=s|UA4v&x9fB^LN8X2X1Zfc^Xqx1MT$+TvI@Z{Hc z#dzR=ismphJIHZ#bo9`^WXgLAyYLA;rD|QFGm1<;F%i5wWOHS*^ty(IhmRjmjkdfV z5eLzCc6Pp6a*H)mE!p;{z5Vpy;NSxaW-RiPo(BN|ynK8XU!spQF)^Jx9UU1dA}-E1 zmSC7E>9>96;6V_}FDke`UTk4w6ZF3+xyGu%0b%Cm==iCn#nRfEvp^(L4fWMG7*37P zh7HGGpaCR^_xx{?wNi$a*@3v|XqDEPn_$}@m@TcXhYTAMU}L0bWPso=z0F;EG%+zj z$H*vj?AY+|@JDG1bEQ0YA z>GNj@+Ysc0+XpB)&ba603sZ*~KR$ zoYmKN10#YQvT${Da+;c$V7+VH{`4t52S+RtNH2%4^U`=b(id`LhmCjU>gxOrz#FKK zrR8B#(ni?ykiMVJu*0eK8*7c-MQ9f_!a4~!lCGCm<*Qe{5JML)UTpeXaP8wmND!cW z$GXNO#1$`Zl4`^ujacdDZcdWo5%(aEg7_wghM|h zV#0r`ym}@k94svEfdA!X*Bdv6;jV^p%GYFN9g~-r=iyOQS3ioCnQOU@9EKfv?C4P> z2bJTe2|M?u&t(m^IU&vuA5MXk5$#)9T8eq~YJ0IwUR>k*F}&Hxs3JA>Ve|6YvuClE z=VqAhz_>xSp$O*LI(~_PF6t6)Ya8NONeUq(=s!|L)cW6TFfR2wpW?w6CEg64-ezL|T9e*p?S?o36 zn^z-2v29V^XX*z|n`jEEf&2zLNnHK=V+AfuG8pNLHw}QROP4MM_~}Z5O4ipa9ZlJ| zJ@nxLSW4g$r>J^a#fk70w|snwpwE zW)=qZ;;QWilSTy~TF6jwJJsf6U41_laJTb7Va~egF+BDl2JvI)46S=Z=brk<{K}V)AQg$@!?&)XWS&Eg43v z$Nuah6%`e>9-i85w*SxX-)x_>;c*z2-MJR`n1)ksWN1j_)G3ZnO$2SKt`#eya{?z$ z9AIXS2oFz9*EcbVh1?qY_{cUY69M=*JNqwS@Zl{K4{Yr1W!3hexu&Dzk{!uSC}$a0 zp<)$YGfT@904XGziku!=PvqtO;$F*B>-*<)_bHC@2U#s<^g9!~w76(}#Pr+_Negq)nW&z_xor-wKKdFx~gV@JXVmv?q_ z?16l0QiZLzBNBkkHUs#IHDQq?M+B00hg0SnD&?0zs|_8#bm5iCnqNZoADku zI2gT_i~s(C$8ap@tpm78Nl2)cBtcB=Oid3J^W+y4EH5o--z?((mqVhTYpU_i+5nYm zX}6iH%F6%62_qM~!Fbu7k(b1T$-4D^A#+JMv)$jn+uPbEq<15G)&KXO?fNB?>(>*b zqV$}c76)4plGktC*w_+~l{Eoe8+qXU0MCq5y@Q!qS+E*tIHZ-{5y|^)3-R&6YFl3X z4^RNZa6xN_VD4P!5olfnLUih8mp8hVjvft3H1>nRD<&Z!@3An_k#fN{${xM}VlRMG z!Veq)dLB}AXI%%kU!5O*{rYuAfd~Jgh_R6o_6N$>&YcTD;cK^OTV0)TDisP3Qxzw> z4D0y@?6Ov|3PR}7>|-M4N=iKQv$Z>b!XrCBKA57&O2@>6%3{1u-uk*H0x>i+^nKsl z{5)0B9nij<%*@0qhcq=?OG-}Lg>~hE{n>s?-POI0%@ws976 z!pXe@bi|a<&x>Pii;IhWy}i&xPX}~#b;bDkt<e92P`x5fc;p`ug7N&f&1oJqY~+ z8wu0AyfFcCK|w*0@jPusg{N3$1NL0^eG-b!@87pJH`7m90Rpfka!jisH`a%LqjUgW zK+Dcf+4yyjG`eZc175ydTmLtusHh0f@3y`D;o79sRI6LJvMVdS+}$OQ-$#jV*Qdq4 zeR3KPz>!Y><@4uD`LrOtjLgisH;d%R_TCQ&*n7lQKAh55)gAf*7CZ$qeIcRvEOKXbkudK>v(1w{O9fMZ4}(5 zS4QGZv1>kk`UL8;@sAgdY)C&1E4v0oLRJ>k+f4%l7O=?T24^=n$kT~JiGC0XWM!M` z^Ry_C;_-st{F2bIylT?!X7<(q&shFsjaOIe^XogYunTpP&A}AK=B6+9feZ#ABT#Fh>+~v z3kQXTjV=1ci)RBm=g$ku$@PEva!f!VXfmU!x|)}l7lS3W%Y(%9t>s62CXc7FyaBd> zI7 z#ED3O7o_Ny2Yhxkv~F*$n1y`at>}k`hl3NlIMf>&8Nr$?8Hj-VfI;b3-P+b>W@e`K zjsU4udH2ufrpAn{EM0AF$gIt#N9zf&q_I3Ga7;K==jZF3Jq+;JpbM;0s7&91nHW(^b>z?7wxCNZ$K4?*AkFBg zJAVDLx3(@de}5l{r+t`7F+C~iBgCeT&O9s?c{AAd7};wXH<+Vh#iPXjsfa>PJ|3Q@ zF)>Z`^>EJhbac*I3CqdVfZAj8AHEO`=UwM;FbX^66hU8DeL)cm|WZOan`6(YEj-H^2?40s>|1Fj-_9)yKW1K^Mxi}LbV7QTAig|rtk zsVEva9RBj9Aufz`^z`W^1Qa}5P%+4e2oLZ~{7Y3-1{53)9>ROp0d+a%U2406LM~E^ zy!)KLk@cplvomIxjErov;&2Q6I)F%GQWC%*U6h%SgdWy-H|SkJ7u=+~uCBY>F^vLh z0rejc+k^z}HMc9I`9O9!$h{KWC&V9X|$E)@U_P>Y$Hn*%g2EG|l%KFt$2=e~)C ziPN`!oD604t0~XS&5etS>fD%uYXLh(loV#s!rWX?Xee^gA(e+_D3ci)8zV(LdiZcT z<34Ih(4{~t@G`xZ6#hDU9?)*?8~O7GvI>O;+grCTjuKNm`iL?la9iEXtTsO0Jdg+m zxXIP4zTS;p8!0BU)9R3=04*<1PgFU6R(rWMgtDSP&EC#VkxW@h33b`Cps*}rs5Nf= z=x|Y9|))y;-@)PpAZQ1Jcx_l(^c zB%f-$Ld3-!sdnb>NKksZjkB}(S~WcXGb3~XBkAwnp&w1?`0>N^!H5_nMkGoctl;M6 zz7Q?M9jS&1M#DamwiH+boY(Vw9jN!tpx35qqQBN}x9SB6$P)=+=I-GEl7RdRa=qb` z>j+9L&pCs&O;1jChFTbE+0Lq{kj5-SS(**lf~BJ};<3IYi2Y|C1*EKfiG~ND8oY$z zd3VuDG)WDr>aYew$z~guhAZ)a!v#Nnj4S|AjV7<)kdUWp$tcD8JrJL9q=+U0r$jHe#4Rs|Vk^#%P@Pd)itC*M{D8;CX zI)HLxrO%(=AFQOVsi{JSX?>!G8f*q^dbH?RV}&7aKnG7DcG0oCxOniBGfMKPVBq&S z+<+j*E2W6duYhfAT!5Wf67!sG$RgoGd^WKbr`v+4qI7Lz zqb){c&!dk}CRN^R9d&hpC=`yZL8y^{FyNM^CiCmp*Vm>$f=j?Tf>caNPanUjVdCO* zoiGxwT41eSTYH{NkN|M;<*x;oT1$Z$9elhSU~mqB+BV2}k9fBqakcI>8=)$+{` z@XN-hrdGC*1ipcv2%A>Le-;eD4#G5`iO+9m%PlSf^@-gk6+b^eW8=O^t-XYYclhY{ zPf-#m;YL19N=kx&K#v}f=g}=QLqlW?>_6|{_4`H5GCvN_!zuyBgIRN8%I)pn6%+tJ zN`ivO5P#r5sCJ?3gZ0H~z~ICZT-MVAN@JsW`(7i=jwbj1c|XAFc94 zZM>oXcZBGM3Ryy{Rp+nwKQDc3zuMY+wf*u!c(9W8rEhuSA~BY?G22DT<|hL5U4a%w87_PX1RJ>8(MyCx!-x7meX=$C@4>hP<=(#A$| zA(&8i0O9AAm6c_TIt-M@Y-PZ?5{Fy2it_U#qoYA_jRvDtN@io`EuymaZ61;(NqFi5!^cWo18_n#PfvFq0#i z;(5C@C(~+aA))3E9}?SCK}B&M3mhL_BKw*W!5rPus|+Z}+y*{?mcsKpRN#olNJtPF z85!iGjYYwN0OZbHe!j9&>p1)nc|RmX9lm`<#jbAx+W|E*0Qzn#D{PV*+xON;IAzj) zL>3+3F*G3$8Bj2qx?{NFKJe3SE$V%9z1|$RDl)@Z8(bMMJq|WDJH*Lg9TBP=f!ZD( zK_zyaH)#R$<687FRg3AHHL zoTLsHxo9DqV32^{`g(IqB*P&=0|^@ImTL?-(U-STGl*IFeoFkMRGAc$v+>fl3fq;S zm)XVqmTaG6;$m-jv0k^Z8L6}$ixSlJ6rSxX#s3{%2%EjdUGU}UxsE~tEI&9izhGxv zj8SOk1EPKTG6iZ0)8A*Eo-EL1s;k;(<20n5k;C{N1)6x;`|_&R)@fKTxKBv-qPtBO zeH*K*;K5S-{3<#+kdNt6NH=T|C&Hh7L5uIYaH%=XN(D~g!%eU z&;^BdfD+oTk$R~?&j0xyBIJ=GbJ{{wU&*;@3#9-fytP%%eHd;%@P28A?A+)oG7%_Z zIjY>jO0Uurb?%MbGI1t8Zx<>ldAOq2fI%yt@9nQL-(MoN0c70h)`7l;Yq#4a<^K96 zDuYNycb%Me?en&VWPl4u{Tms#8Nj=_g#Fyu?s_Uf)WGF~${?en;#jyB9vHZnl=Knm zpT|uqqMGO7PEuOEvb3melzPK6A*8$``^aWV{IcUugMqc72}*knpc7Tt5c4n^AW*wU ztqX7c=fN_DVgw9CX|LsMELsr59)*Y)Nag%i0^vE7z;V;6-CAi+f#th*?;x52gq~Vh zT7tKrZS{S9eR5Wo%*Y_8ocBrbQXz}T@gBwi3zw5w_sO`aO*cCaOVX`XB)=XGd_cKm zJ@}A`|HzTX=H^>y!GS?6zjKz3ED%Hr<)7VRrBfEG{{Uk)+50%O2K+>8XG+T9UGrL) z7zFKIb~Z{q&_t)zzK52%wXMw-b1W`1{L}x+HGAjOl{;pxF>*R1t_E{YXvqK3adHA) zR;TXIHomwI8N}Jp9Y)wx8C=>wcjVcVCqa)N1Dzh+yLT@xj`N-ZOL_{t3nU(2-x{=R zd3kx&+ApsiayF{Wu;1cc&&0EA&)dI_<8!%Sqxo9r$`uvgS_FxdjEt(1(o)8Kl)iAr z4!iOP3Nx6{+@X`4xAglep`eF2*Ks4(?2h&IT$nC zYX}Kb9~m+~FL~zy~VBW&YN|9-ADA=tzBMKKdZt4AR z^0KpYQB936Jkr`0^wmMKgA03V{Zu9^&vvBpdpS_TP45`a`m5>V*$%_r>s}vzTyI5j zXWbrsY}Qs*4fXX)BcB)`L^!)=f0id9#aUXyZ$5J3gq)EFQQe`RY$TMAlz2d`-MzdV z5PFFFJ=HAT8;rsC^3UJ8a`v?s@%5e9ejaOesskbNIKHSH_jDN*b?n;KSEr`N>>EMw zu<$$310dY*mlOPn-eacN*{R1SC)be!(0>C0a?f=b3dPNBfS&I4kCK_~*~=t!iuDtI zr^d^g3reQ(Qpb5*7GV!}8nk|0ha^tcD>WhEeOp`H^XDX&X;1PS&=Eg8Ku=$QBnqMe zOBp5L&CPb;Xd3IQA_vdV2AcW7{joT0dY}D(z~Up5@*vvXCTYwXfY-N*ZWXK#Z~m{o z9-VvdJ38pUWfRQzW5S)${Nv)}gc$e*w*x(lp28)irCAr>8fDf#PaX@hucDi<`y+S# zj%%)}qfR>vJ~$6B*u3PyH}Ap@5Eb1$vf%GeC=nsrP0R#Z#c|LO=mL-zxJ>9-Z9Ufg zbE~s|$eVTQD({u(kDkqXy2{+r(iJQ1F(@e`F(Vzq=|n=xUL^9DT@9xt1k%2Odxr;f zlzf8*5+osai;IoFbqyXR?mO?2_}_s8x8>lB27blfhC~N-D=aKbASd2EeL~o51W6Pc zJwEP8uB1D9BldxMko((@V$`>c(86elu1-eAy)^$3=EN|sbDR^bFK>~HZNJ$)MSI=0 zShmEYPZMP>c+lzh2}?w(kBR2wfkics!i(&E@4L|2J?kFRmS|+4d&5swmy3#P-445y z>-~6XieSNO9D(b-_TCZf^8Y(XLrWN^#+20DgwO1_ez7uzx$CUWIi+w25AHc09B7P} zf!JN0nf!k8&3$hAxBfUBWGmL^IX52s_U)nU0D^)Ul=xiifZ)!RfMSER_`?~WWQjYV zy9~w%{xvW<2;6}Hs1KjM?_^Wb9 zfrjsPQBDs2LHJ6y`vUr4=S59P-;m=NScDU0R;J)j4w6A@j7kA5J~LLMiocKIyb0BD z^}^XRq3>~&(s?fqU=IOY{7U)vvqtwbhj1?JD+MkYKVCo58obXSjpexa zb)n5fbR0M0py?9vZnS~pfB(YBC%=2J^AT4HPG&s7q0ey=M#Z{u6zOS?k+O+la7k(Q zn^7Z07tyt#aO6@eFM+^bx%;^Q!k7&ec!7K}h$xz-H@3@dIN)u~#zacAO!Of;i zUTsHMY5c79C~qL`2X3na;yN_kfi(x>A8XpYAG(ji6;a5`jS#{GB}_J9I~_ea@@#gE*rHfumF@EnDa2p3H4=HrwF6(3b5n zWP}RRV_0zX{nJJ&gx&MaIL2O;p0u{X{Deaz=mzEWv9$r_dmJqT6h|6aie~tK|4vif zR}fjzhis?fq(T@!kd0MiV5C*BrxIs1`f@;dJsuCUz@{k4m+8-MoD_8|y@c#}?YWn3 zwGyvq0&kZkr5^3P6d*dwbKS4++KZaABt_Z(9Cxk<(? z?kDVdDComgy5PX%;wN1nd>eLRLNga4`$eNz!}%w<9skgcPTSfKj&XiJB|vbdQ00lt z1NVt`omr5M`l49S`@rRiPUp8zzfW}hY7hT7low7+SO&7N#Qsz=oQWEHXVADB*C8RW z{KGFtl9Y?u)K0Tqo1c$=|7BW#)7=n8DgRoIT!c$VT&VuJFPb}J#i7~X@I>fEpb8su;nJu<;EuFg^H3n?RHN|d*YBZ!Qz%hklf)LvDb^9sL7g%rfSB9;I!rL<|TZ+#c=LzB4f60 zX!L@cchwm<$GZr%yZ;Diru#i0Ke3~+EKmLIk-^Pl7k0NbJ`IA{pYQPDBTbr0Yi)Vd zqHOMWVFNu-1f2dKu0;zM|7i|KTiWz@re~S*FJofNWA_lGi7}EuTB8aJ5l^2TdF#pT zV1tgF6Ha%`Z`}AOP4<8AM8?f`{J5-+CUwd{Jr2Cn?f*NWuU{$jKO90qi!d;~Zq4Af zuzTL}(v`BR(2S6KEAI5+GXup`yMW38aPxljaVUJ< zb5Y47Nq#{sdL{ou_TJqlRVRXi+aL=+VH=b}Q1jKorzQ%G5`2omWxDyFq(u&Iq{QJG z^;3Yt!!f}z7x+=|(ojX13z0Q9z~N59PQb^ZR(2VSse%NJ7WVwR*Et^9-8s*!bHPVV zt*Jdp6$J|T2w;F{1wh3W-|=PMc&e@maiU=6#C*ax^u<(u|6>>0qqaF#JxSpX_8?ZK zRqpwylEXx0*P^MTnd9Il_>jDn{q*+k{>!7+IRd529J1ZqoRIkOAsY;|v}kR7@b29- zI=4`k%@5y4Ffj+3;NTWv>A{N^`g#Te^CMlitN-9=_>?5?eJ@siLq=v_v5TszXvjp@ z=?qFSa6!R)D_y6-cQ0vcvxr$yc7YtwgL1|Dm0aLRx+y8c;X5Wlam=Y)%P?6k495&+ ztMgFQfgS}#$tx^GhK4pd0o{zRU%zmqfVnS^V7|L{TQ_ZOb-FrpL>IU;cVtVi+d7QM zo#7oEJOtj)*&U?x`|05eD4n3#0S<@*;kLH>sj1~$COgX?RSGi+ArX+ghvk;A8;*a;n2^qH*@3s zWOFUnQhW7wYnETu@65OVDLXnjMcm1Q6!q}*Lkn^d00NgNnX8XD6m zsNJF2zv(g9B|uUx!Vm*eZ%*&F%c0ope?^ylv8H+q@!HMAG) zeo%|nThK{q)LmG`tlmgE-dE%6aF!;hP$8qRgu@mRc67C+i|eQvJc*B&;35hR^m2K& z_;yTv%+j}JLsCk0Haique9X+u^ug%RK&POX-6F99FYa$H3126KLhjbfQ^8a5)EA;? zKmanojmiI!I%6WZ__r{IG%bRYwFNVXqthA}FQSv8tG&G(O>zmOdFSfejB*7PRvr-YY>T;v# z6J_bch%V{t;~Wm$h8Ss=Nd#0L)t2y&fwwu`mb_b;wfdzXllz73g{+uw}k`rvdryJHOELB{Weu?%>U=vcSVH-AU+< za7Nysj0}vq6GZYzjbk7vjOka{_rO+HR^`jnpV%sNJE^F4a;s$`9%uG%5Vkq23?5z^ zU{_Yqxl>J+m>4AR*gxc{%LRs0PL)gtmPexdmn+JbVmh7~+s<6LM$UgfeqT@-SIB*? zRDt|*f6771zUDK2_+*(igL{RWt#{;RYez{w&aSZfRIkp-jBU2=Us_lYjqfwhjegLck<1!eFSL>dwZY1KSUlVir^yz9w@$-rmoYChjJ(dX#XA_ z22i36eBkTXI{e|8Hl%?QPozy_I0u9-`msb``o?t<&FSy{fd+ml3?epB3pE>?aU7?@ zO;)M!2un&_wAs+8%`qaVz94yK4-<;c(h>WczB4T37Z+zVA^Ul6nt@PP2W0&Q+R@Q* z%iiA5nyBU+9Wmh`P8p)LKutrVxTM4?mc*S3kKc@=b93m`fU}Q&itIvyUjiACKWniO z2Lpo>x;}8Y6&0>7NB=a8tnR6L$}2HzO&nc9|4BxM;FM&Ys(>QF{9=Y4I`T{sv{6xh zti*$tyXE=dNGt>S1DqhUv9*1P60my8bq-Q@Qrz?}=nG!?0-xg4&`{7yT(vvnnzWDq z1AxPe4GjuC}J?WF6Ao#IhFJHby z%_w_VVJy%IGvne=yS0pT4|?G!cIhk0?g(QsTcVaB`T6s>{$ApHGyG4#{JC3xRe+Hm z3ABza>Iml4oPa(>gu&G8Y%My`-ccHun)>1{KfieBhxnGye`)z#AQMtybWfX4oJqIO zLAki#{I%(?$ecI|ma|awxF~JQHS9cO^@1%S8qSKy>pMSJR#m2@4 zhjsh?#+z_5oiR+Kp?Zt0!ZYj5nWj&%?4D14JXGSZ-MH7q|MXW%@7)TwFJ+ldH;TC!(LH|ElyB5RSIEMx51N_Mg*Trsu^*|TPi82dEIFtS9(5-Lj!%C0flvW)Gz zNRjzIANTeB6TUxxKF{ZwInP<%=REIoq!q&RF?qhLN~ONAvj}-3{{>E8lOH<2 zr)U1Xn*n8;ZYw7cHPZ@zr-=(s$F&p6e1D&P-|Rm%_f2I+tsuycy!QPCft_LC+Zk`L z4s@`CQlK25?nTh#36|m%+Hz*GznKC`>Tby8SYsBhbky~(41C?2GKPIta(yUEKI@Ps zDAGihm|x-n4c|ZsL9a&AL+3uW*!|(;D%Y-*N8vX=WNemp#j}5Xi>}i_IAa)&SRo-# zL^ES<6}a(W3k2PzS>cYg9eS(y8v{Hci3 z&OnAD7$6cJVQPAsDzU`_9gW^}@LIBUz1@*JR~_|^a=D^*E!xIlE=?Gf>I$bXi~+Xa zmH9MR(T7Cco|qTeb;V%G)mZC|hw^H>xoK$})Jt0`$43lUJZHJpHIH-=yIXD;tP9;U zF8sU;VW9x+@9&JE4~M*nwz-3Y`d?czlZK9!jK)rwMe79f@i3n5C3laxUf!>d!7I@v zmg4bg?7f+B^*RzRn8nFFpM2S2^4bA;+M4n4wF&hTFoyZxY3gElzJ?fSXSj}V$`^%; zZUxg3-(XasAuvh`Cc}FCTV(ay4pprUtv!Y()&(k!GYLzMIReW&AM!Hl6=TmGxN)I-N>;lpmcnXC3 zE)?WMQX7SAG<4t7AhtK%Sh^n;MLdoJyv(uqvaNH_E9fG^k(Sb>+PVfI&YwDD?x~Hh z4M}MBwk~kwitC^O#JBaJkD`X!tH#$YzuVo}h@WY(uyb;aabvo=k7Igv!UKG3s~q#Bob{he|`TS8Sz6{hA%6cgmS&B8kx zP^rJIaYNFL9Z*#s9+d_KK%|ltOSb*NYm*WyHugt})h@^$7$l6B%VDxXN+-gGG>Uj*cG86e*_qe2`dB_ zyV7#kZYCPZ!(S?98uhR2(KkD4b>Y%K2_Z^9h8a+_YKd|}Sqw3poE4*4t@+~2o=)%m zG^QnR{Z%{P*h3jI2Fr1*Gtx1?V(VKSrjlM6_EtR8^)epdU%a298|e`Rse%cVuD$*! zsi#9kFL`q~skr~-pT4#px>~9FRyvdL{ z(z8m<_$0*Tc}TT%otpYu4oOK-jv0n|jr{JmIsK^Ddty|r3>;tn`>8ptwa+;hLhRKZ+&S$tXN*~Lm3nq*LA+P#1Eg^GJ@1V z2{88iE|3L8*5>rxtn{%W!sp4oKGP+`ScPnR(Z&wTh@!if6B3&DgiDx^5lG$6Ahydg zhyg*b@z0*(*Tw@+FDpV?mKqyNUoko&agp1HFS<;HExEYq(yCGxX%iPpe3EdX4K?k6 zJum&45bEk{sI#zY{9AkZ)88Y(tfORM3nn$%N~ZSjnyl5$%xv!Lg{SW~IIL3E$9MGZ zNex3AsOIv)ntIyuy$3prdP&GKv%8n|JdJWvb##9CW}x}5GzcMplpAw?9Mmq+H^UnAOh?YIY4uLiHSw9eG}FZyT?y&Lj#_g=pz-Q#MqCQ1`3^nIG5^ zub%QYIQaS2E7-`B0}@{P=DRjjZK7P8b<$pP%IGfZnIFsythrdK^*36VIxweFz#z}H zTC4-MDk=IF^!ws1wm6|pdMK3k*ZUF`S@`JXBz&LV_( zA;?il%BPNzJCW{WN5;fi{f`V|nt{4i83lF+!^9-876NX6P2sHx2?ivfQQ+_CZa7Sa=zcU>?@mrxWI zb%xy2e$UytLzx*Q^e~1_(3?blo(*tL1+qgq$$fR>`DWVB@bO?z%~X&8M0Vt=cmy>S zJySe_k7>#k-%EdZu-!SnlyXi!5zXTntz8Cl(gS50+BMgZyiGf|t&?Ncg|U;5Ct#V| zS;Fzj$u;b=%$!s(WD4#C5|?n*-flnFDop;=0|ZqOE^J`C6u7h$t>iHrRS-pE1;`WN z*9sJO|Bp6Zq(&X-^9fyC<7ClfE_%BuJDV4Ppkwf&3iMAueqNMfjPs6r%}-wLH_6Ys zaid5zHQzhnGzmg=fVP*IyK`9Pl|K@Vu=e zn-y()Jo`Gu2PzM<9iA@`2H5NS+FC(AMrn9JZ~LcAGp0+K6u~2);J88~Wz}SMrxw#I zu>;7nEYe1sjekp!^c0Y97kw!EM^fHmh$T3|iPgQ8M=Ea^=;>vXPOL-FA&^VDb^p$6 z_E(uQYDQYC*ycqBCd;a*e!l??_+g(AF+bL7I4bTW^wE1JU{24$u7bCb06*fEsbgz> zx()JKnqKN*z;%u#Hpl9{-QCgd@?ue-Jj*dz28%O~4ss&)^ZMUchZ{+-C5n{1HgUh3 z!%`b;7=F|@Idh-fcLR(J2)x3w%mqwcSu)!I(6!pdeO&-8<^KC*cZ%3TDRzlgaWinf zet#~!Pfhi{>$?YrGwiIaBcz_(bH$ka058ogACg`~>0^?49=n3QQDii39Em<^UA~U<8oo!y?E3eG2x09eW7a7L=30 hj!OgokJqPYC%@k0H0)?@=L7}@(?^=!+9RZJ{?!^MGcj`lq{4aBqV#!sVZrakdW$=knDIw zAjQAg)C+FLKgi9`t0<9d6aVv~B0Y+Pgp=f)l7f!=^Qm7R`r6lf1tjW%1=Y?~L;|)2F{W$H#WQx0iD1aqAfuwoBK(X;^rx*f7&&*QFi%E&DB_ zW#s7z_omioBAl95uYJ(^p&RfsNzQYyp`qbWOiaIQvq$3krz`x7WH^yry_KWhJMlwx z6qy16|9GoFi}TFANqZmva*BoQ5dKj{{{Q~3|93b3-`e>9-4282`bkL46u(=G9@Tix z%g;YGGefgykKudUE2^rWqbiS)u#<-pc976a^;O&vvHJSCq2achTSP=eOoL^-xV_J0 zc2VMj_l{X12gZ=$bNxF>%(z;uMO#~2dyDLu2xMIfA$9NEX8Hd#S03|Gjtbv1qjb#bh16n>TNsIdewEYfr4YZf3hW z&-}@LK9UGhKU$JI*M(!r$;mB+W7T=i_&#bJb}V_W#C4LH>iU1j(W6J%BS!!H`7<`w z)!KT9KKQ=k;OzOu<>h(z^7`%LB(uDngdMqpk_m5fax}Rj7Z!=fxwP-e$%n}OYpn9G z@IMU+3CYP345>30j^(Dm@$tBA_q%INai?(0s)x2GyVcH}JEyKrb&q8G&E4JPx?!Y3 zv9V1fm6fu2`uvO`N9co)RUw&WuC|pW*%zUhJhm3m*JncR ze~6YqMyg2tS5c`y>NtP+`{bgXon5o^ytFn921gYi1P7|ryzm*&ua_h{;r>F5b9MfZC4!3X5EiEOUwD|OJ|MAy46Hyhbt)k-M zcii1C@-q0>kSGv%BngTnK8B`Vb9FeUJO-uHr%z{J$sfTt_P)0>ym|9@nDYGMVpwRX zQKi?aHmgs^#N?#EpC2vRJx;PH65?fM-|T!Vv%N82i>oG^>aXhW@As|o+eJk_G}qsC zMkH1p_a-AFgFtrZ8N-WpGLotj(Q2o9I3wgabM>=!GjT7^^s7vUa}msim|whNc{Vm+ zFNEK5b8`z;G8Z}ZkTa;pELWc&=SAEy$uF*%wyX)VQI9IzOq9`B{`+eWHMO$ugXw){ z^mjSQ28tHHiZnl$a=X>`>VjDP9!g5x9Fq#WUNJHSY)x+ZV10gUujar`hs0K^n zyhEwFnb;V_V`{4W&hlGz9*g|$F*CZ&8GD`SIx`_rF);!CToNhNt;CV~SdviU_MmAt z&)XQ{*j|}W+P2+C9vKCr+Gj8vVAHit72Po4P(eTVJA& zb9nNfO}7uVy1Mj3>im=w46?;o!gTne6_Ys$^mKHQ@$rVHrX2M2 z|1y6*XwrGpVEI@rO-4or+yB(5Q&@zkd)xIp!j1AR}x1}E@n2W{RIY!o*Um5*veO33;r97X_wXw0W>S}q)fbZSi9^>DSq-h;s zWPFflCLBAkCk9ZRHnc~x^d4-*u*=~Giv z&#S57@3Wt|a6pBfVEvRwmC`y=K76 z@DOdaa^-hO7qx2gMP??Zq71e8@RmGb2gW3V1 z)96XFX4t$pZrm_2>3ecaJJDn0K(Nxk#ZJ@hziw6Yv$OJ=E#uCCG#tu$VSjszn(OOT zc%n{AN;*ogg(>?d?qg=g8voMR$l*)d!$}{^&d%P|bW@ZCJ%K*>R$qAudh8%^x1ql! zXyBuV2sM9DH>-CR`)+TgosW5@KJw~9JR2JuHJd~(9@YK(_YM0Iy|rfX%m z{{2~EQBO^#fGcSH@&#LqK&EJD7_XQtpK~U)m6VjUr>EyZ1Wyt_U-ZN%@u)WwQZ~G) z#uX{bvKK{D6u5wyDNV~`_1`e|c)%VeaxyZiy?dwot9+NHx|O){($bE|$t5YL+V#Fi zg*hT1aO+p8%UGEzx0aF7PuvqDW8?U^xVI)1EDQ_`?Cb)x1V2B&kLI_}o~_f~f1lu| z%KiA&E1vxBpY82+!;W?)h5G!I0e%gZsL5jSbMCx9X?UcGZQ=?rfvc;lrR5*gzF?&k zlN>qE1#2B0h6D{cY+ite5~H`+MUkphk^;qeim6I2MS^y}ZA zE6{#yYt!b5x~!)ssGmC!tanU1LkB$z8@{Qj$z8gyzkj=q$<%A6VwsDJ%l7^5zDn;q zxRGPcLR@4~J5fF?E@kM9eoJ6wWBXZrJ2f-&xaVwol8>W{%i_XT9OL6-VP@7Zcb|(qtnyUQ6pwMX`C@45ptk3_g zvC**T*011wN42!I)74}B*mE*6=&+KLb6>rBm7l-w7f%rR;O6?Wv9WP0Cor&M30qlD z!Hu@ReHG`q>6uO#D5bZhY2DEK@9^*_(cw#L8@CmUZ?XbHAom~q^G7MQb<9~ZSc%1M zIZ-3M&8CngOgT;KKiqvNh6q`)&3PHVefuWO-rU}PN>Z}t*RO*IN5c*sJzC$~9HEY8 z^yl;E&tqfOS()TZClqM?&XE9?8&U@R{*j?=DeO@neh@oK>XvZqx96utuwakty}gfz z7#L_F82$9=x42Uw#6R%_jRzYWa1_WNx4W{kGDAB(B|T3+>)yQ`Q^o+$%Bhz!eoRbE ztgN^L{0_}=9X?D)288HXQeIVMiW3?Bhbow+l>v%iQ>c-ChW4XXxi$Vc`A%#btqe39 zjdUqVNy*cv=TNUAc0On|^(LU0X{Fy+WNQ|1^aktvWS&*G3FZ z4Jh)giptR=N4m529pmK%Tt$s^mXtYiWS0~g|9;8h! zfeh`AlMqs8bMt0?r4`VkD)*b$uY)2Yie$sbb#uCV)E?V94dBjNXYRMC+w`Pp9x>1qz*je z-%?kXhCZ?OuBN62cPYJXacL=hTlOFu+w1M=PGh&Yn3&P2DgJ0Rz?-P3DBRn!GAUqV z%U!;~JLIvG;+p`GN`M)jo-o|l*l6)rPMw=?mWv1ujuo@(2|Xa9ktkh)!u|ED`eecg z*8k+?^PIHk++}5VoSa0W8nDCB4Ca@Yi>MV#jBbg==M|-fn1gCuyEZ(u8K%;XvZbs0 zK(_PcMP2}Y#*l#eA>fl2RlCUl&z>j2b_UHIo1Xsl-i|fCNttG1aScVAasU2`h41HQ zD%bz^Z*MI(#f$6d>iTSNE_ck>-MDd8h`HzL@8C$HvBX^wt7dYKw~()rBxwsG!hh=2WiD)pMwvJqzd>tNu!d4=fW) z0nl;9tFox5C_g`6H`B$<&Hl!XOMKCNeSL+6h5EEygb!Bv>uYOS99v+})1M;`g9>kL zu5WE^MNBhsT)K5jAifE(N-`l_Idyeq?d=Un&CC1QQW#t}M2YKHnVUUuKYG#r z{rgeffo-uM$H&J#JUnngmcq$;0}c}SSV!ldId@Cp?)LV@;O6l6MMa%Ie!NUi?=N*} zv=&AG_x1G!1aM_n_MdI&1CiOqOh>wYBwCGzL-zTjN3^W0H|Oc&3=hAR2Z#FB)O0~Z z!`0at*nJD-WFI|!u3Lh%$GhBITDui2%5wDFCH&qujVy7LtNp=BVx3gGcSkl@;^Yjn z8M77US8j;LiCq7NP4cp$!k8z@{xQ-6i>($U_&i-IDxiih#O*d4iCyF(=E*0lGlE1jL4*`!=;0o2AEu2X5hQin{D z-9B47T3Dopgq**6^^oT^_K2g5A$GC?+}tlSGL8ue<(8JNiLX?sfaPc;NJd9S z4*vfAy{oJ1#}5Wp){xpkG{yLMR!>rv`?YQtm{w%L)ee2fy66#oW4OLbB0_#ZKOgo(Jlty#{OZR0@DqAc0(-u=r<3+c9%b(#196!1zT!6r{%z_e1GlXm1= zf}x?IlwFVL0uh2|_ha34B#Kl$EWIwWc=42y!%=RG<-{=b^2GmF84!Ny^D> z|E>!ubDjQB+K_s-HH%f-tjqSjL3Uq>Q+id^HpmKKZrszS4;ZJve*31Zyjzh>;X`e0 z>t9uDj)jk?YicoZ#6g9;i2fgl{u4cJJP;&|IT@|8wr5@PB7By{}pT z*f=XID>RfU%d4kLi7QeqO_5b06spYyFx28I(!g3E6hJTr%0h!_JVvw%Gcz+y&4_N> zTXuHC^Q$;&`yP-eO6?WVFJk>vm-zzMu9 zFi^$Cr4WQFzG-W9Gzpp^5ea`>phK3wL+Aw_5+JbQ$%#KZ%LaOp^5u z-DhBE2rjPTb&j9WLdO1vXb;L`hK`N1bAd*Bnad<&f<~3cJWpWlfy#JVg-77H;-^l{ zt$Nv()!8fXMJLL7dk%kzwhq=wZ7l`>#^MEtPHlCc>63})EJ$Q26bRY*KV&Ktd!w+& zpMvHAuJY{Jux(?j{F01}hF7d*rwR>ZBqV|a-W0U<_XCh+AEg@l{rgR3W`@p0qhQt4 z-@hh0I^eex0|PB>Z6X`f;8DP+O;&oBFNbYE;P^Z+FfdpfINF>Pv1>K@xZWSf)sd!n zI)AS_((29T9sP0Qn0%e3IB5V~JXQlW0`&+*rhMS*1e?B|#gLQ*= zmKPO!gi2;usxMTIYQ7N?q1yny4WJ`f5D%17)6&!B#_mA02JA{r9kM=(5*1o(!k|D~ zAbu#M?!&~}LxjiAo^}2FX`837%Kd$5y0_bQsk*uvf|Tg*{fcMK^x`UHWFRK7FMO+i z)ow#`P*N0|y0}xfF zry4^~JL#F@Mm&XZ^667EgM(MKlOQ_uo996(;UoxquQ^i&ymOiC0G`CYq^g33qaekw zRhpk_ki9fJSPT4NJ(Bz|BxL4i$^6=6PO2~ev175a-lZ-~;e%(Zv&*jkpDL%Wh{a@Q zV`GqSwfEj^HS2wpxt8IC)C2%#bQgI}<}EF3epOWhbzpwCl{{A@^mu?WIxU=bx z17JjDx0|O~TY;Dbbw8|?;UYKv)x~RB$!E@Cty}6J6aFLD-g)b1@jcEUfNc0MfG-3x zRxw-m`6lr|H{Q^&u*)3KO^W6XE40MAY9Djca9$30Kk6>%<B1$6_p(^Iy`J#<>OuVaDP|Sx6)EcmM}4|a2gFg;l+d=Zo z0hpBgo@-!4P*C-ovvT*>muJ>jR+y>CZAX7Pa79|X)26Jqw8#`7c{xb1eGT^urUpGF2h&8EI3FY6-O@xa zhMv$$HyG4jKt~2(ejx9K`jsoO7Xog@*Wb4kKJd$2&c3gFvNI=M{S~kQU@-(pG6kIB zTjMggE#DHPT6Hf#K@I-+=Z|HHQMf?ffzf3P<^P)nNC!gONkJV{_j@$SH?eutunK)C zHX&getA~-%>fj@B`#yCI4feTA*4s)!yAJtra?|f74=_vd=Oo)_a`Wb7wqX%ys>mm= z5%(86B>+WJTP1obLvR+(r$YfsJs6#8qWGsul?r6#sk5)3Sd2$2k;Cax@H|c3vG#t zyw-a7v&tUDtcyg!+GdPINCDM4*v`RW_G_#NAdm0ZwUlQ|ORfbQBg_gCzk_lM3PicN zKaQM8C0+$w6rc&o1OZ`TuxnB&=n&ICOWLeO|90n7$Y}6H#hfs7ogZm3%s>#8G2Fo=-0&vBr0>%dV`p;rxgTvgKtekG$g0SYK!g`Lzl;3}2Ip~o( z&*b7--pt;+>1?PP)u5SZaM)8`US8~*Hr7`~$P|DpU0n9xMBmp>m3@MJE-1)h@}u{a zMWhSuKlgI<53uh8B31>3g;ywoGczZlkpjs=qDB8d#d1MWku+NEtkhaKj6nbiUl1E- z$xXob+$$%gBqZDis&@n^=7Cb734DzgZ}zM#NJ;TiOeP~E3w6C+VIn6@xFO1NPhOrW z98&A#XdH<;#C#H9{4w+>;i}SqPNC2tP*bBaSG)E%DE{)~{tPHG9p+L(OS!`bS5E7gabLc@S zSM_+Yrta=lh&6&IPCx~uV1TPxNva&CK~c#pV|(E*R9r{DhVW-B@_bZsH*oN zJaEa-5Yi6-POSP!L)1|bVc{0?$tuc8yMMU*5Y?{;dx9lDd-klAoi{}xNVclr03G|@{yj~f(tK3VxOAbGMs8ywGY4=ZA|mJQTUpns zXZiU~;GC`2>gwtYA$78{vWIUuQSI3?IX=D$tkcq<9<1cJws6DQr=8~wxchC1pMqPJ z%Bih2HCI1gF=vnH?CgY&g|fc*gr8&|5S9BIm`rCw80~rr8X#;->0i1u*Q&w+25KAn z@CA)cLBc;~>^^)3;_U*jVrD-GNx$}z1ar=0Tpm^4{y1mJnQf+ z0wUrEG_|VrUo#Jyy&z#gg#y(KZFQg%Q6@@xqw{s-~ueNP^4 z1q^90EVVR8iRZ_(U%l}CI5fvVnUeAoPF?*gpwOpc4_Hq%C zRD~CVTY^pD>M9mk3!ewTXXJ!h+GBKj!I|eLEtEYxR^gxUGoA-mEtRq4p+ko{3P0?476i1X-DE2kA>jyD z8klm|u3Zqf!S+?TkrUyjKX~w9x2+glKVoc#E=*>DLbO_W;|VxB=&9}QYrei!zy=-K zyW2ZDMy98m%!FvkT5SqZu8~UOSRa{lmky~zZ}O7mFywFh`EzP|y25jDf>rz$#I&yR zVK9MA-E2joPYW3Wst=+dg@udDRofMTf%P^16i`)GaXt3E@1eMXvG}YE2974)xi!_q z4c>x7%5{qQ`q_>=mHW_hu3fv9mzM`~Y~Mj_x1O-#OnYls{{yv#>Tq> zYxmj*_I7p#?)^YXNePAd!SV}cYGa8nqw}qokHVv^sQ6W1f4w#3Eb1dHykp09%e0y2 z{TUrazG1U}d(#wu1}>e7N?=5Urn|elmDLEK6blQ>+S(f8C2^;21}XhUJBp|s1R)-F z^zilF4nHVmg8qK;uM$5aYG*TeL~e$Hc>QaTVDz6SXTS;!QtV6+CkWz?(QoS?U| zRdN<7tE0fQ5^d0usSViMN=eiZ6a(jjjX9U;bssT;#^H_-6=kXqTU$QP4G`9DIXjDX zcskg!QB&MZxxZ@=kYX2UP0HDj5eN}~g>7%%G|V--hYkaLDAFtqDQ|LW%GUV%5h*FD zLx&W2qMDwa9D<&xM61L}TU=I_>aX*krmAIPBKx~mE?YM~IvN`Jenc7M`@eSWeRx}f zO_JAAh#4n_M)F@dJ9SUq;oqMI^pdjTVz6y&#jdW`x|!m)e(GPm_$V$;^0z<{1y@wV z^Mr(K4h~5Pi7tkvAJ(EM_pPmYKz8WDf>j&;Bm}=%IbikV-(mV3-w>FRd;8*YS1m1<3vkIynd_C+A&NZkRV^Sh9{KKRu_0hlcX6);`F}f*-XX`W{qYXv~jQ zU&8DGAg{L+E-We{mZLR)q*9;+A;6QpQ!~8{o9MR0bNrUbyuoyon8_(gP9B6O0}czuteETogl}Iyr|jibj(!~z(-|a^hvnBSvup(A z52VW+$)-L{&`DH6Yf-G{L8p8K`Hu1Pf22&z(0POU4_FV+D8ecBb(kp|U1?bv05Sx{ z5~D5JPKtqJI}R%f&8a8JR-myUVRPzKdwu;Vlt63Aa2c-y7f>!QNS#501FY~?;)#N< z3Wfj}9VVjFV&i}k2n!r$Ib+B*U`r!`7>)GQ#T|UdkK^*e3xgC;u>EU-9zNvb<3mN( zXCi>L!SVY2n~Of!)_~?+_Y^Q9sxzoGqLaAA0S_L$dihe?h>~lFneLrFKhhh?7qg@U zNreW9u`6kJY1zYv4@*fIX11@bu5KbMLd+6W+;IaxJY$p%8}il2(R=c_2k4fTmt+33 zhZ{$}4x%j~V*o=~cJ)sb@B*R)nwpx(?9s@sn5`|0^F>ph*mFHk-^RwK<=SgNN!`q* z#>P7cGGGIt;~$S|pd}lhm{4?Ega6l8eq*FILkVNB<+!cA7 z>=1R}J_4C!0+D1}6YKlqi@a1+R9_n!t`u0)g((}BI9@w*rWSJDpTWV%xVVS6?r@}| zz{Go6kdf9{DY?Utf%ikiu>#n|sMRd2tam&;k)YbSb7$N4@1S>pzX{Ku`}zByW}^;J zM8h4+uLc|;_rIsn5YBiXY?_KdfcuLBYFl@ay(yHN>gu~uQAAA(t73C=GfX*EpWg&f z4410H6$wkV&KzM!d(_fXuBkP1P%EnRn=W(G;^beP_429von$`2J2)Ulf49 zsMMUQW@axCa#T>L0s7c?@SqIV;T-=Bgf3D0U;T)^K-v@v%hYrH z$U+1vnUR1SLZ>WYBPgdvdFE$rZTVeU;U!-IF-w%W^DRkkW_miz#ObH$pVvMR6*z+~ z@|HV|QpYq#t8(k7v6ApGK4aOnZ-0Q|IZaK3VV)!76#@MMQ( zMp;BiNXYAbnZGe65qIk!{1Fe*ii7EZnEAwMP zl`@~g{7Z+Cw90C)|q{MOfUgcN1uym8qz`#IN?%#9n=6Nfva&&i(lEQKzMS2ny z6!vA|Ey^)CoN#C}bUcSXg#qEz*VUo3j`A*U<@46lV?F8xn}~o4YBxkn^P?J-K8s0R3#*s z%hCozbO8EM;NM+qv=xv=^XczTdi+>;BNOxvc*+LGr5`7vGbn(l9)R#Pvv$-XiFAix zX9_Wc#-*o^UXtDOUmj207FrY%JJeKEO6g=LjReF+M6|ggk%>b3l?m!%L|E8ar$$?` z?Lit}UOv8u;o;N-!r0_wZ;8{<0L9Y?WA+q5S7QnLgbL>z;)x!Itk;5P1zNP0*5jh0 z<)FEi&Q3S@8E8m==r8gMnVrQC8C)R`tVP0vn83Q@vJy0l%w}+KaBY#m!qU=jhhn-S zQ3T>1DsfHuLE-_IgmdWe>HCo9(&5`nAhb*_0uV#`>^LQ7r+gn>0$os_ zhB8zmy>^K}ovxdiT}Fdg8ib_utSsB-2V;~)4JK(=K}{eBqdZu+Ws(eJAehY~Ehkq6 zeQ18PIpXPMbsi_!Vq;XJkcE5!a3f^bK94*H;YdkIf!$Y{larH`g(Uvg=8P|h?$Jn9 zs2FV7VcF_DWlp0S2stl|w_y{fwc7lAarndRtb>2eMUWxvfk{0(KOs3eUoi4oiUh>f z6_bmLi;;dNB8zeZU0ty|OSY4TaUODxe^jwSS0)F#n6GQ@78J~ZW&->kscM;jc;DUK zI~W`9HgKLgwg1C(xeutXkQ^{unVM;?Oka8MPzbAiJ%mOe^Y=2sq zvIWi#EM@8MFSyghbA*m@lCL89Fr0xGt=4}WOD?#GPI5j`OGD(ow#<%b9-nr6`R0uc z9+&m_sY$&$^EuoXy)6B!5LKH%j0JH$p);1YhsCN{!oiTOMali=f{yr*xNDg~q1j1# zFBj4eQdoNe6lIg-J`wp>!N-)gaiAuoKdfIR%J~w5m_Tmt-#@J%+D%4U5O3z>G>?zq znYO+?H+1YeT>h4TRD?%mW&3!Hv;1e_31KyKY0Ia!>T7E|E&nx6&_G5JTCpgQ9dc;K zWv)lKxnIzXqP4)(76;8onF1KHva~dtkHT6*&K0iDLT%v#=IbDwzb7X%b@&Vo43v#N z@2u9QMzwj2%m#!s3F0Or_=w_!t$Fqyr4PnhMMeaPsRWHAF*^fu^Aveb+F&K9*CvHF z?dJt6o_ zZhI9_KkmtsCo!fzvBYfIPo(@1K6v=>;mXPiSO7Y~;Nb1wAA_Ne%trWb45485`EG9_ zdgIJL`R5Pp_4ueLk;HhmTN?^vqoW9Va?>|3Yo6TUZc_tz!G87XRqPmmHDjEnby^*| z>owu-S;bz^tQRlLAU}Who$#FXnj&?!gPpF|B%gkryQRQ~x3y z=DhAHpi>MHj4yh|>*aCs3kaYv!Gq4u%7P4!Y+qNUw^T?Slpl?7t}+`2H!>L-8jO?y02y-7dGJ@EG69)F_p4l+$b?`DZ<=CNfbNfiUkmUXL}>*4 z^72kfOK&L15$+ys9L(|EoQ$fZAn`ecQiC|7R%W{xD{~gmR^|LRsU16Z;4h3tqWnQ5 zBTf+@Yaxr#1VjCOLxq{2=H^PF1-`9*kBCgUqX`nS|h2v|62pl*@z;=tW1iVQb|+ zl)==tag-igv30akED3~wkc;Kwg6nc7vpvO_3Q2K9g+O`$0XYi(T0>Ak*5Xr}Usp{{ za(;f4TG|p~q!-<&NfOC{QMAB3F)AXry&(c0+b#pqr0dX_5jgdPl7<}sTF=15R0Vbb zF>ntJO*1ivIEIOuhyN)G=6NvZl$Dgs?6FR8k8r`T&;B=C!*u-iVS?k|pQi<4&htcF z;L$cRV)NrvPR#@DKpRBz3yIQ*dQ1szuO@BdiQc|_J8yS39Bi38OXYxA*5^IR6x7b2 z$8%iql7;y4-yxr$q?QKp3)4c`y2oh=m}$X43v%ti!FNuFnL>6>O6qs_L%t}#o1Z=y z_9&u6X8U$M(>JqWP!|LJ7ZI6XnHvJe{WOu%9TXbM&&yj34s!l{BWQHC+W^qWCLV{q zo!!|z`keq}Vq(zhI5;>^sgo}TJ(aP@(}%`kbL*C@?^Zef8fIxeS^w&6YfA-qs;EF5 z6j&Rs6M~SQo}R@<7|pP^v3X;VjgT?ILo9P#AgWlau;g+DSj>#sZoz9oZ0CZyx>8^| zMxfC7KpSu$!S{HgG64E8Pf*`$Uv-dyVS#s(_Y*AxgEIp2V(}H=nMl(X#9YO5lbR{F zsJKCYH=P###{Cv{27KpPj`T2B=ypy{PWc)Np@1pM%Aas<1qRcAH|Su!Jw3>>qTpR2 zy;nUn2Pqu~+rfi?&;T)&8~++HG64}bxv(3EKqFn77(;@wIfh=%O+WrIcpub2o5DDG z&aZK&&hs*)NNp^5ZVn7^#;UXDam5*A%Q}x;L?-hB(+V+WI+vW8nF)Z6!_17+RT04e zKpC`yEBqakIaQT#x9zJoJJ1sVu%xAbdVY5i1$o-YIENddGeL|Pe*O9t#>L>AJH#<4 z0??(Q`*lq*?-)$uYBg^9SoH^QO^^b#zL%$G>g|G$jg9OrI82Y*=Is2W!4g-9!XxJ}Ag`~lj}7SP z_-^q!CZ7=DfuBAY!Mf<_4eJ32A2Sw6d%GH;rSLOOh9aO@g253Nq8K^sSY+St1DJ{H z6FYgb)l7){$Pu83U&Xg;ak>2bwqKqc8|oI8W5@NPQ24IYa^yA`0>}VD!U;g%0qEUU zK75IU9b(wC0w4edMO7z^uuV0loKAc9x zY^5u!qGIz;LloFUp2-PJCJ_OMeShVz-ro4e;gt3wyFP2-*q2(zdR}(M)tG($@&(Cr zU_s<^+_F{P3dd5hQm3HgWB5qY>lFVz`DY+GV8ej>=@)siS&MADQ(CPN#0ALq*_f-p zd-pDMMG&>YT@w&?OZQqxtEs6$H^^i72vvqL1U}J?&FA~?rc)9x36vO!nEIfk^Wwzi z$t4Enf`8pX`|c9G)ri*@d4+|AVQgq7N}q-?G=3s0s=eu1R?T?0#N0>?y9Kh*OWQ- z$smjg zz87G+2ntbFx)zvEV2MyaS73^@m`eg=QIVhFWiYTW``>UMCnqP0 z5?J0;j$XE|X@zIuyLVbVQSCo|bp6zKaXFQs!Os_VHLf+Ex$aW1x(0+K(lOXGyeRoyWB>fn|*mGSQEt^ZO~!`ufT-3gH{9t#NM= zGB-PKfP?`=2_%}Y2{A+c4re?4dvp|~MLPaCHu7X((Em(kOW_lYjYtW@$%V0#)XYQ~ z0Mvm3_`dI@y>KjqNDO2Y*tGMN7@25k{euN4Atj|5FLnZ9nzDbh;Wc9LRx~ueK^Q_n z08_TnYLH>nvD`82)8mi|jqiU0kQPQ{caK7v>e_?C3)zZI#?u+4K)+WSxt-BbJ#B3f zi%(ROQO6j*w6r`@Os@Wtd!?!Xs_B_PY6NyqBmKWZfw=nCLYpQ!$IJ{7mv(`0v~K!g zQV1~vRc>>IuI}#O`XOMpASH9?3OFmwVg|}JLibK@yQ+Xn{6y`@d&hQLF*g^NUy#(& z(#~>4V#pV_NYBLMEGKQ~`+B(}M~2ZA-Oxx8iG`kx+Xi{r0I9%<2_i}3iiCU!Bg;rY zk1twm<`hd91Tyoy;`i@~8%#O%HZBZGgWT525O5~Z+ufY4r2z=SR1;!pc#6Fc7tV+) zkqyit^5v?;%kb8qZ~~1V)Js45DMG9WF7=QdS_{3Hp%Y&GD^Z;%^$Iqp2%g_$OpA+v zbp$uYZ6Z~j5$8qHPRX+*Y9 zBOM7n8*6K8j1maP0;>{Lv)NfrS^^py;+PgXr3k5F1ymwCO@zr;xeVeEQLu;EgM1#? zJiHEIKv+FbHks52Tw6U`*{QEXs_AusMY7w{CjXDRC&w<&xo_taT+K@WO zk*^2XBLcJeFPuNWjs!V!jjf8k$Td38r*zvAK@QFpeb@NPl@hF)RxQBQ1dTv)#Gi_x z=3ltb1f%Bf=6;6p+Dc-^;%oVg3zE&{`fCz9^rZc-x3*l zdts~C-d-p~+4&yw#cI(p`(7eY80F+#!>1Aw;aBSk!r%OMbn!Ry*tQ86& z)Pq98!Tc+YOwti3<5c##tK!d3(J=HcN< zIo8$Dp^K;QwfgV&QVK4I%}A74oPL_Oc3?lp`%c2aULP5RI)h1|zx3sYt17We^9(p<0@jxn^Im z!|W0w&ZxPV(!h{LVC^|1B!{E*0C4v7&ir(t^f2X zg$b86HsXyIdHHf7wj3%nOd2TGi0Ylj_>I-RLFW0_&phTwAony`i-zB(PSDUicMdA@ zm!KTj=KeL9pM^mtg?^4!ps$&N5xTXNm1=SYq21LY81_MSwYBF4AO>bf;H3h8pfD(L z!DKHqxRBb4fHM#(LfCTU%cWWw(jNb~6Eu9M3p>&E^54CCZ{Lr|G@NYkE{vcO84B3% z$B!QoMTSC*Y3?xPTC02-c4WaiozW!m&o+>BvffMBcqiG8w~7kK5W5f;ACR zw4=I>2Axj|`C())3c{`7(xPf(Am#xl;lHs!qXw^!y5WQ0HG%n6_cAy5awUebZzvos$LqoAFfHe%=o)+ozgE|4`aUef1yY&gh z5hZe{d@o6&qtGczlHbtvfiB@FZg=)oke_lH@gB0JhinVmm=^2aRytVOC;XE9_I~|Cp0-_)R1lM{PC0|Z9_kkrua$1`Wpa_G zkkcy?c9#Thhs^hj#*^%Ay;V25?ol%~clzj25s7|}Zyt&Lnwjmg2*e8duAaBCnSdDQ zu`)|6E=V_FkW=Sp#6s)8-5%O*gK?&( zjl;0^-yVa%fT{E8gJL9BQe=ln&R$}OZ|cKfZ=LyR3>5=XAw4xioV?!N+63JrPJ>Z! zc*)b`ciRfbP7T!f@7uR;rfR!#D16Vu6lN-Ah&9N?V~Eor8>|)-zh$^F21B`cqQ<6E zuz^vBkVb%t(OQ>v^cv!Oh39^GDOeYQcwp<%?A?1o4jNE;Vd3Qr9W91yqAU>yrK}LF zIe-4>i9Agwt@y&iH4{Rj7>4zo`6>)a^>2)bWAp{$8XD#ZhNWNy zYa1BEB_+v?FG+!LV!DM`;-GP+0&c0L5zihD@k?!)hbc%bK{2W@w0Ogc3tajU+Zy^P zg_k<;aI_lW#;blcI`rMhNJQx}+r}|B3Bho_!_XcX$Wm8L9bN{<60f+*P?CHg!~qq& z_vG5utI3mdDE(V-SjNW3;V%=Xrib=v#5Gyn*;u)acW|Hy`M?u+@`M4T7wCN7zFIS( zjm^#Rv&nBml@G@>Cdd*FmR_wSD%J-WN)H_KOJFH#+$tfDeqmvs%^Q6#DQk3{JrG0 zs75c(wj!$Ly=xqJPu-2bC0-mT9dRO=%XJbDQ-0Vij*Y$6NC(hKjaXS*(@<9j>ssex zLHrHzM=Y+UF>(?UM}p#Ajth$$Emrw(--&^3q)9R4+&E0WYu9cvKg3`1^q)h}VrTzb z0MI2e4^Rwd(D3aWVxs_b@81*iTj3UQ@$pDxZKhxj5xK8(tfwT%lPN?LAEsamEB?t7 zBGSaD#kqXluvQVMt{4yo>fHs0QUr*rVuBY6+S~tx)Jo3X|QKw z6Ji+1FMtQ8z+PR~nAoK5"|5 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/random_labelled_graph.png b/GraphRecipes/assets/random_labelled_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..a4681c71a94db1d343347c49777af225e5429bec GIT binary patch literal 46220 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 D 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/assets/selfedges.png b/GraphRecipes/assets/selfedges.png new file mode 100644 index 0000000000000000000000000000000000000000..44b663f220b0c0119916fb63f3d21bfe75434d9d GIT binary patch literal 14323 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 literal 0 HcmV?d00001 diff --git a/GraphRecipes/src/GraphRecipes.jl b/GraphRecipes/src/GraphRecipes.jl new file mode 100644 index 000000000..9147c81cc --- /dev/null +++ b/GraphRecipes/src/GraphRecipes.jl @@ -0,0 +1,24 @@ +module GraphRecipes + +using Graphs +using PlotUtils # ColorGradient +using RecipesBase + +using InteractiveUtils # subtypes +using LinearAlgebra +using SparseArrays +using Statistics +using NaNMath +using GeometryTypes +using Interpolations + +import NetworkLayout +import Graphs: rng_from_rng_or_seed + +include("utils.jl") +include("graph_layouts.jl") +include("graphs.jl") +include("misc.jl") +include("trees.jl") + +end diff --git a/GraphRecipes/src/graph_layouts.jl b/GraphRecipes/src/graph_layouts.jl new file mode 100644 index 000000000..6fa1b63d7 --- /dev/null +++ b/GraphRecipes/src/graph_layouts.jl @@ -0,0 +1,496 @@ + +# ----------------------------------------------------- +infer_size_from(args...) = maximum(maximum.(args)) + +# see: http://www.research.att.com/export/sites/att_labs/groups/infovis/res/legacy_papers/DBLP-journals-camwa-Koren05.pdf +# also: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.3.2055&rep=rep1&type=pdf + +function spectral_graph( + adjmat::AbstractMatrix; + node_weights::AbstractVector = ones(size(adjmat, 1)), + kw..., +) + positions = + NetworkLayout.spectral(adjmat; nodeweights = convert(Vector{Float64}, node_weights)) + + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], [p[3] for p ∈ positions]) +end + +function spectral_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + spectral_graph(get_adjacency_matrix(source, destiny, weights); kw...) +end + +function spring_graph( + adjmat::AbstractMatrix; + dim = 2, + rng = nothing, + x = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + y = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + z = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + maxiter = 100, + initialtemp = 2.0, + C = 2.0, + kw..., +) + @assert dim == 2 || dim == 3 + T = Float64 + adjmat = make_symmetric(adjmat) + startpostions = if dim == 2 + [Point(T(x[i]), T(y[i])) for i ∈ 1:length(x)] + elseif dim == 3 + [Point(T(x[i]), T(y[i]), T(z[i])) for i ∈ 1:length(x)] + end + + positions = NetworkLayout.spring( + adjmat; + dim, + Ptype = T, + iterations = maxiter, + initialtemp = initialtemp, + C = C, + initialpos = startpostions, + ) + if dim == 2 + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], nothing) + else + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], [p[3] for p ∈ positions]) + end +end + +function spring_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + spring_graph(get_adjacency_matrix(source, destiny, weights); kw...) +end + +function sfdp_graph( + adjmat::AbstractMatrix; + dim = 2, + rng = nothing, + x = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + y = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + z = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + maxiter = 100, + tol = 1e-10, + C = 1.0, + K = 1.0, + kw..., +) + @assert dim == 2 || dim == 3 + adjmat = make_symmetric(adjmat) + T = Float64 + startpostions = if dim == 2 + [Point(T(x[i]), T(y[i])) for i ∈ 1:length(x)] + elseif dim == 3 + [Point(T(x[i]), T(y[i]), T(z[i])) for i ∈ 1:length(x)] + end + + positions = NetworkLayout.sfdp( + adjmat; + dim, + Ptype = T, + iterations = maxiter, + tol = tol, + C = C, + K = K, + initialpos = startpostions, + ) + if dim == 2 + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], nothing) + else + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], [p[3] for p ∈ positions]) + end +end + +function sfdp_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + sfpd_graph(get_adjacency_matrix(source, destiny, weights); kw...) +end + +circular_graph(args...; kwargs...) = shell_graph(args...; kwargs...) + +function shell_graph( + adjmat::AbstractMatrix; + dim = 2, + rng = nothing, + x = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + y = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + z = rand(rng_from_rng_or_seed(rng, nothing), size(adjmat)[1]), + nlist = Vector{Int}[], + kw..., +) + @assert dim == 2 + positions = NetworkLayout.shell(adjmat; nlist = nlist) + + ([p[1] for p ∈ positions], [p[2] for p ∈ positions], nothing) +end + +function shell_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + shell_graph(get_adjacency_matrix(source, destiny, weights); kw...) +end + +# ----------------------------------------------------- + +# Axis-by-Axis Stress Minimization -- Yehuda Koren and David Harel +# See: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.437.3177&rep=rep1&type=pdf + +# # NOTES: +# # - dᵢⱼ = the "graph-theoretical distance between nodes i and j" +# # = Aᵢⱼ +# # - kᵢⱼ = dᵢⱼ⁻² +# # - b̃ᵢ = ∑ᵢ≠ⱼ ((x̃ⱼ ≤ x̃ᵢ ? 1 : -1) / dᵢⱼ) +# # - need to solve for x each iteration: Lx = b̃ + +# # Solve for one axis at a time while holding the others constant. +# # dims is 2 (2D) or 3 (3D). free_dims is a vector of the dimensions to update (for example if you fix y and solve for x) +# function by_axis_stress_graph(adjmat::AbstractMatrix, node_weights::AbstractVector = ones(size(adjmat,1)); +# dims = 2, free_dims = 1:dims, +# rng = nothing, +# x = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights)), +# y = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights)), +# z = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights))) +# adjmat = make_symmetric(adjmat) +# L, D = compute_laplacian(adjmat, node_weights) + +# n = length(node_weights) +# maxiter = 100 # TODO: something else + +# @assert dims == 2 + +# @show adjmat L + +# for _ in 1:maxiter +# x̃ = x +# b̃ = Float64[sum(Float64[(i==j || adjmat[i,j] == 0) ? 0.0 : ((x̃[j] <= x̃[i] ? 1.0 : -1.0) / adjmat[i,j]) for j=1:n]) for i=1:n] +# @show x̃ b̃ +# x = L \ b̃ + +# xdiff = x - x̃ +# @show norm(xdiff) +# if norm(xdiff) < 1e-4 +# info("converged. norm(xdiff) = $(norm(xdiff))") +# break +# end +# end +# @show x y +# x, y, z +# end + +norm_ij(X, i, j) = sqrt(sum(Float64[(v[i] - v[j])^2 for v ∈ X])) +stress(X, dist, w, i, j) = w[i, j] * (norm_ij(X, i, j) - dist[i, j])^2 +function stress(X, dist, w) + tot = 0.0 + for i ∈ 1:size(X, 1), j ∈ 1:(i - 1) + tot += stress(X, dist, w, i, j) + end + tot +end + +# follows section 2.3 from http://link.springer.com/chapter/10.1007%2F978-3-540-31843-9_25#page-1 +# Localized optimization, updates: x +function by_axis_local_stress_graph( + adjmat::AbstractMatrix; + node_weights::AbstractVector = ones(size(adjmat, 1)), + dim = 2, + free_dims = 1:dim, + rng = nothing, + x = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights)), + y = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights)), + z = rand(rng_from_rng_or_seed(rng, nothing), length(node_weights)), + maxiter = 1000, + kw..., +) + adjmat = make_symmetric(adjmat) + n = length(node_weights) + + # graph-theoretical distance between node i and j (i.e. shortest path distance) + # TODO: calculate a real distance + dist = estimate_distance(adjmat) + # @show dist + + # also known as kᵢⱼ in "axis-by-axis stress minimization". the -2 could also be 0 or -1? + w = dist .^ -2 + + # in each iteration, we update one dimension/node at a time, reducing the total stress with each update + X = dim == 2 ? (x, y) : (x, y, z) + laststress = stress(X, dist, w) + for k ∈ 1:maxiter + for p ∈ free_dims + for i ∈ 1:n + numer, denom = 0.0, 0.0 + for j ∈ 1:n + i == j && continue + numer += + w[i, j] * + (X[p][j] + dist[i, j] * (X[p][i] - X[p][j]) / norm_ij(X, i, j)) + denom += w[i, j] + end + if denom != 0 + X[p][i] = numer / denom + end + end + end + + # check for convergence of the total stress + thisstress = stress(X, dist, w) + if abs(thisstress - laststress) / abs(laststress) < 1e-6 + # info("converged. numiter=$k last=$laststress this=$thisstress") + break + end + laststress = thisstress + end + + dim == 2 ? (X..., nothing) : X +end + +function by_axis_local_stress_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + by_axis_local_stress_graph(get_adjacency_matrix(source, destiny, weights); kw...) +end + +# ----------------------------------------------------- + +function buchheim_graph( + adjlist::AbstractVector; + node_weights::AbstractVector = ones(length(adjlist)), + root::Symbol = :top, # flow of tree: left, right, top, bottom + layers_scalar = 1.0, + layers = nothing, + dim = 2, + kw..., +) + # @show adjlist typeof(adjlist) + positions = + NetworkLayout.buchheim(adjlist; nodesize = convert(Vector{Float64}, node_weights)) + Float64[p[1] for p ∈ positions], Float64[p[2] for p ∈ positions], nothing +end + +# ----------------------------------------------------- + +tree_graph(adjmat::AbstractMatrix; kw...) = + tree_graph(get_source_destiny_weight(adjmat)...; kw...) + +function tree_graph( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + node_weights::AbstractVector = ones(infer_size_from(source, destiny)), + root::Symbol = :top, # flow of tree: left, right, top, bottom + layers_scalar = 1.0, + layers = nothing, + positions = nothing, + dim = 2, + rng = nothing, + add_noise = true, + kw..., +) + extrakw = Dict{Symbol,Any}(kw) + # @show root layers positions dim add_noise extrakw + n = length(node_weights) + + # TODO: compute layers, which get bigger as you go away from the root + if layers == nothing + # layers = rand(rng_from_rng_or_seed(rng, nothing), 1:4, n) + layers = compute_tree_layers2(source, destiny, n) + end + + # reverse direction? + if root in (:top, :right) + layers = -layers + end + + # add noise + if add_noise + layers = layers + 0.6rand(rng_from_rng_or_seed(rng, nothing), size(layers)...) + end + + # TODO: normalize layers somehow so it's in line with distances + layers .*= layers_scalar + if dim == 2 + if root in (:top, :bottom) + extrakw[:y] = layers + extrakw[:free_dims] = if isnothing(positions) + [1] + else + extrakw[:x] = positions + Int[] + end + elseif root in (:left, :right) + extrakw[:x] = layers + # extrakw[:free_dims] = [2] + extrakw[:free_dims] = if isnothing(positions) + [2] + else + extrakw[:y] = positions + Int[] + end + else + error("unknown root: $root") + end + else + error("3d not supported") + end + + # now that we've fixed one dimension, let the stress algo solve for the other(s) + by_axis_local_stress_graph( + get_adjacency_matrix(source, destiny, weights); + node_weights = node_weights, + rng = rng, + dim = dim, + extrakw..., + ) +end + +function adjlist_and_degrees(source, destiny, n) + # build a list of children (adjacency list) + alist = Vector{Int}[Int[] for i ∈ 1:n] + indeg, outdeg = zeros(Int, n), zeros(Int, n) + for (si, di) ∈ zip(source, destiny) + push!(alist[si], di) + indeg[di] += 1 + outdeg[si] += 1 + end + alist, indeg, outdeg +end + +function compute_tree_layers(source, destiny, n) + alist, indeg, outdeg = adjlist_and_degrees(source, destiny, n) + + # choose root to be the node with lots going out, but few coming in + netdeg = outdeg - 50indeg + idxs = sortperm(netdeg, rev = true) + # rootidx = findmax(netdeg) + # @show outdeg indeg netdeg idxs alist + placed = Int[] + + layers = zeros(n) + for i ∈ 1:n + idx = shift!(idxs) + + # first, place this after its parents + for j ∈ placed + if idx in alist[j] + layers[idx] = max(layers[idx], layers[j] + 1) + end + end + + # next, shift its children lower + for j ∈ idxs + if j in alist[idx] + layers[j] = max(layers[j], layers[idx] + 1) + end + end + + push!(placed, idx) + end + layers +end + +# an alternative algo to pick tree layers... generate a list of roots, +# and for each root, make a pass through the tree (without recurrency) +# and push the children below their parents +function compute_tree_layers2(source, destiny, n) + alist, indeg, outdeg = adjlist_and_degrees(source, destiny, n) + roots = filter(i -> indeg[i] == 0, 1:n) + if isempty(roots) + roots = [1] + end + + layers = zeros(Int, n) + for i ∈ roots + shift_children!(layers, alist, Int[], i) + end + + # now that we've shifted children out, move parents closer to their closest children + while true + shifted = false + for parent ∈ 1:n + if !(isempty(alist[parent])) + minidx = minimum(layers[child] for child ∈ alist[parent]) + if layers[parent] < minidx - 1 + shifted = true + layers[parent] = minidx - 1 + end + end + end + shifted || break + end + + layers +end + +function shift_children!(layers, alist, placed, parent) + for idx ∈ alist[parent] + if !(idx in placed) && layers[idx] <= layers[parent] + layers[idx] = layers[parent] + 1 + end + end + for idx ∈ alist[parent] + if idx != parent && !(idx in placed) + push!(placed, idx) + shift_children!(layers, alist, placed, idx) + end + end +end + +# ----------------------------------------------------- + +# TODO: maybe also implement Catmull-Rom Splines? http://www.mvps.org/directx/articles/catmull/ + +# ----------------------------------------------------- + +function arc_diagram( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + N = infer_size_from(source, destiny) + X = collect(1:N) + O = zero(X) + X, O, O +end + +# ----------------------------------------------------- + +function chord_diagram( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector; + kw..., +) + N = infer_size_from(source, destiny) + nodes = collect(1:N) + δ = 2pi / N + + x = Array{Float64}(undef, N) + y = Array{Float64}(undef, N) + for i ∈ 1:N + v = (i - 1) * δ + x[i] = sin(v) + y[i] = cos(v) + end + + x, y, zero(x) +end diff --git a/GraphRecipes/src/graphs.jl b/GraphRecipes/src/graphs.jl new file mode 100644 index 000000000..c44158812 --- /dev/null +++ b/GraphRecipes/src/graphs.jl @@ -0,0 +1,1160 @@ +const _graph_funcs = Dict{Symbol,Any}( + :spectral => spectral_graph, + :sfdp => sfdp_graph, + :circular => circular_graph, + :shell => shell_graph, + :spring => spring_graph, + :stress => by_axis_local_stress_graph, + :tree => tree_graph, + :buchheim => buchheim_graph, + :arcdiagram => arc_diagram, + :chorddiagram => chord_diagram, +) + +const _graph_inputs = Dict{Symbol,Any}( + :spectral => :adjmat, + :sfdp => :adjmat, + :circular => :adjmat, + :shell => :adjmat, + :stress => :adjmat, + :spring => :adjmat, + :tree => :sourcedestiny, + :buchheim => :adjlist, + :arcdiagram => :sourcedestiny, + :chorddiagram => :sourcedestiny, +) + +function prepare_graph_inputs(method::Symbol, inputs...; display_n = nothing) + input_type = get(_graph_inputs, method, :sourcedestiny) + if input_type === :adjmat + mat = if display_n === nothing + get_adjacency_matrix(inputs...) + else + get_adjacency_matrix(inputs..., display_n) + end + (mat,) + elseif input_type === :sourcedestiny + get_source_destiny_weight(inputs...) + elseif input_type === :adjlist + (get_adjacency_list(inputs...),) + end +end + +# ----------------------------------------------------- + +function get_source_destiny_weight(mat::AbstractArray{T,2}) where {T} + nrow, ncol = size(mat) # rows are sources and columns are destinies + @assert nrow == ncol + + nosymmetric = !issymmetric(mat) # plots only triu for symmetric matrices + nosparse = !issparse(mat) # doesn't plot zeros from a sparse matrix + + L = length(mat) + + source = Array{Int}(undef, L) + destiny = Array{Int}(undef, L) + weights = Array{T}(undef, L) + + idx = 0 + for i ∈ 1:nrow, j ∈ 1:ncol + value = mat[i, j] + if !isnan(value) && (nosparse || value != zero(T)) # TODO: deal with Nullable + if i < j + idx += 1 + source[idx] = i + destiny[idx] = j + weights[idx] = value + elseif nosymmetric && (i > j) + idx += 1 + source[idx] = i + destiny[idx] = j + weights[idx] = value + end + end + end + resize!(source, idx), resize!(destiny, idx), resize!(weights, idx) +end + +function get_source_destiny_weight(source::AbstractVector, destiny::AbstractVector) + if length(source) != length(destiny) + throw(ArgumentError("Source and destiny must have the same length.")) + end + source, destiny, ones(length(source)) +end + +function get_source_destiny_weight( + source::AbstractVector, + destiny::AbstractVector, + weights::AbstractVector, +) + if !(length(source) == length(destiny) == length(weights)) + throw(ArgumentError("Source, destiny and weights must have the same length.")) + end + source, destiny, weights +end + +function get_source_destiny_weight( + adjlist::AbstractVector{V}, +) where {V<:AbstractVector{T}} where {T<:Any} + source = Int[] + destiny = Int[] + for (i, l) ∈ enumerate(adjlist) + for j ∈ l + push!(source, i) + push!(destiny, j) + end + end + get_source_destiny_weight(source, destiny) +end + +# ----------------------------------------------------- + +get_adjacency_matrix(mat::AbstractMatrix) = mat + +get_adjacency_matrix( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector, + n = infer_size_from(source, destiny), +) = Matrix(sparse(source, destiny, weights, n, n)) + +get_adjacency_matrix( + adjlist::AbstractVector{V}, +) where {V<:AbstractVector{T}} where {T<:Any} = + get_adjacency_matrix(get_source_destiny_weight(adjlist)...) + +# ----------------------------------------------------- + +get_adjacency_list(mat::AbstractMatrix) = get_adjacency_list(get_source_destiny_weight(mat)) + +function get_adjacency_list( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + weights::AbstractVector, +) + n = infer_size_from(source, destiny) + adjlist = [Int[] for i ∈ 1:n] + for (s, d) ∈ zip(source, destiny) + push!(adjlist[s], d) + end + adjlist +end + +get_adjacency_list(adjlist::AbstractVector{<:AbstractVector{Int}}) = adjlist + +# ----------------------------------------------------- + +function make_symmetric(A::AbstractMatrix) + A = copy(A) + for i ∈ 1:size(A, 1), j ∈ (i + 1):size(A, 2) + A[i, j] = A[j, i] = A[i, j] + A[j, i] + end + A +end + +function compute_laplacian(adjmat::AbstractMatrix, node_weights::AbstractVector) + n, m = size(adjmat) + @assert n == m == length(node_weights) + + # scale the edge values by the product of node_weights, so that "heavier" nodes also form + # stronger connections + adjmat = adjmat .* sqrt(node_weights * node_weights') + + # D is a diagonal matrix with the degrees (total weights for that node) on the diagonal + deg = vec(sum(adjmat; dims = 1)) - diag(adjmat) + D = diagm(0 => deg) + + # Laplacian (L = D - adjmat) + L = eltype(adjmat)[i == j ? deg[i] : -adjmat[i, j] for i ∈ 1:n, j ∈ 1:n] + + L, D +end + +import Graphs + +# TODO: so much wasteful conversion... do better +function estimate_distance(adjmat::AbstractMatrix) + source, destiny, weights = get_source_destiny_weight(sparse(adjmat)) + + g = Graphs.Graph(adjmat) + dists = convert( + Matrix{Float64}, + hcat(map(i -> Graphs.dijkstra_shortest_paths(g, i).dists, Graphs.vertices(g))...), + ) + tot = 0.0 + cnt = 0 + for (i, d) ∈ enumerate(dists) + if d < 1e10 + tot += d + cnt += 1 + end + end + avg = cnt > 0 ? tot / cnt : 1.0 + for (i, d) ∈ enumerate(dists) + if d > 1e10 + dists[i] = 3avg + end + end + dists +end + +function get_source_destiny_weight(g::Graphs.AbstractGraph) + source = Vector{Int}() + destiny = Vector{Int}() + sizehint!(source, Graphs.nv(g)) + sizehint!(destiny, Graphs.nv(g)) + for e ∈ Graphs.edges(g) + push!(source, Graphs.src(e)) + push!(destiny, Graphs.dst(e)) + end + get_source_destiny_weight(source, destiny) +end + +get_adjacency_matrix(g::Graphs.AbstractGraph) = adjacency_matrix(g) + +get_adjacency_matrix( + source::AbstractVector{Int}, + destiny::AbstractVector{Int}, + n = infer_size_from(source, destiny), +) = get_adjacency_matrix(source, destiny, ones(length(source)), n) + +get_adjacency_list(g::Graphs.AbstractGraph) = g.fadjlist + +function format_nodeproperty(prop, n_edges, edge_boxes = 0; fill_value = nothing) + prop isa Array ? + permutedims(vcat(fill(fill_value, edge_boxes + n_edges), vec(prop), fill_value)) : prop +end +# ----------------------------------------------------- + +# a graphplot takes in either an (N x N) adjacency matrix +# note: you may want to pass node weights to markersize or marker_z +# A graph has N nodes where adj_mat[i,j] is the strength of edge i --> j. (adj_mat[i,j]==0 implies no edge) + +# NOTE: this is for undirected graphs... adjmat should be symmetric and non-negative + +const graph_aliases = Dict( + :curvature_scalar => [:curvaturescalar, :curvature], + :node_weights => [:nodeweights], + :nodeshape => [:node_shape, :markershape], + :nodesize => [:node_size, :markersize], + :nodecolor => [:marker_color, :markercolor], + :node_z => [:marker_z], + :nodestrokealpha => [:markerstrokealpha], + :nodealpha => [:markeralpha], + :nodestrokewidth => [:markerstrokewidth], + :nodestrokealpha => [:markerstrokealpha], + :nodestrokecolor => [:markerstrokecolor], + :nodestrokestyle => [:markerstrokestyle], + :shorten => [:shorten_edge], + :axis_buffer => [:axisbuffer], + :edgewidth => [:edge_width, :ew], + :edgelabel => [:edge_label, :el], + :edgelabel_offset => [:edgelabeloffset, :elo], + :self_edge_size => [:selfedgesize, :ses], + :edge_label_box => [:edgelabelbox, :edgelabel_box, :elb], +) + +""" + graphplot(g; kwargs...) + +Visualize the graph `g`, where `g` represents a graph via a matrix or a +`Graphs.graph`. +## Keyword arguments +``` +dim = 2 +free_dims = nothing +T = Float64 +curves = true +curvature_scalar = 0.05 +root = :top +node_weights = nothing +names = [] +fontsize = 7 +nodeshape = :hexagon +nodesize = 0.1 +node_z = nothing +nodecolor = 1 +nodestrokealpha = 1 +nodealpha = 1 +nodestrokewidth = 1 +nodestrokecolor = :black +nodestrokestyle = :solid +nodestroke_z = nothing +rng = nothing +x = nothing +y = nothing +z = nothing +method = :stress +func = get(_graph_funcs, method, by_axis_local_stress_graph) +shorten = 0.0 +axis_buffer = 0.2 +layout_kw = Dict{Symbol,Any}() +edgewidth = (s,d,w)->1 +edgelabel = nothing +edgelabel_offset = 0.0 +self_edge_size = 0.1 +edge_label_box = true +edge_z = nothing +edgecolor = :black +edgestyle = :solid +trim = false +``` + +See the [documentation]( http://docs.juliaplots.org/latest/graphrecipes/introduction/ ) for +more details. +""" +@userplot GraphPlot + +@recipe function f( + g::GraphPlot; + dim = 2, + free_dims = nothing, + T = Float64, + curves = true, + curvature_scalar = 0.05, + root = :top, + node_weights = nothing, + names = [], + fontsize = 7, + nodeshape = :hexagon, + nodesize = 0.1, + node_z = nothing, + nodecolor = 1, + nodestrokealpha = 1, + nodealpha = 1, + nodestrokewidth = 1, + nodestrokecolor = :black, + nodestrokestyle = :solid, + nodestroke_z = nothing, + rng = nothing, + x = nothing, + y = nothing, + z = nothing, + method = :stress, + func = get(_graph_funcs, method, by_axis_local_stress_graph), + shorten = 0.0, + axis_buffer = 0.2, + layout_kw = Dict{Symbol,Any}(), + edgewidth = (s, d, w) -> 1, + edgelabel = nothing, + edgelabel_offset = 0.0, + self_edge_size = 0.1, + edge_label_box = true, + edge_z = nothing, + edgecolor = :black, + edgestyle = :solid, + trim = false, +) + # Process the args so that they are a Graphs.Graph. + if length(g.args) <= 1 && + !(eltype(g.args[1]) <: AbstractArray) && + !(g.args[1] isa Graphs.AbstractGraph) && + method != :chorddiagram && + method != :arcdiagram + if !LinearAlgebra.issymmetric(g.args[1]) || + any(diag(g.args[1]) .!= zeros(length(diag(g.args[1])))) + g.args = (Graphs.DiGraph(g.args[1]),) + elseif LinearAlgebra.issymmetric(g.args[1]) + g.args = (Graphs.Graph(g.args[1]),) + end + end + + # To process aliases that are unique to graphplot, find aliases that are in + # plotattributes and replace the attributes with their aliases. Then delete the alias + # names from the plotattributes dictionary. + @process_aliases plotattributes graph_aliases + for arg ∈ keys(graph_aliases) + remove_aliases!(arg, plotattributes, graph_aliases) + end + # The above process will remove all marker properties from the plotattributes + # dictionary. To ensure consistency between markers and nodes, we replace all marker + # properties with the corresponding node property. + marker_node_collection = zip( + [ + :markershape, + :markersize, + :markercolor, + :marker_z, + :markerstrokealpha, + :markeralpha, + :markerstrokewidth, + :markerstrokealpha, + :markerstrokecolor, + :markerstrokestyle, + ], + [ + nodeshape, + nodesize, + nodecolor, + node_z, + nodestrokealpha, + nodealpha, + nodestrokewidth, + nodestrokealpha, + nodestrokecolor, + nodestrokestyle, + ], + ) + for (markerproperty, nodeproperty) ∈ marker_node_collection + # Make sure that the node properties are row vectors. + nodeproperty isa Array && (nodeproperty = permutedims(vec(nodeproperty))) + plotattributes[markerproperty] = nodeproperty + end + + # If we pass a value of plotattributes[:markershape] that the backend does not + # recognize, then the backend will throw an error. The error is thrown despite the + # fact that we override the default behavior. Custom nodehapes are incompatible + # with the backend's markershapes and thus replaced. + if nodeshape isa Function || + nodeshape isa Array && any([s isa Function for s ∈ nodeshape]) + plotattributes[:markershape] = :circle + end + + @assert dim in (2, 3) + is3d = dim == 3 + adj_mat = get_adjacency_matrix(g.args...) + nr, nc = size(adj_mat) # number of nodes == number of rows + @assert nr == nc + isdirected = + (g.args[1] isa DiGraph || !issymmetric(adj_mat)) && + !in(method, (:tree, :buchheim)) && + !(get(plotattributes, :arrow, true) == false) + if isdirected && (g.args[1] isa Matrix) + g = GraphPlot((adjacency_matrix(DiGraph(g.args[1])),)) + end + + source, destiny, weights = get_source_destiny_weight(g.args...) + if !(eltype(source) <: Integer) + names = unique(sort(vcat(source, destiny))) + source = Int[findfirst(names, si) for si ∈ source] + destiny = Int[findfirst(names, di) for di ∈ destiny] + end + n = infer_size_from(source, destiny) + display_n = trim ? n : nr # number of displayed nodes + n_edges = length(source) + + isnothing(node_weights) && (node_weights = ones(display_n)) + + xyz = is3d ? (x, y, z) : (x, y) + numnothing = count(isnothing, xyz) + + # do we want to compute coordinates? + if numnothing > 0 + isnothing(free_dims) && (free_dims = findall(isnothing, xyz)) # compute free_dims + dat = prepare_graph_inputs(method, source, destiny, weights; display_n = display_n) + x, y, z = func( + dat...; + node_weights = node_weights, + dim = dim, + free_dims = free_dims, + root = root, + rng = rng, + layout_kw..., + ) + end + + # reorient the points after root + if root in (:left, :right) + x, y = y, -x + end + if root == :left + x, y = -x, y + end + if root == :bottom + x, y = x, -y + end + + # Since we do nodehapes manually, they only work with aspect_ratio=1. + # TODO: rescale the nodeshapes based on the ranges of x,y,z. + aspect_ratio --> 1 + if length(axis_buffer) == 1 + axis_buffer = fill(axis_buffer, dim) + end + + # center and rescale to the widest of all dimensions + if method == :arcdiagram + xl, yl = arcdiagram_limits(x, source, destiny) + xlims --> xl + ylims --> yl + aspect_ratio --> :equal + elseif all(axis_buffer .< 0) # equal axes + ahw = 1.2 * 0.5 * maximum(v -> maximum(v) - minimum(v), xyz) + xcenter = mean(extrema(x)) + #xlims --> (xcenter-ahw, xcenter+ahw) + ycenter = mean(extrema(y)) + #ylims --> (ycenter-ahw, ycenter+ahw) + if is3d + zcenter = mean(extrema(z)) + #zlims --> (zcenter-ahw, zcenter+ahw) + end + else + xlims = ignorenan_extrema(x) + if method != :chorddiagram && numnothing > 0 + x .-= mean(x) + x /= (xlims[2] - xlims[1]) + y .-= mean(y) + ylims = ignorenan_extrema(y) + y /= (ylims[2] - ylims[1]) + end + xlims --> extrema_plus_buffer(x, axis_buffer[1]) + ylims --> extrema_plus_buffer(y, axis_buffer[2]) + if is3d + if method != :chorddiagram && numnothing > 0 + zlims = ignorenan_extrema(z) + z .-= mean(z) + z /= (zlims[2] - zlims[1]) + end + zlims --> extrema_plus_buffer(z, axis_buffer[3]) + end + end + xyz = is3d ? (x, y, z) : (x, y) + # Get the coordinates for the edges of the nodes. + node_vec_vec_xy = [] + nodewidth = 0.0 + nodewidth_array = Vector{Float64}(undef, length(x)) + if !(nodeshape isa Array) + nodeshape = repeat([nodeshape], length(x)) + end + if !is3d + for i ∈ eachindex(x) + node_number = + i % length(nodeshape) == 0 ? length(nodeshape) : i % length(nodeshape) + node_weight = + isnothing(node_weights) ? 1 : + (10 + 100node_weights[i] / sum(node_weights)) / 50 + xextent, yextent = if isempty(names) + [ + x[i] .+ [-0.5nodesize * node_weight, 0.5nodesize * node_weight], + y[i] .+ [-0.5nodesize * node_weight, 0.5nodesize * node_weight], + ] + else + annotation_extent( + plotattributes, + ( + x[i], + y[i], + names[ifelse( + i % length(names) == 0, + length(names), + i % length(names), + )], + fontsize * nodesize * node_weight, + ), + ) + end + nodewidth = xextent[2] - xextent[1] + nodewidth_array[i] = nodewidth + if nodeshape[node_number] == :circle + push!( + node_vec_vec_xy, + partialcircle(0, 2π, [x[i], y[i]], 80, nodewidth / 2), + ) + elseif (nodeshape[node_number] == :rect) || + (nodeshape[node_number] == :rectangle) + push!( + node_vec_vec_xy, + [ + (xextent[1], yextent[1]), + (xextent[2], yextent[1]), + (xextent[2], yextent[2]), + (xextent[1], yextent[2]), + (xextent[1], yextent[1]), + ], + ) + elseif nodeshape[node_number] == :hexagon + push!(node_vec_vec_xy, partialcircle(0, 2π, [x[i], y[i]], 7, nodewidth / 2)) + elseif nodeshape[node_number] == :ellipse + nodeheight = (yextent[2] - yextent[1]) + push!( + node_vec_vec_xy, + partialellipse(0, 2π, [x[i], y[i]], 80, nodewidth / 2, nodeheight / 2), + ) + elseif applicable(nodeshape[node_number], x[i], y[i], 0.0, 0.0) + nodeheight = (yextent[2] - yextent[1]) + push!( + node_vec_vec_xy, + nodeshape[node_number](x[i], y[i], nodewidth, nodeheight), + ) + elseif applicable(nodeshape[node_number], x[i], y[i], 0.0) + push!(node_vec_vec_xy, nodeshape[node_number](x[i], y[i], nodewidth)) + else + error( + "Unknown nodeshape: $(nodeshape[node_number]). Choose from :circle, ellipse, :hexagon, :rect or :rectangle or or a custom shape. Custom shapes can be passed as a function customshape such that customshape(x, y, nodeheight, nodewidth) -> nodeperimeter/ customshape(x, y, nodescale) -> nodeperimeter. nodeperimeter must be an array of 2-tuples, where each tuple is a corner of your custom shape, centered at (x, y) and with height nodeheight, width nodewidth or only a nodescale for symmetrically scaling shapes.", + ) + end + end + else + @assert is3d # TODO Make 3d work. + end + # The node_perimter_info list contains the information needed to construct the + # information in node_vec_vec_xy. For example, if (nodeshape[i]==:circle && !is3d), + # then all of the information in node_vec_vec_xy[i] can be summarised with three + # numbers describing the center and the radius of the circle. + node_perimeter_info = [] + for i ∈ eachindex(node_vec_vec_xy) + if nodeshape[i] == :circle + push!( + node_perimeter_info, + GeometryTypes.Circle( + Point((convert(T, x[i]), convert(T, y[i]))), + nodewidth_array[i] / 2, + ), + ) + else + push!(node_perimeter_info, node_vec_vec_xy[i]) + end + end + + # generate a list of colors, one per segment + segment_colors = get(plotattributes, :linecolor, nothing) + edge_label_array = Vector{Tuple}() + edge_label_box_vertices_array = Vector{Array}() + if !isa(edgelabel, Dict) && !isnothing(edgelabel) + tmp = Dict() + if length(size(edgelabel)) < 2 + matrix_size = round(Int, sqrt(length(edgelabel))) + edgelabel = reshape(edgelabel, matrix_size, matrix_size) + end + for i ∈ 1:size(edgelabel)[1], j ∈ 1:size(edgelabel)[2] + if islabel(edgelabel[i, j]) + tmp[(i, j)] = edgelabel[i, j] + end + end + edgelabel = tmp + end + # If the edgelabel dictionary is full of length two tuples, then make all of the + # tuples length three with last element 1. (i.e. a multigraph that has no extra + # edges). + if edgelabel isa Dict + edgelabel = convert(Dict{Any,Any}, edgelabel) + for key ∈ keys(edgelabel) + if length(key) == 2 + edgelabel[(key..., 1)] = edgelabel[key] + end + end + end + edge_has_been_seen = Dict() + for edge ∈ zip(source, destiny) + edge_has_been_seen[edge] = 0 + end + if length(curvature_scalar) == 1 + curvature_scalar = fill(curvature_scalar, size(adj_mat, 1), size(adj_mat, 1)) + end + + edges_list = (T[], T[], T[], T[]) + # TODO do a proper job of calculating nsegments. + nsegments = if curves && (method in (:tree, :buchheim)) + 4 + elseif method == :chorddiagram + 3 + elseif method == :arcdiagram + 30 + elseif curves + 50 + else + 2 + end + + for (edge_num, (si, di, wi)) ∈ enumerate(zip(source, destiny, weights)) + edge_has_been_seen[(si, di)] += 1 + xseg = Float64[] + yseg = Float64[] + zseg = Float64[] + l_wg = Float64[] + + # add a line segment + xsi, ysi, xdi, ydi = shorten_segment(x[si], y[si], x[di], y[di], shorten) + θ = (edge_has_been_seen[(si, di)] - 1) * pi / 8 + if isdirected && si != di && !is3d + xpt, ypt = if method != :chorddiagram + control_point( + xsi, + xdi, + ysi, + ydi, + edge_has_been_seen[(si, di)] * curvature_scalar[si, di] * sign(si - di), + ) + else + (0.0, 0.0) + end + # For directed graphs, shorten the line segment so that the edge ends at + # the perimeter of the destiny node. + if isdirected + _, _, xdi, ydi = + nearest_intersection(xpt, ypt, x[di], y[di], node_perimeter_info[di]) + end + end + if curves + if method in (:tree, :buchheim) + # for trees, shorten should be on one axis only + # dist = sqrt((x[di]-x[si])^2 + (y[di]-y[si])^2) * shorten + dist = shorten * (root in (:left, :bottom) ? 1 : -1) + ishoriz = root in (:left, :right) + xsi, xdi = (ishoriz ? (x[si] + dist, x[di] - dist) : (x[si], x[di])) + ysi, ydi = (ishoriz ? (y[si], y[di]) : (y[si] + dist, y[di] - dist)) + xpts, ypts = directed_curve( + xsi, + xdi, + ysi, + ydi, + xview = get(plotattributes, :xlims, (0, 1)), + yview = get(plotattributes, :ylims, (0, 1)), + root = root, + rng = rng, + ) + append!(xseg, xpts) + append!(yseg, ypts) + append!(l_wg, [wi for i ∈ 1:length(xpts)]) + elseif method == :arcdiagram + r = (xdi - xsi) / 2 + x₀ = (xdi + xsi) / 2 + θ = range(0, stop = π, length = 30) + xpts = x₀ .+ r .* cos.(θ) + ypts = r .* sin.(θ) .+ ysi # ysi == ydi + for x ∈ xpts + push!(xseg, x) + push!(l_wg, wi) + end + # push!(xseg, NaN) + for y ∈ ypts + push!(yseg, y) + end + # push!(yseg, NaN) + else + xpt, ypt = if method != :chorddiagram + control_point( + xsi, + x[di], + ysi, + y[di], + edge_has_been_seen[(si, di)] * + curvature_scalar[si, di] * + sign(si - di), + ) + else + (0.0, 0.0) + end + xpts = [xsi, xpt, xdi] + ypts = [ysi, ypt, ydi] + t = range(0, stop = 1, length = 3) + A = hcat(xpts, ypts) + itp = scale(interpolate(A, BSpline(Cubic(Natural(OnGrid())))), t, 1:2) + tfine = range(0, stop = 1, length = nsegments) + xpts, ypts = [itp(t, 1) for t ∈ tfine], [itp(t, 2) for t ∈ tfine] + if !isnothing(edgelabel) && + haskey(edgelabel, (si, di, edge_has_been_seen[(si, di)])) + q = control_point( + xsi, + x[di], + ysi, + y[di], + ( + edgelabel_offset + + edge_has_been_seen[(si, di)] * curvature_scalar[si, di] + ) * sign(si - di), + ) + + if !any(isnan.(q)) + push!( + edge_label_array, + ( + q..., + string(edgelabel[(si, di, edge_has_been_seen[(si, di)])]), + fontsize, + ), + ) + edge_label_box_vertices = (annotation_extent( + plotattributes, + ( + q[1], + q[2], + edgelabel[(si, di, edge_has_been_seen[(si, di)])], + 0.05fontsize, + ), + )) + push!(edge_label_box_vertices_array, edge_label_box_vertices) + end + end + if method != :chorddiagram && !is3d + append!(xseg, xpts) + append!(yseg, ypts) + push!(l_wg, wi) + else + push!(xseg, xsi, xpt, xdi) + push!(yseg, ysi, ypt, ydi) + is3d && push!(zseg, z[si], z[si], z[di]) + push!(l_wg, wi) + end + end + else + push!(xseg, xsi, xdi) + push!(yseg, ysi, ydi) + is3d && push!(zseg, z[si], z[di]) + if !isnothing(edgelabel) && + haskey(edgelabel, (si, di, edge_has_been_seen[(si, di)])) + q = [(xsi + xdi) / 2, (ysi + ydi) / 2] + + if !any(isnan.(q)) + push!( + edge_label_array, + ( + q..., + string(edgelabel[(si, di, edge_has_been_seen[(si, di)])]), + fontsize, + ), + ) + edge_label_box_vertices = (annotation_extent( + plotattributes, + ( + q[1], + q[2], + edgelabel[(si, di, edge_has_been_seen[(si, di)])], + 0.05fontsize, + ), + )) + push!(edge_label_box_vertices_array, edge_label_box_vertices) + end + end + end + if si == di && !is3d + inds = 1:n .!= si + self_edge_angle = pi / 8 + (edge_has_been_seen[(si, di)] - 1) * pi / 8 + θ1 = unoccupied_angle(xsi, ysi, x[inds], y[inds]) - self_edge_angle / 2 + θ2 = θ1 + self_edge_angle + nodewidth = nodewidth_array[si] + if nodeshape == :circle + xpts = [ + xsi + nodewidth * cos(θ1) / 2, + NaN, + NaN, + NaN, + xsi + nodewidth * cos(θ2) / 2, + ] + xpts[2] = + mean([xpts[1], xpts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * cos(θ1) + xpts[3] = + mean([xpts[1], xpts[end]]) + + edge_has_been_seen[(si, di)] * self_edge_size * cos((θ1 + θ2) / 2) + xpts[4] = + mean([xpts[1], xpts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * cos(θ2) + ypts = [ + ysi + nodewidth * sin(θ1) / 2, + NaN, + NaN, + NaN, + ysi + nodewidth * sin(θ2) / 2, + ] + ypts[2] = + mean([ypts[1], ypts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * sin(θ1) + ypts[3] = + mean([ypts[1], ypts[end]]) + + edge_has_been_seen[(si, di)] * self_edge_size * sin((θ1 + θ2) / 2) + ypts[4] = + mean([ypts[1], ypts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * sin(θ2) + t = range(0, stop = 1, length = 5) + A = hcat(xpts, ypts) + itp = scale(interpolate(A, BSpline(Cubic(Natural(OnGrid())))), t, 1:2) + tfine = range(0, stop = 1, length = nsegments) + xpts, ypts = [itp(t, 1) for t ∈ tfine], [itp(t, 2) for t ∈ tfine] + else + _, _, start_point1, start_point2 = nearest_intersection( + xsi, + ysi, + xsi + 2nodewidth * cos(θ1), + ysi + 2nodewidth * sin(θ1), + node_vec_vec_xy[si], + ) + _, _, end_point1, end_point2 = nearest_intersection( + xsi + + edge_has_been_seen[(si, di)] * (nodewidth + self_edge_size) * cos(θ2), + ysi + + edge_has_been_seen[(si, di)] * (nodewidth + self_edge_size) * sin(θ2), + xsi, + ysi, + node_vec_vec_xy[si], + ) + xpts = [start_point1, NaN, NaN, NaN, end_point1] + xpts[2] = + mean([xpts[1], xpts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * cos(θ1) + xpts[3] = + mean([xpts[1], xpts[end]]) + + edge_has_been_seen[(si, di)] * self_edge_size * cos((θ1 + θ2) / 2) + xpts[4] = + mean([xpts[1], xpts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * cos(θ2) + ypts = [start_point2, NaN, NaN, NaN, end_point2] + ypts[2] = + mean([ypts[1], ypts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * sin(θ1) + ypts[3] = + mean([ypts[1], ypts[end]]) + + edge_has_been_seen[(si, di)] * self_edge_size * sin((θ1 + θ2) / 2) + ypts[4] = + mean([ypts[1], ypts[end]]) + + 0.5 * (0.5 + edge_has_been_seen[(si, di)]) * self_edge_size * sin(θ2) + t = range(0, stop = 1, length = 5) + A = hcat(xpts, ypts) + itp = scale(interpolate(A, BSpline(Cubic(Natural(OnGrid())))), t, 1:2) + tfine = range(0, stop = 1, length = nsegments) + xpts, ypts = [itp(t, 1) for t ∈ tfine], [itp(t, 2) for t ∈ tfine] + end + append!(xseg, xpts) + append!(yseg, ypts) + mid_ind = div(length(xpts), 2) + q = [ + xpts[mid_ind] + edgelabel_offset * cos((θ1 + θ2) / 2), + ypts[mid_ind] + edgelabel_offset * sin((θ1 + θ2) / 2), + ] + if !isnothing(edgelabel) && + haskey(edgelabel, (si, di, edge_has_been_seen[(si, di)])) + push!( + edge_label_array, + ( + q..., + string(edgelabel[(si, di, edge_has_been_seen[(si, di)])]), + fontsize, + ), + ) + edge_label_box_vertices = annotation_extent( + plotattributes, + (q..., edgelabel[(si, di, edge_has_been_seen[(si, di)])], 0.05fontsize), + ) + if !any(isnan.(q)) + push!(edge_label_box_vertices_array, edge_label_box_vertices) + end + end + end + append!(edges_list[1], xseg[.!isnan.(xseg)]) + append!(edges_list[2], yseg[.!isnan.(yseg)]) + is3d && append!(edges_list[3], zseg[.!isnan.(zseg)]) + append!(edges_list[4], l_wg[.!isnan.(l_wg)]) + end + + if is3d + edges_list = ( + reshape(edges_list[1], 3, round(Int, length(edges_list[1]) / 3)), + reshape(edges_list[2], 3, round(Int, length(edges_list[2]) / 3)), + reshape(edges_list[3], 3, round(Int, length(edges_list[3]) / 3)), + ) + else + edges_list = ( + reshape( + edges_list[1], + nsegments, + round(Int, length(edges_list[1]) / nsegments), + ), + reshape( + edges_list[2], + nsegments, + round(Int, length(edges_list[2]) / nsegments), + ), + ) + edges_list = ( + [edges_list[1][:, j] for j ∈ 1:size(edges_list[1], 2)], + [edges_list[2][:, j] for j ∈ 1:size(edges_list[2], 2)], + ) + end + + @series begin + @debug num_edges_nodes := (length(edges_list[1]), length(node_vec_vec_xy)) # for debugging / tests + + seriestype := if method in (:tree, :buchheim, :chorddiagram) + :curves + else + if is3d + # TODO make curves work + if curves + :curves + end + else + :path + end + end + + colorbar_entry := true + + edge_z = process_edge_attribute(edge_z, source, destiny, weights) + edgewidth = process_edge_attribute(edgewidth, source, destiny, weights) + edgecolor = process_edge_attribute(edgecolor, source, destiny, weights) + edgestyle = process_edge_attribute(edgestyle, source, destiny, weights) + + !isnothing(edge_z) && (line_z := edge_z) + linewidthattr = get(plotattributes, :linewidth, 1) + linewidth := linewidthattr * edgewidth + fillalpha := 1 + linecolor := edgecolor + linestyle := get(plotattributes, :linestyle, edgestyle) + markershape := :none + markersize := 0 + markeralpha := 0 + markercolor := :black + marker_z := nothing + isdirected && (arrow --> :simple, :head, 0.3, 0.3) + primary := false + + is3d ? edges_list[1:3] : edges_list[1:2] + end + # The boxes around edge labels are defined as another list of series that sits on top + # of the series for the edges. + edge_has_been_seen = Dict() + for edge ∈ zip(source, destiny) + edge_has_been_seen[edge] = 0 + end + index = 0 + if edge_label_box && !isnothing(edgelabel) + for (edge_num, (si, di, wi)) ∈ enumerate(zip(source, destiny, weights)) + edge_has_been_seen[(si, di)] += 1 + if haskey(edgelabel, (si, di, edge_has_been_seen[(si, di)])) + index += 1 + @series begin + seriestype := :shape + + colorbar_entry --> false + fillcolor --> get(plotattributes, :background_color, :white) + linewidth --> 0 + linealpha --> 0 + edge_label_box_vertices = edge_label_box_vertices_array[index] + ( + [ + edge_label_box_vertices[1][1], + edge_label_box_vertices[1][2], + edge_label_box_vertices[1][2], + edge_label_box_vertices[1][1], + edge_label_box_vertices[1][1], + ], + [ + edge_label_box_vertices[2][1], + edge_label_box_vertices[2][1], + edge_label_box_vertices[2][2], + edge_label_box_vertices[2][2], + edge_label_box_vertices[2][1], + ], + ) + end + end + end + end + + framestyle := :none + axis := nothing + legend --> false + + # Make sure that the node properties are row vectors. + nodeshape = format_nodeproperty(nodeshape, n_edges, index) + nodesize = format_nodeproperty(nodesize, n_edges, index) + nodecolor = format_nodeproperty(nodecolor, n_edges, index) + node_z = format_nodeproperty(node_z, n_edges, index) + nodestrokealpha = format_nodeproperty(nodestrokealpha, n_edges, index) + nodealpha = format_nodeproperty(nodealpha, n_edges, index) + nodestrokewidth = format_nodeproperty(nodestrokewidth, n_edges, index) + nodestrokealpha = format_nodeproperty(nodestrokealpha, n_edges, index) + nodestrokecolor = format_nodeproperty(nodestrokecolor, n_edges, index) + nodestrokestyle = + format_nodeproperty(nodestrokestyle, n_edges, index, fill_value = :solid) + + if method == :chorddiagram + seriestype := :scatter + markersize := 0 + markeralpha := 0 + aspect_ratio --> :equal + if length(names) == length(x) + annotations := [(x[i], y[i], names[i]) for i ∈ eachindex(x)] + end + @series begin + seriestype := :shape + N = length(x) + angles = Vector{Float64}(undef, N) + for i ∈ 1:N + if y[i] > 0 + angles[i] = acos(x[i]) + else + angles[i] = 2pi - acos(x[i]) + end + end + δ = 0.4 * (angles[2] - angles[1]) + vec_vec_xy = [arcshape(Θ - δ, Θ + δ) for Θ ∈ angles] # Shape + [[xy[1] for xy ∈ vec_xy] for vec_xy ∈ vec_vec_xy], + [[xy[2] for xy ∈ vec_xy] for vec_xy ∈ vec_vec_xy] + end + else + if is3d + seriestype := :scatter3d + linewidth := 0 + linealpha := 0 + markercolor := nodecolor + series_annotations --> map(string, names) + markersize --> (10 .+ (100 .* node_weights) ./ sum(node_weights)) + else + @series begin + seriestype := :shape + + colorbar_entry := true + fill_z --> node_z + fillalpha := nodealpha + fillcolor := nodecolor + markersize := 0 + markeralpha := 0 + linewidth := nodestrokewidth + linealpha := nodestrokealpha + linecolor := nodestrokecolor + linestyle := nodestrokestyle + line_z := nodestroke_z + + nodeperimeters = (Any[], Any[]) + for vec_xy ∈ node_vec_vec_xy + push!(nodeperimeters[1], [xy[1] for xy ∈ vec_xy]) + push!(nodeperimeters[2], [xy[2] for xy ∈ vec_xy]) + end + + nodeperimeters + + # if is3d + # seriestype := :volume + # ([[xyz[1] for xyz in vec_xyz] for vec_xyz in node_vec_vec_xyz], + # [[xyz[2] for xyz in vec_xyz] for vec_xyz in node_vec_vec_xyz], + # [[xyz[3] for xyz in vec_xyz] for vec_xyz in node_vec_vec_xyz]) + # end + end + + if isempty(names) + seriestype := :scatter + + colorbar_entry --> false + markersize := 0 + markeralpha := 0 + markerstrokesize := 0 + !isnothing(edgelabel) && (annotations --> edge_label_array) + else + seriestype := :scatter + + colorbar_entry --> false + markersize := 0 + markeralpha := 0 + markerstrokesize := 0 + annotations --> [ + edge_label_array + [ + ( + x[i], + y[i], + names[ifelse( + i % length(names) == 0, + length(names), + i % length(names), + )], + fontsize, + ) for i ∈ eachindex(x) + ] + ] + end + end + end + xyz +end + +@recipe f(g::AbstractGraph) = GraphPlot(get_source_destiny_weight(get_adjacency_list(g))) diff --git a/GraphRecipes/src/misc.jl b/GraphRecipes/src/misc.jl new file mode 100644 index 000000000..5dfa60be1 --- /dev/null +++ b/GraphRecipes/src/misc.jl @@ -0,0 +1,115 @@ + +# ------------------------------------------------------------------- +# AST trees + +function add_ast(adjlist, names, depthdict, depthlists, nodetypes, ex::Expr, parent_idx) + idx = length(names) + 1 + iscall = ex.head == :call + push!(names, iscall ? string(ex.args[1]) : string(ex.head)) + push!(nodetypes, iscall ? :call : :expr) + l = Int[] + push!(adjlist, l) + + depth = parent_idx == 0 ? 1 : depthdict[parent_idx] + 1 + depthdict[idx] = depth + while length(depthlists) < depth + push!(depthlists, Int[]) + end + push!(depthlists[depth], idx) + + for arg ∈ (iscall ? ex.args[2:end] : ex.args) + if isa(arg, LineNumberNode) + continue + end + push!(l, add_ast(adjlist, names, depthdict, depthlists, nodetypes, arg, idx)) + end + idx +end + +function add_ast(adjlist, names, depthdict, depthlists, nodetypes, x, parent_idx) + push!(names, string(x)) + push!(nodetypes, :leaf) + push!(adjlist, Int[]) + idx = length(names) + + depth = parent_idx == 0 ? 1 : depthdict[parent_idx] + 1 + depthdict[idx] = depth + while length(depthlists) < depth + push!(depthlists, Int[]) + end + push!(depthlists[depth], idx) + + idx +end + +@recipe function f(ex::Expr) + names = String[] + adjlist = Vector{Int}[] + depthdict = Dict{Int,Int}() + depthlists = Vector{Int}[] + nodetypes = Symbol[] + add_ast(adjlist, names, depthdict, depthlists, nodetypes, ex, 0) + names := names + # method := :tree + method := :buchheim + root --> :top + + # markercolor --> Symbol[(nt == :call ? :pink : nt == :leaf ? :white : :lightgreen) for nt in nodetypes] + + # # compute the y-values from the depthdict dict + # n = length(depthlists)-1 + # layers = Float64[(depthdict[i]-1)/n for i=1:length(names)] + # # add_noise --> false + # + # positions = zeros(length(names)) + # for (depth, lst) in enumerate(depthlists) + # n = length(lst) + # pos = n > 1 ? linspace(0, 1, n) : [0.5] + # for (i, idx) in enumerate(lst) + # positions[idx] = pos[i] + # end + # end + # + # layout_kw := Dict{Symbol,Any}(:layers => layers, :add_noise => false, :positions => positions) + + GraphPlot(get_source_destiny_weight(adjlist)) +end + +# ------------------------------------------------------------------- +# Type trees + +function add_subs!(nodes, source, destiny, ::Type{T}, supidx) where {T} + for sub ∈ subtypes(T) + push!(nodes, sub) + subidx = length(nodes) + push!(source, supidx) + push!(destiny, subidx) + add_subs!(nodes, source, destiny, sub, subidx) + end +end + +# recursively build a graph of subtypes of T +@recipe function f( + ::Type{T}; + namefunc = node -> isa(node, UnionAll) ? split(string(node), '.')[end] : node.name.name, +) where {T} + # get the supertypes + sups = Any[T] + sup = T + while sup != Any + sup = supertype(sup) + pushfirst!(sups, sup) + end + + # add the subtypes + n = length(sups) + nodes = copy(sups) + source, destiny = collect(1:(n - 1)), collect(2:n) + add_subs!(nodes, source, destiny, T, n) + + # set up the graphplot + names := map(namefunc, nodes) + method --> :buchheim + root --> :top + GraphPlot((source, destiny)) +end diff --git a/GraphRecipes/src/trees.jl b/GraphRecipes/src/trees.jl new file mode 100644 index 000000000..a08a1ff47 --- /dev/null +++ b/GraphRecipes/src/trees.jl @@ -0,0 +1,60 @@ +import AbstractTrees +using AbstractTrees: children + +export TreePlot + +""" + TreePlot(root) + +Wrap a tree-like object for plotting. Uses `AbstractTrees.children()` to recursively add children to the plot and `AbstractTrees.printnode()` to generate the labels. + +# Example + +```julia +using AbstractTrees, GraphRecipes +AbstractTrees.children(d::Dict) = [p for p in d] +AbstractTrees.children(p::Pair) = AbstractTrees.children(p[2]) +function AbstractTrees.printnode(io::IO, p::Pair) + str = isempty(AbstractTrees.children(p[2])) ? string(p[1], ": ", p[2]) : string(p[1], ": ") + print(io, str) +end + +d = Dict(:a => 2,:d => Dict(:b => 4,:c => "Hello"),:e => 5.0) + +plot(TreePlot(d)) +```` +""" +struct TreePlot{T} + root::T +end + +function add_children!(nodes, source, destiny, node, parent_idx) + for child ∈ children(node) + push!(nodes, child) + child_idx = length(nodes) + push!(source, parent_idx) + push!(destiny, child_idx) + add_children!(nodes, source, destiny, child, child_idx) + end +end + +function string_from_node(node) + io = IOBuffer() + AbstractTrees.printnode(io, node) + String(take!(io)) +end + +# recursively build a graph of children of `tree_wrapper.root` +@recipe function f(tree_wrapper::TreePlot; namefunc = string_from_node) + root = tree_wrapper.root + # recursively add children + nodes = Any[root] + source, destiny = Int[], Int[] + add_children!(nodes, source, destiny, root, 1) + + # set up the graphplot + names --> map(namefunc, nodes) + method --> :buchheim + root --> :top + GraphPlot((source, destiny)) +end diff --git a/GraphRecipes/src/utils.jl b/GraphRecipes/src/utils.jl new file mode 100644 index 000000000..f7b9d5e86 --- /dev/null +++ b/GraphRecipes/src/utils.jl @@ -0,0 +1,378 @@ +""" +This function builds a BezierCurve which leaves point p vertically upwards and +arrives point q vertically upwards. It may create a loop if necessary. +It assumes the view is [0,1]. That can be modified using the `xview` and +`yview` keyword arguments (default: `0:1`). +""" +function directed_curve( + x1, + x2, + y1, + y2; + xview = 0:1, + yview = 0:1, + root::Symbol = :bottom, + rng = nothing, +) + if root in (:left, :right) + # flip x/y to simplify + x1, x2, y1, y2, xview, yview = y1, y2, x1, x2, yview, xview + end + x = Float64[x1, x1] + y = Float64[y1] + + minx, maxx = extrema(xview) + miny, maxy = extrema(yview) + dist = sqrt((x2 - x1)^2 + (y2 - y1)^2) + flip = root in (:top, :right) + need_loop = (flip && y1 <= y2) || (!flip && y1 >= y2) + + # these points give the initial/final "rise" + # note: this is a function of distance between points and axis scale + y_offset = if need_loop + 0.3dist + else + min(0.3dist, 0.5 * abs(y2 - y1)) + end + y_offset = max(0.02 * (maxy - miny), y_offset) + + if flip + # got the other direction + y_offset *= -1 + end + push!(y, y1 + y_offset) + + # try to figure out when to loop around vs just connecting straight + if need_loop + if abs(x2 - x1) > 0.1 * (maxx - minx) + # go between + sgn = x2 > x1 ? 1 : -1 + x_offset = 0.5 * abs(x2 - x1) + append!(x, [x1 + sgn * x_offset, x2 - sgn * x_offset]) + else + # add curve points which will create a loop + x_offset = + 0.3 * + (maxx - minx) * + (rand(rng_from_rng_or_seed(rng, nothing), Bool) ? 1 : -1) + append!(x, [x1 + x_offset, x2 + x_offset]) + end + append!(y, [y1 + y_offset, y2 - y_offset]) + end + + append!(x, [x2, x2]) + append!(y, [y2 - y_offset, y2]) + if root in (:left, :right) + # flip x/y to simplify + x, y = y, x + end + x, y +end + +function shorten_segment(x1, y1, x2, y2, shorten) + xshort = shorten * (x2 - x1) + yshort = shorten * (y2 - y1) + x1 + xshort, y1 + yshort, x2 - xshort, y2 - yshort +end + +# """ +# shorten_segment_absolute(x1, y1, x2, y2, shorten) +# +# Remove an amount `shorten` from the end of the line [x1,y1] -> [x2,y2]. +# """ +# function shorten_segment_absolute(x1, y1, x2, y2, shorten) +# if x1 == x2 && y1 == y2 +# return x1, y1, x2, y2 +# end +# t = shorten/sqrt(x1*(x1-2x2) + x2^2 + y1*(y1-2y2) + y2^2) +# x1, y1, (1.0-t)*x2 + t*x1, (1.0-t)*y2 + t*y1 +# end + +""" + nearest_intersection(xs, ys, xd, yd, vec_xy_d) + +Find where the line defined by [xs,ys] -> [xd,yd] intersects with the closed shape who's +vertices are stored in `vec_xy_d`. Return the intersection that is closest to the point +[xs,ys] (the source node). +""" +function nearest_intersection(xs, ys, xd, yd, vec_xy_d) + if xs == xd && ys == yd + return xs, ys, xd, yd + end + t = Vector{Float64}(undef, 2) + xvec = Vector{Float64}(undef, 2) + yvec = Vector{Float64}(undef, 2) + xy_d_edge = Vector{Float64}(undef, 2) + ret = Vector{Float64}(undef, 2) + A = Array{Float64}(undef, 2, 2) + nearest = Inf + for i ∈ 1:(length(vec_xy_d) - 1) + xvec .= [vec_xy_d[i][1], vec_xy_d[i + 1][1]] + yvec .= [vec_xy_d[i][2], vec_xy_d[i + 1][2]] + A .= [-xs+xd -xvec[1]+xvec[2]; -ys+yd -yvec[1]+yvec[2]] + t .= (A + eps() * I) \ [xs - xvec[1]; ys - yvec[1]] + xy_d_edge .= + [(1 - t[2]) * xvec[1] + t[2] * xvec[2], (1 - t[2]) * yvec[1] + t[2] * yvec[2]] + if 0 <= t[2] <= 1 + tmp = abs2(xy_d_edge[1] - xs) + abs2(xy_d_edge[2] - ys) + if tmp < nearest + ret .= xy_d_edge + nearest = tmp + end + end + end + xs, ys, ret[1], ret[2] +end + +function nearest_intersection(xs, ys, xd, yd, vec_xy_d::GeometryTypes.Circle) + if xs == xd && ys == yd + return xs, ys, xd, yd + end + + α = atan(ys - yd, xs - xd) + xd = xd + vec_xy_d.r * cos(α) + yd = yd + vec_xy_d.r * sin(α) + + xs, ys, xd, yd +end + +function nearest_intersection(xs, ys, zs, xd, yd, zd, vec_xyz_d) + # TODO make 3d work. +end + +""" +Randomly pick a point to be the center control point of a bezier curve, +which is both equidistant between the endpoints and normally distributed +around the midpoint. +""" +function random_control_point( + xi, + xj, + yi, + yj, + curvature_scalar; + rng = rng_from_rng_or_seed(rng, nothing), +) + xmid = 0.5 * (xi + xj) + ymid = 0.5 * (yi + yj) + + # get the angle of y relative to x + theta = atan((yj - yi) / (xj - xi)) + 0.5pi + + # calc random shift relative to dist between x and y + dist = sqrt((xj - xi)^2 + (yj - yi)^2) + dist_from_mid = curvature_scalar * (rand(rng) - 0.5) * dist + + # now we have polar coords, we can compute the position, adding to the midpoint + (xmid + dist_from_mid * cos(theta), ymid + dist_from_mid * sin(theta)) +end + +function control_point(xi, xj, yi, yj, dist_from_mid) + xmid = 0.5 * (xi + xj) + ymid = 0.5 * (yi + yj) + + # get the angle of y relative to x + theta = atan((yj - yi) / (xj - xi)) + 0.5pi + + # dist = sqrt((xj-xi)^2 + (yj-yi)^2) + # dist_from_mid = curvature_scalar * 0.5dist + + # now we have polar coords, we can compute the position, adding to the midpoint + (xmid + dist_from_mid * cos(theta), ymid + dist_from_mid * sin(theta)) +end + +function annotation_extent(p, annotation; width_scalar = 0.06, height_scalar = 0.096) + str = string(annotation[3]) + position = annotation[1:2] + plot_size = get(p, :size, (600, 400)) + fontsize = annotation[4] + xextent_length = width_scalar * (600 / plot_size[1]) * fontsize * length(str)^0.8 + xextent = [position[1] - xextent_length, position[1] + xextent_length] + yextent_length = height_scalar * (400 / plot_size[2]) * fontsize + yextent = [position[2] - yextent_length, position[2] + yextent_length] + + [xextent, yextent] +end + +clockwise_difference(angle1, angle2) = pi - abs(abs(angle1 - angle2) - pi) + +function clockwise_mean(angles) + if clockwise_difference(angles[2], angles[1]) > angles[2] - angles[1] + return mean(angles) + pi + else + return mean(angles) + end +end + +""" + unoccupied_angle(x1, y1, x, y) + +Starting from the point [x1,y1], find the angle theta such that a line leaving at an angle +theta will have maximum distance from the points [x[i],y[i]] +""" +function unoccupied_angle(x1, y1, x, y) + @assert length(x) == length(y) + + if length(x) == 1 + return atan(y[1] - y1, x[1] - x1) + pi + end + + max_range = zeros(2) + # Calculate all angles between the point [x1,y1] and all points [x[i],y[i]], make sure + # that all of the angles are between 0 and 2pi + angles = [atan(y[i] - y1, x[i] - x1) for i ∈ 1:length(x)] + for i ∈ 1:length(angles) + if angles[i] < 0 + angles[i] += 2pi + end + end + # Sort all of the angles and calculate which two angles subtend the largest gap. + sort!(angles) + max_range .= [angles[end], angles[1]] + for i ∈ 2:length(x) + if ( + clockwise_difference(angles[i], angles[i - 1]) > + clockwise_difference(max_range[2], max_range[1]) + ) + max_range .= [angles[i - 1], angles[i]] + end + end + # Return the angle that is in the middle of the two angles subtending the largest + # empty angle. + clockwise_mean(max_range) +end + +function process_edge_attribute(attr, source, destiny, weights) + if isnothing(attr) || (attr isa Symbol) + return attr + elseif attr isa Graphs.AbstractGraph + mat = incidence_matrix(attr) + attr = [mat[si, di] for (si, di) ∈ zip(source, destiny)][:] |> permutedims + elseif attr isa Function + attr = + [ + attr(si, di, wi) for + (i, (si, di, wi)) ∈ enumerate(zip(source, destiny, weights)) + ][:] |> permutedims + elseif attr isa Dict + attr = [attr[(si, di)] for (si, di) ∈ zip(source, destiny)][:] |> permutedims + elseif all(size(attr) .!= 1) + attr = [attr[si, di] for (si, di) ∈ zip(source, destiny)][:] |> permutedims + end + attr +end +# Function from Plots/src/components.jl +"get an array of tuples of points on a circle with radius `r`" +function partialcircle(start_θ, end_θ, n = 20, r = 1) + Tuple{Float64,Float64}[ + (r * cos(u), r * sin(u)) for u ∈ range(start_θ, stop = end_θ, length = n) + ] +end + +function partialcircle(start_θ, end_θ, circle_center::Array{T,1}, n = 20, r = 1) where {T} + Tuple{Float64,Float64}[ + (r * cos(u) + circle_center[1], r * sin(u) + circle_center[2]) for + u ∈ range(start_θ, stop = end_θ, length = n) + ] +end + +function partialellipse(start_θ, end_θ, n = 20, major_axis = 2, minor_axis = 1) + Tuple{Float64,Float64}[ + (major_axis * cos(u), minor_axis * sin(u)) for + u ∈ range(start_θ, stop = end_θ, length = n) + ] +end + +function partialellipse( + start_θ, + end_θ, + ellipse_center::Array{T,1}, + n = 20, + major_axis = 2, + minor_axis = 1, +) where {T} + Tuple{Float64,Float64}[ + (major_axis * cos(u) + ellipse_center[1], minor_axis * sin(u) + ellipse_center[2]) + for u ∈ range(start_θ, stop = end_θ, length = n) + ] +end + +# for chord diagrams: +function arcshape(θ1, θ2) + vcat(partialcircle(θ1, θ2, 15, 1.05), reverse(partialcircle(θ1, θ2, 15, 0.95))) +end + +# x and y limits for arc diagram () +function arcdiagram_limits(x, source, destiny) + @assert length(x) >= 2 + margin = abs(0.1 * (x[2] - x[1])) + xmin, xmax = extrema(x) + r = abs(0.5 * (xmax - xmin)) + mean_upside = mean(source .< destiny) + ylims = if mean_upside == 1.0 + (-margin, r + margin) + elseif mean_upside == 0.0 + (-r - margin, margin) + else + (-r - margin, r + margin) + end + (xmin - margin, xmax + margin), ylims +end + +function islabel(item) + ismissing(item) && return false + ((item isa AbstractFloat) && isnan(item)) && return false + !in(item, (nothing, false, "")) +end + +function replacement_kwarg(sym, name, plotattributes, graph_aliases) + replacement = name + for alias ∈ graph_aliases[sym] + if haskey(plotattributes, alias) + replacement = plotattributes[alias] + end + end + replacement +end + +macro process_aliases(plotattributes, graph_aliases) + ex = Expr(:block) + attributes = getfield(__module__, graph_aliases) |> keys + ex.args = [ + Expr( + :(=), + esc(sym), + :($(esc(replacement_kwarg))( + $(QuoteNode(sym)), + $(esc(sym)), + $(esc(plotattributes)), + $(esc(graph_aliases)), + )), + ) for sym ∈ attributes + ] + ex +end + +remove_aliases!(sym, plotattributes, graph_aliases) = + for alias ∈ graph_aliases[sym] + if haskey(plotattributes, alias) + delete!(plotattributes, alias) + end + end + +# From Plots/src/utils.jl +isnothing(x::Nothing) = true +isnothing(x) = false + +# From Plots/src/Plots.jl +ignorenan_extrema(x) = Base.extrema(x) +# From Plots/src/utils.jl +ignorenan_extrema(x::AbstractArray{F}) where {F<:AbstractFloat} = NaNMath.extrema(x) +# From Plots/src/components.jl +function extrema_plus_buffer(v, buffmult = 0.2) + vmin, vmax = extrema(v) + vdiff = vmax - vmin + zero_buffer = vdiff == 0 ? 1.0 : 0.0 + buffer = (vdiff + zero_buffer) * buffmult + vmin - buffer, vmax + buffer +end diff --git a/GraphRecipes/test/functions.jl b/GraphRecipes/test/functions.jl new file mode 100644 index 000000000..bb2a9bb42 --- /dev/null +++ b/GraphRecipes/test/functions.jl @@ -0,0 +1,283 @@ +using Plots +using StableRNGs +using GraphRecipes +using GraphRecipes.Colors +using GraphRecipes.AbstractTrees +function random_labelled_graph() + n = 15 + rng = StableRNG(1) + A = Float64[rand(rng) < 0.5 ? 0 : rand(rng) for i ∈ 1:n, j ∈ 1:n] + for i ∈ 1:n + A[i, 1:(i - 1)] = A[1:(i - 1), i] + A[i, i] = 0 + end + x = rand(rng, n) + y = rand(rng, n) + z = rand(rng, n) + p = graphplot( + A, + nodesize = 0.2, + node_weights = 1:n, + nodecolor = range(colorant"yellow", stop = colorant"red", length = n), + names = 1:n, + fontsize = 10, + linecolor = :darkgrey, + layout_kw = Dict(:x => x, :y => y), + rng = rng, + ) + p, n, A, x, y, z +end + +function random_3d_graph() + n, A, x, y, z = random_labelled_graph()[2:end] + graphplot( + A, + node_weights = 1:n, + markercolor = :darkgray, + dim = 3, + markersize = 5, + markershape = :circle, + linecolor = :darkgrey, + linealpha = 0.5, + layout_kw = Dict(:x => x, :y => y, :z => z), + rng = StableRNG(1), + ) +end + +function light_graphs() + g = wheel_graph(10) + graphplot(g, curves = false, rng = StableRNG(1)) +end + +function directed() + g = [ + 0 1 1 + 0 0 1 + 0 1 0 + ] + graphplot(g, names = 1:3, curvature_scalar = 0.1, rng = StableRNG(1)) +end + +function edgelabel() + n = 8 + g = wheel_digraph(n) + edgelabel_dict = Dict() + for i ∈ 1:n + for j ∈ 1:n + edgelabel_dict[(i, j)] = string("edge ", i, " to ", j) + end + end + + graphplot( + g, + names = 1:n, + edgelabel = edgelabel_dict, + curves = false, + nodeshape = :rect, + rng = StableRNG(1), + ) +end + +function selfedges() + g = [ + 1 1 1 + 0 0 1 + 0 0 1 + ] + graphplot(DiGraph(g), self_edge_size = 0.2, rng = StableRNG(1)) +end + +multigraphs() = graphplot( + [[1, 1, 2, 2], [1, 1, 1], [1]], + names = "node_" .* string.(1:3), + nodeshape = :circle, + self_edge_size = 0.25, + rng = StableRNG(1), +) + +function arc_chord_diagrams() + rng = StableRNG(1) + adjmat = Symmetric(sparse(rand(rng, 0:1, 8, 8))) + plot( + graphplot( + adjmat, + method = :chorddiagram, + names = [text(string(i), 8) for i ∈ 1:8], + linecolor = :black, + fillcolor = :lightgray, + rng = rng, + ), + graphplot( + adjmat, + method = :arcdiagram, + markersize = 0.5, + markershape = :circle, + linecolor = :black, + markercolor = :black, + rng = rng, + ), + ) +end + +function marker_properties() + N = 8 + seed = 42 + rng = StableRNG(seed) + g = barabasi_albert(N, 1; rng = rng) + weights = [length(neighbors(g, i)) for i ∈ 1:nv(g)] + graphplot( + g, + curvature_scalar = 0, + node_weights = weights, + nodesize = 0.25, + linecolor = :gray, + linewidth = 2.5, + nodeshape = :circle, + node_z = rand(rng, N), + markercolor = :viridis, + nodestrokewidth = 1.5, + markerstrokestyle = :solid, + markerstrokealpha = 1.0, + markerstrokecolor = :lightgray, + colorbar = true, + rng = rng, + ) +end + +function ast_example() + code = :(function mysum(list) + out = 0 + for value ∈ list + out += value + end + out + end) + plot( + code, + fontsize = 10, + shorten = 0.01, + axis_buffer = 0.15, + nodeshape = :rect, + size = (1000, 1000), + rng = StableRNG(1), + ) +end + +julia_type_tree() = plot( + AbstractFloat, + method = :tree, + fontsize = 10, + nodeshape = :ellipse, + size = (1000, 1000), + rng = StableRNG(1), +) + +AbstractTrees.children(d::Dict) = [p for p ∈ d] +AbstractTrees.children(p::Pair) = AbstractTrees.children(p[2]) +function AbstractTrees.printnode(io::IO, p::Pair) + str = + isempty(AbstractTrees.children(p[2])) ? string(p[1], ": ", p[2]) : + string(p[1], ": ") + print(io, str) +end + +function julia_dict_tree() + d = Dict(:a => 2, :d => Dict(:b => 4, :c => "Hello"), :e => 5.0) + plot( + TreePlot(d), + method = :tree, + fontsize = 10, + nodeshape = :ellipse, + size = (1000, 1000), + rng = StableRNG(1), + ) +end + +diamond_nodeshape(x_i, y_i, s) = + [(x_i + 0.5s * dx, y_i + 0.5s * dy) for (dx, dy) ∈ [(1, 1), (-1, 1), (-1, -1), (1, -1)]] + +function diamond_nodeshape_wh(x_i, y_i, h, w) + out = Tuple{Float64,Float64}[(-0.5, 0), (0, -0.5), (0.5, 0), (0, 0.5)] + map(out) do t + x = t[1] * h + y = t[2] * w + (x + x_i, y + y_i) + end +end + +function custom_nodeshapes_single() + rng = StableRNG(1) + g = rand(rng, 5, 5) + g[g .> 0.5] .= 0 + for i ∈ 1:5 + g[i, i] = 0 + end + graphplot(g, nodeshape = diamond_nodeshape, rng = rng) +end + +function custom_nodeshapes_various() + rng = StableRNG(1) + g = rand(rng, 5, 5) + g[g .> 0.5] .= 0 + for i ∈ 1:5 + g[i, i] = 0 + end + graphplot( + g, + nodeshape = [ + :circle, + diamond_nodeshape, + diamond_nodeshape_wh, + :hexagon, + diamond_nodeshape_wh, + ], + rng = rng, + ) +end + +function funky_edge_and_marker_args() + n = 5 + g = SimpleDiGraph(n) + + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + add_edge!(g, 4, 4) + add_edge!(g, 4, 5) + + curviness_matrix = zeros(n, n) + edgewidth_matrix = zeros(n, n) + edgestyle_dict = Dict() + for e ∈ edges(g) + curviness_matrix[e.src, e.dst] = 0.5sin(e.src) + edgewidth_matrix[e.src, e.dst] = 0.8e.dst + edgestyle_dict[(e.src, e.dst)] = e.src < 2.0 ? :solid : e.src > 3.0 ? :dash : :dot + end + edge_z_function = (s, d, w) -> curviness_matrix[s, d] + + graphplot( + g, + names = [" I ", " am ", " a ", "funky", "graph"], + x = [1, 2, 3, 4, 5], + y = [5, 4, 3, 2, 1], + nodesize = 0.3, + size = (1000, 1000), + axis_buffer = 0.6, + fontsize = 16, + self_edge_size = 1.3, + curvature_scalar = curviness_matrix, + edgestyle = edgestyle_dict, + edgewidth = edgewidth_matrix, + edge_z = edge_z_function, + nodeshape = :circle, + node_z = [1, 2, 3, 4, 5], + nodestroke_z = [5, 4, 3, 2, 1], + edgecolor = :viridis, + markercolor = :viridis, + nodestrokestyle = [:dash, :solid, :dot], + nodestrokewidth = 6, + linewidth = 2, + colorbar = true, + rng = StableRNG(1), + ) +end diff --git a/GraphRecipes/test/parse_readme.jl b/GraphRecipes/test/parse_readme.jl new file mode 100644 index 000000000..cd0ebd10f --- /dev/null +++ b/GraphRecipes/test/parse_readme.jl @@ -0,0 +1,21 @@ +using Markdown +using GraphRecipes +using Plots + +cd(@__DIR__) + +readme = read("../README.md", String) |> Markdown.parse +content = readme.content + +code_blocks = [] +for paragraph ∈ content + if paragraph isa Markdown.Code + push!(code_blocks, paragraph.code) + end +end + +# Parse the code examples on the README into expressions. Ignore the first one, which is +# the installation instructions. +readme_exprs = [Meta.parse("begin $(code_blocks[i]) end") for i ∈ 2:length(code_blocks)] + +julia_logo_pun() = eval(readme_exprs[1]) diff --git a/GraphRecipes/test/pkg_deps.jl b/GraphRecipes/test/pkg_deps.jl new file mode 100644 index 000000000..6e08a4750 --- /dev/null +++ b/GraphRecipes/test/pkg_deps.jl @@ -0,0 +1,116 @@ + +module PkgDeps + +using GraphRecipes + +# const _pkgs = Pkg.available() +# const _idxmap = Dict(p=>i for (i,p) in enumerate(_pkgs)) +# const _alist = [Int[] for i=1:length(_pkgs)] + +# for pkg in _pkgs +# i = _idxmap[pkg] +# for dep in Pkg.dependents(pkg) +# if !haskey(_idxmap, dep) +# push!(_pkgs, dep) +# push!(_alist, []) +# _idxmap[dep] = length(_pkgs) +# end +# j = _idxmap[dep] +# push!(_alist[j], i) +# end +# end + +@userplot DepsGraph +@recipe function f(g::DepsGraph) + source, destiny, names = g.args + arrow --> arrow() + markersize --> 50 + markeralpha --> 0.2 + linealpha --> 0.4 + linewidth --> 2 + names --> names + func --> :tree + root --> :left + GraphRecipes.GraphPlot((source, destiny)) +end + +# const args = (source, destiny, pkgs) + +const all_pkgs = Pkg.available() +@show all_pkgs +const deplists = Dict(pkg => Pkg.dependents(pkg) for pkg ∈ all_pkgs) +@show deplists + +const childlists = Dict(pkg => Set{String}() for pkg ∈ all_pkgs) +for pkg ∈ all_pkgs + for dep ∈ deplists[pkg] + if haskey(childlists, dep) + push!(childlists[dep], pkg) + else + warn("Package $dep wasn't in Pkg.available()") + deplists[dep] = [] + childlists[dep] = Set([pkg]) + end + end +end +@show childlists + +function add_deps(pkg, deps = Set([pkg])) + for dep ∈ deplists[pkg] + if !(dep in deps) + push!(deps, dep) + add_deps(dep, deps) + end + end + deps +end + +function add_children(pkg, children = Set([pkg])) + for child ∈ childlists[pkg] + if !(child in children) + push!(children, child) + add_children(child, children) + end + end + children +end + +function plotdeps(pkg) + pkgs = unique(union(add_deps(pkg), add_children(pkg))) + idxmap = Dict(p => i for (i, p) ∈ enumerate(pkgs)) + + source, destiny = Int[], Int[] + for pkg ∈ pkgs + i = idxmap[pkg] + for dep ∈ deplists[pkg] + # if !haskey(_idxmap, dep) + # push!(pkgs, dep) + # push!(_alist, []) + # _idxmap[dep] = length(pkgs) + # end + if !haskey(idxmap, dep) + warn("missing: ", dep) + continue + end + j = idxmap[dep] + push!(source, j) + push!(destiny, i) + # push!(_alist[j], i) + end + end + depsgraph(source, destiny, pkgs, root = :bottom) +end + +# # pkgs = Set([pkg]) +# idx = _idxmap[pkg] +# source, destiny = Int[], Int[] +# for j in _alist[i] +# push!(pkgs, _pkgs[j]) +# push!(source, j) +# push!(destiny, i) +# end + +# to use: +# depsgraph(PkgDeps.args...) + +end # module diff --git a/GraphRecipes/test/runtests.jl b/GraphRecipes/test/runtests.jl new file mode 100644 index 000000000..5e83a161f --- /dev/null +++ b/GraphRecipes/test/runtests.jl @@ -0,0 +1,187 @@ +using VisualRegressionTests +using AbstractTrees +using LinearAlgebra +using Logging +using GraphRecipes +using SparseArrays +using ImageMagick +using StableRNGs +using Graphs +using Plots +using Test +using Gtk # for popup + +import Plots: PlotsBase + +isci() = get(ENV, "CI", "false") == "true" +itol(tol = nothing) = something(tol, isci() ? 1e-3 : 1e-5) + +include("functions.jl") +include("parse_readme.jl") + +default(show = false, reuse = true) + +cd(joinpath(@__DIR__, "..", "assets")) do + @testset "FIGURES" begin + @plottest random_labelled_graph() "random_labelled_graph.png" popup = !isci() tol = + itol() + + @plottest random_3d_graph() "random_3d_graph.png" popup = !isci() tol = itol() + + @plottest light_graphs() "light_graphs.png" popup = !isci() tol = itol() + + @plottest directed() "directed.png" popup = !isci() tol = itol() + + @plottest marker_properties() "marker_properties.png" popup = !isci() tol = itol() + + @plottest edgelabel() "edgelabel.png" popup = !isci() tol = itol() + + @plottest selfedges() "selfedges.png" popup = !isci() tol = itol() + + @plottest multigraphs() "multigraphs.png" popup = !isci() tol = itol() + + @plottest arc_chord_diagrams() "arc_chord_diagrams.png" popup = !isci() tol = itol() + + @plottest ast_example() "ast_example.png" popup = !isci() tol = itol() + + @plottest julia_type_tree() "julia_type_tree.png" popup = !isci() tol = itol(2e-2) + @plottest julia_dict_tree() "julia_dict_tree.png" popup = !isci() tol = itol() + + @plottest funky_edge_and_marker_args() "funky_edge_and_marker_args.png" popup = + !isci() tol = itol() + + @plottest custom_nodeshapes_single() "custom_nodeshapes_single.png" popup = !isci() tol = + itol() + + @plottest custom_nodeshapes_various() "custom_nodeshapes_various.png" popup = + !isci() tol = itol() + end + + @testset "README" begin + @plottest julia_logo_pun() "readme_julia_logo_pun.png" popup = !isci() tol = itol() + end +end + +@testset "issues" begin + @testset "143" begin + g = SimpleGraph(7) + + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + @test g.ne == 2 + al = GraphRecipes.get_adjacency_list(g) + @test isempty(al[1]) + @test al[2] == [3] + @test al[3] == [2, 4] + @test al[4] == [3] + @test isempty(al[5]) + @test isempty(al[6]) + @test isempty(al[7]) + s, d, w = GraphRecipes.get_source_destiny_weight(al) + @test s == [2, 3, 3, 4] + @test d == [3, 2, 4, 3] + @test all(w .≈ 1) + + with_logger(ConsoleLogger(stderr, Logging.Debug)) do + pl = graphplot(g) + @test first(pl.series_list)[:extra_kwargs][:num_edges_nodes] == (2, 7) + + add_edge!(g, 6, 7) + @test g.ne == 3 + pl = graphplot(g) + @test first(pl.series_list)[:extra_kwargs][:num_edges_nodes] == (3, 7) + + # old behavior (see issue), can be recovered using `trim=true` + g = SimpleGraph(7) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + pl = graphplot(g; trim = true) + @test first(pl.series_list)[:extra_kwargs][:num_edges_nodes] == (2, 4) + end + end + + @testset "180" begin + rng = StableRNG(1) + mat = Symmetric(sparse(rand(rng, 0:1, 8, 8))) + graphplot(mat, method = :arcdiagram, rng = rng) + end +end + +@testset "utils.jl" begin + rng = StableRNG(1) + @test GraphRecipes.directed_curve(0.0, 1.0, 0.0, 1.0, rng = rng) == + GraphRecipes.directed_curve(0, 1, 0, 1, rng = rng) + + @test GraphRecipes.isnothing(nothing) == PlotsBase.isnothing(nothing) + @test GraphRecipes.isnothing(missing) == PlotsBase.isnothing(missing) + @test GraphRecipes.isnothing(NaN) == PlotsBase.isnothing(NaN) + @test GraphRecipes.isnothing(0) == PlotsBase.isnothing(0) + @test GraphRecipes.isnothing(1) == PlotsBase.isnothing(1) + @test GraphRecipes.isnothing(0.0) == PlotsBase.isnothing(0.0) + @test GraphRecipes.isnothing(1.0) == PlotsBase.isnothing(1.0) + + for (s, e) ∈ [(rand(rng), rand(rng)) for i ∈ 1:100] + @test GraphRecipes.partialcircle(s, e) == PlotsBase.partialcircle(s, e) + end + + @testset "nearest_intersection" begin + @test GraphRecipes.nearest_intersection(0, 0, 3, 3, [(1, 0), (0, 1)]) == + (0, 0, 0.5, 0.5) + @test GraphRecipes.nearest_intersection(1, 2, 1, 2, []) == (1, 2, 1, 2) + end + + @testset "unoccupied_angle" begin + @test GraphRecipes.unoccupied_angle(1, 1, [1, 1, 1, 1], [2, 0, 3, -1]) == 2pi + end + + @testset "islabel" begin + @test GraphRecipes.islabel("hi") + @test GraphRecipes.islabel(1) + @test !GraphRecipes.islabel(missing) + @test !GraphRecipes.islabel(NaN) + @test !GraphRecipes.islabel(false) + @test !GraphRecipes.islabel("") + end + + @testset "control_point" begin + @test GraphRecipes.control_point(0, 0, 6, 0, 4) == (4, 3) + end + + # TODO: Actually test that the aliases produce the same plots, rather than just + # checking that they don't error. Also, test all of the different aliases. + @testset "Aliases" begin + A = [1 0 1 0; 0 0 1 1; 1 1 1 1; 0 0 1 1] + graphplot(A, markercolor = :red, markershape = :rect, markersize = 0.5, rng = rng) + graphplot(A, nodeweights = 1:4, rng = rng) + graphplot(A, curvaturescalar = 0, rng = rng) + graphplot(A, el = Dict((1, 2) => ""), elb = true, rng = rng) + graphplot(A, ew = (s, d, w) -> 3, rng = rng) + graphplot(A, ses = 0.5, rng = rng) + end +end + +# ----------------------------------------- +# marginalhist + +# using Distributions +# n = 1000 +# x = rand(RNG, Gamma(2), n) +# y = -0.5x + randn(RNG, n) +# marginalhist(x, y) + +# ----------------------------------------- +# portfolio composition map + +# # fake data +# tickers = ["IBM", "Google", "Apple", "Intel"] +# N = 10 +# D = length(tickers) +# weights = rand(RNG, N, D) +# weights ./= sum(weights, 2) +# returns = sort!((1:N) + D*randn(RNG, N)) + +# # plot it +# portfoliocomposition(weights, returns, labels = tickers') + +# ----------------------------------------- +# diff --git a/StatsPlots/LICENSE.md b/StatsPlots/LICENSE.md new file mode 100644 index 000000000..6fed4c2e6 --- /dev/null +++ b/StatsPlots/LICENSE.md @@ -0,0 +1,22 @@ +The StatsPlots.jl package is licensed under the MIT "Expat" License: + +> Copyright (c) 2016: Thomas Breloff. +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/StatsPlots/Project.toml b/StatsPlots/Project.toml new file mode 100644 index 000000000..c0c4a6d62 --- /dev/null +++ b/StatsPlots/Project.toml @@ -0,0 +1,51 @@ +name = "StatsPlots" +uuid = "f3b207a7-027a-5e70-b257-86293d7955fd" +version = "1.0" + +[deps] +AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" +Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MultivariateStats = "6f286f6a-111f-5878-ab1e-185364afe411" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" + +[compat] +AbstractFFTs = "1.1" +Clustering = "0.13 - 0.15" +DataStructures = "0.17 - 0.18" +Distributions = "0.21 - 0.25" +Interpolations = "0.12 - 0.15" +KernelDensity = "0.5 - 0.6" +MultivariateStats = "0.9 - 0.10" +NaNMath = "1" +Observables = "0.3 - 0.5" +Plots = "2" +RecipesBase = "1" +RecipesPipeline = "1" +Reexport = "0.2, 1" +StatsBase = "0.32 - 0.34" +TableOperations = "1" +Tables = "1" +Widgets = "0.5 - 0.6" +julia = "1.10" + +[extras] +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test", "NaNMath", "StableRNGs"] diff --git a/StatsPlots/README.md b/StatsPlots/README.md new file mode 100644 index 000000000..39db1cd61 --- /dev/null +++ b/StatsPlots/README.md @@ -0,0 +1,516 @@ +# StatsPlots + +[![Build Status](https://travis-ci.org/JuliaPlots/StatsPlots.jl.svg?branch=master)](https://travis-ci.org/JuliaPlots/StatsPlots.jl) +[![Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://docs.juliaplots.org/latest/generated/statsplots/) +[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://julialang.zulipchat.com/#narrow/stream/236493-plots) + + +### Original author: Thomas Breloff (@tbreloff), maintained by the JuliaPlots members + +This package is a drop-in replacement for Plots.jl that contains many statistical recipes for concepts and types introduced in the JuliaStats organization. + +- Types: + - DataFrames + - Distributions +- Recipes: + - histogram/histogram2d + - groupedhist + - [boxplot](https://en.wikipedia.org/wiki/Box_plot) + - [dotplot](https://en.wikipedia.org/wiki/Dot_plot_(statistics)) + - [violin](https://en.wikipedia.org/wiki/Violin_plot) + - marginalhist + - corrplot/cornerplot + - [andrewsplot](https://en.wikipedia.org/wiki/Andrews_plot) + - errorline ([ribbon](https://ggplot2.tidyverse.org/reference/geom_ribbon.html), [stick](https://www.mathworks.com/help/matlab/ref/errorbar.html), [plume](https://www.e-education.psu.edu/files/meteo410/file/Plume.pdf)) + - MDS plot + - [qq-plot](https://en.wikipedia.org/wiki/Q%E2%80%93Q_plot) + +It is thus slightly less lightweight, but has more functionality. + + +Initialize: + +```julia +#]add StatsPlots # install the package if it isn't installed +using StatsPlots # no need for `using Plots` as that is reexported here +gr(size=(400,300)) +``` + +Table-like data structures, including `DataFrames`, `IndexedTables`, `DataStreams`, etc... (see [here](https://github.com/davidanthoff/IterableTables.jl) for an exhaustive list), are supported thanks to the macro `@df` which allows passing columns as symbols. Those columns can then be manipulated inside the `plot` call, like normal `Arrays`: +```julia +using DataFrames, IndexedTables +df = DataFrame(a = 1:10, b = 10 .* rand(10), c = 10 .* rand(10)) +@df df plot(:a, [:b :c], colour = [:red :blue]) +@df df scatter(:a, :b, markersize = 4 .* log.(:c .+ 0.1)) +t = table(1:10, rand(10), names = [:a, :b]) # IndexedTable +@df t scatter(2 .* :b) +``` + +Inside a `@df` macro call, the `cols` utility function can be used to refer to a range of columns: + +```julia +@df df plot(:a, cols(2:3), colour = [:red :blue]) +``` + +or to refer to a column whose symbol is represented by a variable: + +```julia +s = :b +@df df plot(:a, cols(s)) +``` + +`cols()` will refer to all columns of the data table. + +In case of ambiguity, symbols not referring to `DataFrame` columns must be escaped by `^()`: +```julia +df[:red] = rand(10) +@df df plot(:a, [:b :c], colour = ^([:red :blue])) +``` + +The `@df` macro plays nicely with the new syntax of the [Query.jl](https://github.com/davidanthoff/Query.jl) data manipulation package (v0.8 and above), in that a plot command can be added at the end of a query pipeline, without having to explicitly collect the outcome of the query first: + +```julia +using Query, StatsPlots +df |> + @filter(_.a > 5) |> + @map({_.b, d = _.c-10}) |> + @df scatter(:b, :d) +``` + +The `@df` syntax is also compatible with the Plots.jl grouping machinery: + +```julia +using RDatasets +school = RDatasets.dataset("mlmRev","Hsb82") +@df school density(:MAch, group = :Sx) +``` + +To group by more than one column, use a tuple of symbols: + +```julia +@df school density(:MAch, group = (:Sx, :Sector), legend = :topleft) +``` + +![grouped](https://user-images.githubusercontent.com/6333339/35101563-eacf9be4-fc57-11e7-88d3-db5bb47b08ac.png) + +To name the legend entries with custom or automatic names (i.e. `Sex = Male, Sector = Public`) use the curly bracket syntax `group = {Sex = :Sx, :Sector}`. Entries with `=` get the custom name you give, whereas entries without `=` take the name of the column. + +--- + +The old syntax, passing the `DataFrame` as the first argument to the `plot` call is no longer supported. + +--- + +## Visualizing a table interactively + +A GUI based on the Interact package is available to create plots from a table interactively, using any of the recipes defined below. This small app can be deployed in a Jupyter lab / notebook, Juno plot pane, a Blink window or in the browser, see [here](http://juliagizmos.github.io/Interact.jl/latest/deploying/) for instructions. + +```julia +import RDatasets +iris = RDatasets.dataset("datasets", "iris") +using StatsPlots, Interact +using Blink +w = Window() +body!(w, dataviewer(iris)) +``` + +![dataviewer](https://user-images.githubusercontent.com/6333339/43359702-abd82d74-929e-11e8-8fc9-b589287f1c23.png) + + +## marginalhist with DataFrames + +```julia +using RDatasets +iris = dataset("datasets","iris") +@df iris marginalhist(:PetalLength, :PetalWidth) +``` + +![marginalhist](https://user-images.githubusercontent.com/6333339/29869938-fbe08d02-8d7c-11e7-9409-ca47ee3aaf35.png) + +--- + +## marginalscatter with DataFrames + +```julia +using RDatasets +iris = dataset("datasets","iris") +@df iris marginalscatter(:PetalLength, :PetalWidth) +``` + +![marginalscatter](https://user-images.githubusercontent.com/12200202/92408723-3aa78e00-f0f3-11ea-8ddc-9517f0f58207.png) + +--- + +## marginalkde + +```julia +x = randn(1024) +y = randn(1024) +marginalkde(x, x+y) +``` + +![correlated-marg](https://user-images.githubusercontent.com/90048/96789354-04804e00-13c3-11eb-82d3-6130e8c9d48a.png) + + +* `levels=N` can be used to set the number of contour levels (default 10); levels are evenly-spaced in the cumulative probability mass. +* `clip=((-xl, xh), (-yl, yh))` (default `((-3, 3), (-3, 3))`) can be used to adjust the bounds of the plot. Clip values are expressed as multiples of the `[0.16-0.5]` and `[0.5,0.84]` percentiles of the underlying 1D distributions (these would be 1-sigma ranges for a Gaussian). + + +## corrplot and cornerplot +This plot type shows the correlation among input variables. The marker color in scatter plots reveal the degree of correlation. Pass the desired colorgradient to `markercolor`. With the default gradient positive correlations are blue, neutral are yellow and negative are red. In the 2d-histograms the color gradient show the frequency of points in that bin (as usual controlled by `seriescolor`). + +```julia +gr(size = (600, 500)) +``` +then +```julia +@df iris corrplot([:SepalLength :SepalWidth :PetalLength :PetalWidth], grid = false) +``` +or also: +```julia +@df iris corrplot(cols(1:4), grid = false) +``` + +![corrplot](https://user-images.githubusercontent.com/8429802/51600111-8a771880-1f01-11e9-818f-6cbfc5efad74.png) + + +A correlation plot may also be produced from a matrix: + +```julia +M = randn(1000,4) +M[:,2] .+= 0.8sqrt.(abs.(M[:,1])) .- 0.5M[:,3] .+ 5 +M[:,3] .-= 0.7M[:,1].^2 .+ 2 +corrplot(M, label = ["x$i" for i=1:4]) +``` + +![](https://user-images.githubusercontent.com/8429802/51600126-91059000-1f01-11e9-9d37-f49bee5ff534.png) + +```julia +cornerplot(M) +``` + +![](https://user-images.githubusercontent.com/8429802/51600133-96fb7100-1f01-11e9-9943-4a10f1ad2907.png) + + +```julia +cornerplot(M, compact=true) +``` + +![](https://user-images.githubusercontent.com/8429802/51600140-9bc02500-1f01-11e9-87e3-746ae4daccbb.png) + +--- + +## boxplot, dotplot, and violin + +```julia +import RDatasets +singers = RDatasets.dataset("lattice", "singer") +@df singers violin(string.(:VoicePart), :Height, linewidth=0) +@df singers boxplot!(string.(:VoicePart), :Height, fillalpha=0.75, linewidth=2) +@df singers dotplot!(string.(:VoicePart), :Height, marker=(:black, stroke(0))) +``` + +![violin](https://user-images.githubusercontent.com/16589944/98538614-506c3780-228b-11eb-881c-158c2f781798.png) + +Asymmetric violin or dot plots can be created using the `side` keyword (`:both` - default,`:right` or `:left`), e.g.: + +```julia +singers_moscow = deepcopy(singers) +singers_moscow[:Height] = singers_moscow[:Height] .+ 5 +@df singers violin(string.(:VoicePart), :Height, side=:right, linewidth=0, label="Scala") +@df singers_moscow violin!(string.(:VoicePart), :Height, side=:left, linewidth=0, label="Moscow") +@df singers dotplot!(string.(:VoicePart), :Height, side=:right, marker=(:black,stroke(0)), label="") +@df singers_moscow dotplot!(string.(:VoicePart), :Height, side=:left, marker=(:black,stroke(0)), label="") +``` + +Dot plots can spread their dots over the full width of their column `mode = :uniform`, or restricted to the kernel density +(i.e. width of violin plot) with `mode = :density` (default). Horizontal position is random, so dots are repositioned +each time the plot is recreated. `mode = :none` keeps the dots along the center. + + +![violin2](https://user-images.githubusercontent.com/16589944/98538618-52ce9180-228b-11eb-83d2-6d7b7c89fd52.png) + +--- + +## Equal-area histograms + +The ea-histogram is an alternative histogram implementation, where every 'box' in +the histogram contains the same number of sample points and all boxes have the same +area. Areas with a higher density of points thus get higher boxes. This type of +histogram shows spikes well, but may oversmooth in the tails. The y axis is not +intuitively interpretable. + +```julia +a = [randn(100); randn(100) .+ 3; randn(100) ./ 2 .+ 3] +ea_histogram(a, bins = :scott, fillalpha = 0.4) +``` + +![equal area histogram](https://user-images.githubusercontent.com/8429802/29754490-8d1b01f6-8b86-11e7-9f86-e1063a88dfd8.png) + +--- + +## AndrewsPlot + +AndrewsPlots are a way to visualize structure in high-dimensional data by depicting each +row of an array or table as a line that varies with the values in columns. +https://en.wikipedia.org/wiki/Andrews_plot + +```julia +using RDatasets +iris = dataset("datasets", "iris") +@df iris andrewsplot(:Species, cols(1:4), legend = :topleft) +``` + +![iris_andrews_curve](https://user-images.githubusercontent.com/1159782/46241166-c392e800-c368-11e8-93de-125c6eb38b52.png) + +--- + +## ErrorLine +The ErrorLine function shows error distributions for lines plots in a variety of styles. + +```julia +x = 1:10 +y = fill(NaN, 10, 100, 3) +for i = axes(y,3) + y[:,:,i] = collect(1:2:20) .+ rand(10,100).*5 .* collect(1:2:20) .+ rand()*100 +end + +errorline(1:10, y[:,:,1], errorstyle=:ribbon, label="Ribbon") +errorline!(1:10, y[:,:,2], errorstyle=:stick, label="Stick", secondarycolor=:matched) +errorline!(1:10, y[:,:,3], errorstyle=:plume, label="Plume") +``` + +![ErrorLine Styles](https://user-images.githubusercontent.com/24966610/186655231-2b7b9e37-0beb-4796-ad08-cbb84020ffd8.svg) + +--- + +## Distributions + +```julia +using Distributions +plot(Normal(3,5), fill=(0, .5,:orange)) +``` + +![](https://cloud.githubusercontent.com/assets/933338/16718702/561510f6-46f0-11e6-834a-3cf17a5b77d6.png) + +```julia +dist = Gamma(2) +scatter(dist, leg=false) +bar!(dist, func=cdf, alpha=0.3) +``` + +![](https://cloud.githubusercontent.com/assets/933338/16718720/729b6fea-46f0-11e6-9bff-fdf2541ce305.png) + +### Quantile-Quantile plots + +The `qqplot` function compares the quantiles of two distributions, and accepts either a vector of sample values or a `Distribution`. The `qqnorm` is a shorthand for comparing a distribution to the normal distribution. If the distributions are similar the points will be on a straight line. + +```julia +x = rand(Normal(), 100) +y = rand(Cauchy(), 100) + +plot( + qqplot(x, y, qqline = :fit), # qqplot of two samples, show a fitted regression line + qqplot(Cauchy, y), # compare with a Cauchy distribution fitted to y; pass an instance (e.g. Normal(0,1)) to compare with a specific distribution + qqnorm(x, qqline = :R) # the :R default line passes through the 1st and 3rd quartiles of the distribution +) +``` +![skaermbillede 2017-09-28 kl 22 46 28](https://user-images.githubusercontent.com/8429802/30989741-0c4f9dac-a49f-11e7-98ff-028192a8d5b1.png) + +## Grouped Bar plots + +```julia +groupedbar(rand(10,3), bar_position = :stack, bar_width=0.7) +``` + +![tmp](https://cloud.githubusercontent.com/assets/933338/18962081/58a2a5e0-863d-11e6-8638-94f88ecc544d.png) + +This is the default: + +```julia +groupedbar(rand(10,3), bar_position = :dodge, bar_width=0.7) +``` + +![tmp](https://cloud.githubusercontent.com/assets/933338/18962092/673f6c78-863d-11e6-9ee9-8ca104e5d2a3.png) + +The `group` syntax is also possible in combination with `groupedbar`: + +```julia +ctg = repeat(["Category 1", "Category 2"], inner = 5) +nam = repeat("G" .* string.(1:5), outer = 2) + +groupedbar(nam, rand(5, 2), group = ctg, xlabel = "Groups", ylabel = "Scores", + title = "Scores by group and category", bar_width = 0.67, + lw = 0, framestyle = :box) +``` + +![](https://user-images.githubusercontent.com/6645258/32116755-b7018f02-bb2a-11e7-82c7-ca471ecaeecf.png) + +## Grouped Histograms + +``` +using RDatasets +iris = dataset("datasets", "iris") +@df iris groupedhist(:SepalLength, group = :Species, bar_position = :dodge) +``` +![dodge](https://user-images.githubusercontent.com/6033297/77240750-a11d0c00-6ba6-11ea-9715-81a8a7e20cd6.png) + +``` +@df iris groupedhist(:SepalLength, group = :Species, bar_position = :stack) +``` +![stack](https://user-images.githubusercontent.com/6033297/77240749-9c585800-6ba6-11ea-85ea-e023341cb246.png) + +## Dendrograms + +```julia +using Clustering +D = rand(10, 10) +D += D' +hc = hclust(D, linkage=:single) +plot(hc) +``` + +![dendrogram](https://user-images.githubusercontent.com/381464/43355211-855d5aa2-920d-11e8-82d7-2bf1a7aeccb5.png) + +The `branchorder=:optimal` option in `hclust()` can be used to minimize +the distance between neighboring leaves: + +```julia +using Clustering +using Distances +using StatsPlots +using Random + +n = 40 + +mat = zeros(Int, n, n) +# create banded matrix +for i in 1:n + last = minimum([i+Int(floor(n/5)), n]) + for j in i:last + mat[i,j] = 1 + end +end + +# randomize order +mat = mat[:, randperm(n)] +dm = pairwise(Euclidean(), mat, dims=2) + +# normal ordering +hcl1 = hclust(dm, linkage=:average) +plot( + plot(hcl1, xticks=false), + heatmap(mat[:, hcl1.order], colorbar=false, xticks=(1:n, ["$i" for i in hcl1.order])), + layout=grid(2,1, heights=[0.2,0.8]) + ) +``` + +![heatmap dendrogram non-optimal](https://user-images.githubusercontent.com/3502975/59949267-a1824e00-9440-11e9-96dd-4628a8372ae2.png) + +Compare to: + +```julia +# optimal ordering +hcl2 = hclust(dm, linkage=:average, branchorder=:optimal) +plot( + plot(hcl2, xticks=false), + heatmap(mat[:, hcl2.order], colorbar=false, xticks=(1:n, ["$i" for i in hcl2.order])), + layout=grid(2,1, heights=[0.2,0.8]) + ) +``` + +![heatmap dendrogram optimal](https://user-images.githubusercontent.com/3502975/59949464-20778680-9441-11e9-8ed7-9a639b50dfb2.png) + +### Dendrogram on the right side + +```julia +using Distances +using Clustering +using StatsBase +using StatsPlots + +pd=rand(Float64,16,7) + +dist_col=pairwise(CorrDist(),pd,dims=2) +hc_col=hclust(dist_col, branchorder=:optimal) +dist_row=pairwise(CorrDist(),pd,dims=1) +hc_row=hclust(dist_row, branchorder=:optimal) + +pdz=similar(pd) +for row in hc_row.order + pdz[row,hc_col.order]=zscore(pd[row,hc_col.order]) +end +nrows=length(hc_row.order) +rowlabels=(1:16)[hc_row.order] +ncols=length(hc_col.order) +collabels=(1:7)[hc_col.order] +l = grid(2,2,heights=[0.2,0.8,0.2,0.8],widths=[0.8,0.2,0.8,0.2]) +plot( + layout = l, + plot(hc_col,xticks=false), + plot(ticks=nothing,border=:none), + plot( + pdz[hc_row.order,hc_col.order], + st=:heatmap, + #yticks=(1:nrows,rowlabels), + yticks=(1:nrows,rowlabels), + xticks=(1:ncols,collabels), + xrotation=90, + colorbar=false + ), + plot(hc_row,yticks=false,xrotation=90,orientation=:horizontal,xlim=(0,1)) +) + +``` + +![heatmap with dendrograms on top and on the right](https://user-images.githubusercontent.com/13688320/224165246-bb3aba7d-5df2-47b5-9678-3384d13610fd.png) + + +## GroupedErrors.jl for population analysis + +Population analysis on a table-like data structures can be done using the highly recommended [GroupedErrors](https://github.com/piever/GroupedErrors.jl) package. + +This external package, in combination with StatsPlots, greatly simplifies the creation of two types of plots: + +### 1. Subject by subject plot (generally a scatter plot) + +Some simple summary statistics are computed for each experimental subject (mean is default but any scalar valued function would do) and then plotted against some other summary statistics, potentially splitting by some categorical experimental variable. + +### 2. Population plot (generally a ribbon plot in continuous case, or bar plot in discrete case) + +Some statistical analysis is computed at the single subject level (for example the density/hazard/cumulative of some variable, or the expected value of a variable given another) and the analysis is summarized across subjects (taking for example mean and s.e.m), potentially splitting by some categorical experimental variable. + + +For more information please refer to the [README](https://github.com/piever/GroupedErrors.jl/blob/master/README.md). + +A GUI based on QML and the GR Plots.jl backend to simplify the use of StatsPlots.jl and GroupedErrors.jl even further can be found [here](https://github.com/piever/PlugAndPlot.jl) (usable but still in alpha stage). + +## Ordinations + +MDS from [`MultivariateStats.jl`](https://github.com/JuliaStats/MultivariateStats.jl) +can be plotted as scatter plots. + +```julia +using MultivariateStats, RDatasets, StatsPlots + +iris = dataset("datasets", "iris") +X = convert(Matrix, iris[:, 1:4]) +M = fit(MDS, X'; maxoutdim=2) + +plot(M, group=iris.Species) +``` + +![MDS plot](https://user-images.githubusercontent.com/3502975/64883550-a6186600-d62d-11e9-8f6b-c5094abf5573.png) + +PCA will be added once the API in MultivariateStats is changed. +See https://github.com/JuliaStats/MultivariateStats.jl/issues/109 and https://github.com/JuliaStats/MultivariateStats.jl/issues/95. + + +## Covariance ellipses + +A 2×2 covariance matrix `Σ` can be plotted as an ellipse, which is a contour line of a Gaussian density function with variance `Σ`. +``` +covellipse([0,2], [2 1; 1 4], n_std=2, aspect_ratio=1, label="cov1") +covellipse!([1,0], [1 -0.5; -0.5 3], showaxes=true, label="cov2") +``` +![covariance ellipses](https://user-images.githubusercontent.com/4170948/84170978-f0c2f380-aa82-11ea-95de-ce2fe14e16ec.png) diff --git a/StatsPlots/src/StatsPlots.jl b/StatsPlots/src/StatsPlots.jl new file mode 100644 index 000000000..bfd7daef9 --- /dev/null +++ b/StatsPlots/src/StatsPlots.jl @@ -0,0 +1,55 @@ +module StatsPlots + +using Reexport +import RecipesBase: recipetype +import Tables +import TableOperations +using RecipesPipeline +@reexport using Plots +import Plots: _cycle +using StatsBase +using Distributions +using LinearAlgebra: eigen, diagm +using Widgets, Observables +import Observables: AbstractObservable, @map, observe +import Widgets: @nodeps +import DataStructures: OrderedDict +import Clustering: Hclust, nnodes +using Interpolations +using MultivariateStats: MultivariateStats +using AbstractFFTs: fft, ifft +import KernelDensity +using NaNMath +@recipe f(k::KernelDensity.UnivariateKDE) = k.x, k.density +@recipe f(k::KernelDensity.BivariateKDE) = k.x, k.y, permutedims(k.density) + +@shorthands cdensity + +export @df, dataviewer + +isvertical(plotattributes) = + let val = get(plotattributes, :orientation, missing) + val === missing || val in (:vertical, :v) + end + +include("df.jl") +include("interact.jl") +include("corrplot.jl") +include("cornerplot.jl") +include("distributions.jl") +include("boxplot.jl") +include("dotplot.jl") +include("violin.jl") +include("ecdf.jl") +include("hist.jl") +include("marginalhist.jl") +include("marginalscatter.jl") +include("marginalkde.jl") +include("bar.jl") +include("dendrogram.jl") +include("andrews.jl") +include("ordinations.jl") +include("covellipse.jl") +include("errorline.jl") + +end # module diff --git a/StatsPlots/src/andrews.jl b/StatsPlots/src/andrews.jl new file mode 100644 index 000000000..720de17cb --- /dev/null +++ b/StatsPlots/src/andrews.jl @@ -0,0 +1,63 @@ +@userplot AndrewsPlot + +""" + andrewsplot(args...; kw...) +Shows each row of an array (or table) as a line. The `x` argument specifies a +grouping variable. This is a way to visualize structure in high-dimensional data. +https://en.wikipedia.org/wiki/Andrews_plot +#Examples +```julia +using RDatasets, StatsPlots +iris = dataset("datasets", "iris") +@df iris andrewsplot(:Species, cols(1:4)) +``` +""" +andrewsplot + +@recipe function f(h::AndrewsPlot) + if length(h.args) == 2 # specify x if not given + x, y = h.args + else + y = h.args[1] + x = ones(size(y, 1)) + end + + seriestype := :andrews + + # series in a user recipe will have different colors + for g ∈ unique(x) + @series begin + label := "$g" + range(-π, stop = π, length = 200), Surface(y[g .== x, :]) #surface needed, or the array will be split into columns + end + end + nothing +end + +# the series recipe +@recipe function f(::Type{Val{:andrews}}, x, y, z) + y = y.surf + rows, cols = size(y) + seriestype := :path + + # these series are the lines, will keep the same colors + for j ∈ 1:rows + @series begin + primary := false + ys = zeros(length(x)) + terms = + [isodd(i) ? cos((i ÷ 2) .* ti) : sin((i ÷ 2) .* ti) for i ∈ 2:cols, ti ∈ x] + for ti ∈ eachindex(x) + ys[ti] = y[j, 1] / sqrt(2) + sum(y[j, i] .* terms[i - 1, ti] for i ∈ 2:cols) + end + + x := x + y := ys + () + end + end + + x := [] + y := [] + () +end diff --git a/StatsPlots/src/bar.jl b/StatsPlots/src/bar.jl new file mode 100644 index 000000000..ab02217f9 --- /dev/null +++ b/StatsPlots/src/bar.jl @@ -0,0 +1,97 @@ +@userplot GroupedBar + +recipetype(::Val{:groupedbar}, args...) = GroupedBar(args) + +PlotsBase.group_as_matrix(g::GroupedBar) = true + +grouped_xy(x::AbstractVector, y::AbstractArray) = x, y +grouped_xy(y::AbstractArray) = 1:size(y, 1), y + +@recipe function f(g::GroupedBar; spacing = 0) + x, y = grouped_xy(g.args...) + + nr, nc = size(y) + isstack = pop!(plotattributes, :bar_position, :dodge) === :stack + isylog = pop!(plotattributes, :yscale, :identity) ∈ (:log10, :log) + the_ylims = pop!(plotattributes, :ylims, (-Inf, Inf)) + + # extract xnums and set default bar width. + # might need to set xticks as well + xnums = if eltype(x) <: Number + xdiff = length(x) > 1 ? mean(diff(x)) : 1 + bar_width --> 0.8 * xdiff + x + else + bar_width --> 0.8 + ux = unique(x) + xnums = (1:length(ux)) .- 0.5 + xticks --> (xnums, ux) + xnums + end + @assert length(xnums) == nr + + # compute the x centers. for dodge, make a matrix for each column + x = if isstack + x + else + bws = plotattributes[:bar_width] / nc + bar_width := bws * clamp(1 - spacing, 0, 1) + xmat = zeros(nr, nc) + for r ∈ 1:nr + bw = _cycle(bws, r) + farleft = xnums[r] - 0.5 * (bw * nc) + for c ∈ 1:nc + xmat[r, c] = farleft + 0.5bw + (c - 1) * bw + end + end + xmat + end + + fill_bottom = if isylog + if isfinite(the_ylims[1]) + min(minimum(y) / 100, the_ylims[1]) + else + minimum(y) / 100 + end + else + 0 + end + # compute fillrange + y, fr = + isstack ? groupedbar_fillrange(y) : + (y, get(plotattributes, :fillrange, [fill_bottom])) + if isylog + replace!(fr, 0 => fill_bottom) + end + fillrange := fr + + seriestype := :bar + x, y +end + +function groupedbar_fillrange(y) + nr, nc = size(y) + # bar series fills from y[nr, nc] to fr[nr, nc], y .>= fr + fr = zeros(nr, nc) + y = copy(y) + y[.!isfinite.(y)] .= 0 + for r ∈ 1:nr + y_neg = 0 + # upper & lower bounds for positive bar + y_pos = sum([e for e ∈ y[r, :] if e > 0]) + # division subtract towards 0 + for c ∈ 1:nc + el = y[r, c] + if el >= 0 + y[r, c] = y_pos + y_pos -= el + fr[r, c] = y_pos + else + fr[r, c] = y_neg + y_neg += el + y[r, c] = y_neg + end + end + end + y, fr +end diff --git a/StatsPlots/src/boxplot.jl b/StatsPlots/src/boxplot.jl new file mode 100644 index 000000000..aa1eb4e47 --- /dev/null +++ b/StatsPlots/src/boxplot.jl @@ -0,0 +1,259 @@ + +# --------------------------------------------------------------------------- +# Box Plot + +notch_width(q2, q4, N) = 1.58 * (q4 - q2) / sqrt(N) + +@recipe function f( + ::Type{Val{:boxplot}}, + x, + y, + z; + notch = false, + whisker_range = 1.5, + outliers = true, + whisker_width = :half, + sort_labels_by = identity, + xshift = 0.0, +) + # if only y is provided, then x will be UnitRange 1:size(y,2) + if typeof(x) <: AbstractRange + x = if step(x) == first(x) == 1 + plotattributes[:series_plotindex] + else + [getindex(x, plotattributes[:series_plotindex])] + end + end + xsegs, ysegs = Plots.PlotsBase.Segments(), Plots.PlotsBase.Segments() + texts = String[] + glabels = sort(collect(unique(x))) + warning = false + outliers_x, outliers_y = zeros(0), zeros(0) + bw = plotattributes[:bar_width] + isnothing(bw) && (bw = 0.8) + @assert whisker_width === :match || whisker_width == :half || whisker_width >= 0 "whisker_width must be :match, :half, or a positive number" + ww = whisker_width === :match ? bw : whisker_width == :half ? bw / 2 : whisker_width + for (i, glabel) ∈ enumerate(sort(glabels; by = sort_labels_by)) + # filter y + values = y[filter(i -> _cycle(x, i) == glabel, 1:length(y))] + + # compute quantiles + q1, q2, q3, q4, q5 = quantile(values, range(0, stop = 1, length = 5)) + + # notch + n = notch_width(q2, q4, length(values)) + + # warn on inverted notches? + if notch && !warning && ((q2 > (q3 - n)) || (q4 < (q3 + n))) + @warn("Boxplot's notch went outside hinges. Set notch to false.") + warning = true # Show the warning only one time + end + + # make the shape + center = PlotsBase.discrete_value!(plotattributes, :x, glabel)[1] + xshift + hw = 0.5_cycle(bw, i) # Box width + HW = 0.5_cycle(ww, i) # Whisker width + l, m, r = center - hw, center, center + hw + lw, rw = center - HW, center + HW + + # internal nodes for notches + L, R = center - 0.5 * hw, center + 0.5 * hw + + # outliers + if Float64(whisker_range) != 0.0 # if the range is 0.0, the whiskers will extend to the data + limit = whisker_range * (q4 - q2) + inside = Float64[] + for value ∈ values + if (value < (q2 - limit)) || (value > (q4 + limit)) + if outliers + push!(outliers_y, value) + push!(outliers_x, center) + end + else + push!(inside, value) + end + end + # change q1 and q5 to show outliers + # using maximum and minimum values inside the limits + q1, q5 = PlotsBase.ignorenan_extrema(inside) + q1, q5 = (min(q1, q2), max(q4, q5)) # whiskers cannot be inside the box + end + # Box + push!(xsegs, m, lw, rw, m, m) # lower T + push!(ysegs, q1, q1, q1, q1, q2) # lower T + push!( + texts, + "Lower fence: $q1", + "Lower fence: $q1", + "Lower fence: $q1", + "Lower fence: $q1", + "Q1: $q2", + "", + ) + + if notch + push!(xsegs, r, r, R, L, l, l, r, r) # lower box + push!(xsegs, r, r, l, l, L, R, r, r) # upper box + + push!(ysegs, q2, q3 - n, q3, q3, q3 - n, q2, q2, q3 - n) # lower box + push!( + texts, + "Q1: $q2", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Q1: $q2", + "Q1: $q2", + "Median: $q3 ± $n", + "", + ) + + push!(ysegs, q3 + n, q4, q4, q3 + n, q3, q3, q3 + n, q4) # upper box + push!( + texts, + "Median: $q3 ± $n", + "Q3: $q4", + "Q3: $q4", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Median: $q3 ± $n", + "Q3: $q4", + "", + ) + else + push!(xsegs, r, r, l, l, r, r) # lower box + push!(xsegs, r, l, l, r, r, m) # upper box + push!(ysegs, q2, q3, q3, q2, q2, q3) # lower box + push!( + texts, + "Q1: $q2", + "Median: $q3", + "Median: $q3", + "Q1: $q2", + "Q1: $q2", + "Median: $q3", + "", + ) + push!(ysegs, q4, q4, q3, q3, q4, q4) # upper box + push!( + texts, + "Q3: $q4", + "Q3: $q4", + "Median: $q3", + "Median: $q3", + "Q3: $q4", + "Q3: $q4", + "", + ) + end + + push!(xsegs, m, lw, rw, m, m) # upper T + push!(ysegs, q5, q5, q5, q5, q4) # upper T + push!( + texts, + "Upper fence: $q5", + "Upper fence: $q5", + "Upper fence: $q5", + "Upper fence: $q5", + "Q3: $q4", + "", + ) + end + + if !isvertical(plotattributes) + # We should draw the plot horizontally! + xsegs, ysegs = ysegs, xsegs + outliers_x, outliers_y = outliers_y, outliers_x + + # Now reset the orientation, so that the axes limits are set correctly. + orientation := default(:orientation) + end + + @series begin + # To prevent linecolor equal to fillcolor (It makes the median visible) + if plotattributes[:linecolor] == plotattributes[:fillcolor] + plotattributes[:linecolor] = plotattributes[:markerstrokecolor] + end + primary := true + seriestype := :shape + x := xsegs.pts + y := ysegs.pts + () + end + + # Outliers + if outliers && !isempty(outliers) + @series begin + primary := false + seriestype := :scatter + if get!(plotattributes, :markershape, :circle) === :none + plotattributes[:markershape] = :circle + end + + fillrange := nothing + x := outliers_x + y := outliers_y + () + end + end + + # Hover + primary := false + seriestype := :path + marker := false + if PlotsBase.is_attr_supported(PlotsBase.backend(), :hover) + hover := texts + end + linewidth := 0 + x := xsegs.pts + y := ysegs.pts + () +end + +PlotsBase.@deps boxplot shape scatter + +# ------------------------------------------------------------------------------ +# Grouped Boxplot + +@userplot GroupedBoxplot + +recipetype(::Val{:groupedboxplot}, args...) = GroupedBoxplot(args) + +@recipe function f(g::GroupedBoxplot; spacing = 0.1) + x, y = grouped_xy(g.args...) + + # extract xnums and set default bar width. + # might need to set xticks as well + ux = unique(x) + x = if eltype(x) <: Number + bar_width --> (0.8 * mean(diff(sort(ux)))) + float.(x) + else + bar_width --> 0.8 + xnums = [findfirst(isequal(xi), ux) for xi ∈ x] .- 0.5 + xticks --> (eachindex(ux) .- 0.5, ux) + xnums + end + + # shift x values for each group + group = get(plotattributes, :group, nothing) + if group != nothing + gb = RecipesPipeline._extract_group_attributes(group) + labels, idxs = getfield(gb, 1), getfield(gb, 2) + n = length(labels) + bws = plotattributes[:bar_width] / n + bar_width := bws * clamp(1 - spacing, 0, 1) + for i ∈ 1:n + groupinds = idxs[i] + Δx = _cycle(bws, i) * (i - (n + 1) / 2) + x[groupinds] .+= Δx + end + end + + seriestype := :boxplot + x, y +end + +PlotsBase.@deps groupedboxplot boxplot diff --git a/StatsPlots/src/cornerplot.jl b/StatsPlots/src/cornerplot.jl new file mode 100644 index 000000000..01da3f649 --- /dev/null +++ b/StatsPlots/src/cornerplot.jl @@ -0,0 +1,120 @@ +@userplot CornerPlot + +recipetype(::Val{:cornerplot}, args...) = CornerPlot(args) + +@recipe function f(cp::CornerPlot; compact = false, maxvariables = 30, histpct = 0.1) + mat = cp.args[1] + C = cor(mat) + @assert typeof(mat) <: AbstractMatrix + N = size(mat, 2) + if N > maxvariables + error( + "Requested to plot $N variables in $(N^2) subplots! Likely, the first input needs transposing, otherwise increase maxvariables.", + ) + end + + # k is the number of rows/columns to hide + k = compact ? 1 : 0 + + # n is the total number of rows/columns. hists always shown + n = N + 1 - k + + labs = pop!(plotattributes, :label, ["x$i" for i ∈ 1:N]) + if labs != [""] && length(labs) != N + error("Number of labels not identical to number of datasets") + end + + # build a grid layout, where the histogram sizes are a fixed percentage, and we + scatterpcts = ones(n - 1) * (1 - histpct) / (n - 1) + g = grid( + n, + n, + widths = vcat(scatterpcts, histpct), + heights = vcat(histpct, scatterpcts), + ) + spidx = 1 + indices = zeros(Int, n, n) + for i ∈ 1:n, j ∈ 1:n + isblank = (i == 1 && j == n) || (compact && i > 1 && j < n && j >= i) + g[i, j].attr[:blank] = isblank + if !isblank + indices[i, j] = spidx + spidx += 1 + end + end + layout := g + + # some defaults + legend := false + foreground_color_border := nothing + margin --> 1mm + titlefont --> font(11) + fillcolor --> PlotsBase.fg_color(plotattributes) + linecolor --> PlotsBase.fg_color(plotattributes) + grid --> true + ticks := nothing + xformatter := x -> "" + yformatter := y -> "" + link := :both + grad = cgrad(get(plotattributes, :markercolor, :RdYlBu)) + + # figure out good defaults for scatter plot dots: + pltarea = 1 / (2n) + nsamples = size(mat, 1) + markersize --> clamp(pltarea * 800 / sqrt(nsamples), 1, 10) + markeralpha --> clamp(pltarea * 100 / nsamples^0.42, 0.005, 0.4) + + # histograms in the right column + for i ∈ 1:N + compact && i == 1 && continue + @series begin + orientation := :h + seriestype := :histogram + subplot := indices[i + 1 - k, n] + grid := false + view(mat, :, i) + end + end + + # histograms in the top row + for j ∈ 1:N + compact && j == N && continue + @series begin + seriestype := :histogram + subplot := indices[1, j] + grid := false + view(mat, :, j) + end + end + + # scatters + for i ∈ 1:N + vi = view(mat, :, i) + for j ∈ 1:N + # only the lower triangle + if compact && i <= j + continue + end + + vj = view(mat, :, j) + @series begin + ticks := :auto + if i == N + xformatter := :auto + xguide := _cycle(labs, j) + end + if j == 1 + yformatter := :auto + yguide := _cycle(labs, i) + end + seriestype := :scatter + subplot := indices[i + 1 - k, j] + markercolor := grad[0.5 + 0.5C[i, j]] + smooth --> true + markerstrokewidth --> 0 + vj, vi + end + end + # end + end +end diff --git a/StatsPlots/src/corrplot.jl b/StatsPlots/src/corrplot.jl new file mode 100644 index 000000000..01b8134e2 --- /dev/null +++ b/StatsPlots/src/corrplot.jl @@ -0,0 +1,121 @@ +""" + corrplot + +This plot type shows the correlation among input variables. +A correlation plot may be produced by a matrix. + + +A correlation matrix can also be created from the columns of a `DataFrame` +using the [`@df`](@ref) macro like so: + +```julia +@df iris corrplot([:SepalLength :SepalWidth :PetalLength :PetalWidth]) +``` + +The marker color in scatter plots reveals the degree of correlation. +Pass the desired colorgradient to `markercolor`. + +With the default gradient positive correlations are blue, neutral are yellow +and negative are red. In the 2d-histograms, the color gradient shows the frequency +of points in that bin (as usual, controlled by `seriescolor`). +""" +@userplot CorrPlot + +recipetype(::Val{:corrplot}, args...) = CorrPlot(args) + +""" + to_corrplot_matrix(mat) + +Transforms the input into a correlation plot matrix. +Meant to be overloaded by other types! +""" +to_corrplot_matrix(x) = x + +function update_ticks_guides(d::KW, labs, i, j, n) + # d[:title] = (i==1 ? _cycle(labs,j) : "") + # d[:xticks] = (i==n) + d[:xguide] = (i == n ? _cycle(labs, j) : "") + # d[:yticks] = (j==1) + d[:yguide] = (j == 1 ? _cycle(labs, i) : "") +end + +@recipe function f(cp::CorrPlot) + mat = to_corrplot_matrix(cp.args[1]) + n = size(mat, 2) + C = cor(mat) + labs = pop!(plotattributes, :label, [""]) + + link := :x # need custom linking for y + layout := (n, n) + legend := false + foreground_color_border := nothing + margin := 1mm + titlefont := font(11) + fillcolor --> PlotsBase.fg_color(plotattributes) + linecolor --> PlotsBase.fg_color(plotattributes) + markeralpha := 0.4 + grad = cgrad(get(plotattributes, :markercolor, :RdYlBu)) + indices = reshape(1:(n^2), n, n)' + title = get(plotattributes, :title, "") + title_location = get(plotattributes, :title_location, :center) + title := "" + + # histograms on the diagonal + for i ∈ 1:n + @series begin + if title != "" && title_location === :left && i == 1 + title := title + end + seriestype := :histogram + subplot := indices[i, i] + grid := false + xformatter --> ((i == n) ? :auto : (x -> "")) + yformatter --> ((i == 1) ? :auto : (y -> "")) + update_ticks_guides(plotattributes, labs, i, i, n) + view(mat, :, i) + end + end + + # scatters + for i ∈ 1:n + ylink := setdiff(vec(indices[i, :]), indices[i, i]) + vi = view(mat, :, i) + for j ∈ 1:n + j == i && continue + vj = view(mat, :, j) + subplot := indices[i, j] + update_ticks_guides(plotattributes, labs, i, j, n) + if i > j + #below diag... scatter + @series begin + seriestype := :scatter + markercolor := grad[0.5 + 0.5C[i, j]] + smooth := true + markerstrokewidth --> 0 + xformatter --> ((i == n) ? :auto : (x -> "")) + yformatter --> ((j == 1) ? :auto : (y -> "")) + vj, vi + end + else + #above diag... hist2d + @series begin + seriestype := get(plotattributes, :seriestype, :histogram2d) + if title != "" && + i == 1 && + ( + (title_location === :center && j == div(n, 2) + 1) || + (title_location === :right && j == n) + ) + if iseven(n) + title_location := :left + end + title := title + end + xformatter --> ((i == n) ? :auto : (x -> "")) + yformatter --> ((j == 1) ? :auto : (y -> "")) + vj, vi + end + end + end + end +end diff --git a/StatsPlots/src/covellipse.jl b/StatsPlots/src/covellipse.jl new file mode 100644 index 000000000..f131701fb --- /dev/null +++ b/StatsPlots/src/covellipse.jl @@ -0,0 +1,40 @@ +""" + covellipse(μ, Σ; showaxes=false, n_std=1, n_ellipse_vertices=100) + +Plot a confidence ellipse of the 2×2 covariance matrix `Σ`, centered at `μ`. +The ellipse is the contour line of a Gaussian density function with mean `μ` +and variance `Σ` at `n_std` standard deviations. +If `showaxes` is true, the two axes of the ellipse are also plotted. +""" +@userplot CovEllipse + +@recipe function f(c::CovEllipse; showaxes = false, n_std = 1, n_ellipse_vertices = 100) + μ, S = _covellipse_args(c.args; n_std = n_std) + + θ = range(0, 2π; length = n_ellipse_vertices) + A = S * [cos.(θ)'; sin.(θ)'] + + @series begin + seriesalpha --> 0.3 + Shape(μ[1] .+ A[1, :], μ[2] .+ A[2, :]) + end + showaxes && @series begin + label := false + linecolor --> "gray" + ([μ[1] + S[1, 1], μ[1], μ[1] + S[1, 2]], [μ[2] + S[2, 1], μ[2], μ[2] + S[2, 2]]) + end +end + +function _covellipse_args( + (μ, Σ)::Tuple{AbstractVector{<:Real},AbstractMatrix{<:Real}}; + n_std::Real, +) + size(μ) == (2,) && size(Σ) == (2, 2) || + error("covellipse requires mean of length 2 and covariance of size 2×2.") + λ, U = eigen(Σ) + μ, n_std * U * diagm(.√λ) +end +_covellipse_args(args; n_std) = error( + "Wrong inputs for covellipse: $(typeof.(args)). " * + "Expected real-valued vector μ, real-valued matrix Σ.", +) diff --git a/StatsPlots/src/dendrogram.jl b/StatsPlots/src/dendrogram.jl new file mode 100644 index 000000000..5f114ef4e --- /dev/null +++ b/StatsPlots/src/dendrogram.jl @@ -0,0 +1,54 @@ +function treepositions(hc::Hclust, useheight::Bool, orientation = :vertical) + order = StatsBase.indexmap(hc.order) + nodepos = Dict(-i => (float(order[i]), 0.0) for i ∈ hc.order) + + xs = Array{Float64}(undef, 4, size(hc.merges, 1)) + ys = Array{Float64}(undef, 4, size(hc.merges, 1)) + + for i ∈ 1:size(hc.merges, 1) + x1, y1 = nodepos[hc.merges[i, 1]] + x2, y2 = nodepos[hc.merges[i, 2]] + + xpos = (x1 + x2) / 2 + ypos = useheight ? hc.heights[i] : (max(y1, y2) + 1) + + nodepos[i] = (xpos, ypos) + xs[:, i] .= [x1, x1, x2, x2] + ys[:, i] .= [y1, ypos, ypos, y2] + end + if orientation === :horizontal + return ys, xs + else + return xs, ys + end +end + +@recipe function f(hc::Hclust; useheight = true, orientation = :vertical) + typeof(useheight) <: Bool || error("'useheight' argument must be true or false") + + legend --> false + linecolor --> :black + + if orientation === :horizontal + yforeground_color_axis --> :white + ygrid --> false + ylims --> (0.5, length(hc.order) + 0.5) + yticks --> (1:nnodes(hc), string.(1:nnodes(hc))[hc.order]) + if useheight + hs = sum(hc.heights) + xlims --> (0, hs + hs * 0.01) + else + xlims --> (0, Inf) + end + xshowaxis --> useheight + else + xforeground_color_axis --> :white + xgrid --> false + xlims --> (0.5, length(hc.order) + 0.5) + xticks --> (1:nnodes(hc), string.(1:nnodes(hc))[hc.order]) + ylims --> (0, Inf) + yshowaxis --> useheight + end + + treepositions(hc, useheight, orientation) +end diff --git a/StatsPlots/src/df.jl b/StatsPlots/src/df.jl new file mode 100644 index 000000000..f4df36d88 --- /dev/null +++ b/StatsPlots/src/df.jl @@ -0,0 +1,226 @@ +""" + `@df d x` + +Convert every symbol in the expression `x` with the respective column in `d` if it exists. + +If you want to avoid replacing the symbol, escape it with `^`. + +`NA` values are replaced with `NaN` for columns of `Float64` and `""` or `Symbol()` +for strings and symbols respectively. + +`x` can be either a plot command or a block of plot commands. +""" +macro df(d, x) + esc(Expr(:call, df_helper(x), d)) +end + +""" + `@df x` + +Curried version of `@df d x`. Outputs an anonymous function `d -> @df d x`. +""" +macro df(x) + esc(df_helper(x)) +end + +function df_helper(x) + i = gensym() + Expr(:(->), i, df_helper(i, x)) +end + +function df_helper(d, x) + if isa(x, Expr) && x.head === :block # meaning that there were multiple plot commands + commands = [ + df_helper(d, xx) for xx ∈ x.args if + !(isa(xx, Expr) && xx.head === :line || isa(xx, LineNumberNode)) + ] # apply the helper recursively + return Expr(:block, commands...) + + elseif isa(x, Expr) && x.head === :call # each function call is operated on alone + syms = Any[] + vars = Symbol[] + plot_call = parse_table_call!(d, x, syms, vars) + names = gensym() + compute_vars = Expr( + :(=), + Expr(:tuple, Expr(:tuple, vars...), names), + Expr(:call, :($(@__MODULE__).extract_columns_and_names), d, syms...), + ) + argnames = _argnames(names, x) + if (length(plot_call.args) >= 2) && + isa(plot_call.args[2], Expr) && + (plot_call.args[2].head === :parameters) + label_plot_call = Expr( + :call, + :($(@__MODULE__).add_label), + plot_call.args[2], + argnames, + plot_call.args[1], + plot_call.args[3:end]..., + ) + else + label_plot_call = + Expr(:call, :($(@__MODULE__).add_label), argnames, plot_call.args...) + end + return Expr(:block, compute_vars, label_plot_call) + + else + error("Second argument ($x) can only be a block or function call") + end +end + +parse_table_call!(d, x, syms, vars) = x + +function parse_table_call!(d, x::QuoteNode, syms, vars) + new_var = gensym(x.value) + push!(syms, x) + push!(vars, new_var) + return new_var +end + +function parse_table_call!(d, x::Expr, syms, vars) + if x.head === :. && length(x.args) == 2 + isa(x.args[2], QuoteNode) && return x + elseif x.head === :call + x.args[1] === :^ && length(x.args) == 2 && return x.args[2] + if x.args[1] === :cols + if length(x.args) == 1 + push!(x.args, :($(@__MODULE__).column_names($d))) + return parse_table_call!(d, x, syms, vars) + end + range = x.args[2] + new_vars = gensym("range") + push!(syms, range) + push!(vars, new_vars) + return new_vars + end + elseif x.head === :braces # From Query: use curly brackets to simplify writing named tuples + new_ex = Expr(:tuple, x.args...) + + for (j, field_in_NT) ∈ enumerate(new_ex.args) + if isa(field_in_NT, Expr) && field_in_NT.head === :(=) + new_ex.args[j] = Expr(:(=), field_in_NT.args...) + elseif field_in_NT isa QuoteNode + new_ex.args[j] = Expr(:(=), field_in_NT.value, field_in_NT) + elseif isa(field_in_NT, Expr) + new_ex.args[j] = Expr( + :(=), + Symbol(filter(t -> t != ':', string(field_in_NT))), + field_in_NT, + ) + elseif isa(field_in_NT, Symbol) + new_ex.args[j] = Expr(:(=), field_in_NT, field_in_NT) + end + end + return parse_table_call!(d, new_ex, syms, vars) + end + return Expr(x.head, (parse_table_call!(d, arg, syms, vars) for arg ∈ x.args)...) +end + +function column_names(t) + s = Tables.schema(t) + s === nothing ? propertynames(first(Tables.rows(t))) : s.names +end + +not_kw(x) = true +not_kw(x::Expr) = !(x.head in [:kw, :parameters]) + +function insert_kw!(x::Expr, s::Symbol, v) + index = isa(x.args[2], Expr) && x.args[2].head === :parameters ? 3 : 2 + x.args = vcat(x.args[1:(index - 1)], Expr(:kw, s, v), x.args[index:end]) +end + +function _argnames(names, x::Expr) + Expr(:vect, [_arg2string(names, s) for s ∈ x.args[2:end] if not_kw(s)]...) +end + +_arg2string(names, x) = stringify(x) +function _arg2string(names, x::Expr) + if x.head === :call && x.args[1] == :cols + return :($(@__MODULE__).compute_name($names, $(x.args[2]))) + elseif x.head === :call && x.args[1] == :hcat + return hcat(stringify.(x.args[2:end])...) + elseif x.head === :hcat + return hcat(stringify.(x.args)...) + else + return stringify(x) + end +end + +stringify(x) = filter(t -> t != ':', string(x)) + +compute_name(names, i::Int) = names[i] +compute_name(names, i::Symbol) = i +compute_name(names, i) = reshape([compute_name(names, ii) for ii ∈ i], 1, :) + +""" + add_label(argnames, f, args...; kwargs...) + +This function ensures that labels are passed to the plotting command, if it accepts them. + +If `f` does not accept keyword arguments, and `kwargs` is empty, it will only +forward `args...`. + +If the user has provided keyword arguments, but `f` does not accept them, +then it will error. +""" +function add_label(argnames, f, args...; kwargs...) + i = findlast(t -> isa(t, Expr) || isa(t, AbstractArray), argnames) + try + if (i === nothing) + return f(args...; kwargs...) + else + return f(label = stringify.(argnames[i]), args...; kwargs...) + end + catch e + if e isa MethodError || + (e isa ErrorException && occursin("does not accept keyword arguments", e.msg)) + # check if the user has supplied kwargs, then we need to rethrow the error + isempty(kwargs) || rethrow(e) + # transmit only args to `f` + return f(args...) + else + rethrow(e) + end + end +end + +get_col(s::Int, col_nt, names) = col_nt[names[s]] +get_col(s::Symbol, col_nt, names) = get(col_nt, s, s) +get_col(syms, col_nt, names) = hcat((get_col(s, col_nt, names) for s ∈ syms)...) + +# get the appropriate name when passed an Integer +add_sym!(cols, i::Integer, names) = push!(cols, names[i]) +# check for errors in Symbols +add_sym!(cols, s::Symbol, names) = s in names ? push!(cols, s) : cols +# recursively extract column names +function add_sym!(cols, s, names) + for si ∈ s + add_sym!(cols, si, names) + end + cols +end + +""" + extract_columns_and_names(df, syms...) + +Extracts columns and their names (if the column number is an integer) +into a slightly complex `Tuple`. + +The structure goes as `((columndata...), names)`. This is unpacked by the [`@df`](@ref) macro into `gensym`'ed variables, which are passed to the plotting function. + +!!! note + If you want to extend the [`@df`](@ref) macro + to work with your custom type, this is the + function you should overload! +""" +function extract_columns_and_names(df, syms...) + Tables.istable(df) || error("Only tables are supported") + names = column_names(df) + + # extract selected column names + selected_cols = add_sym!(Symbol[], syms, names) + + cols = Tables.columntable(TableOperations.select(df, unique(selected_cols)...)) + return Tuple(get_col(s, cols, names) for s ∈ syms), names +end diff --git a/StatsPlots/src/distributions.jl b/StatsPlots/src/distributions.jl new file mode 100644 index 000000000..6bbd25fc2 --- /dev/null +++ b/StatsPlots/src/distributions.jl @@ -0,0 +1,105 @@ + +# pick a nice default x range given a distribution +function default_range(dist::Distribution, alpha = 0.0001) + minval = isfinite(minimum(dist)) ? minimum(dist) : quantile(dist, alpha) + maxval = isfinite(maximum(dist)) ? maximum(dist) : quantile(dist, 1 - alpha) + minval, maxval +end + +function default_range(m::Distributions.UnivariateMixture, alpha = 0.0001) + mapreduce(_minmax, 1:Distributions.ncomponents(m)) do k + default_range(Distributions.component(m, k), alpha) + end +end + +_minmax((xmin, xmax), (ymin, ymax)) = (min(xmin, ymin), max(xmax, ymax)) + +yz_args(dist) = default_range(dist) +function yz_args(dist::DiscreteUnivariateDistribution) + minval, maxval = extrema(dist) + if isfinite(minval) && isfinite(maxval) # bounded + sup = support(dist) + return sup isa AbstractVector ? (sup,) : ([sup...],) + else # unbounded + return (UnitRange(promote(default_range(dist)...)...),) + end +end + +# this "user recipe" adds a default x vector based on the distribution's μ and σ +@recipe function f(dist::Distribution) + if dist isa DiscreteUnivariateDistribution + seriestype --> :sticks + end + (dist, yz_args(dist)...) +end + +@recipe function f(m::Distributions.UnivariateMixture; components = true) + if m isa DiscreteUnivariateDistribution + seriestype --> :sticks + end + if components + for k ∈ 1:Distributions.ncomponents(m) + c = Distributions.component(m, k) + @series begin + (c, yz_args(c)...) + end + end + else + (m, yz_args(m)...) + end +end + +@recipe function f(distvec::AbstractArray{<:Distribution}, yz...) + for di ∈ distvec + @series begin + seriesargs = isempty(yz) ? yz_args(di) : yz + if di isa DiscreteUnivariateDistribution + seriestype --> :sticks + end + (di, seriesargs...) + end + end +end + +# this "type recipe" replaces any instance of a distribution with a function mapping xi to yi +@recipe f(::Type{T}, dist::T; func = pdf) where {T<:Distribution} = xi -> func(dist, xi) + +#----------------------------------------------------------------------------- +# qqplots + +@recipe function f(h::QQPair; qqline = :identity) + if qqline in (:fit, :quantile, :identity, :R) + xs = [extrema(h.qx)...] + if qqline === :identity + ys = xs + elseif qqline === :fit + itc, slp = hcat(fill!(similar(h.qx), 1), h.qx) \ h.qy + ys = slp .* xs .+ itc + else # if qqline === :quantile || qqline == :R + quantx, quanty = quantile(h.qx, [0.25, 0.75]), quantile(h.qy, [0.25, 0.75]) + slp = diff(quanty) ./ diff(quantx) + ys = quanty .+ slp .* (xs .- quantx) + end + + @series begin + primary := false + seriestype := :path + xs, ys + end + end + + seriestype --> :scatter + legend --> false + h.qx, h.qy +end + +loc(D::Type{T}, x) where {T<:Distribution} = fit(D, x), x +loc(D, x) = D, x + +@userplot QQPlot +recipetype(::Val{:qqplot}, args...) = QQPlot(args) +@recipe f(h::QQPlot) = qqbuild(loc(h.args[1], h.args[2])...) + +@userplot QQNorm +recipetype(::Val{:qqnorm}, args...) = QQNorm(args) +@recipe f(h::QQNorm) = QQPlot((Normal, h.args[1])) diff --git a/StatsPlots/src/dotplot.jl b/StatsPlots/src/dotplot.jl new file mode 100644 index 000000000..0e9216fbe --- /dev/null +++ b/StatsPlots/src/dotplot.jl @@ -0,0 +1,116 @@ + +# --------------------------------------------------------------------------- +# Dot Plot (strip plot, beeswarm) + +@recipe function f(::Type{Val{:dotplot}}, x, y, z; mode = :density, side = :both) + # if only y is provided, then x will be UnitRange 1:size(y, 2) + if typeof(x) <: AbstractRange + if step(x) == first(x) == 1 + x = plotattributes[:series_plotindex] + else + x = [getindex(x, plotattributes[:series_plotindex])] + end + end + + grouplabels = sort(collect(unique(x))) + barwidth = plotattributes[:bar_width] + barwidth == nothing && (barwidth = 0.8) + + getoffsets(halfwidth, y) = + mode === :uniform ? (rand(length(y)) .* 2 .- 1) .* halfwidth : + mode === :density ? violinoffsets(halfwidth, y) : zeros(length(y)) + + points_x, points_y = zeros(0), zeros(0) + + for (i, grouplabel) ∈ enumerate(grouplabels) + # filter y + groupy = y[filter(i -> _cycle(x, i) == grouplabel, 1:length(y))] + + center = PlotsBase.discrete_value!(plotattributes, :x, grouplabel)[1] + halfwidth = 0.5_cycle(barwidth, i) + + offsets = getoffsets(halfwidth, groupy) + + if side === :left + offsets = -abs.(offsets) + elseif side === :right + offsets = abs.(offsets) + end + + append!(points_y, groupy) + append!(points_x, center .+ offsets) + end + + seriestype := :scatter + x := points_x + y := points_y + () +end + +PlotsBase.@deps dotplot scatter +PlotsBase.@shorthands dotplot + +function violinoffsets(maxwidth, y) + normalizewidths(maxwidth, widths) = + maxwidth * widths / PlotsBase.ignorenan_maximum(widths) + + function getlocalwidths(widths, centers, y) + upperbounds = + [violincenters[violincenters .> yval] for yval ∈ y] .|> findmin .|> first + lowercenters = findmax.([violincenters[violincenters .≤ yval] for yval ∈ y]) + lowerbounds, lowerindexes = first.(lowercenters), last.(lowercenters) + δs = (y .- lowerbounds) ./ (upperbounds .- lowerbounds) + + itp = interpolate(widths, BSpline(Quadratic(Reflect(OnCell())))) + localwidths = itp.(lowerindexes .+ δs) + end + + violinwidths, violincenters = violin_coords(y) + violinwidths = normalizewidths(maxwidth, violinwidths) + localwidths = getlocalwidths(violinwidths, violincenters, y) + offsets = (rand(length(y)) .* 2 .- 1) .* localwidths +end + +# ------------------------------------------------------------------------------ +# Grouped dotplot + +@userplot GroupedDotplot + +recipetype(::Val{:groupeddotplot}, args...) = GroupedDotplot(args) + +@recipe function f(g::GroupedDotplot; spacing = 0.1) + x, y = grouped_xy(g.args...) + + # extract xnums and set default bar width. + # might need to set xticks as well + ux = unique(x) + x = if eltype(x) <: Number + bar_width --> (0.8 * mean(diff(sort(ux)))) + float.(x) + else + bar_width --> 0.8 + xnums = [findfirst(isequal(xi), ux) for xi ∈ x] .- 0.5 + xticks --> (eachindex(ux) .- 0.5, ux) + xnums + end + + # shift x values for each group + group = get(plotattributes, :group, nothing) + if group != nothing + gb = RecipesPipeline._extract_group_attributes(group) + labels, idxs = getfield(gb, 1), getfield(gb, 2) + n = length(labels) + bws = plotattributes[:bar_width] / n + bar_width := bws * clamp(1 - spacing, 0, 1) + for i ∈ 1:n + groupinds = idxs[i] + Δx = _cycle(bws, i) * (i - (n + 1) / 2) + x[groupinds] .+= Δx + end + end + + seriestype := :dotplot + x, y +end + +PlotsBase.@deps groupeddotplot dotplot diff --git a/StatsPlots/src/ecdf.jl b/StatsPlots/src/ecdf.jl new file mode 100644 index 000000000..8edfd12a0 --- /dev/null +++ b/StatsPlots/src/ecdf.jl @@ -0,0 +1,26 @@ + +# --------------------------------------------------------------------------- +# empirical CDF + +@recipe function f(ecdf::StatsBase.ECDF) + seriestype := :steppost + legend --> :topleft + x = [ecdf.sorted_values[1]; ecdf.sorted_values] + if :weights in propertynames(ecdf) && !isempty(ecdf.weights) + # support StatsBase versions >v0.32.0 + y = [0; cumsum(ecdf.weights) ./ sum(ecdf.weights)] + else + y = range(0, 1; length = length(x)) + end + x, y +end + +@userplot ECDFPlot +recipetype(::Val{:ecdfplot}, args...) = ECDFPlot(args) +@recipe function f(p::ECDFPlot) + x = p.args[1] + if !isa(x, StatsBase.ECDF) + x = StatsBase.ecdf(x) + end + x +end diff --git a/StatsPlots/src/errorline.jl b/StatsPlots/src/errorline.jl new file mode 100644 index 000000000..b29582b16 --- /dev/null +++ b/StatsPlots/src/errorline.jl @@ -0,0 +1,272 @@ +@userplot ErrorLine +""" +# StatsPlots.errorline(x, y, arg): + Function for parsing inputs to easily make a [`ribbons`] (https://ggplot2.tidyverse.org/reference/geom_ribbon.html), + stick errorbar (https://www.mathworks.com/help/matlab/ref/errorbar.html), or plume + (https://stackoverflow.com/questions/65510619/how-to-prepare-my-data-for-plume-plots) plot while allowing + for easily controlling error type and NaN handling. + +# Inputs: default values are indicated with *s + + x (vector, unit range) - the values along the x-axis for each y-point + + y (matrix [x, repeat, group]) - values along y-axis wrt x. The first dimension must be of equal length to that of x. + The second dimension is treated as the repeated observations and error is computed along this dimension. If the + matrix has a 3rd dimension this is treated as a new group. + + error_style (`Symbol` - *:ribbon*, :stick, :plume) - determines whether to use a ribbon style or stick style error + representation. + + centertype (symbol - *:mean* or :median) - which approach to use to represent the central value of y at each x-value. + + errortype (symbol - *:std*, :sem, :percentile) - which error metric to use to show the distribution of y at each x-value. + + percentiles (Vector{Int64} *[25, 75]*) - if using errortype === :percentile then which percentiles to use as bounds. + + groupcolor (Symbol, RGB, Vector of Symbol or RGB) - Declares the color for each group. If no value is passed then will use + the default colorscheme. If one value is given then it will use that color for all groups. If multiple colors are + given then it will use a different color for each group. + + secondarycolor (`Symbol`, `RGB`, `:matched` - *:Gray60*) - When using stick mode this will allow for the setting of the stick color. + If `:matched` is given then the color of the sticks with match that of the main line. + + secondarylinealpha (float *.1*) - alpha value of plume lines. + + numsecondarylines (int *100*) - number of plume lines to plot behind central line. + + stickwidth (Float64 *.01*) - How much of the x-axis the horizontal aspect of the error stick should take up. + +# Example +```julia +x = 1:10 +y = fill(NaN, 10, 100, 3) +for i = axes(y,3) + y[:,:,i] = collect(1:2:20) .+ rand(10,100).*5 .* collect(1:2:20) .+ rand()*100 +end + +y = reshape(1:100, 10, 10); +errorline(1:10, y) +``` +""" +errorline + +function compute_error( + y::AbstractMatrix, + centertype::Symbol, + errortype::Symbol, + percentiles::AbstractVector, +) + y_central = fill(NaN, size(y, 1)) + + # NaNMath doesn't accept Ints so convert to AbstractFloat if necessary + if eltype(y) <: Integer + y = float(y) + end + # First compute the center + y_central = if centertype === :mean + mapslices(NaNMath.mean, y, dims = 2) + elseif centertype === :median + mapslices(NaNMath.median, y, dims = 2) + else + error("Invalid center type. Valid symbols include :mean or :median") + end + + # Takes 2d matrix [x,y] and computes the desired error type for each row (value of x) + if errortype === :std || errortype === :sem + y_error = mapslices(NaNMath.std, y, dims = 2) + if errortype == :sem + y_error = y_error ./ sqrt(size(y, 2)) + end + + elseif errortype === :percentile + y_lower = fill(NaN, size(y, 1)) + y_upper = fill(NaN, size(y, 1)) + if any(isnan.(y)) # NaNMath does not have a percentile function so have to go via StatsBase + for i ∈ axes(y, 1) + yi = y[i, .!isnan.(y[i, :])] + y_lower[i] = percentile(yi, percentiles[1]) + y_upper[i] = percentile(yi, percentiles[2]) + end + else + y_lower = mapslices(Y -> percentile(Y, percentiles[1]), y, dims = 2) + y_upper = mapslices(Y -> percentile(Y, percentiles[2]), y, dims = 2) + end + + y_error = (y_central .- y_lower, y_upper .- y_central) # Difference from center value + else + error("Invalid error type. Valid symbols include :std, :sem, :percentile") + end + + return y_central, y_error +end + +@recipe function f( + e::ErrorLine; + errorstyle = :ribbon, + centertype = :mean, + errortype = :std, + percentiles = [25, 75], + groupcolor = nothing, + secondarycolor = nothing, + stickwidth = 0.01, + secondarylinealpha = 0.1, + numsecondarylines = 100, + secondarylinewidth = 1, +) + if length(e.args) == 1 # If only one input is given assume it is y-values in the form [x,obs] + y = e.args[1] + x = 1:size(y, 1) + else # Otherwise assume that the first two inputs are x and y + x = e.args[1] + y = e.args[2] + + # Check y orientation + ndims(y) > 3 && error("ndims(y) > 3") + + if !any(size(y) .== length(x)) + error("Size of x and y do not match") + elseif ndims(y) == 2 && size(y, 1) != length(x) && size(y, 2) == length(x) # Check if y needs to be transposed or transmuted + y = transpose(y) + elseif ndims(y) == 3 && size(y, 1) != length(x) + error( + "When passing a 3 dimensional matrix as y, the axes must be [x, repeat, group]", + ) + end + end + + # Determine if a color palette is being used so it can be passed to secondary lines + if :color_palette ∉ keys(plotattributes) + color_palette = :default + else + color_palette = plotattributes[:color_palette] + end + + # Parse different color type + if groupcolor isa Symbol || groupcolor isa RGB{Float64} || groupcolor isa RGBA{Float64} + groupcolor = [groupcolor] + end + + # Check groupcolor format + if (groupcolor !== nothing && ndims(y) > 2) && length(groupcolor) == 1 + groupcolor = repeat(groupcolor, size(y, 3)) # Use the same color for all groups + elseif (groupcolor !== nothing && ndims(y) > 2) && length(groupcolor) < size(y, 3) + error("$(length(groupcolor)) colors given for a matrix with $(size(y,3)) groups") + elseif groupcolor === nothing + gsi_counter = 0 + for i ∈ 1:length(plotattributes[:plot_object].series_list) + if plotattributes[:plot_object].series_list[i].plotattributes[:primary] + gsi_counter += 1 + end + end + # Start at next index and allow wrapping of indices + gsi_counter += 1 + idx = (gsi_counter:(gsi_counter + size(y, 3))) .% length(palette(color_palette)) + idx[findall(x -> x == 0, idx)] .= length(palette(color_palette)) + groupcolor = palette(color_palette)[idx] + end + + if errorstyle === :plume && numsecondarylines > size(y, 2) # Override numsecondarylines + numsecondarylines = size(y, 2) + end + + for g ∈ axes(y, 3) # Iterate through 3rd dimension + # Compute center and distribution for each value of x + y_central, y_error = compute_error(y[:, :, g], centertype, errortype, percentiles) + + if errorstyle === :ribbon + seriestype := :path + @series begin + x := x + y := y_central + ribbon := y_error + fillalpha --> 0.1 + linecolor := groupcolor[g] + fillcolor := groupcolor[g] + () # Suppress implicit return + end + + elseif errorstyle === :stick + x_offset = diff(extrema(x) |> collect)[1] * stickwidth + seriestype := :path + for (i, xi) ∈ enumerate(x) + # Error sticks + @series begin + primary := false + x := + [xi - x_offset, xi + x_offset, xi, xi, xi + x_offset, xi - x_offset] + if errortype === :percentile + y := [ + repeat([y_central[i] - y_error[1][i]], 3) + repeat([y_central[i] + y_error[2][i]], 3) + ] + else + y := [ + repeat([y_central[i] - y_error[i]], 3) + repeat([y_central[i] + y_error[i]], 3) + ] + end + # Set the stick color + if secondarycolor === nothing + linecolor := :gray60 + elseif secondarycolor === :matched + linecolor := groupcolor[g] + else + linecolor := secondarycolor + end + linewidth := secondarylinewidth + () # Suppress implicit return + end + end + + # Base line + seriestype := :line + @series begin + primary := true + x := x + y := y_central + linecolor := groupcolor[g] + () + end + + elseif errorstyle === :plume + num_obs = size(y, 2) + if num_obs > numsecondarylines + sub_sample_idx = sample(1:num_obs, numsecondarylines, replace = false) + y_sub_sample = y[:, sub_sample_idx, g] + else + y_sub_sample = y[:, :, g] + end + seriestype := :path + for i ∈ 1:numsecondarylines + # Background paths + @series begin + primary := false + x := x + y := y_sub_sample[:, i] + # Set the stick color + if secondarycolor === nothing || secondarycolor === :matched + linecolor := groupcolor[g] + else + linecolor := secondarycolor + end + linealpha := secondarylinealpha + linewidth := secondarylinewidth + () # Suppress implicit return + end + end + + # Base line + seriestype := :line + @series begin + primary := true + x := x + y := y_central + linecolor := groupcolor[g] + linewidth --> 3 # Make it stand out against the plume better + () + end + else + error("Invalid error style. Valid symbols include :ribbon, :stick, or :plume.") + end + end +end diff --git a/StatsPlots/src/hist.jl b/StatsPlots/src/hist.jl new file mode 100644 index 000000000..e55e9a81a --- /dev/null +++ b/StatsPlots/src/hist.jl @@ -0,0 +1,252 @@ + +# --------------------------------------------------------------------------- +# density + +@recipe function f( + ::Type{Val{:density}}, + x, + y, + z; + trim = false, + bandwidth = KernelDensity.default_bandwidth(y), +) + newx, newy = + violin_coords(y, trim = trim, wts = plotattributes[:weights], bandwidth = bandwidth) + if isvertical(plotattributes) + newx, newy = newy, newx + end + x := newx + y := newy + seriestype := :path + () +end +PlotsBase.@deps density path + +# --------------------------------------------------------------------------- +# cumulative density + +@recipe function f( + ::Type{Val{:cdensity}}, + x, + y, + z; + trim = false, + npoints = 200, + bandwidth = KernelDensity.default_bandwidth(y), +) + newx, newy = + violin_coords(y, trim = trim, wts = plotattributes[:weights], bandwidth = bandwidth) + + if isvertical(plotattributes) + newx, newy = newy, newx + end + + newy = cumsum(float(yi) for yi ∈ newy) + newy ./= newy[end] + + x := newx + y := newy + seriestype := :path + () +end +PlotsBase.@deps cdensity path + +ea_binnumber(y, bin::AbstractVector) = + error("You cannot specify edge locations for equal area histogram") +ea_binnumber(y, bin::Real) = + (floor(bin) == bin || error("Only integer or symbol values accepted by bins"); Int(bin)) +ea_binnumber(y, bin::Int) = bin +ea_binnumber(y, bin::Symbol) = PlotsBase._auto_binning_nbins((y,), 1, mode = bin) + +@recipe function f(::Type{Val{:ea_histogram}}, x, y, z) + bin = ea_binnumber(y, plotattributes[:bins]) + bins := quantile(y, range(0, stop = 1, length = bin + 1)) + normalize := :density + seriestype := :barhist + () +end +PlotsBase.@deps histogram barhist + +push!(PlotsBase.Commons._histogram_like, :ea_histogram) + +@shorthands ea_histogram + +@recipe function f(::Type{Val{:testhist}}, x, y, z) + markercolor --> :red + seriestype := :scatter + () +end +@shorthands testhist + +# --------------------------------------------------------------------------- +# grouped histogram + +@userplot GroupedHist + +PlotsBase.group_as_matrix(g::GroupedHist) = true + +@recipe function f(p::GroupedHist) + _, v = grouped_xy(p.args...) + group = get(plotattributes, :group, nothing) + bins = get(plotattributes, :bins, :auto) + normed = get(plotattributes, :normalize, false) + weights = get(plotattributes, :weights, nothing) + + # compute edges from ungrouped data + h = PlotsBase._make_hist((vec(copy(v)),), bins; normed = normed, weights = weights) + nbins = length(h.weights) + edges = h.edges[1] + bar_width --> mean(map(i -> edges[i + 1] - edges[i], 1:nbins)) + x = map(i -> (edges[i] + edges[i + 1]) / 2, 1:nbins) + + if group === nothing + y = reshape(h.weights, nbins, 1) + else + gb = RecipesPipeline._extract_group_attributes(group) + labels, idxs = getfield(gb, 1), getfield(gb, 2) + ngroups = length(labels) + ntot = count(x -> !isnan(x), v) + + # compute weights (frequencies) by group using those edges + y = fill(NaN, nbins, ngroups) + for i ∈ 1:ngroups + groupinds = idxs[i] + v_i = filter(x -> !isnan(x), v[:, i]) + w_i = weights == nothing ? nothing : weights[groupinds] + h_i = PlotsBase._make_hist((v_i,), h.edges; normed = false, weights = w_i) + if normed + y[:, i] .= h_i.weights .* (length(v_i) / ntot / sum(h_i.weights)) + else + y[:, i] .= h_i.weights + end + end + end + + GroupedBar((x, y)) +end + +# --------------------------------------------------------------------------- +# Compute binsizes using Wand (1997)'s criterion +# Ported from R code located here https://github.com/cran/KernSmooth/tree/master/R + +"Returns optimal histogram edge positions in accordance to Wand (1995)'s criterion'" +PlotsBase.wand_edges(x::AbstractVector, args...) = (binwidth = wand_bins(x, args...); +(minimum(x) - binwidth):binwidth:(maximum(x) + binwidth)) + +"Returns optimal histogram bin widths in accordance to Wand (1995)'s criterion'" +function wand_bins(x, scalest = :minim, gridsize = 401, range_x = extrema(x), trun = true) + n = length(x) + minx, maxx = range_x + gpoints = range(minx, stop = maxx, length = gridsize) + gcounts = linbin(x, gpoints, trun = trun) + + scalest = if scalest === :stdev + sqrt(var(x)) + elseif scalest === :iqr + (quantile(x, 3 // 4) - quantile(x, 1 // 4)) / 1.349 + elseif scalest === :minim + min((quantile(x, 3 // 4) - quantile(x, 1 // 4)) / 1.349, sqrt(var(x))) + else + error("scalest must be one of :stdev, :iqr or :minim (default)") + end + + scalest == 0 && error("scale estimate is zero for input data") + sx = (x .- mean(x)) ./ scalest + sa = (minx - mean(x)) / scalest + sb = (maxx - mean(x)) / scalest + + gpoints = range(sa, stop = sb, length = gridsize) + gcounts = linbin(sx, gpoints, trun = trun) + + hpi = begin + alpha = ((2 / (11 * n))^(1 / 13)) * sqrt(2) + psi10hat = bkfe(gcounts, 10, alpha, [sa, sb]) + alpha = (-105 * sqrt(2 / pi) / (psi10hat * n))^(1 // 11) + psi8hat = bkfe(gcounts, 8, alpha, [sa, sb]) + alpha = (15 * sqrt(2 / pi) / (psi8hat * n))^(1 / 9) + psi6hat = bkfe(gcounts, 6, alpha, [sa, sb]) + alpha = (-3 * sqrt(2 / pi) / (psi6hat * n))^(1 / 7) + psi4hat = bkfe(gcounts, 4, alpha, [sa, sb]) + alpha = (sqrt(2 / pi) / (psi4hat * n))^(1 / 5) + psi2hat = bkfe(gcounts, 2, alpha, [sa, sb]) + (6 / (-psi2hat * n))^(1 / 3) + end + + scalest * hpi +end + +function linbin(X, gpoints; trun = true) + n, M = length(X), length(gpoints) + + a, b = gpoints[1], gpoints[M] + gcnts = zeros(M) + delta = (b - a) / (M - 1) + + for i ∈ 1:n + lxi = ((X[i] - a) / delta) + 1 + li = floor(Int, lxi) + rem = lxi - li + + if 1 <= li < M + gcnts[li] += 1 - rem + gcnts[li + 1] += rem + end + + if !trun + if lt < 1 + gcnts[1] += 1 + end + + if li >= M + gcnts[M] += 1 + end + end + end + gcnts +end + +"binned kernel function estimator" +function bkfe(gcounts, drv, bandwidth, range_x) + bandwidth <= 0 && error("'bandwidth' must be strictly positive") + + a, b = range_x + h = bandwidth + M = length(gcounts) + gpoints = range(a, stop = b, length = M) + + ## Set the sample size and bin width + + n = sum(gcounts) + delta = (b - a) / (M - 1) + + ## Obtain kernel weights + + tau = 4 + drv + L = min(Int(fld(tau * h, delta)), M) + + lvec = 0:L + arg = lvec .* delta / h + + kappam = pdf.(Normal(), arg) ./ h^(drv + 1) + hmold0, hmnew = ones(length(arg)), ones(length(arg)) + hmold1 = arg + + if drv >= 2 + for i ∈ (2:drv) + hmnew = arg .* hmold1 .- (i - 1) .* hmold0 + hmold0 = hmold1 # Compute mth degree Hermite polynomial + hmold1 = hmnew # by recurrence. + end + end + kappam = hmnew .* kappam + + ## Now combine weights and counts to obtain estimate + ## we need P >= 2L+1L, M: L <= M. + P = nextpow(2, M + L + 1) + kappam = [kappam; zeros(P - 2 * L - 1); reverse(kappam[2:end])] + Gcounts = [gcounts; zeros(P - M)] + kappam = fft(kappam) + Gcounts = fft(Gcounts) + + sum(gcounts .* (real(ifft(kappam .* Gcounts)))[1:M]) / (n^2) +end diff --git a/StatsPlots/src/interact.jl b/StatsPlots/src/interact.jl new file mode 100644 index 000000000..180afd60b --- /dev/null +++ b/StatsPlots/src/interact.jl @@ -0,0 +1,110 @@ +plot_function(plt::Function, grouped) = plt +plot_function(plt::Tuple, grouped) = grouped ? plt[2] : plt[1] + +combine_cols(dict, ns) = length(ns) > 1 ? hcat((dict[n] for n ∈ ns)...) : dict[ns[1]] + +function dataviewer(t; throttle = 0.1, nbins = 30, nbins_range = 1:100) + (t isa AbstractObservable) || (t = Observable{Any}(t)) + + coltable = map(Tables.columntable, t) + + @show names = map(collect ∘ keys, coltable) + + dict = @map Dict((key, val) for (key, val) ∈ pairs(&coltable)) + x = Widgets.dropdown(names, placeholder = "First axis", multiple = true) + y = Widgets.dropdown(names, placeholder = "Second axis", multiple = true) + y_toggle = Widgets.togglecontent(y, value = false, label = "Second axis") + plot_type = Widgets.dropdown( + OrderedDict( + "line" => PlotsBase.plot, + "scatter" => PlotsBase.scatter, + "bar" => (PlotsBase.bar, StatsPlots.groupedbar), + "boxplot" => (StatsPlots.boxplot, StatsPlots.groupedboxplot), + "corrplot" => StatsPlots.corrplot, + "cornerplot" => StatsPlots.cornerplot, + "density" => StatsPlots.density, + "cdensity" => StatsPlots.cdensity, + "histogram" => StatsPlots.histogram, + "marginalhist" => StatsPlots.marginalhist, + "violin" => (StatsPlots.violin, StatsPlots.groupedviolin), + ), + placeholder = "Plot type", + ) + + # Add bins if the plot allows it + display_nbins = + @map (&plot_type) in [corrplot, cornerplot, histogram, marginalhist] ? "block" : + "none" + nbins = (Widgets.slider( + nbins_range, + extra_obs = ["display" => display_nbins], + value = nbins, + label = "number of bins", + )) + nbins.scope.dom = Widgets.div( + nbins.scope.dom, + attributes = Dict("data-bind" => "style: {display: display}"), + ) + nbins_throttle = Observables.throttle(throttle, nbins) + + by = Widgets.dropdown(names, multiple = true, placeholder = "Group by") + by_toggle = Widgets.togglecontent(by, value = false, label = "Split data") + plt = Widgets.button("plot") + output = @map begin + if (&plt == 0) + plot() + else + args = Any[] + # add first and maybe second argument + push!(args, combine_cols(&dict, x[])) + has_y = y_toggle[] && !isempty(y[]) + has_y && push!(args, combine_cols(&dict, y[])) + + # compute automatic kwargs + kwargs = Dict() + + # grouping kwarg + has_by = by_toggle[] && !isempty(by[]) + by_tup = Tuple(getindex(&dict, b) for b ∈ by[]) + has_by && (kwargs[:group] = NamedTuple{Tuple(by[])}(by_tup)) + + # label kwarg + if length(x[]) > 1 + kwargs[:label] = x[] + elseif y_toggle[] && length(y[]) > 1 + kwargs[:label] = y[] + end + + # x and y labels + densityplot1D = plot_type[] in [cdensity, density, histogram] + (length(x[]) == 1 && (densityplot1D || has_y)) && (kwargs[:xlabel] = x[][1]) + if has_y && length(y[]) == 1 + kwargs[:ylabel] = y[][1] + elseif !has_y && !densityplot1D && length(x[]) == 1 + kwargs[:ylabel] = x[][1] + end + + plot_func = plot_function(plot_type[], has_by) + plot_func(args...; nbins = &nbins_throttle, kwargs...) + end + end + wdg = Widget{:dataviewer}( + [ + "x" => x, + "y" => y, + "y_toggle" => y_toggle, + "by" => by, + "by_toggle" => by_toggle, + "plot_type" => plot_type, + "plot_button" => plt, + "nbins" => nbins, + ], + output = output, + ) + @layout! wdg Widgets.div( + Widgets.div(:x, :y_toggle, :plot_type, :by_toggle, :plot_button), + Widgets.div(style = Dict("width" => "3em")), + Widgets.div(Widgets.observe(_), :nbins), + style = Dict("display" => "flex", "direction" => "row"), + ) +end diff --git a/StatsPlots/src/marginalhist.jl b/StatsPlots/src/marginalhist.jl new file mode 100644 index 000000000..fe50662fa --- /dev/null +++ b/StatsPlots/src/marginalhist.jl @@ -0,0 +1,75 @@ +@shorthands marginalhist + +@recipe function f(::Type{Val{:marginalhist}}, plt::AbstractPlot; density = false) + x, y = plotattributes[:x], plotattributes[:y] + i = isfinite.(x) .& isfinite.(y) + x, y = x[i], y[i] + bns = get(plotattributes, :bins, :auto) + scale = get(plotattributes, :scale, :identity) + edges1, edges2 = PlotsBase._hist_edges((x, y), bns) + xlims, ylims = map( + x -> PlotsBase.Axes.scale_lims( + PlotsBase.ignorenan_extrema(x)..., + PlotsBase.Axes.default_widen_factor, + scale, + ), + (x, y), + ) + + # set up the subplots + legend --> false + link := :both + grid --> false + layout --> @layout [ + tophist _ + hist2d{0.9w,0.9h} righthist + ] + + # main histogram2d + @series begin + seriestype := :histogram2d + right_margin --> 0PlotsBase.mm + top_margin --> 0PlotsBase.mm + subplot := 2 + bins := (edges1, edges2) + xlims --> xlims + ylims --> ylims + end + + # these are common to both marginal histograms + ticks := nothing + xguide := "" + yguide := "" + foreground_color_border := nothing + fillcolor --> PlotsBase.fg_color(plotattributes) + linecolor --> PlotsBase.fg_color(plotattributes) + + if density + trim := true + seriestype := :density + else + seriestype := :histogram + end + + # upper histogram + @series begin + subplot := 1 + bottom_margin --> 0PlotsBase.mm + bins := edges1 + y := x + xlims --> xlims + end + + # right histogram + @series begin + orientation := :h + subplot := 3 + left_margin --> 0PlotsBase.mm + bins := edges2 + y := y + ylims --> ylims + end +end + +# # now you can plot like: +# marginalhist(rand(1000), rand(1000)) diff --git a/StatsPlots/src/marginalkde.jl b/StatsPlots/src/marginalkde.jl new file mode 100644 index 000000000..e89c034c4 --- /dev/null +++ b/StatsPlots/src/marginalkde.jl @@ -0,0 +1,75 @@ +@userplot MarginalKDE + +@recipe function f(kc::MarginalKDE; levels = 10, clip = ((-3.0, 3.0), (-3.0, 3.0))) + x, y = kc.args + + x = vec(x) + y = vec(y) + + m_x = median(x) + m_y = median(y) + + dx_l = m_x - quantile(x, 0.16) + dx_h = quantile(x, 0.84) - m_x + + dy_l = m_y - quantile(y, 0.16) + dy_h = quantile(y, 0.84) - m_y + + xmin = m_x + clip[1][1] * dx_l + xmax = m_x + clip[1][2] * dx_h + + ymin = m_y + clip[2][1] * dy_l + ymax = m_y + clip[2][2] * dy_h + + k = KernelDensity.kde((x, y)) + kx = KernelDensity.kde(x) + ky = KernelDensity.kde(y) + + ps = pdf.(Ref(k), x, y) + + ls = [] + for p ∈ range(1.0 / levels, stop = 1 - 1.0 / levels, length = levels - 1) + push!(ls, quantile(ps, p)) + end + + legend --> false + layout := @layout [ + topdensity _ + contour{0.9w,0.9h} rightdensity + ] + + @series begin + seriestype := :contour + levels := ls + fill := false + colorbar := false + subplot := 2 + xlims := (xmin, xmax) + ylims := (ymin, ymax) + + (collect(k.x), collect(k.y), k.density') + end + + ticks := nothing + xguide := "" + yguide := "" + + @series begin + seriestype := :density + subplot := 1 + xlims := (xmin, xmax) + ylims := (0, 1.1 * maximum(kx.density)) + + x + end + + @series begin + seriestype := :density + subplot := 3 + orientation := :h + xlims := (0, 1.1 * maximum(ky.density)) + ylims := (ymin, ymax) + + y + end +end diff --git a/StatsPlots/src/marginalscatter.jl b/StatsPlots/src/marginalscatter.jl new file mode 100644 index 000000000..5641fca53 --- /dev/null +++ b/StatsPlots/src/marginalscatter.jl @@ -0,0 +1,74 @@ +@shorthands marginalscatter + +@recipe function f(::Type{Val{:marginalscatter}}, plt::AbstractPlot; density = false) + x, y = plotattributes[:x], plotattributes[:y] + i = isfinite.(x) .& isfinite.(y) + x, y = x[i], y[i] + scale = get(plotattributes, :scale, :identity) + xlims, ylims = map( + x -> PlotsBase.Axes.scale_lims( + PlotsBase.ignorenan_extrema(x)..., + PlotsBase.Axes.default_widen_factor, + scale, + ), + (x, y), + ) + + # set up the subplots + legend --> false + link := :both + grid --> false + layout --> @layout [ + topscatter _ + scatter2d{0.9w,0.9h} rightscatter + ] + + # main scatter2d + @series begin + seriestype := :scatter + right_margin --> 0PlotsBase.mm + top_margin --> 0PlotsBase.mm + subplot := 2 + xlims --> xlims + ylims --> ylims + end + + # these are common to both marginal scatter + ticks := nothing + xguide := "" + yguide := "" + fillcolor --> PlotsBase.fg_color(plotattributes) + linecolor --> PlotsBase.fg_color(plotattributes) + + if density + trim := true + seriestype := :density + else + seriestype := :scatter + end + + # upper scatter + @series begin + subplot := 1 + bottom_margin --> 0PlotsBase.mm + showaxis := :x + x := x + y := ones(y |> size) + xlims --> xlims + ylims --> (0.95, 1.05) + end + + # right scatter + @series begin + orientation := :h + showaxis := :y + subplot := 3 + left_margin --> 0PlotsBase.mm + # bins := edges2 + y := y + x := ones(x |> size) + end +end + +# # now you can plot like: +# marginalscatter(rand(1000), rand(1000)) diff --git a/StatsPlots/src/ordinations.jl b/StatsPlots/src/ordinations.jl new file mode 100644 index 000000000..5615b6424 --- /dev/null +++ b/StatsPlots/src/ordinations.jl @@ -0,0 +1,24 @@ +@recipe function f(mds::MultivariateStats.MDS{<:Real}; mds_axes = (1, 2)) + length(mds_axes) in [2, 3] || throw(ArgumentError("Can only accept 2 or 3 mds axes")) + xax = mds_axes[1] + yax = mds_axes[2] + tfm = collect(MultivariateStats.predict(mds)') + + xlabel --> "MDS$xax" + ylabel --> "MDS$yax" + seriestype := :scatter + aspect_ratio --> 1 + + if length(mds_axes) == 3 + zax = mds_axes[3] + zlabel --> "MDS$zax" + tfm[:, xax], tfm[:, yax], tfm[:, zax] + else + tfm[:, xax], tfm[:, yax] + end +end + +#= This needs to wait on a different PCA API in MultivariateStats.jl +@recipe function f(pca::PCA{<:Real}; pca_axes=(1,2)) +end +=# diff --git a/StatsPlots/src/violin.jl b/StatsPlots/src/violin.jl new file mode 100644 index 000000000..f582ecb42 --- /dev/null +++ b/StatsPlots/src/violin.jl @@ -0,0 +1,215 @@ + +# --------------------------------------------------------------------------- +# Violin Plot + +const _violin_warned = [false] + +function violin_coords( + y; + wts = nothing, + trim::Bool = false, + bandwidth = KernelDensity.default_bandwidth(y), +) + kd = + wts === nothing ? KernelDensity.kde(y, npoints = 200, bandwidth = bandwidth) : + KernelDensity.kde(y, weights = weights(wts), npoints = 200, bandwidth = bandwidth) + if trim + xmin, xmax = PlotsBase.ignorenan_extrema(y) + inside = Bool[xmin <= x <= xmax for x ∈ kd.x] + return (kd.density[inside], kd.x[inside]) + end + kd.density, kd.x +end + +get_quantiles(quantiles::AbstractVector) = quantiles +get_quantiles(x::Real) = [x] +get_quantiles(b::Bool) = b ? [0.5] : Float64[] +get_quantiles(n::Int) = range(0, 1, length = n + 2)[2:(end - 1)] + +@recipe function f( + ::Type{Val{:violin}}, + x, + y, + z; + trim = true, + side = :both, + show_mean = false, + show_median = false, + quantiles = Float64[], + bandwidth = KernelDensity.default_bandwidth(y), +) + # if only y is provided, then x will be UnitRange 1:size(y,2) + if typeof(x) <: AbstractRange + x = if step(x) == first(x) == 1 + plotattributes[:series_plotindex] + else + [getindex(x, plotattributes[:series_plotindex])] + end + end + xsegs, ysegs = Plots.PlotsBase.Segments(), Plots.PlotsBase.Segments() + qxsegs, qysegs = Plots.PlotsBase.Segments(), Plots.PlotsBase.Segments() + mxsegs, mysegs = Plots.PlotsBase.Segments(), Plots.PlotsBase.Segments() + glabels = sort(collect(unique(x))) + bw = plotattributes[:bar_width] + bw == nothing && (bw = 0.8) + msc = plotattributes[:markerstrokecolor] + for (i, glabel) ∈ enumerate(glabels) + fy = y[filter(i -> _cycle(x, i) == glabel, 1:length(y))] + widths, centers = violin_coords( + fy, + trim = trim, + wts = plotattributes[:weights], + bandwidth = bandwidth, + ) + isempty(widths) && continue + + # normalize + hw = 0.5_cycle(bw, i) + widths = hw * widths / PlotsBase.ignorenan_maximum(widths) + + # make the violin + xcenter = PlotsBase.discrete_value!(plotattributes, :x, glabel)[1] + xcoords = if (side === :right) + vcat(widths, zeros(length(widths))) .+ xcenter + elseif (side === :left) + vcat(zeros(length(widths)), -reverse(widths)) .+ xcenter + else + vcat(widths, -reverse(widths)) .+ xcenter + end + ycoords = vcat(centers, reverse(centers)) + + push!(xsegs, xcoords) + push!(ysegs, ycoords) + + if show_mean + mea = StatsBase.mean(fy) + mw = maximum(widths) + mx = xcenter .+ [-mw, mw] * 0.75 + my = [mea, mea] + if side === :right + mx[1] = xcenter + elseif side === :left + mx[2] = xcenter + end + + push!(mxsegs, mx) + push!(mysegs, my) + end + + if show_median + med = StatsBase.median(fy) + mw = maximum(widths) + mx = xcenter .+ [-mw, mw] / 2 + my = [med, med] + if side === :right + mx[1] = xcenter + elseif side === :left + mx[2] = xcenter + end + + push!(qxsegs, mx) + push!(qysegs, my) + end + + quantiles = get_quantiles(quantiles) + if !isempty(quantiles) + qy = quantile(fy, quantiles) + maxw = maximum(widths) + + for i ∈ eachindex(qy) + qxi = xcenter .+ [-maxw, maxw] * (0.5 - abs(0.5 - quantiles[i])) + qyi = [qy[i], qy[i]] + if side === :right + qxi[1] = xcenter + elseif side === :left + qxi[2] = xcenter + end + + push!(qxsegs, qxi) + push!(qysegs, qyi) + end + + push!(qxsegs, [xcenter, xcenter]) + push!(qysegs, [extrema(qy)...]) + end + end + + @series begin + seriestype := :shape + x := xsegs.pts + y := ysegs.pts + () + end + + if !isempty(mxsegs.pts) + @series begin + primary := false + seriestype := :shape + linestyle := :dot + x := mxsegs.pts + y := mysegs.pts + () + end + end + + if !isempty(qxsegs.pts) + @series begin + primary := false + seriestype := :shape + x := qxsegs.pts + y := qysegs.pts + () + end + end + + seriestype := :shape + primary := false + x := [] + y := [] + () +end +PlotsBase.@deps violin shape + +# ------------------------------------------------------------------------------ +# Grouped Violin + +@userplot GroupedViolin + +recipetype(::Val{:groupedviolin}, args...) = GroupedViolin(args) + +@recipe function f(g::GroupedViolin; spacing = 0.1) + x, y = grouped_xy(g.args...) + + # extract xnums and set default bar width. + # might need to set xticks as well + ux = unique(x) + x = if eltype(x) <: Number + bar_width --> (0.8 * mean(diff(sort(ux)))) + float.(x) + else + bar_width --> 0.8 + xnums = [findfirst(isequal(xi), ux) for xi ∈ x] .- 0.5 + xticks --> (eachindex(ux) .- 0.5, ux) + xnums + end + + # shift x values for each group + group = get(plotattributes, :group, nothing) + if group != nothing + gb = RecipesPipeline._extract_group_attributes(group) + labels, idxs = getfield(gb, 1), getfield(gb, 2) + n = length(labels) + bws = plotattributes[:bar_width] / n + bar_width := bws * clamp(1 - spacing, 0, 1) + for i ∈ 1:n + groupinds = idxs[i] + Δx = _cycle(bws, i) * (i - (n + 1) / 2) + x[groupinds] .+= Δx + end + end + + seriestype := :violin + x, y +end + +PlotsBase.@deps groupedviolin violin diff --git a/StatsPlots/test/runtests.jl b/StatsPlots/test/runtests.jl new file mode 100644 index 000000000..831191285 --- /dev/null +++ b/StatsPlots/test/runtests.jl @@ -0,0 +1,494 @@ +using MultivariateStats +using Distributions +using StatsPlots +using StableRNGs +using Clustering +using NaNMath +using Plots +using Test + +import Plots: PlotsBase + +@testset "Grouped histogram" begin + rng = StableRNG(1337) + gpl = groupedhist( + rand(rng, 1000), + yscale = :log10, + ylims = (1e-2, 1e4), + bar_position = :stack, + ) + @test NaNMath.minimum(gpl[1][1][:y]) <= 1e-2 + @test NaNMath.minimum(gpl[1][1][:y]) > 0 + rng = StableRNG(1337) + gpl = groupedhist( + rand(rng, 1000), + yscale = :log10, + ylims = (1e-2, 1e4), + bar_position = :dodge, + ) + @test NaNMath.minimum(gpl[1][1][:y]) <= 1e-2 + @test NaNMath.minimum(gpl[1][1][:y]) > 0 + + data = [1, 1, 1, 1, 2, 1] + mask = (collect(1:6) .< 5) + gpl1 = groupedhist(data[mask], group = mask[mask], color = 1) + gpl2 = groupedhist(data[.!mask], group = mask[.!mask], color = 2) + gpl12 = groupedhist(data, group = mask, nbins = 5, bar_position = :stack) + @test NaNMath.maximum(gpl12[1][end][:y]) == NaNMath.maximum(gpl1[1][1][:y]) + data = [10 12; 1 1; 0.25 0.25] + gplr = groupedbar(data) + @test NaNMath.maximum(gplr[1][1][:y]) == 10 + @test NaNMath.maximum(gplr[1][end][:y]) == 12 + gplr = groupedbar(data, bar_position = :stack) + @test NaNMath.maximum(gplr[1][1][:y]) == 22 + @test NaNMath.maximum(gplr[1][end][:y]) == 12 +end # testset + +@testset "dendrogram" begin + # Example from https://en.wikipedia.org/wiki/Complete-linkage_clustering + wiki_example = [ + 0 17 21 31 23 + 17 0 30 34 21 + 21 30 0 28 39 + 31 34 28 0 43 + 23 21 39 43 0 + ] + clustering = hclust(wiki_example, linkage = :complete) + + xs, ys = StatsPlots.treepositions(clustering, true, :vertical) + + @test xs == [ + 2.0 1.0 4.0 1.75 + 2.0 1.0 4.0 1.75 + 3.0 2.5 5.0 4.5 + 3.0 2.5 5.0 4.5 + ] + + @test ys == [ + 0.0 0.0 0.0 23.0 + 17.0 23.0 28.0 43.0 + 17.0 23.0 28.0 43.0 + 0.0 17.0 0.0 28.0 + ] +end + +@testset "Histogram" begin + data = randn(1000) + @test 0.2 < StatsPlots.wand_bins(data) < 0.4 +end + +@testset "Distributions" begin + @testset "univariate" begin + @testset "discrete" begin + pbern = plot(Bernoulli(0.25)) + @test pbern[1][1][:x][1:2] == zeros(2) + @test pbern[1][1][:x][4:5] == ones(2) + @test pbern[1][1][:y][[1, 4]] == zeros(2) + @test pbern[1][1][:y][[2, 5]] == [0.75, 0.25] + + pdirac = plot(Dirac(0.25)) + @test pdirac[1][1][:x][1:2] == [0.25, 0.25] + @test pdirac[1][1][:y][1:2] == [0, 1] + + ppois_unbounded = plot(Poisson(1)) + @test ppois_unbounded[1][1][:x] isa AbstractVector + @test ppois_unbounded[1][1][:x][1:2] == zeros(2) + @test ppois_unbounded[1][1][:x][4:5] == ones(2) + @test ppois_unbounded[1][1][:y][[1, 4]] == zeros(2) + @test ppois_unbounded[1][1][:y][[2, 5]] == + pdf.(Poisson(1), ppois_unbounded[1][1][:x][[1, 4]]) + + pnonint = plot(Bernoulli(0.75) - 1 // 2) + @test pnonint[1][1][:x][1:2] == [-1 // 2, -1 // 2] + @test pnonint[1][1][:x][4:5] == [1 // 2, 1 // 2] + @test pnonint[1][1][:y][[1, 4]] == zeros(2) + @test pnonint[1][1][:y][[2, 5]] == [0.25, 0.75] + + pmix = plot( + MixtureModel([Bernoulli(0.75), Bernoulli(0.5)], [0.5, 0.5]); + components = false, + ) + @test pmix[1][1][:x][1:2] == zeros(2) + @test pmix[1][1][:x][4:5] == ones(2) + @test pmix[1][1][:y][[1, 4]] == zeros(2) + @test pmix[1][1][:y][[2, 5]] == [0.375, 0.625] + + dzip = MixtureModel([Dirac(0), Poisson(1)], [0.1, 0.9]) + pzip = plot(dzip; components = false) + @test pzip[1][1][:x] isa AbstractVector + @test pzip[1][1][:y][2:3:end] == pdf.(dzip, Int.(pzip[1][1][:x][1:3:end])) + end + end +end + +@testset "ordinations" begin + @testset "MDS" begin + X = randn(4, 100) + M = fit(MultivariateStats.MDS, X; maxoutdim = 3, distances = false) + Y = MultivariateStats.predict(M)' + + mds_plt = plot(M) + @test mds_plt[1][1][:x] == Y[:, 1] + @test mds_plt[1][1][:y] == Y[:, 2] + @test mds_plt[1][:xaxis][:guide] == "MDS1" + @test mds_plt[1][:yaxis][:guide] == "MDS2" + + mds_plt2 = plot(M; mds_axes = (3, 1, 2)) + @test mds_plt2[1][1][:x] == Y[:, 3] + @test mds_plt2[1][1][:y] == Y[:, 1] + @test mds_plt2[1][1][:z] == Y[:, 2] + @test mds_plt2[1][:xaxis][:guide] == "MDS3" + @test mds_plt2[1][:yaxis][:guide] == "MDS1" + @test mds_plt2[1][:zaxis][:guide] == "MDS2" + end +end + +@testset "errorline" begin + rng = StableRNG(1337) + x = 1:10 + # Test for floats + y = rand(rng, 10, 100) .* collect(1:2:20) + @test errorline(1:10, y)[1][1][:x] == x # x-input + @test all( + round.(errorline(1:10, y)[1][1][:y], digits = 3) .== + round.(mean(y, dims = 2), digits = 3), + ) # mean of y + @test all( + round.(errorline(1:10, y)[1][1][:ribbon], digits = 3) .== + round.(std(y, dims = 2), digits = 3), + ) # std of y + # Test for ints + y = reshape(1:100, 10, 10) + @test all(errorline(1:10, y)[1][1][:y] .== mean(y, dims = 2)) + @test all( + round.(errorline(1:10, y)[1][1][:ribbon], digits = 3) .== + round.(std(y, dims = 2), digits = 3), + ) + # Test colors + y = rand(rng, 10, 100, 3) .* collect(1:2:20) + c = palette(:default) + e = errorline(1:10, y) + @test colordiff(c[1], e[1][1][:linecolor]) == 0.0 + @test colordiff(c[2], e[1][2][:linecolor]) == 0.0 + @test colordiff(c[3], e[1][3][:linecolor]) == 0.0 +end + +@testset "marginalhist" begin + rng = StableRNG(1337) + pl = marginalhist(rand(rng, 100), rand(rng, 100)) + @test show(devnull, pl) isa Nothing +end + +@testset "marginalscatter" begin + rng = StableRNG(1337) + pl = marginalscatter(rand(rng, 100), rand(rng, 100)) + @test show(devnull, pl) isa Nothing +end + +@testset "violin" begin + rng = StableRNG(1337) + pl = violin(repeat([0.1, 0.2, 0.3], outer = 100), randn(300), side = :right) + @test show(devnull, pl) isa Nothing +end + +@testset "density" begin + rng = StableRNG(1337) + pl = density(rand(100_000), label = "density(rand())") + @test show(devnull, pl) isa Nothing +end + +@testset "boxplot" begin + # credits to stackoverflow.com/a/71467031 + boxed = [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 7, + 26, + 80, + 170, + 322, + 486, + 688, + 817, + 888, + 849, + 783, + 732, + 624, + 500, + 349, + 232, + 130, + 49, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 28, + 83, + 181, + 318, + 491, + 670, + 761, + 849, + 843, + 862, + 799, + 646, + 481, + 361, + 225, + 98, + 50, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 8, + 28, + 80, + 179, + 322, + 493, + 660, + 753, + 803, + 832, + 823, + 783, + 657, + 541, + 367, + 223, + 121, + 62, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 7, + 23, + 84, + 171, + 312, + 463, + 640, + 778, + 834, + 820, + 763, + 752, + 655, + 518, + 374, + 244, + 133, + 52, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 21, + 70, + 169, + 342, + 527, + 725, + 808, + 861, + 857, + 799, + 688, + 622, + 523, + 369, + 232, + 115, + 41, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 9, + 28, + 76, + 150, + 301, + 492, + 660, + 760, + 823, + 862, + 790, + 749, + 646, + 525, + 352, + 223, + 116, + 54, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 6, + 21, + 64, + 165, + 290, + 434, + 585, + 771, + 852, + 847, + 785, + 739, + 630, + 535, + 354, + 230, + 114, + 42, + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 4, + 19, + 76, + 190, + 337, + 506, + 680, + 775, + 851, + 853, + 816, + 705, + 588, + 496, + 388, + 232, + 127, + 54, + ], + ] + + boxes = -0.002:0.0001:0.0012 + + xx = repeat(boxes, outer = length(boxed)) + yy = collect(Iterators.flatten(boxed)) + + xtick = collect(-0.002:0.0005:0.0012) + + pl = boxplot(xx * 20_000, yy, xticks = (xtick * 20_000, xtick)) + @test show(devnull, pl) isa Nothing +end diff --git a/ci/downstream.jl b/ci/downstream.jl index 070dd9033..02b3c9d36 100644 --- a/ci/downstream.jl +++ b/ci/downstream.jl @@ -3,11 +3,11 @@ using Pkg const LibGit2 = Pkg.GitTools.LibGit2 const TOML = Pkg.TOML -failsafe_clone_checkout(path, url) = begin +failsafe_clone_checkout(path, url; branch="master", stable=true) = begin local repo for i in 1:6 try - repo = Pkg.GitTools.ensure_clone(stdout, path, url) + repo = Pkg.GitTools.ensure_clone(stdout, path, url; branch) break catch err @warn err @@ -27,11 +27,14 @@ failsafe_clone_checkout(path, url) = begin end @assert isfile(versions) - version_dict = TOML.parse(read(versions, String)) - stable = VersionNumber.(keys(version_dict)) |> maximum - tag = LibGit2.GitObject(repo, "v$stable") - hash = string(LibGit2.target(tag)) - LibGit2.checkout!(repo, hash) + if stable + version_dict = TOML.parse(read(versions, String)) + stable = VersionNumber.(keys(version_dict)) |> maximum + tag = LibGit2.GitObject(repo, "v$stable") + hash = string(LibGit2.target(tag)) + LibGit2.checkout!(repo, hash) + else + end nothing end @@ -65,7 +68,11 @@ test_stable(pkg::AbstractString) = begin end pkg_dir = joinpath(tmpd, "$pkg.jl") - failsafe_clone_checkout(pkg_dir, "https://github.com/JuliaPlots/$pkg.jl") + if true # v2, remove when stable + failsafe_clone_checkout(pkg_dir, "https://github.com/JuliaPlots/$pkg.jl"; branch="v2", stable=false) + else + failsafe_clone_checkout(pkg_dir, "https://github.com/JuliaPlots/$pkg.jl") + end fake_supported_versions!(pkg_dir) Pkg.develop(; path = pkg_dir) diff --git a/docs/Project.toml b/docs/Project.toml index 94fdaffe3..3b609cdc3 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -36,6 +36,7 @@ RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" diff --git a/docs/ci_build.sh b/docs/ci_build.sh index 7104249a2..4c5dbd245 100644 --- a/docs/ci_build.sh +++ b/docs/ci_build.sh @@ -54,10 +54,6 @@ export JULIA_CONDAPKG_BACKEND=MicroMamba julia='xvfb-run -a julia --color=yes --project=docs' -# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="RecipesBase"));' #FIXME: not needed when registered -# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="RecipesPipeline"));' #FIXME: not needed when registered -# $julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/JuliaPlots/Plots.jl", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3], subdir="PlotsBase"));' #FIXME: not needed when registered -$julia -e 'using Pkg; Pkg.develop([(;path="."), (;path="./RecipesBase"), (;path="./RecipesPipeline"), (;path="./PlotsBase")]);' #FIXME: not needed when registered $julia -e ' using Pkg; Pkg.add("CondaPkg") using CondaPkg; CondaPkg.resolve() @@ -80,13 +76,12 @@ $julia -e ' ' echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $GITHUB_ACTOR on $GITHUB_EVENT_NAME ==" -if [ "$GITHUB_REPOSITORY" == 'JuliaPlots/PlotDocs.jl' ]; then - $julia -e 'using Pkg; Pkg.add(PackageSpec(name="Plots", rev="master"))' - $julia docs/make.jl -elif [ "$GITHUB_REPOSITORY" == 'JuliaPlots/Plots.jl' ]; then - $julia -e 'using Pkg; Pkg.add(PackageSpec(name="Plots", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3])); Pkg.instantiate()' - $julia docs/make.jl -else - echo "something is wrong with $GITHUB_REPOSITORY" - exit 1 -fi +$julia -e 'using Pkg; Pkg.develop([ + (; path="./RecipesBase"), + (; path="./RecipesPipeline"), + (; path="./PlotsBase"), + (; path="./GraphRecipes"), + (; path="./StatsPlots"), +)' +$julia -e 'using Pkg; Pkg.add(PackageSpec(name="Plots", rev=split(ENV["GITHUB_REF"], "/", limit=3)[3])); Pkg.instantiate()' +$julia docs/make.jl diff --git a/docs/make.jl b/docs/make.jl index b86de99eb..523334efb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,7 +8,7 @@ import PGFPlotsX import PlotlyJS import Gaston import UnicodePlots -# import StatsPlots +import StatsPlots const SRC_DIR = joinpath(@__DIR__, "src") const WORK_DIR = joinpath(@__DIR__, "work") @@ -611,7 +611,7 @@ function main() for (pkg, dest) ∈ ( (PlotThemes, "plotthemes.md"), - # (StatsPlots, "statsplots.md"), # TODO: uncomment after having compatible StatsPlots + (StatsPlots, "statsplots.md"), ) cp(pkgdir(pkg, "README.md"), joinpath(GEN_DIR, dest); force = true) end @@ -640,21 +640,21 @@ function main() "Getting Started" => [ "Installation" => "install.md", "Basics" => "basics.md", - # "Tutorial" => "tutorial.md", # TODO: uncomment once StatsPlots is ready + "Tutorial" => "tutorial.md", "Series Types" => [ "Contour Plots" => "series_types/contour.md", "Histograms" => "series_types/histogram.md", ], ], "Manual" => [ - # "Input Data" => "input_data.md", # TODO: uncomment once StatsPlots is ready + "Input Data" => "input_data.md", "Output" => "output.md", "Attributes" => "attributes.md", "Series Attributes" => "generated/attributes_series.md", "Plot Attributes" => "generated/attributes_plot.md", "Subplot Attributes" => "generated/attributes_subplot.md", "Axis Attributes" => "generated/attributes_axis.md", - # "Layouts" => "layouts.md", # TODO: uncomment once StatsPlots is ready + "Layouts" => "layouts.md", "Recipes" => [ "Overview" => "recipes.md", "RecipesBase" => [ @@ -679,12 +679,12 @@ function main() "Learning" => "learning.md", "Contributing" => "contributing.md", "Ecosystem" => [ - # "StatsPlots" => "generated/statsplots.md", # TODO: uncomment once StatsPlots is ready - # "GraphRecipes" => [ - # "Introduction" => "GraphRecipes/introduction.md", - # "Examples" => "GraphRecipes/examples.md", - # "Attributes" => "generated/graph_attributes.md", - # ], # TODO: uncomment once GraphRecipes is ready + "StatsPlots" => "generated/statsplots.md", + "GraphRecipes" => [ + "Introduction" => "GraphRecipes/introduction.md", + "Examples" => "GraphRecipes/examples.md", + "Attributes" => "generated/graph_attributes.md", + ], "UnitfulExt" => [ "Introduction" => "UnitfulExt/unitfulext.md", "Examples" => [ @@ -692,7 +692,7 @@ function main() "Plots" => "generated/unitfulext_plots.md", ] ], - # "Overview" => "ecosystem.md", # TODO: uncomment once StatsPlots is ready + "Overview" => "ecosystem.md", ], "Advanced Topics" => ["Plot objects" => "plot_objects.md","Plotting pipeline" => "pipeline.md"], "Gallery" => gallery, diff --git a/docs/src/UnitfulExt/unitfulext_examples.jl b/docs/src/UnitfulExt/unitfulext_examples.jl index dc891dd42..53857100a 100644 --- a/docs/src/UnitfulExt/unitfulext_examples.jl +++ b/docs/src/UnitfulExt/unitfulext_examples.jl @@ -64,7 +64,7 @@ plot([plot(y, ylab="mass", title=repr(s), unitformat=s) for s in (nothing, true, # `unitformat` can be one of a number of predefined symbols, defined in URsymbols = if isdefined(Base, :get_extension) - getproperty(Base.get_extension(Plots, :UnitfulExt), :UNIT_FORMATS) + getproperty(Base.get_extension(Plots.PlotsBase, :UnitfulExt), :UNIT_FORMATS) else Plots.UnitfulExt.UNIT_FORMATS end |> keys diff --git a/docs/src/UnitfulExt/unitfulext_plots.jl b/docs/src/UnitfulExt/unitfulext_plots.jl index e33c30d10..b0a63fa4e 100644 --- a/docs/src/UnitfulExt/unitfulext_plots.jl +++ b/docs/src/UnitfulExt/unitfulext_plots.jl @@ -108,7 +108,7 @@ plot( # ## Marker types -markers = intersect(Plots._shape_keys, PlotsBase.supported_markers()) +markers = intersect(PlotsBase.Commons._shape_keys, PlotsBase.supported_markers()) markers = reshape(markers, 1, length(markers)) n = length(markers) x = (range(0, stop=10, length=n + 2))[2:end - 1] * u"km" diff --git a/docs/src/backends.md b/docs/src/backends.md index 9c8ac3ef3..d91c49ce7 100644 --- a/docs/src/backends.md +++ b/docs/src/backends.md @@ -1,5 +1,5 @@ ```@setup backends -# using StatsPlots # NOTE: restore when StatsPlots compatible +using StatsPlots using Plots, RecipesBase, Statistics; gr() Plots.Commons.reset_defaults() @@ -18,13 +18,10 @@ Plots.Commons.reset_defaults() [f g] end - #= - # NOTE: restore when StatsPlots compatible @series begin subplot := 2 + (n > 2) RecipesBase.recipetype(:groupedbar, d) end - =# if n > 2 @series begin diff --git a/docs/src/input_data.md b/docs/src/input_data.md index 08dc77466..14b9e1970 100644 --- a/docs/src/input_data.md +++ b/docs/src/input_data.md @@ -171,7 +171,7 @@ function make_batman() for (i, mi) in enumerate(m) append!( pts, - map(BezierCurve([p[i], m[i], p[i + 1]]), range(0, 1, length = 30)) + map(PlotsBase.BezierCurves.BezierCurve([p[i], m[i], p[i + 1]]), range(0, 1, length = 30)) ) end x, y = Plots.unzip(Tuple.(pts)) diff --git a/docs/src/layouts.md b/docs/src/layouts.md index 74f8584a6..4baa4a09c 100644 --- a/docs/src/layouts.md +++ b/docs/src/layouts.md @@ -74,7 +74,7 @@ Use `px`/`mm`/`inch` for absolute coords, `w`/`h` for percentage relative to the ```@example layouts_2 # boxplot is defined in StatsPlots -using StatsPlots, StatsPlots.PlotMeasures +using StatsPlots gr(leg = false, bg = :lightgrey) # Create a filled contour and boxplot side by side. @@ -96,7 +96,7 @@ histogram!( # relative to a subplot) sticks!( randn(100), - inset = bbox(0, -0.2, 200px, 100px, :center), + inset = bbox(0, -0.2, 200Plots.px, 100Plots.px, :center), ticks = nothing, subplot = 4 ) From 379b5b225eb28fb90b269349cf88e4f080ffddb9 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 20:46:58 +0200 Subject: [PATCH 41/85] Update ci_build.sh --- docs/ci_build.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/ci_build.sh b/docs/ci_build.sh index 4c5dbd245..dd83e2d0f 100644 --- a/docs/ci_build.sh +++ b/docs/ci_build.sh @@ -76,12 +76,16 @@ $julia -e ' ' echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $GITHUB_ACTOR on $GITHUB_EVENT_NAME ==" -$julia -e 'using Pkg; Pkg.develop([ +$julia < Date: Sun, 13 Oct 2024 21:06:15 +0200 Subject: [PATCH 42/85] Update ci_build.sh --- docs/ci_build.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/ci_build.sh b/docs/ci_build.sh index dd83e2d0f..d12fa6002 100644 --- a/docs/ci_build.sh +++ b/docs/ci_build.sh @@ -76,16 +76,22 @@ $julia -e ' ' echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $GITHUB_ACTOR on $GITHUB_EVENT_NAME ==" -$julia < Date: Sun, 13 Oct 2024 21:06:39 +0200 Subject: [PATCH 43/85] Update ci_build.sh --- docs/ci_build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ci_build.sh b/docs/ci_build.sh index d12fa6002..cce35d060 100644 --- a/docs/ci_build.sh +++ b/docs/ci_build.sh @@ -79,7 +79,7 @@ echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $G $julia <<'EOF' using Pkg -rev = split(ENV["GITHUB_REF"], "/", limit=3)[3]) +rev = split(ENV["GITHUB_REF"], "/", limit=3)[3] println("rev=$rev") Pkg.develop([ @@ -90,7 +90,7 @@ Pkg.develop([ (; path="./GraphRecipes"), (; path="./StatsPlots"), ]) -Pkg.add(PackageSpec(; name="Plots", rev) +Pkg.add(PackageSpec(; name="Plots", rev)) Pkg.instantiate() EOF From cc021f6740981a305efc127f7827ffdd96a7db38 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 21:21:45 +0200 Subject: [PATCH 44/85] update docs (#5001) --- .github/workflows/docs.yml | 2 +- docs/ci_build.sh => ci/build-docs.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/ci_build.sh => ci/build-docs.sh (98%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bdd67b72f..5caa76a78 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,4 +28,4 @@ jobs: PYTHON: "" DOCUMENTER_KEY: ${{secrets.DOCUMENTER_KEY}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - run: bash docs/ci_build.sh + run: bash ci/build-docs.sh diff --git a/docs/ci_build.sh b/ci/build-docs.sh similarity index 98% rename from docs/ci_build.sh rename to ci/build-docs.sh index cce35d060..a84a587b3 100644 --- a/docs/ci_build.sh +++ b/ci/build-docs.sh @@ -54,7 +54,7 @@ export JULIA_CONDAPKG_BACKEND=MicroMamba julia='xvfb-run -a julia --color=yes --project=docs' -$julia -e ' +JULIA_PKG_PRECOMPILE_AUTO=0 $julia -e ' using Pkg; Pkg.add("CondaPkg") using CondaPkg; CondaPkg.resolve() libgcc = if Sys.islinux() From 12d1a41830149b69c7d96b4428a197e414d83b5e Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 21:27:27 +0200 Subject: [PATCH 45/85] Update build-docs.sh --- ci/build-docs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/build-docs.sh b/ci/build-docs.sh index a84a587b3..f7e2bd72e 100644 --- a/ci/build-docs.sh +++ b/ci/build-docs.sh @@ -76,7 +76,7 @@ JULIA_PKG_PRECOMPILE_AUTO=0 $julia -e ' ' echo "== build documentation for $GITHUB_REPOSITORY@$GITHUB_REF, triggered by $GITHUB_ACTOR on $GITHUB_EVENT_NAME ==" -$julia <<'EOF' +JULIA_PKG_PRECOMPILE_AUTO=0 $julia -e ' using Pkg rev = split(ENV["GITHUB_REF"], "/", limit=3)[3] @@ -93,5 +93,5 @@ Pkg.develop([ Pkg.add(PackageSpec(; name="Plots", rev)) Pkg.instantiate() -EOF +' $julia docs/make.jl From f7ed0f7d17d9d2c772a2d6c15184fe74960ddf16 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 22:00:01 +0200 Subject: [PATCH 46/85] add `Pkg.precompile` --- ci/build-docs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build-docs.sh b/ci/build-docs.sh index f7e2bd72e..8ce9acc12 100644 --- a/ci/build-docs.sh +++ b/ci/build-docs.sh @@ -91,7 +91,7 @@ Pkg.develop([ (; path="./StatsPlots"), ]) Pkg.add(PackageSpec(; name="Plots", rev)) - Pkg.instantiate() +Pkg.precompile() ' $julia docs/make.jl From 27d0c52848223bcbc320d27c9e42e448480fd7ad Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 22:19:53 +0200 Subject: [PATCH 47/85] change urls in `docs/make.jl` --- docs/make.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 523334efb..431928a60 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -57,7 +57,7 @@ end # ---------------------------------------------------------------------- edit_url(args...) = - "https://github.com/JuliaPlots/PlotDocs.jl/blob/master/docs/" * if length(args) == 0 + "https://github.com/JuliaPlots/Plots.jl/blob/master/docs/" * if length(args) == 0 "make.jl" else joinpath("src", args...) @@ -65,7 +65,7 @@ edit_url(args...) = autogenerated() = "(Automatically generated: " * Dates.format(now(), RFC1123Format) * ')' -author() = "[PlotDocs.jl](https://github.com/JuliaPlots/PlotDocs.jl)" +author() = "[Plots.jl](https://github.com/JuliaPlots/Plots.jl)" recursive_rmlines(x) = x function recursive_rmlines(x::Expr) From 38c6c95acbec62d8226dc98afebc516def34570d Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 22:50:22 +0200 Subject: [PATCH 48/85] restore test order in ci --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7a621134..42e421a6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,12 +92,7 @@ jobs: cmd=(xvfb-run ${cmd[@]}) fi echo ${cmd[@]} - ${cmd[@]} -e ' - using Pkg - Pkg.test(["GraphRecipes", "StatsPlots"]; coverage=true) - Pkg.test(["RecipesBase", "RecipesPipeline", "PlotsBase", "Plots"]; coverage=true) - ' - + ${cmd[@]} -e 'using Pkg; Pkg.test(["RecipesBase", "RecipesPipeline", "PlotsBase", "Plots", "GraphRecipes", "StatsPlots"]; coverage=true)' - uses: julia-actions/julia-processcoverage@latest if: startsWith(matrix.os, 'ubuntu') with: From 324fb8e3e1c019a1305ae9678713e24451b2c5ad Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 23:23:00 +0200 Subject: [PATCH 49/85] prevent segfault --- docs/make.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/make.jl b/docs/make.jl index 431928a60..30ad86d30 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -585,6 +585,8 @@ function main() unicodeplots() gaston() + PythonPlot.pygui(false) # prevent segfault on event loop in ci + # NOTE: for a faster representative test build use `PLOTDOCS_PACKAGES='GR' PLOTDOCS_EXAMPLES='1'` default_packages = "GR,PythonPlot,PlotlyJS,PGFPlotsX,UnicodePlots,Gaston" packages = get(ENV, "PLOTDOCS_PACKAGES", default_packages) From fb045e0a26bc9b75e018055e223c0a8e0a6730b0 Mon Sep 17 00:00:00 2001 From: t-bltg Date: Sun, 13 Oct 2024 23:23:58 +0200 Subject: [PATCH 50/85] add `MPLBACKEND` to ci script --- ci/build-docs.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/build-docs.sh b/ci/build-docs.sh index 8ce9acc12..47afc7938 100644 --- a/ci/build-docs.sh +++ b/ci/build-docs.sh @@ -48,6 +48,7 @@ fi export LD_PRELOAD=$(g++ --print-file-name=libstdc++.so) export GKSwstype=nul # Plots.jl/issues/3664 +export MPLBACKEND=agg export COLORTERM=truecolor # UnicodePlots.jl export PLOTDOCS_ANSICOLOR=true export JULIA_CONDAPKG_BACKEND=MicroMamba From a6562c14dc4e9f23b58958807f1afd19073dc8dd Mon Sep 17 00:00:00 2001 From: t-bltg Date: Mon, 14 Oct 2024 00:20:56 +0200 Subject: [PATCH 51/85] fix url in docs build script --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 30ad86d30..11f4fa103 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -820,7 +820,7 @@ function main() @info "deploydocs" deploydocs( - repo = "github.com/JuliaPlots/PlotDocs.jl.git", + repo = "github.com/JuliaPlots/Plots.jl.git", versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], push_preview = true, forcepush = true, From 52c63408657b72c8d4ab09cc16e48bf2392bb61a Mon Sep 17 00:00:00 2001 From: t-bltg Date: Mon, 14 Oct 2024 10:45:25 +0200 Subject: [PATCH 52/85] use `devbranch` in `docs/make.jl` --- docs/make.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/make.jl b/docs/make.jl index 11f4fa103..2dfe5fc62 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -822,6 +822,7 @@ function main() deploydocs( repo = "github.com/JuliaPlots/Plots.jl.git", versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], + devbranch = "v2", push_preview = true, forcepush = true, ) From c4ca40dc0aef6fe0cd067bce1c1458247c51bceb Mon Sep 17 00:00:00 2001 From: t-bltg Date: Mon, 14 Oct 2024 12:01:11 +0200 Subject: [PATCH 53/85] revert `devbranch` changes --- docs/make.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 2dfe5fc62..11f4fa103 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -822,7 +822,6 @@ function main() deploydocs( repo = "github.com/JuliaPlots/Plots.jl.git", versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], - devbranch = "v2", push_preview = true, forcepush = true, ) From 912824f7011f071303c57280186bd7e688044e91 Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Thu, 24 Oct 2024 15:39:03 +0200 Subject: [PATCH 54/85] correct deploy repo --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 11f4fa103..30ad86d30 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -820,7 +820,7 @@ function main() @info "deploydocs" deploydocs( - repo = "github.com/JuliaPlots/Plots.jl.git", + repo = "github.com/JuliaPlots/PlotDocs.jl.git", versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], push_preview = true, forcepush = true, From b51c665989ef38b4480e9dd9bf816ebee229284f Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 24 Oct 2024 15:46:22 +0200 Subject: [PATCH 55/85] animation: autoplay