diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index b338f8d9b..92d7deead 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -1,3 +1,4 @@ +for_in_replacement = "∈" always_for_in = true import_to_using = false align_pair_arrow = true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 18b49b51d..94713f441 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,3 @@ - ## Description ## Attribution 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..2dd781f61 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,7 +1,9 @@ name: benchmarks on: + workflow_dispatch: pull_request: + branches: [master] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e7445d06..760ff1b03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: ci on: + workflow_dispatch: pull_request: push: - branches: [master] + branches: [master, v2] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -17,10 +18,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,37 +29,21 @@ jobs: fail-fast: false matrix: version: - - '1.6' # LTS (minimal declared julia compat in `Project.toml`) - - '1' # latest stable + - '1' # latest stable + - '1.10' # 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: 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' - + version: 'pre' # upcoming julia version (`alpha`, `beta` or `rc`) + - os: macOS-latest + arch: aarch64 + version: '1' + experimental: false + steps: - uses: actions/checkout@v4 @@ -66,7 +51,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,64 +61,56 @@ 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} + - uses: julia-actions/cache@v2 + + - name: Develop all Plots packages + 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 + Pkg.develop([ + (; path="./RecipesBase"), + (; path="./RecipesPipeline"), + (; path="./PlotThemes"), + (; path="./PlotsBase"), + (; path="./GraphRecipes"), + (; path="./StatsPlots"), + (; path="."), + ]) - 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() + env: + JULIA_PKG_PRECOMPILE_AUTO: 0 + run: julia --color=yes ci/matplotlib.jl - - uses: julia-actions/julia-runtest@latest + - name: Test all Plots packages 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; Pkg.test([ + "RecipesBase", + "RecipesPipeline", + "PlotThemes", + "PlotsBase", + "GraphRecipes", + "StatsPlots", + "Plots", + ]; coverage=true)' - uses: julia-actions/julia-processcoverage@latest if: startsWith(matrix.os, 'ubuntu') with: - directories: RecipesBase/src,RecipesPipeline/src,src - - uses: codecov/codecov-action@v4 + directories: RecipesBase/src,RecipesPipeline/src,PlotsBase/src,src,GraphRecipes/src,StatsPlots/src,PlotThemes/src + - uses: codecov/codecov-action@v5 if: startsWith(matrix.os, 'ubuntu') with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false file: lcov.info Skip: diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 000000000..ec5e4972d --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,17 @@ +name: codespell + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@v2 + with: + ignore_words_list: nd,nax,namin,namax,linez,ist + only_warn: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b74399ab3..5caa76a78 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 @@ -30,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/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 951358a7d..337100005 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -1,9 +1,10 @@ name: format on: + workflow_dispatch: pull_request: push: - branches: [master] + branches: [master, v2] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -15,44 +16,24 @@ jobs: steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@latest - - name: Install dependencies + - name: format Julia files run: | - using Pkg - Pkg.add([ - PackageSpec("JuliaFormatter"), - PackageSpec(url = "https://github.com/tkf/JuliaProjectFormatter.jl.git"), - ]) - shell: julia --color=yes {0} - - - name: Format Julia files - run: | - using JuliaFormatter - format(["RecipesBase", "RecipesPipeline", "src", "test", "ext"]) - shell: julia --color=yes --compile=min -O0 {0} - - name: suggester / JuliaFormatter + julia --color=yes --compile=min -O0 -e ' + using Pkg; Pkg.add("JuliaFormatter") + using JuliaFormatter + format(["RecipesBase", "RecipesPipeline", "PlotsBase", "src", "test", "GraphRecipes", "StatsPlots", "PlotThemes"]) + ' + - name: suggester + if: success() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/v2' ) uses: reviewdog/action-suggester@v1 with: tool_name: JuliaFormatter fail_on_error: true # reviewdog/action-suggester not using `cleanup` flag? - - name: Cleanup + - name: cleanup if: success() || failure() run: | 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 diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml index 14979d97e..19db47078 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 * *' @@ -19,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]" diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml deleted file mode 100644 index 98d42f9cf..000000000 --- a/.github/workflows/invalidations.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: invalidations -on: - pull_request: - push: - branches: [master] - -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 diff --git a/.gitignore b/.gitignore index 39b6f9a2d..35558ab62 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,9 @@ test/tmpplotsave.hdf5 /.benchmarkci /benchmark/*.json .vscode/ -.CondaPkg/ \ No newline at end of file +.CondaPkg/ + +# docs +**/generated/ +docs/build/ +docs/work/ \ No newline at end of file diff --git a/.zenodo.json b/.zenodo.json index 539036d03..01b2c5b08 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -770,6 +770,29 @@ "name": "Syver Døving Agdestein", "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", + "type": "Other" + }, + { + "name": "Leon Becker", + "type": "Other" + }, + { + "name": "Patrick Jaap", + "type": "Other" + }, + { + "name": "Wolf, Ron", + "type": "Other" } ], "upload_type": "software" 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..b8ebdd0de --- /dev/null +++ b/GraphRecipes/Project.toml @@ -0,0 +1,47 @@ +name = "GraphRecipes" +uuid = "bd48cda9-67a9-57be-86fa-5b3c104eda73" +version = "1.0.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" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +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.15" +NaNMath = "1" +NetworkLayout = "0.4" +PlotUtils = "1" +RecipesBase = "1" +Statistics = "1" +julia = "1.10" + +[extras] +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +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" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +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", "GR", "ImageMagick", "LinearAlgebra", "Logging", "Markdown", "PlotsBase", "Random", "SparseArrays", "StableRNGs", "Test", "VisualRegressionTests"] diff --git a/GraphRecipes/README.md b/GraphRecipes/README.md new file mode 100644 index 000000000..b59c42fdd --- /dev/null +++ b/GraphRecipes/README.md @@ -0,0 +1,46 @@ +[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 PlotsBase + +import GR; gr() + +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 000000000..76a8e65f8 Binary files /dev/null and b/GraphRecipes/assets/arc_chord_diagrams.png differ diff --git a/GraphRecipes/assets/ast_example.png b/GraphRecipes/assets/ast_example.png new file mode 100644 index 000000000..36498ed1e Binary files /dev/null and b/GraphRecipes/assets/ast_example.png differ diff --git a/GraphRecipes/assets/custom_nodeshapes_single.png b/GraphRecipes/assets/custom_nodeshapes_single.png new file mode 100644 index 000000000..1430d0e04 Binary files /dev/null and b/GraphRecipes/assets/custom_nodeshapes_single.png differ diff --git a/GraphRecipes/assets/custom_nodeshapes_various.png b/GraphRecipes/assets/custom_nodeshapes_various.png new file mode 100644 index 000000000..7277664ab Binary files /dev/null and b/GraphRecipes/assets/custom_nodeshapes_various.png differ diff --git a/GraphRecipes/assets/directed.png b/GraphRecipes/assets/directed.png new file mode 100644 index 000000000..d12812a45 Binary files /dev/null and b/GraphRecipes/assets/directed.png differ diff --git a/GraphRecipes/assets/edgelabel.png b/GraphRecipes/assets/edgelabel.png new file mode 100644 index 000000000..02658ea82 Binary files /dev/null and b/GraphRecipes/assets/edgelabel.png differ diff --git a/GraphRecipes/assets/funky_edge_and_marker_args.png b/GraphRecipes/assets/funky_edge_and_marker_args.png new file mode 100644 index 000000000..8111eecd3 Binary files /dev/null and b/GraphRecipes/assets/funky_edge_and_marker_args.png differ diff --git a/GraphRecipes/assets/julia_dict_tree.png b/GraphRecipes/assets/julia_dict_tree.png new file mode 100644 index 000000000..6cc7407a0 Binary files /dev/null and b/GraphRecipes/assets/julia_dict_tree.png differ diff --git a/GraphRecipes/assets/julia_type_tree.png b/GraphRecipes/assets/julia_type_tree.png new file mode 100644 index 000000000..0666a6e2e Binary files /dev/null and b/GraphRecipes/assets/julia_type_tree.png differ diff --git a/GraphRecipes/assets/light_graphs.png b/GraphRecipes/assets/light_graphs.png new file mode 100644 index 000000000..fee0e9c61 Binary files /dev/null and b/GraphRecipes/assets/light_graphs.png differ diff --git a/GraphRecipes/assets/marker_properties.png b/GraphRecipes/assets/marker_properties.png new file mode 100644 index 000000000..e5ac9e124 Binary files /dev/null and b/GraphRecipes/assets/marker_properties.png differ diff --git a/GraphRecipes/assets/multigraphs.png b/GraphRecipes/assets/multigraphs.png new file mode 100644 index 000000000..01b6bd396 Binary files /dev/null and b/GraphRecipes/assets/multigraphs.png differ diff --git a/GraphRecipes/assets/random_3d_graph.png b/GraphRecipes/assets/random_3d_graph.png new file mode 100644 index 000000000..80291f7aa Binary files /dev/null and b/GraphRecipes/assets/random_3d_graph.png differ diff --git a/GraphRecipes/assets/random_labelled_graph.png b/GraphRecipes/assets/random_labelled_graph.png new file mode 100644 index 000000000..a4681c71a Binary files /dev/null and b/GraphRecipes/assets/random_labelled_graph.png differ diff --git a/GraphRecipes/assets/readme_julia_logo_pun.png b/GraphRecipes/assets/readme_julia_logo_pun.png new file mode 100644 index 000000000..d9a471c0c Binary files /dev/null and b/GraphRecipes/assets/readme_julia_logo_pun.png differ diff --git a/GraphRecipes/assets/selfedges.png b/GraphRecipes/assets/selfedges.png new file mode 100644 index 000000000..44b663f22 Binary files /dev/null and b/GraphRecipes/assets/selfedges.png differ 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..88df22985 --- /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) + + ([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 + num, den = 0.0, 0.0 + for j ∈ 1:n + i == j && continue + num += + w[i, j] * + (X[p][j] + dist[i, j] * (X[p][i] - X[p][j]) / norm_ij(X, i, j)) + den += w[i, j] + end + if den != 0 + X[p][i] = num / den + 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..eeb83da4e --- /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 +@eval AbstractTrees children(d::AnstractDict) = [p for p in d] +@eval AbstractTrees children(p::Pair) = AbstractTrees.children(p[2]) +@eval AbstractTrees function printnode(io::IO, p::Pair) + str = isempty(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..9061f6ac8 --- /dev/null +++ b/GraphRecipes/test/functions.jl @@ -0,0 +1,279 @@ + +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), +) + +@eval AbstractTrees children(d::AbstractDict) = [p for p ∈ d] +@eval AbstractTrees children(p::Pair) = AbstractTrees.children(p[2]) +@eval AbstractTrees function printnode(io::IO, p::Pair) + str = + isempty(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..5b0544985 --- /dev/null +++ b/GraphRecipes/test/parse_readme.jl @@ -0,0 +1,20 @@ +using GraphRecipes +using Markdown + +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..28096b354 --- /dev/null +++ b/GraphRecipes/test/pkg_deps.jl @@ -0,0 +1,115 @@ +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..dd7a50f76 --- /dev/null +++ b/GraphRecipes/test/runtests.jl @@ -0,0 +1,189 @@ +using VisualRegressionTests +using AbstractTrees +using LinearAlgebra +using GraphRecipes.AbstractTrees +using GraphRecipes.Colors +using GraphRecipes +using SparseArrays +using ImageMagick +using StableRNGs +using PlotsBase +using Logging +using Graphs +using Test +using Gtk # for popup + +import GR; gr() + +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/NEWS.md b/NEWS.md index fcb363c42..edd7fc079 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 @@ -46,7 +62,7 @@ - stephist logscale improvements ## 0.25.2 -- improvements to handle missings +- improvements to handle `missing`s - pyplot: allow setting the color gradient for z values - document :colorbar_entry - limit number of automatic bins @@ -146,7 +162,7 @@ - implement guide position in gr, pyplot and pgfplots - inspectdr fixes - default appveyor -- rudimentary missings support +- rudimentary `missing`s support - deprecation fixes for PGFPlots ## 0.20.0 @@ -552,7 +568,7 @@ Many updates, min julia 1.0 - GR: - manually draw 2D axes... fixes several issues and missing features - fontsize fix -- PGFPlots: pass axis syle +- PGFPlots: pass axis style #### 0.8.0 diff --git a/PlotThemes/LICENSE.md b/PlotThemes/LICENSE.md new file mode 100644 index 000000000..775bc2312 --- /dev/null +++ b/PlotThemes/LICENSE.md @@ -0,0 +1,22 @@ +The PlotThemes.jl package is licensed under the MIT "Expat" License: + +> Copyright (c) 2016: Patrick Kofod Mogensen. +> +> 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/PlotThemes/Project.toml b/PlotThemes/Project.toml new file mode 100644 index 000000000..418c81f93 --- /dev/null +++ b/PlotThemes/Project.toml @@ -0,0 +1,17 @@ +name = "PlotThemes" +uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +version = "3.3.0" + +[deps] +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[compat] +PlotUtils = "1" +julia = "1" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/PlotThemes/README.md b/PlotThemes/README.md new file mode 100644 index 000000000..5ce6439f9 --- /dev/null +++ b/PlotThemes/README.md @@ -0,0 +1,114 @@ +# PlotThemes + +[![Build Status](https://travis-ci.org/JuliaPlots/PlotThemes.jl.svg?branch=master)](https://travis-ci.org/JuliaPlots/PlotThemes.jl) + +#### Primary author: Patrick Kofod Mogensen (@pkofod) + +PlotThemes is a package to spice up the plots made with [Plots.jl](https://github.com/JuliaPlots/Plots.jl). To install: + +```julia +Pkg.add("PlotThemes") +``` + +## Using PlotThemes + +Currently the following themes are available: +- `:default` +- `:dark` +- `:ggplot2` +- `:juno` +- `:lime` +- `:orange` +- `:sand` +- `:solarized` +- `:solarized_light` +- `:wong` +- `:wong2` +- `:gruvbox_dark` +- `:gruvbox_light` +- `:bright` +- `:vibrant` +- `:mute` +- `:dao` +- `:dracula` +- `:rose_pine` +- `:rose_pine_dawn` + + +When using Plots, a theme can be set using the `theme` function: +```julia +using Plots +theme(thm::Symbol; kwargs...) +``` +`theme` accepts any Plots [attribute](https://docs.juliaplots.org/stable/attributes/) as keyword argument and sets its value as default for subsequent plots. + +Themes can be previewed using `Plots.showtheme(thm::Symbol)`: + +### `:default` +![theme_default](https://user-images.githubusercontent.com/16589944/70847841-7ca7ea00-1e69-11ea-851e-e99d8559260d.png) + +### `:dark` +![theme_dark](https://user-images.githubusercontent.com/16589944/70847843-8d586000-1e69-11ea-9e39-5d4c44865750.png) + +### `:ggplot2` +![theme_ggplot2](https://user-images.githubusercontent.com/16589944/70847847-99442200-1e69-11ea-9ae8-ddd10ec4a303.png) + +### `:juno` +![theme_juno](https://user-images.githubusercontent.com/16589944/70847860-be389500-1e69-11ea-88b9-2a3bb3bbcf64.png) + +### `:lime` +![theme_lime](https://user-images.githubusercontent.com/16589944/70847863-c55fa300-1e69-11ea-8de7-3ef2a3a8ce30.png) + +### `:orange` +![theme_orange](https://user-images.githubusercontent.com/16589944/70847866-cf81a180-1e69-11ea-813a-7e4394311ef2.png) + +### `:sand` +![theme_sand](https://user-images.githubusercontent.com/16589944/70847868-d4deec00-1e69-11ea-8aab-de94af02bbfe.png) + +### `:solarized` +![theme_solarized](https://user-images.githubusercontent.com/16589944/70847869-dad4cd00-1e69-11ea-930e-b145e19bcff5.png) + +### `:solarized_light` +![theme_solarized_light](https://user-images.githubusercontent.com/16589944/70847872-df998100-1e69-11ea-955a-7fcc2e7043de.png) + +### `:wong` +![theme_wong](https://user-images.githubusercontent.com/16589944/70847875-e58f6200-1e69-11ea-904d-d4f7e27bb181.png) + +### `:wong2` +![theme_wong2](https://user-images.githubusercontent.com/16589944/70847880-eaecac80-1e69-11ea-9b59-cdf937427121.png) + +### `:gruvbox_dark` +![theme_gruvbox_dark](https://user-images.githubusercontent.com/16589944/70847882-f049f700-1e69-11ea-94f8-255f2cd17288.png) + +### `:gruvbox_light` +![theme_gruvbox_light](https://user-images.githubusercontent.com/16589944/70847885-fb048c00-1e69-11ea-9546-4dc0d9e4154c.png) + +### `:bright` +![theme_bright](https://user-images.githubusercontent.com/16589944/70848065-8ed75780-1e6c-11ea-8e59-9882b3c4a4db.png) + +### `:vibrant` +![theme_vibrant](https://user-images.githubusercontent.com/16589944/70848066-926ade80-1e6c-11ea-91ba-fd08d14e6963.png) + +### `:mute` +![theme_mute](https://user-images.githubusercontent.com/16589944/70848069-9860bf80-1e6c-11ea-9cac-8a797d526835.png) + +### `:dao` +![theme_dao](https://user-images.githubusercontent.com/7330605/106512313-54de7000-64c9-11eb-98d5-2aee3603879a.png) + +### `:dracula` +![theme_dracula](https://user-images.githubusercontent.com/64332767/136754217-31d4348a-c873-4496-8b66-905e4d8a7e36.png) + +### `:rose_pine` +![theme_rose_pine](https://github.com/BaerLKR/PlotThemes.jl/assets/83340886/30c869e5-2b90-405a-bc49-cf4ef3c43d75) + +### `:rose_pine_dawn` +![theme_rose_pine_dawn](https://github.com/BaerLKR/PlotThemes.jl/assets/83340886/e30c0b46-1be3-49f5-afc5-ceede0b0c27d) + +## Contributing +A theme specifies default values for different Plots [attributes](https://docs.juliaplots.org/stable/attributes/). +At the moment these are typically colors, palettes and colorgradients, but any Plots attribute can be controlled by a theme in general. +PRs for new themes very welcome! Adding a new theme (e.g. `mytheme`) is as easy as adding a new file (mytheme.jl) that contains at least the following line: +```julia +_themes[:mytheme] = PlotTheme(; kwargs...) +``` +The keyword arguments can be any collection of Plots attributes plus a colorgradient keyword argument. diff --git a/PlotThemes/src/PlotThemes.jl b/PlotThemes/src/PlotThemes.jl new file mode 100644 index 000000000..8a5488f88 --- /dev/null +++ b/PlotThemes/src/PlotThemes.jl @@ -0,0 +1,78 @@ +module PlotThemes + +using PlotUtils + +export add_theme, theme_palette, PlotTheme + +_255_to_1(c::Symbol, colors) = RGBA(map(x -> x / 255, colors[c])...) +RGB255(r, g, b) = RGB(r / 255, g / 255, b / 255) + +function expand_palette(bg, cs; kwargs...) + colors = palette(cs).colors.colors + c = convert.(RGBA, distinguishable_colors(20, vcat(bg, colors); kwargs...))[2:end] + return palette(c) +end + +const KW = Dict{Symbol,Any} + +struct PlotTheme + defaults::KW +end + +PlotTheme(; kw...) = PlotTheme(KW(kw)) + +# adjust an existing theme +PlotTheme(base::PlotTheme; kw...) = PlotTheme(KW(base.defaults..., KW(kw)...)) + +"Get the palette of a PlotTheme" +function theme_palette(s::Symbol) + if haskey(_themes, s) && haskey(_themes[s].defaults, :palette) + return _themes[s].defaults[:palette] + else + return palette(:default) + end +end + +# add themes +include("dark.jl") +include("ggplot2.jl") +include("solarized.jl") +include("sand.jl") +include("lime.jl") +include("orange.jl") +include("wong.jl") +include("boxed.jl") +include("juno.jl") +include("gruvbox.jl") +include("sheet.jl") +include("dao.jl") +include("dracula.jl") +include("rose_pine.jl") + +const _themes = Dict{Symbol,PlotTheme}([ + :default => PlotTheme(), + :dao => _dao, + :dark => _dark, + :ggplot2 => _ggplot2, + :gruvbox_light => _gruvbox_light, + :gruvbox_dark => _gruvbox_dark, + :solarized => _solarized, + :solarized_light => _solarized_light, + :sand => _sand, + :bright => _bright, + :vibrant => _vibrant, + :mute => _mute, + :wong => _wong, + :wong2 => _wong2, + :boxed => _boxed, + :juno => _juno, + :lime => _lime, + :orange => _orange, + :dracula => _dracula, + :rose_pine => _rose_pine, + :rose_pine_dawn => _rose_pine_dawn, +]) + +add_theme(s::Symbol, thm::PlotTheme) = _themes[s] = thm + +end # module diff --git a/PlotThemes/src/boxed.jl b/PlotThemes/src/boxed.jl new file mode 100644 index 000000000..cd1063f8e --- /dev/null +++ b/PlotThemes/src/boxed.jl @@ -0,0 +1,21 @@ +const _boxed = PlotTheme( + Dict([ + :minorticks => true, + :grid => false, + :frame => :box, + :guidefontvalign => :top, + :guidefonthalign => :right, + :foreground_color_legend => nothing, + :legendfontsize => 9, + :legend => :topright, + :xlim => (:auto, :auto), + :ylim => (:auto, :auto), + :label => "", + :palette => expand_palette( + colorant"white", + [RGB(0, 0, 0); wong_palette]; + lchoices = [57], + cchoices = [100], + ), + ]), +) diff --git a/PlotThemes/src/dao.jl b/PlotThemes/src/dao.jl new file mode 100644 index 000000000..816ee530c --- /dev/null +++ b/PlotThemes/src/dao.jl @@ -0,0 +1,30 @@ +const dao_palette = [ + colorant"#d77255", + colorant"#009afa", + colorant"#707070", + colorant"#21ab74", + colorant"#ba3030", + colorant"#9467bd", +] + +const _dao = PlotTheme( + Dict([ + :background => :white, + :framestyle => :box, + :grid => true, + :gridalpha => 0.4, + :linewidth => 1.4, + :markerstrokewidth => 0, + :fontfamily => "Computer Modern", + :colorgradient => :magma, + :guidefontsize => 12, + :titlefontsize => 12, + :tickfontsize => 8, + :palette => dao_palette, + :minorgrid => true, + :minorticks => 5, + :gridlinewidth => 0.7, + :minorgridalpha => 0.06, + :legend => :outertopright, + ]), +) diff --git a/PlotThemes/src/dark.jl b/PlotThemes/src/dark.jl new file mode 100644 index 000000000..22d2e4479 --- /dev/null +++ b/PlotThemes/src/dark.jl @@ -0,0 +1,25 @@ +#inspired by nucleus theme for Atom +const dark_palette = [ + colorant"#FE4365", # red + colorant"#eca25c", # orange + colorant"#3f9778", # green + colorant"#005D7F", # blue +] +const dark_bg = colorant"#363D46" + +const _dark = PlotTheme( + Dict([ + :bg => dark_bg, + :bginside => colorant"#30343B", + :fg => colorant"#ADB2B7", + :fgtext => colorant"#FFFFFF", + :fgguide => colorant"#FFFFFF", + :fglegend => colorant"#FFFFFF", + :legendfontcolor => colorant"#FFFFFF", + :legendtitlefontcolor => colorant"#FFFFFF", + :titlefontcolor => colorant"#FFFFFF", + :palette => + expand_palette(dark_bg, dark_palette; lchoices = [57], cchoices = [100]), + :colorgradient => :fire, + ]), +) diff --git a/PlotThemes/src/dracula.jl b/PlotThemes/src/dracula.jl new file mode 100644 index 000000000..a1ddd1422 --- /dev/null +++ b/PlotThemes/src/dracula.jl @@ -0,0 +1,30 @@ +# Names follow: +# https://draculatheme.com/contribute#color-palette +const dracula_palette = [ + colorant"#8be9fd" # Cyan + colorant"#ff79c6" # Pink + colorant"#50fa7b" # Green + colorant"#bd93f9" # Purple + colorant"#ffb86c" # Orange + colorant"#ff5555" # Red + colorant"#f1fa8c" # Yellow + colorant"#6272a4" # Comment +] +const dracula_bg = colorant"#282a36" +const dracula_fg = colorant"#f8f8f2" + +const _dracula = PlotTheme( + Dict([ + :bg => dracula_bg, + :bginside => colorant"#30343B", + :fg => dracula_fg, + :fgtext => dracula_fg, + :fgguide => dracula_fg, + :fglegend => dracula_fg, + :legendfontcolor => dracula_fg, + :legendtitlefontcolor => dracula_fg, + :titlefontcolor => dracula_fg, + :palette => expand_palette(dracula_bg, dracula_palette), + :colorgradient => :viridis, + ]), +) diff --git a/PlotThemes/src/ggplot2.jl b/PlotThemes/src/ggplot2.jl new file mode 100644 index 000000000..36f989874 --- /dev/null +++ b/PlotThemes/src/ggplot2.jl @@ -0,0 +1,65 @@ +# # unfished +# add_theme(:ggplot2_base, +# bglegend = _invisible, +# fg = :white, +# fglegend = _invisible, +# fgguide = :black) +# +# add_theme(:ggplot2, +# base = :ggplot2_base, +# bginside = :lightgray, +# fg = :lightgray, +# fgtext = :gray, +# fglegend = :gray, +# fgguide = :black) +# +# add_theme(:ggplot2_grey, base = :ggplot2) +# +# add_theme(:ggplot2_bw, +# base = :ggplot2_base, +# bginside = :white, +# fg = :black, +# fgtext = :lightgray, +# fglegend = :lightgray, +# fgguide = :black) + +const _ggplot_colors = Dict( + :gray92 => RGB(fill(0.92, 3)...), + :gray20 => RGB(fill(0.2, 3)...), + :gray30 => RGB(fill(0.3, 3)...), +) + +const _ggplot2 = PlotTheme( + Dict([ + ## Background etc + :bg => :white, + :bginside => _ggplot_colors[:gray92], + :bglegend => _ggplot_colors[:gray92], + :fglegend => :white, + :fgguide => :black, + :widen => true, + ## Axes / Ticks + #framestyle => :grid, + #foreground_color_tick => _ggplot_colors[:gray20], # tick color not yet implemented + :foreground_color_axis => _ggplot_colors[:gray20], # tick color + :tick_direction => :out, + :foreground_color_border => :white, # axis color + :foreground_color_text => _ggplot_colors[:gray30], # tick labels + :gridlinewidth => 1, + #tick label size => *0.8, + ### Grid + :foreground_color_grid => :white, + :gridalpha => 1, + ### Minor Grid + :minorgrid => true, + :minorgridalpha => 1, + :minorgridlinewidth => 0.5, # * 0.5 + :foreground_color_minor_grid => :white, + #foreground_color_minortick=>:white, ## not yet implemented + :minorticks => 2, + ## Lines and markers + :markerstrokealpha => 0, + :markerstrokewidth => 0, + ]), + #showaxis=> :false +) diff --git a/PlotThemes/src/gruvbox.jl b/PlotThemes/src/gruvbox.jl new file mode 100644 index 000000000..31de14616 --- /dev/null +++ b/PlotThemes/src/gruvbox.jl @@ -0,0 +1,94 @@ +# https://github.com/morhetz/gruvbox +const _gruvbox_dark_palette = ( + :bright_green, + :bright_yellow, + :bright_blue, + :bright_aqua, + :bright_purple, + :bright_red, + :bright_orange, +) + +const _gruvbox_light_palette = ( + :faded_green, + :faded_yellow, + :faded_blue, + :faded_aqua, + :faded_purple, + :faded_red, + :faded_orange, +) + +const _gruvbox_colors = Dict( + :dark0_hard => RGB255(29, 32, 33), + :dark0 => RGB255(40, 40, 40), + :dark0_soft => RGB255(50, 48, 47), + :dark1 => RGB255(60, 56, 54), + :dark2 => RGB255(80, 73, 69), + :dark3 => RGB255(102, 92, 84), + :dark4 => RGB255(124, 111, 100), + :dark4_256 => RGB255(124, 111, 100), + :gray_245 => RGB255(146, 131, 116), + :gray_244 => RGB255(146, 131, 116), + :light0_hard => RGB255(249, 245, 215), + :light0 => RGB255(253, 244, 193), + :light0_soft => RGB255(242, 229, 188), + :light1 => RGB255(235, 219, 178), + :light2 => RGB255(213, 196, 161), + :light3 => RGB255(189, 174, 147), + :light4 => RGB255(168, 153, 132), + :light4_256 => RGB255(168, 153, 132), + :bright_red => RGB255(251, 73, 52), + :bright_green => RGB255(184, 187, 38), + :bright_yellow => RGB255(250, 189, 47), + :bright_blue => RGB255(131, 165, 152), + :bright_purple => RGB255(211, 134, 155), + :bright_aqua => RGB255(142, 192, 124), + :bright_orange => RGB255(254, 128, 25), + :neutral_red => RGB255(204, 36, 29), + :neutral_green => RGB255(152, 151, 26), + :neutral_yellow => RGB255(215, 153, 33), + :neutral_blue => RGB255(69, 133, 136), + :neutral_purple => RGB255(177, 98, 134), + :neutral_aqua => RGB255(104, 157, 106), + :neutral_orange => RGB255(214, 93, 14), + :faded_red => RGB255(157, 0, 6), + :faded_green => RGB255(121, 116, 14), + :faded_yellow => RGB255(181, 118, 20), + :faded_blue => RGB255(7, 102, 120), + :faded_purple => RGB255(143, 63, 113), + :faded_aqua => RGB255(66, 123, 88), + :faded_orange => RGB255(175, 58, 3), +) + +const _gruvbox_dark = PlotTheme( + Dict([ + :bg => _gruvbox_colors[:dark2], + :bginside => _gruvbox_colors[:dark0], + :fg => _gruvbox_colors[:light3], + :fgtext => _gruvbox_colors[:light3], + :fgguide => _gruvbox_colors[:dark1], + :fglegend => _gruvbox_colors[:light3], + :palette => expand_palette( + _gruvbox_colors[:dark3], + [_gruvbox_colors[c] for c ∈ _gruvbox_dark_palette], + ), + :colorgradient => cgrad(:YlOrRd, rev = true), + ]), +) + +const _gruvbox_light = PlotTheme( + Dict([ + :bg => _gruvbox_colors[:light1], + :bginside => _gruvbox_colors[:light0], + :fg => _gruvbox_colors[:dark1], + :fgtext => _gruvbox_colors[:dark1], + :fgguide => _gruvbox_colors[:dark1], + :fglegend => _gruvbox_colors[:dark1], + :palette => expand_palette( + _gruvbox_colors[:light3], + [_gruvbox_colors[c] for c ∈ _gruvbox_light_palette], + ), + :colorgradient => cgrad(:YlOrRd, rev = true), + ]), +) diff --git a/PlotThemes/src/juno.jl b/PlotThemes/src/juno.jl new file mode 100644 index 000000000..07ca2b233 --- /dev/null +++ b/PlotThemes/src/juno.jl @@ -0,0 +1,23 @@ +#inspired by nucleus theme for Atom +const juno_palette = [ + colorant"#FE4365", # red + colorant"#eca25c", # orange + colorant"#3f9778", # green + colorant"#005D7F", # blue +] + +const juno_bg = colorant"#282C34" + +const _juno = PlotTheme( + Dict([ + :bg => juno_bg, + :bginside => colorant"#21252B", + :fg => colorant"#ADB2B7", + :fgtext => colorant"#9EB1BE", + :fgguide => colorant"#9EB1BE", + :fglegend => colorant"#9EB1BE", + :palette => + expand_palette(juno_bg, juno_palette; lchoices = [57], cchoices = [100]), + :colorgradient => :fire, + ]), +) diff --git a/PlotThemes/src/lime.jl b/PlotThemes/src/lime.jl new file mode 100644 index 000000000..48050d190 --- /dev/null +++ b/PlotThemes/src/lime.jl @@ -0,0 +1,24 @@ +# a blue/green/yellow theme of no specific origin +const lime_palette = reverse([ + colorant"#271924", # dark blue + colorant"#394256", # semi dark blue + colorant"#30727F", # green blue + colorant"#36A58F", # turqoise + colorant"#80D584", # green, + colorant"#EBFB73", +]) # yellow + +const black = lime_palette[6] + +const _lime = PlotTheme( + Dict([ + :bg => black, + :bginside => black, + :fg => lime_palette[1], + :fgtext => lime_palette[2], + :fgguide => lime_palette[2], + :fglegend => lime_palette[2], + :palette => expand_palette(black, lime_palette[1:4]), + :colorgradient => :viridis, + ]), +) diff --git a/PlotThemes/src/orange.jl b/PlotThemes/src/orange.jl new file mode 100644 index 000000000..c358a3f01 --- /dev/null +++ b/PlotThemes/src/orange.jl @@ -0,0 +1,23 @@ +#252634,#234B57,#207269,#4F9866,#9BB858,#FACF5A +# a blue/green/yellow theme of no specific origin +const orange_palette = reverse([ + colorant"#271924", # dark blue + colorant"#20545D", # semi dark blue + colorant"#32856A", # green blue + colorant"#86B15B", # green, + colorant"#FACF5A", # yellow +]) +const _black = orange_palette[5] + +const _orange = PlotTheme( + Dict([ + :bg => _black, + :bginside => _black, + :fg => orange_palette[1], + :fgtext => orange_palette[2], + :fgguide => orange_palette[2], + :fglegend => orange_palette[2], + :palette => expand_palette(black, orange_palette[1:4]), + :colorgradient => :viridis, + ]), +) diff --git a/PlotThemes/src/rose_pine.jl b/PlotThemes/src/rose_pine.jl new file mode 100644 index 000000000..c9519e494 --- /dev/null +++ b/PlotThemes/src/rose_pine.jl @@ -0,0 +1,50 @@ +# https://rosepinetheme.com +const rose_pine_palette = [ + colorant"#524f67", # Highlight High + colorant"#31748f", # pine + colorant"#9ccfd8", # foam + colorant"#ebbcba", # rose + colorant"#f6c177", # gold + colorant"#eb6f92", # love + colorant"#c4a7e7", # Iris +] + +const rose_pine_bg = colorant"#191724" + +const _rose_pine = PlotTheme( + Dict([ + :bg => rose_pine_bg, + :bginside => colorant"#1f1d2e", + :fg => colorant"#e0def4", + :fgtext => colorant"#e0def4", + :fgguide => colorant"#e0def4", + :fglegend => colorant"#e0def4", + :palette => expand_palette(rose_pine_bg, rose_pine_palette), + :colorgradient => cgrad(rose_pine_palette), + ]), +) + +const rose_pine_dawn_palette = [ + colorant"#907aa9", # Iris + colorant"#286983", # pine + colorant"#56949f", # foam + colorant"#cecacd", # Highlight High + colorant"#ea9d34", # gold + colorant"#d7827e", # rose + colorant"#b4637a", # love +] + +const rose_pine_dawn_bg = colorant"#faf4ed" + +const _rose_pine_dawn = PlotTheme( + Dict([ + :bg => rose_pine_dawn_bg, + :bginside => colorant"#fffaf3", + :fg => colorant"#575279", + :fgtext => colorant"#575279", + :fgguide => colorant"#575279", + :fglegend => colorant"#575279", + :palette => expand_palette(rose_pine_dawn_bg, rose_pine_dawn_palette), + :colorgradient => cgrad(rose_pine_dawn_palette), + ]), +) diff --git a/PlotThemes/src/sand.jl b/PlotThemes/src/sand.jl new file mode 100644 index 000000000..f3071452d --- /dev/null +++ b/PlotThemes/src/sand.jl @@ -0,0 +1,24 @@ +#inspired by flatwhite syntax theme for Atom +const sand_palette = [ + colorant"#6494ED", # blue + colorant"#73C990", # green + colorant"#E2C08D", # brown + colorant"#FF6347", # red + colorant"#2E2C29", # dark, + colorant"#4B4844", # medium +] + +const sand_bg = colorant"#F7F3EE" + +const _sand = PlotTheme( + Dict([ + :bg => sand_bg, + :bginside => colorant"#E2DCD4", + :fg => colorant"#CBBFAF", + :fgtext => colorant"#725B61", + :fgguide => colorant"#725B61", + :fglegend => colorant"#725B61", + :palette => expand_palette(sand_bg, sand_palette), + :colorgradient => :dense, + ]), +) diff --git a/PlotThemes/src/sheet.jl b/PlotThemes/src/sheet.jl new file mode 100644 index 000000000..a9770795c --- /dev/null +++ b/PlotThemes/src/sheet.jl @@ -0,0 +1,135 @@ +const sheet_args = Dict{Symbol,Any}([ + :fglegend => plot_color(colorant"#225", 0.1), + :bglegend => plot_color(:white, 0.9), + :gridcolor => colorant"#225", + :minorgridcolor => colorant"#225", + :framestyle => :grid, + :minorgrid => true, + :linewidth => 1.2, + :markersize => 6, + :markerstrokewidth => 0, +]) + +#= NOTE ======================================================================== +Colors are taken from https://personal.sron.nl/~pault/ +===============================================================================# + +# ------------------------------------------------------------------------------ +# Mute +# ------------------------------------------------------------------------------ + +const mute_palette = [ # bright + colorant"#c67", # rose + colorant"#328", # indigo + colorant"#dc7", # sand + colorant"#173", # green + colorant"#8ce", # cyan + colorant"#825", # wine + colorant"#4a9", # teal + colorant"#993", # olive + colorant"#a49", # purple + colorant"#ddd", # grey +] + +const ylorbr_gradient = [ + colorant"#fefbe9", + colorant"#fcf7d5", + colorant"#f5f3c1", + colorant"#eaf0b5", + colorant"#ddecbf", + colorant"#d0e7ca", + colorant"#c2e3d2", + colorant"#b5ddd8", + colorant"#a8d8dc", + colorant"#9bd2e1", + colorant"#8dcbe4", + colorant"#81c4e7", + colorant"#7bbce7", + colorant"#7eb2e4", + colorant"#88a5dd", + colorant"#9398d2", + colorant"#9b8ac4", + colorant"#9d7db2", + colorant"#9a709e", + colorant"#906388", + colorant"#805770", + colorant"#684957", + colorant"#46353a", +] + +const _mute = PlotTheme( + merge!( + Dict{Symbol,Any}([ + :palette => mute_palette, + :colorgradient => reverse(ylorbr_gradient), + ]), + sheet_args, + ), +) + +# ------------------------------------------------------------------------------ +# Vibrant +# ------------------------------------------------------------------------------ + +const vibrant_palette = [ # vibrant + colorant"#e73", # orange + colorant"#07b", # blue + colorant"#3be", # cyan + colorant"#e37", # magenta + colorant"#c31", # red + colorant"#098", # teal + colorant"#bbb", # grey +] + +const sunset_gradient = [ + colorant"#364b9a", + colorant"#4a7bb7", + colorant"#6ea6cd", + colorant"#98cae1", + colorant"#c2e4ef", + colorant"#eaeccc", + colorant"#feda8b", + colorant"#fdb366", + colorant"#f67e4b", + colorant"#dd3d2d", + colorant"#a50026", +] + +const _vibrant = + PlotTheme(; palette = vibrant_palette, colorgradient = sunset_gradient, sheet_args...) + +# ------------------------------------------------------------------------------ +# Bright +# ------------------------------------------------------------------------------ + +const bright_palette = [ # bright + colorant"#47a", # blue + colorant"#e67", # red + colorant"#283", # green + colorant"#cb4", # yellow + colorant"#6ce", # cyan + colorant"#a37", # purple + colorant"#bbb", # grey +] + +const iridescent_gradient = [ + colorant"#ffffe5", + colorant"#fff7bc", + colorant"#fee391", + colorant"#fec44f", + colorant"#fb9a29", + colorant"#ec7014", + colorant"#cc4c02", + colorant"#993404", + colorant"#662506", +] + +const _bright = PlotTheme( + merge!( + Dict{Symbol,Any}([ + :palette => bright_palette, + :colorgradient => reverse(iridescent_gradient), + ]), + sheet_args, + ), +) diff --git a/PlotThemes/src/solarized.jl b/PlotThemes/src/solarized.jl new file mode 100644 index 000000000..89ac9cfdb --- /dev/null +++ b/PlotThemes/src/solarized.jl @@ -0,0 +1,53 @@ +# https://github.com/altercation/solarized +const _solarized_palette = (:red, :yellow, :blue, :green, :orange, :magenta, :violet, :cyan) + +const _solarized_colors = Dict([ + :base03 => RGB255(0, 43, 54), + :base02 => RGB255(7, 54, 66), + :base01 => RGB255(88, 110, 117), + :base00 => RGB255(101, 123, 131), + :base0 => RGB255(131, 148, 150), + :base1 => RGB255(147, 161, 161), + :base2 => RGB255(238, 232, 213), + :base3 => RGB255(253, 246, 227), + :blue => RGB255(38, 139, 210), + :orange => RGB255(203, 75, 22), + :red => RGB255(220, 50, 47), + :green => RGB255(133, 153, 0), + :yellow => RGB255(181, 137, 0), + :magenta => RGB255(211, 54, 130), + :violet => RGB255(108, 113, 196), + :cyan => RGB255(42, 161, 152), +]) + +const _solarized = PlotTheme( + Dict([ + :bg => _solarized_colors[:base03], + :bginside => _solarized_colors[:base02], + :fg => _solarized_colors[:base00], + :fgtext => _solarized_colors[:base01], + :fgguide => _solarized_colors[:base01], + :fglegend => _solarized_colors[:base01], + :palette => expand_palette( + _solarized_colors[:base03], + [_solarized_colors[c] for c ∈ _solarized_palette], + ), + :colorgradient => :YlOrRd, + ]), +) + +const _solarized_light = PlotTheme( + Dict([ + :bg => _solarized_colors[:base3], + :bginside => _solarized_colors[:base2], + :fg => _solarized_colors[:base0], + :fgtext => _solarized_colors[:base1], + :fgguide => _solarized_colors[:base1], + :fglegend => _solarized_colors[:base1], + :palette => expand_palette( + _solarized_colors[:base3], + [_solarized_colors[c] for c ∈ _solarized_palette], + ), + :colorgradient => cgrad(:YlOrRd, rev = true), + ]), +) diff --git a/PlotThemes/src/wong.jl b/PlotThemes/src/wong.jl new file mode 100644 index 000000000..43699325d --- /dev/null +++ b/PlotThemes/src/wong.jl @@ -0,0 +1,35 @@ +# colors chosen by according to https://www.nature.com/articles/nmeth.1618?WT.ec_id=NMETH-201106 +# as proposed by @tpoisot in https://github.com/JuliaPlots/Plots.jl/issues/1144 +const wong_palette = [ + RGB(([230, 159, 0] / 255)...), # orange + RGB(([86, 180, 233] / 255)...), # sky blue + RGB(([0, 158, 115] / 255)...), # blueish green + RGB(([240, 228, 66] / 255)...), # yellow + RGB(([0, 114, 178] / 255)...), # blue + RGB(([213, 94, 0] / 255)...), # vermilion + RGB(([204, 121, 167] / 255)...), # reddish purple +] + +const _wong = PlotTheme( + Dict([ + :palette => expand_palette( + colorant"white", + wong_palette; + lchoices = [57], + cchoices = [100], + ), + :colorgradient => cgrad(:viridis).colors, + ]), +) + +const _wong2 = PlotTheme( + Dict([ + :palette => expand_palette( + colorant"white", + [RGB(0, 0, 0); wong_palette]; + lchoices = [57], + cchoices = [100], + ), + :colorgradient => :viridis, + ]), +) diff --git a/PlotThemes/test/runtests.jl b/PlotThemes/test/runtests.jl new file mode 100644 index 000000000..08431a48d --- /dev/null +++ b/PlotThemes/test/runtests.jl @@ -0,0 +1,4 @@ +using PlotThemes +using Test, PlotUtils + +@test in(:sand, keys(PlotThemes._themes)) diff --git a/PlotsBase/Project.toml b/PlotsBase/Project.toml new file mode 100644 index 000000000..04fc4d016 --- /dev/null +++ b/PlotsBase/Project.toml @@ -0,0 +1,149 @@ +name = "PlotsBase" +uuid = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +version = "0.1" + +[deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +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" +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" +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" +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" +Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" + +[weakdeps] +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +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" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +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" +UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" + +[extensions] +FileIOExt = "FileIO" +GRExt = "GR" +GastonExt = "Gaston" +GeometryBasicsExt = "GeometryBasics" +HDF5Ext = "HDF5" +IJuliaExt = "IJulia" +ImageInTerminalExt = "ImageInTerminal" +PGFPlotsXExt = ["Colors", "Contour", "Latexify", "LaTeXStrings", "PGFPlotsX"] +PlotlyJSExt = "PlotlyJS" +PlotlyKaleidoExt = "PlotlyKaleido" +PythonPlotExt = "PythonPlot" +UnicodePlotsExt = "UnicodePlots" +UnitfulExt = ["Latexify", "LaTeXStrings", "UnitfulLatexify", "Unitful"] + +[compat] +Base64 = "1" +Colors = "0.12 - 0.13" +Contour = "0.6" +Dates = "1" +Downloads = "1" +FFMPEG = "0.4" +FixedPointNumbers = "0.8" +GR = "0.73" +Gaston = "1" +HDF5 = "0.17" +JLFzf = "0.1" +JSON = "0.21" +LaTeXStrings = "1" +Latexify = "0.16" +Measures = "0.3" +NaNMath = "1" +PGFPlotsX = "1" +Pkg = "1" +PlotThemes = "3" +PlotUtils = "1" +PlotlyJS = "0.18" +PlotlyKaleido = "2.2" +PrecompileTools = "1" +Preferences = "1" +Printf = "1" +PythonPlot = "1" +Random = "1" +REPL = "1" +RecipesBase = "1.3.1" +RecipesPipeline = "1" +Reexport = "1" +Scratch = "1" +Showoff = "1" +SparseArrays = "1" +Statistics = "1" +StatsBase = "0.34" +UnicodeFun = "0.4" +UnicodePlots = "3" +UnitfulLatexify = "1" +Unzip = "0.2" +UUIDs = "1" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +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" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +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" +UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" + +[targets] +test = ["Aqua", "Colors", "Contour", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "GR", "Gtk", "HDF5", "Images", "Latexify", "LaTeXStrings", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyKaleido", "PythonPlot", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "Test", "TestImages", "UnicodePlots", "Unitful", "UnitfulLatexify", "VisualRegressionTests"] diff --git a/ext/FileIOExt.jl b/PlotsBase/ext/FileIOExt.jl similarity index 50% rename from ext/FileIOExt.jl rename to PlotsBase/ext/FileIOExt.jl index 2aba42921..ca5019c89 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") @@ -23,14 +23,21 @@ function _show_pdfbackends(io::IO, ::MIME"image/png", plt::Plot) # now write from the file 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) + # cleanup + rm("$fn.pdf") + rm("$fn.png") + nothing 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 ( +# PlotsBase.PGFPlotsBackend, # NOTE: I guess this can be removed in PlotsBase@2.0 +# ) +# showable(MIME"image/png"(), Plot{be}) && continue +# @eval PlotsBase._show(io::IO, mime::MIME"image/png", plt::Plot{$be}) = +# _show_pdfbackends(io, mime, plt) +# end + end # module diff --git a/src/backends/gr.jl b/PlotsBase/ext/GRExt.jl similarity index 84% rename from src/backends/gr.jl rename to PlotsBase/ext/GRExt.jl index 896e978fb..43fabed6f 100644 --- a/src/backends/gr.jl +++ b/PlotsBase/ext/GRExt.jl @@ -1,3 +1,184 @@ +module GRExt + +import PlotsBase: PlotsBase, PrecompileTools, RecipesPipeline, _cycle + +import NaNMath +import GR + +using PlotsBase.Annotations +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 + +struct GRBackend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static GRBackend gr + +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] + +PlotsBase.is_marker_supported(::GRBackend, shape::Shape) = true + # https://github.com/jheinen/GR.jl - significant contributions by @jheinen const gr_projections = (auto = 1, ortho = 1, orthographic = 1, persp = 2, perspective = 2) @@ -46,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, @@ -122,7 +304,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)) @@ -185,7 +367,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 @@ -193,7 +375,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 @@ -223,7 +405,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 @@ -231,7 +413,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 @@ -250,8 +432,8 @@ 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 + if (occursin('\\', s) || occursin(r"10\^{|2\^{|e\^{", s)) && + match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.inqtextext(x, y, s) else GR.inqtext(x, y, s) @@ -259,8 +441,8 @@ 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 + if (occursin('\\', s) || occursin(r"10\^{|2\^{|e\^{", s)) && + match(r".*\$[^\$]+?\$.*", String(s)) ≡ nothing GR.textext(x, y, s) else GR.text(x, y, s) @@ -286,7 +468,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 @@ -300,7 +482,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 @@ -315,14 +497,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 @@ -361,7 +543,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) @@ -395,7 +577,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]) @@ -492,11 +674,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 @@ -524,7 +707,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. @@ -581,7 +764,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 @@ -599,7 +782,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 @@ -608,7 +791,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 @@ -622,28 +805,29 @@ 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 end # -------------------------------------------------------------------------------------- +gr_get_markershape(s::Symbol) = s in gr_marker_keys ? s : Shape(s) 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 @@ -651,7 +835,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) @@ -743,7 +927,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) @@ -751,12 +935,6 @@ function gr_get_ticks_size(ticks, rot) w, h end -function labelfunc(scale::Symbol, backend::GRBackend) - texfunc = 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) @@ -789,7 +967,13 @@ function gr_axis_width(sp, axis) w end -function _update_min_padding!(sp::Subplot{GRBackend}) +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) @@ -814,7 +998,7 @@ function _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( @@ -845,7 +1029,7 @@ function _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)) @@ -859,14 +1043,14 @@ function _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 + for (ax, tc, (a, b)) ∈ ((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)) @@ -901,7 +1085,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 @@ -927,7 +1111,7 @@ 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) @@ -960,7 +1144,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 @@ -972,8 +1156,8 @@ 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] - x, y = if is3d(sp) + for ann ∈ sp[:annotations] + 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) @@ -1006,7 +1190,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 @@ -1019,7 +1202,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) @@ -1041,7 +1224,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) @@ -1053,10 +1236,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 @@ -1072,19 +1252,20 @@ 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) + msh = gr_get_markershape.(msh) msw = max(first(series[:markerstrokewidth]), 0) mfac = 0.8 * lfps / (msz + 0.5 * msw + 1e-20) gr_draw_marker( @@ -1096,7 +1277,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 @@ -1118,7 +1299,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] @@ -1127,7 +1308,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), @@ -1142,7 +1323,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) @@ -1160,13 +1341,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 @@ -1194,7 +1375,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) @@ -1204,20 +1385,20 @@ 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 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) @@ -1301,7 +1482,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" : "") @@ -1322,7 +1503,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 @@ -1333,8 +1514,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 @@ -1364,13 +1545,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) @@ -1394,8 +1575,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) @@ -1407,26 +1588,52 @@ 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) # 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_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 = axis_drawing_info(sp, letter) + 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) @@ -1438,12 +1645,10 @@ 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 - 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) @@ -1491,7 +1696,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) @@ -1505,7 +1710,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) @@ -1522,14 +1727,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 @@ -1537,7 +1742,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 @@ -1564,12 +1769,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 @@ -1579,7 +1784,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ϕ) @@ -1612,7 +1817,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)) @@ -1627,26 +1832,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]) @@ -1657,7 +1860,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( @@ -1674,13 +1877,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() @@ -1689,11 +1892,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]), @@ -1719,12 +1922,12 @@ 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 - _, frng = convert_to_polar(x, frng, extrema_r) + if frng ≢ nothing + _, 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? @@ -1736,43 +1939,44 @@ 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 - x, y = straightline_data(series) + 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 = 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 + elseif st ≡ :image gr_draw_image(series, x, y, z, clims) 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 - 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) @@ -1790,20 +1994,20 @@ 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 = is_2tuple(fillrange) ? fillrange : (y, fillrange) end # 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 - 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))) @@ -1838,15 +2042,18 @@ function gr_draw_markers( ) isempty(x) && return GR.setfillintstyle(GR.INTSTYLE_SOLID) - (shapes = series[:markershape]) === :none && return - for segment in series_segments(series, :scatter) + (shapes = series[:markershape]) ≡ :none && return + 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 + if !(shape isa Shape) + shape = gr_get_markershape.(shape) + end + for j ∈ rng gr_draw_marker( series, _cycle(x, j), @@ -1863,8 +2070,8 @@ function gr_draw_markers( end function gr_draw_shapes(series, clims) - x, y = shape_data(series) - for segment in series_segments(series, :shape) + x, y = PlotsBase.shape_data(series) + for segment ∈ series_segments(series, :shape) i, rng = segment.attr_index, segment.range if length(rng) > 1 # connect to the beginning @@ -1896,7 +2103,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) @@ -1908,7 +2115,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 @@ -1927,10 +2134,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]) @@ -1978,7 +2185,7 @@ function gr_z_normalized_log_scaled(scale, z, clims) any(x -> !isfinite(x), loglims) && throw( DomainError( loglims, - "Non-finite value in colorbar limits. Please provide explicits limits via `clims`.", + "Non-finite value in colorbar limits. Please provide explicit limits via `clims`.", ), ) z_log, get_z_normalized.(z_log, loglims...) @@ -1990,19 +2197,21 @@ 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. # 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 ∈ _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 - 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)) @@ -2010,14 +2219,14 @@ 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 ∈ _logScales + elseif scale ∈ _log_scales gr_z_normalized_log_scaled(scale, 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) @@ -2042,14 +2251,14 @@ end # ---------------------------------------------------------------- -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/pdf" => "pdf", "image/png" => "png", "application/postscript" => "ps", "image/svg+xml" => "svg", ) - @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) - dpi_factor = $fmt == "png" ? plt[:dpi] / Plots.DPI : 1 + @eval function PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) + dpi_factor = $fmt == "png" ? plt[:dpi] / DPI : 1 filepath = tempname() * "." * $fmt # workaround windows bug github.com/JuliaLang/julia/issues/46989 touch(filepath) @@ -2067,8 +2276,8 @@ for (mime, fmt) in ( end end -function _display(plt::Plot{GRBackend}) - if plt[:display_type] === :inline +function PlotsBase._display(plt::Plot{GRBackend}) + if plt[:display_type] ≡ :inline filepath = tempname() * ".pdf" GR.emergencyclosegks() withenv( @@ -2081,7 +2290,7 @@ function _display(plt::Plot{GRBackend}) GR.emergencyclosegks() println( "\033]1337;File=inline=1;preserveAspectRatio=0:", - base64encode(open(read, filepath)), + Base64.base64encode(open(read, filepath)), "\a", ) rm(filepath) @@ -2092,4 +2301,8 @@ function _display(plt::Plot{GRBackend}) end end -closeall(::GRBackend) = GR.emergencyclosegks() +PlotsBase.closeall(::GRBackend) = GR.emergencyclosegks() + +PlotsBase.@precompile_backend GR + +end # module diff --git a/src/backends/gaston.jl b/PlotsBase/ext/GastonExt.jl similarity index 72% rename from src/backends/gaston.jl rename to PlotsBase/ext/GastonExt.jl index 10b055ca7..3e5b74a2c 100644 --- a/src/backends/gaston.jl +++ b/PlotsBase/ext/GastonExt.jl @@ -1,14 +1,132 @@ +module GastonExt + +import PlotsBase: PlotsBase, PrecompileTools, RecipesPipeline, PlotUtils +import Gaston + +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 + +struct GastonBackend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static GastonBackend gaston + +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] + # 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[] @@ -23,9 +141,9 @@ function _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 - sp === nothing && continue - for ann in sp[:annotations] + for sp ∈ plt.subplots + sp ≡ nothing && continue + 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 @@ -38,47 +156,48 @@ 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) nothing end -for (mime, term) in ( +for (mime, term) ∈ ( "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" - - 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 + 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)) + end + isfile(tmpfile) && rm(tmpfile, force = true) end nothing end end -_display(plt::Plot{GastonBackend}) = display(plt.o) +PlotsBase._display(plt::Plot{GastonBackend}) = display(plt.o) # -------------------------------------------- # These functions are gaston specific @@ -87,8 +206,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] / DPI push!( saveopts, @@ -110,7 +229,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 @@ -128,8 +247,8 @@ 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 + 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 gaston_init_subplots(plt, sp) @@ -143,19 +262,19 @@ function gaston_init_subplot( plt::Plot{GastonBackend}, sp::Union{Nothing,Subplot{GastonBackend}}, ) - obj = if sp === nothing + obj = if sp ≡ nothing sp else 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 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) @@ -165,7 +284,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] @@ -178,8 +297,8 @@ 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] + 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 @@ -193,16 +312,15 @@ 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 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 @@ -211,15 +329,15 @@ 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 - for (n, seg) in enumerate(series_segments(series, st; check = true)) + if gsp.dims == 2 && z ≡ nothing + 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 @@ -227,7 +345,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 @@ -240,21 +358,21 @@ 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 - x, y, z = mesh3d_triangles(x, y, z, series[:connections]) - elseif st === :surface + if st ≡ :mesh3d + 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) x, y = unique(x), unique(y) 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) @@ -303,31 +421,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) @@ -338,7 +456,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 @@ -349,14 +467,14 @@ 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 "Plots(Gaston): $st is not implemented yet" + @warn "PlotsBase(Gaston): $st is not implemented yet" end [curveconf, extra_curves...] @@ -392,7 +510,7 @@ gaston_fillstyle(x) = "solid" end -function gaston_parse_axes_args( +function gaston_parse_axes_attrs( plt::Plot{GastonBackend}, sp::Subplot{GastonBackend}, dims::Int, @@ -404,8 +522,8 @@ function gaston_parse_axes_args( polar = ispolar(sp) && dims == 2 # cannot splot in polar coordinates fs = sp[:framestyle] - for letter in (:x, :y, :z) - (letter === :z && dims == 2) && continue + for letter ∈ (:x, :y, :z) + (letter ≡ :z && dims == 2) && continue axis = sp[get_attr_symbol(letter, :axis)] # NOTE: there is no `z2tics` concept in gnuplot (only 2D) @@ -418,8 +536,8 @@ function gaston_parse_axes_args( # guide labels guide_font = guidefont(axis) - if letter === :y && dims == 2 - # vertical by default (consistency witht other backends) + if letter ≡ :y && dims == 2 + # vertical by default (consistency with other backends) guide_font = font(guide_font; rotation = guide_font.rotation + 90) end push!( @@ -427,19 +545,19 @@ function gaston_parse_axes_args( "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 @@ -449,7 +567,7 @@ function gaston_parse_axes_args( ) # major tick locations - if axis[:ticks] !== :native + if axis[:ticks] ≢ :native if axis[:flip] hi, lo = axis_limits(sp, letter) else @@ -468,7 +586,7 @@ function gaston_parse_axes_args( 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 @@ -478,7 +596,7 @@ function gaston_parse_axes_args( 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 @@ -488,14 +606,14 @@ function gaston_parse_axes_args( 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 @@ -515,11 +633,11 @@ function gaston_parse_axes_args( 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 @@ -556,10 +674,10 @@ 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 = PlotsBase.ticks_type(rticks)) ≡ :ticks string.(rticks) - elseif ttype === :ticks_and_labels - ["'$l' $t" for (t, l) in zip(rticks...)] + elseif ttype ≡ :ticks_and_labels + ["'$l' $t" for (t, l) ∈ zip(rticks...)] end push!( axesconf, @@ -588,19 +706,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 = ticksType(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)) @@ -608,7 +726,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 @@ -616,7 +734,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) @@ -636,7 +754,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 @@ -656,7 +774,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)), @@ -679,7 +797,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 @@ -689,38 +807,42 @@ 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 - # @debug "Plots(Gaston): unsupported marker $marker" + 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 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) - 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 + +PlotsBase.@precompile_backend Gaston + +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/src/backends/hdf5.jl b/PlotsBase/ext/HDF5Ext.jl similarity index 64% rename from src/backends/hdf5.jl rename to PlotsBase/ext/HDF5Ext.jl index c81c5690b..70647ae33 100644 --- a/src/backends/hdf5.jl +++ b/PlotsBase/ext/HDF5Ext.jl @@ -1,68 +1,128 @@ -#= - -# 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" +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 PlotsBase: PlotsBase, PrecompileTools + +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 -# 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! +struct HDF5Backend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static HDF5Backend hdf5 + +const _hdf5_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, + :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_seriestypes = [ + :path, + :steppre, + :stepmid, + :steppost, + :shape, + :straightline, + :scatter, + :hexbin, + :heatmap, + :image, + :contour, + :contour3d, + :path3d, + :scatter3d, + :surface, + :wireframe, +] +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": +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} @@ -101,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, @@ -111,16 +171,16 @@ 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 # Helper functions -h5plotpath(plotname::String) = "plots/$plotname" +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 @@ -135,26 +195,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 +226,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 @@ -199,20 +259,20 @@ 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, "_") _write_typed(sgrp, "v$idxstr", elem) end - _write_size_attr(sgrp, v) + _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 + for (k, v) ∈ d kstr = string(k) _write_typed(sgrp, kstr, v) end @@ -220,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 @@ -240,22 +300,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 +323,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,8 +350,8 @@ 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) - for (i, series) in enumerate(sp.series_list) + _write_length_attrs(listgrp, sp.series_list) + for (i, series) ∈ enumerate(sp.series_list) # Just write .plotattributes part: _write(listgrp, "$i", series.plotattributes) end @@ -301,25 +361,13 @@ 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) - for (i, sp) in enumerate(plt.subplots) + _write_length_attrs(listgrp, plt.subplots) + for (i, sp) ∈ 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: @@ -341,7 +389,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,13 +400,13 @@ _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(): 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...) @@ -368,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 @@ -385,12 +433,12 @@ 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) - for iter in eachindex(result) + for iter ∈ eachindex(result) coord = lidx[iter] idxstr = join(coord, "_") result[iter] = _read_typed(grp, "v$idxstr") @@ -398,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 @@ -426,7 +474,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) = @@ -436,13 +484,13 @@ _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 + for i ∈ 1:nseries 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 @@ -455,16 +503,16 @@ 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) + plt = PlotsBase.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) + for (i, sp) ∈ enumerate(plt.subplots) sgrp = HDF5.open_group(listgrp, "$i") _read(sgrp, sp) end @@ -472,58 +520,66 @@ 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 - -end # module _hdf5_implementation +# Implement PlotsBase.jl backend interface for HDF5Backend -# Implement Plots.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 `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 +PlotsBase.hdf5plot_write(path::AbstractString; kw...) = + PlotsBase.hdf5plot_write(current(), path; kw...) + +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 + +PlotsBase.@precompile_backend HDF5 -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/PlotsBase/ext/IJuliaExt.jl b/PlotsBase/ext/IJuliaExt.jl new file mode 100644 index 000000000..b354981d9 --- /dev/null +++ b/PlotsBase/ext/IJuliaExt.jl @@ -0,0 +1,59 @@ +module IJuliaExt + +import PlotsBase: PlotsBase, Plot +import 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 + PlotsBase._use_local_plotlyjs[] = + PlotsBase._plotly_local_file_path[] ≡ nothing ? false : + isfile(PlotsBase._plotly_local_file_path[]) + + ENV["MPLBACKEND"] = "Agg" +end + +function _ijulia_display_dict(plt::Plot) + output_type = Symbol(plt.attr[:html_output_format]) + 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 + mime = "text/plain" + out[mime] = sprint(show, MIME(mime), plt) + elseif output_type ≡ :png + mime = "image/png" + out[mime] = Base64.base64encode(show, MIME(mime), plt) + elseif output_type ≡ :svg + mime = "image/svg+xml" + out[mime] = sprint(show, MIME(mime), plt) + elseif output_type ≡ :html + mime = "text/html" + out[mime] = sprint(show, MIME(mime), plt) + PlotsBase._ijulia__extra_mime_info!(plt, out) + elseif output_type ≡ :pdf + mime = "application/pdf" + out[mime] = Base64.base64encode(show, MIME(mime), plt) + else + error("Unsupported output type $output_type") + end + out +end + +if IJulia.inited + _init_ijulia_plotting() + IJulia.display_dict(plt::Plot) = _ijulia_display_dict(plt) +end + +# IJulia only... inline display +function PlotsBase.inline(plt::Plot = PlotsBase.current()) + IJulia.clear_output(true) + display(IJulia.InlineDisplay(), plt) +end + +end # module diff --git a/PlotsBase/ext/ImageInTerminalExt.jl b/PlotsBase/ext/ImageInTerminalExt.jl new file mode 100644 index 000000000..0fd305ac3 --- /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 ∈ ( + 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/src/backends/pgfplotsx.jl b/PlotsBase/ext/PGFPlotsXExt.jl similarity index 82% rename from src/backends/pgfplotsx.jl rename to PlotsBase/ext/PGFPlotsXExt.jl index 28479b1c8..eeda1ce8c 100644 --- a/src/backends/pgfplotsx.jl +++ b/PlotsBase/ext/PGFPlotsXExt.jl @@ -1,3 +1,203 @@ +module PGFPlotsXExt + +import PlotsBase: + PlotsBase, PrecompileTools, RecipesPipeline, PlotUtils, pgfx_sanitize_string, Plot +import LaTeXStrings: LaTeXString +import Printf: @sprintf + +import Latexify +import Contour # PGFPlotsX extension +import Colors # PGFPlotsX extension +import PGFPlotsX + +using PlotsBase.Annotations +using PlotsBase.DataSeries +using PlotsBase.Colorbars +using PlotsBase.Subplots +using PlotsBase.Surfaces +using PlotsBase.Commons +using PlotsBase.Colors +using PlotsBase.Shapes +using PlotsBase.Arrows +using PlotsBase.Plots +using PlotsBase.Fonts +using PlotsBase.Ticks +using PlotsBase.Axes + +struct PGFPlotsXBackend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static PGFPlotsXBackend pgfplotsx + +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() + const Options = PGFPlotsX.Options const Table = PGFPlotsX.Table @@ -43,7 +243,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 @@ -58,7 +258,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 @@ -73,7 +273,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))}" @@ -81,7 +281,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] @@ -91,8 +292,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 @@ -111,7 +312,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) @@ -162,8 +363,8 @@ 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) + for letter ∈ (:x, :y, :z) + if letter ≢ :z || RecipesPipeline.is3d(sp) pgfx_axis!(axis_opt, sp, letter) end end @@ -173,7 +374,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) @@ -196,7 +397,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]), @@ -205,7 +406,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)", @@ -231,15 +432,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 @@ -248,8 +446,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)), @@ -265,9 +463,9 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) ), ) end - for (series_index, series) in enumerate(series_list(sp)) - # give each series a uuid for fillbetween - series_id = uuid4() + for (series_index, series) ∈ enumerate(series_list(sp)) + # 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] @@ -280,15 +478,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) @@ -298,12 +496,12 @@ 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 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), @@ -313,7 +511,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!( @@ -345,10 +543,10 @@ 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 + if opt[:markershape] ≢ :none if (marker = _cycle(opt[:markershape], i)) isa Shape scale_factor = 0.00125 msize = opt[:markersize] * scale_factor @@ -369,11 +567,11 @@ 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 - for sfi in sf + elseif sf isa Tuple && series[:ribbon] ≢ nothing + for sfi ∈ sf pgfx_fillrange_series!( axis, series, @@ -386,7 +584,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) @@ -407,18 +605,18 @@ 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([ :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) @@ -429,7 +627,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!( @@ -501,7 +699,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([ @@ -573,7 +771,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( @@ -583,7 +781,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]] @@ -626,7 +824,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 @@ -652,10 +850,10 @@ 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 - straightline_data(series) - elseif st === :shape - shape_data(series) + elseif st ≡ :straightline + PlotsBase.straightline_data(series) + elseif st ≡ :shape + PlotsBase.shape_data(series) elseif ispolar(series) theta, r = opt[:x], opt[:y] rad2deg.(theta), r @@ -757,12 +955,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) @@ -819,9 +1018,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!( @@ -833,7 +1032,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 @@ -863,11 +1062,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 @@ -880,10 +1079,10 @@ 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( +pgfx_colormap(cg::PlotUtils.ColorGradient) = join( map(1:length(cg)) do i @sprintf( "rgb(%.8f)=(%.8f,%.8f,%.8f)", @@ -900,7 +1099,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 @@ -910,7 +1109,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) @@ -926,7 +1125,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) @@ -992,11 +1191,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 @@ -1049,22 +1248,22 @@ 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 - straightline_data(series) + elseif series[:seriestype] ≡ :straightline + PlotsBase.straightline_data(series) 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]] @@ -1087,24 +1286,24 @@ 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 - if key === :annotations && subplot.attr[:annotations] !== nothing + 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])) end elseif value isa Union{AbstractString,AVec{<:AbstractString}} subplot.attr[key] = pgfx_sanitize_string.(value) - elseif value isa Axis - for (k, v) in value.plotattributes + elseif value isa PlotsBase.Axis + for (k, v) ∈ value.plotattributes if v isa Union{AbstractString,AVec{<:AbstractString}} value.plotattributes[k] = pgfx_sanitize_string.(v) end @@ -1112,12 +1311,12 @@ function pgfx_sanitize_plot!(plt) end end end - for series in plt.series_list - for (key, value) in series.plotattributes - if key === :series_annotations && - series.plotattributes[:series_annotations] !== nothing + 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}} @@ -1143,7 +1342,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 @@ -1172,9 +1371,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 "" @@ -1202,38 +1401,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, ',') @@ -1241,7 +1440,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 @@ -1255,7 +1454,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" @@ -1275,8 +1474,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!( @@ -1306,7 +1505,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 @@ -1315,7 +1514,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, @@ -1348,7 +1547,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 +1558,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}) +for mime ∈ ("application/pdf", "image/svg+xml", "image/png") + @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 +1589,11 @@ 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 + +PlotsBase.@precompile_backend PGFPlotsX + +end # module diff --git a/PlotsBase/ext/PlotlyJSExt.jl b/PlotsBase/ext/PlotlyJSExt.jl new file mode 100644 index 000000000..6d6590334 --- /dev/null +++ b/PlotsBase/ext/PlotlyJSExt.jl @@ -0,0 +1,79 @@ +module PlotlyJSExt + +import PlotsBase: PlotsBase, PrecompileTools, Plot +using PlotsBase.Commons +using PlotsBase.Plotly +using PlotsBase.Plots + +import PlotlyJS: PlotlyJS, WebIO + +struct PlotlyJSBackend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static PlotlyJSBackend plotlyjs + +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 + +function plotlyjs_syncplot(plt::Plot{PlotlyJSBackend}) + plt[:overwrite_figure] && PlotsBase.closeall() + plt.o = PlotlyJS.plot() + traces = PlotlyJS.GenericTrace[] + for series_dict ∈ 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) ∈ ( + "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 + +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 + +PlotsBase.@precompile_backend PlotlyJS + +end # module diff --git a/PlotsBase/ext/PlotlyKaleidoExt.jl b/PlotsBase/ext/PlotlyKaleidoExt.jl new file mode 100644 index 000000000..b893ecafa --- /dev/null +++ b/PlotsBase/ext/PlotlyKaleidoExt.jl @@ -0,0 +1,30 @@ +module PlotlyKaleidoExt + +import PlotsBase: PlotsBase, Plot, PlotlyBackend +import PlotlyKaleido + +function __init__() + ccall(:jl_generating_output, Cint, ()) == 1 && return + PlotlyKaleido.start() + atexit() do + PlotlyKaleido.kill_kaleido() + end +end + +for (mime, fmt) ∈ ( + "application/pdf" => "pdf", + "image/png" => "png", + "image/svg+xml" => "svg", + "image/eps" => "eps", +) + @eval PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyBackend}) = + PlotlyKaleido.savefig( + io, + sprint(io -> PlotsBase.plotly_show_js(io, plt)), + height = plt[:size][2], + width = plt[:size][1], + format = $fmt, + ) +end + +end # module diff --git a/src/backends/pythonplot.jl b/PlotsBase/ext/PythonPlotExt.jl similarity index 77% rename from src/backends/pythonplot.jl rename to PlotsBase/ext/PythonPlotExt.jl index 38ef52574..57bcc024d 100644 --- a/src/backends/pythonplot.jl +++ b/PlotsBase/ext/PythonPlotExt.jl @@ -1,28 +1,226 @@ -# github.com/stevengj/PythonPlot.jl +module PythonPlotExt -is_marker_supported(::PythonPlotBackend, shape::Shape) = true +import PlotsBase: PlotsBase, PrecompileTools, RecipesPipeline, PlotUtils -# 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 +import PythonPlot +import NaNMath -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 - """ +const PythonCall = PythonPlot.PythonCall +const pyisnone = @static if isdefined(PythonCall, :pyisnone) + PythonCall.pyisnone +else + PythonCall.Core.pyisnone end -for k in (:linthresh, :base, :label) - # add PythonPlot 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) +const mpl_toolkits = PythonCall.pynew() +const numpy = PythonCall.pynew() +const mpl = PythonCall.pynew() + +using PlotsBase.Annotations +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 + +struct PythonPlotBackend <: PlotsBase.AbstractBackend end + +function PlotsBase.extension_init(::PythonPlotBackend) + 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") + + @static if false + # FIXME: __init__ is bypassed in PythonPlot see PythonPlot.jl/src/init.jl + # we duplicate the code of PythonPlot here + PythonPlot.version = + PythonPlot.vparse(PythonCall.pyconvert(String, mpl.__version__)) + backend_gui = PythonPlot.find_backend(mpl) + PythonPlot.backend = backend_gui[1] + PythonPlot.gui = backend_gui[2] + PythonCall.pycopy!(PythonPlot.pyplot, PythonCall.pyimport("matplotlib.pyplot")) # raw Python module + PythonCall.pycopy!( + PythonPlot.Gcf, + PythonCall.pyimport("matplotlib._pylab_helpers").Gcf, + ) + PythonCall.pycopy!(PythonPlot.orig_gcf, PythonPlot.pyplot.gcf) + PythonCall.pycopy!(PythonPlot.orig_figure, PythonPlot.pyplot.figure) + PythonPlot.pyplot.gcf = PythonPlot.gcf + PythonPlot.pyplot.figure = PythonPlot.figure + end + + PythonPlot.ioff() # we don't want every command to update the figure + + # WARNING: matplotlib uses a reverse convention: `labeltop` instead of `toplabel` + for keyword ∈ (:linthresh, :base, :label) + Commons.new_attr_dict!(keyword) + for letter ∈ (:x, :y, :z, Symbol(), :top, :bottom, :left, :right) + Commons.set_attr_symbol!(keyword, string(letter)) + end + 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 PlotsBase.jl + integration update your Matplotlib library to a version ≥ 3.4.0 + """ + 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.@extension_static PythonPlotBackend pythonplot + +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] + +# github.com/stevengj/PythonPlot.jl + +PlotsBase.is_marker_supported(::PythonPlotBackend, shape::Shape) = true + _py_handle_surface(v) = v _py_handle_surface(z::Surface) = z.surf @@ -32,7 +230,7 @@ _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) +function _py_colormap(cg::PlotUtils.ColorGradient) pyvals = collect(zip(cg.values, _py_color(PlotUtils.color_list(cg)))) cm = mpl.colors.LinearSegmentedColormap.from_list("tmp", pyvals) cm.set_bad(color = (0, 0, 0, 0.0), alpha = 0.0) @@ -56,11 +254,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 @@ -69,7 +267,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 @@ -79,23 +277,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 @@ -113,16 +312,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 @@ -156,7 +355,7 @@ _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 Surface || nx == ny) @@ -220,9 +419,9 @@ 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 - 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) = BoundingBox(0mm, 0mm) @@ -230,7 +429,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 @@ -261,7 +460,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 @@ -274,9 +473,9 @@ _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]) - # reuse the current figure? +function PlotsBase._create_backend_figure(plt::Plot{PythonPlotBackend}) + w, h = map(s -> Commons.px2inch(s * plt[:dpi] / DPI), plt[:size]) + # reuse the current figure ? plt[:overwrite_figure] ? PythonPlot.gcf() : PythonPlot.figure() end @@ -322,29 +521,29 @@ 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)) - if st === :straightline - x, y = straightline_data(series) - elseif st === :shape - x, y = shape_data(series) + 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 + x, y = PlotsBase.shape_data(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] -= π 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) 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() @@ -376,7 +575,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)...; @@ -394,7 +593,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 @@ -421,14 +620,10 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end # add markers ? - if series[:markershape] !== :none && st ∈ _py_marker_series - for segment in series_segments(series, :scatter) + if series[:markershape] ≢ :none && st ∈ _py_marker_series + for segment ∈ 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 + args = x[rng], y[rng] RecipesPipeline.is3d(sp) && (args = (args..., z[rng])) ax.scatter( args...; @@ -450,8 +645,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) end end - if st === :shape - for segment in series_segments(series) + if st ≡ :shape + for segment ∈ series_segments(series) i, rng = segment.attr_index, segment.range if length(rng) > 1 lc = get_linecolor(series, clims, i, cbar_scale) @@ -497,7 +692,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) @@ -513,7 +708,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" @@ -527,8 +722,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series) zorder, aspect, ) |> push_h - elseif st === :heatmap - x, y = heatmap_edges(x, xaxis[:scale], y, yaxis[:scale], size(z)) + elseif st ≡ :heatmap + x, y = PlotsBase.heatmap_edges(x, xaxis[:scale], y, yaxis[:scale], size(z)) expand_extrema!(xaxis, x) expand_extrema!(yaxis, y) ax.pcolormesh( @@ -542,7 +737,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 @@ -552,10 +747,10 @@ 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 + for n ∈ 1:ntris m = 4(n - 1) + 1 push!( polys, @@ -582,7 +777,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) @@ -590,7 +785,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, @@ -599,7 +794,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)) @@ -622,10 +817,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, @@ -641,8 +836,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])) @@ -666,7 +861,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, @@ -702,14 +897,10 @@ 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 - for segment in series_segments(series) + if (fillrange = series[:fillrange]) ≢ nothing && st ≢ :contour + for segment ∈ 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 @@ -740,7 +931,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 @@ -753,20 +944,20 @@ _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) + for dir ∈ (:top, :bottom, :left, :right) kw[dir] = kw[get_attr_symbol(:label, dir)] = false end axis.set_tick_params(; which = "both", kw...) 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 + elseif ttype ≡ :ticks_and_labels ticks else error("Invalid input for $(letter)ticks: $ticks") @@ -779,7 +970,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 @@ -790,13 +981,14 @@ 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" - scl, kw = if scale === :identity + scale ∈ PlotsBase.supported_scales() || + return @warn "Unhandled scale value in PythonPlot: $scale" + scl, kw = if scale ≡ :identity "linear", KW() else "symlog", KW( - get_attr_symbol(:base, Symbol()) => _logScaleBases[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)]), @@ -816,8 +1008,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])) : @@ -828,7 +1020,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 @@ -836,7 +1028,7 @@ 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 @@ -855,8 +1047,8 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend}) foreach(series -> _py_add_series(plt, series), plt.series_list) # update subplots - for sp in plt.subplots - (ax = sp.o) === nothing && continue + for sp ∈ plt.subplots + (ax = sp.o) ≡ nothing && continue xaxis, yaxis = sp[:xaxis], sp[:yaxis] # add the annotations @@ -891,7 +1083,7 @@ function _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]) @@ -899,17 +1091,17 @@ function _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 + 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 + for func ∈ (_py_linecolormap, _py_fillcolormap, _py_markercolormap) + (cmap = func(cbar_series)) ≡ nothing || break end c_map = mpl.cm.ScalarMappable(; cmap, norm) c_map.set_array(PythonPlot.pylist([])) @@ -927,22 +1119,22 @@ function _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") @@ -959,7 +1151,7 @@ function _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) @@ -969,10 +1161,9 @@ function _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() + 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( @@ -984,9 +1175,9 @@ function _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), ) @@ -998,13 +1189,13 @@ function _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 # Then set visible some of them - if framestyle === :semi + if framestyle ≡ :semi intensity = 0.5 pyspine = getproperty(ax.spines, yaxis[:mirror] ? "left" : "right") @@ -1014,21 +1205,21 @@ function _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) - for loc in + for loc ∈ (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]), @@ -1044,37 +1235,37 @@ function _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) + for letter ∈ (:x, :y, :z) + 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 = ticksType(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 @@ -1087,7 +1278,7 @@ function _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()) @@ -1101,7 +1292,7 @@ function _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 @@ -1109,9 +1300,9 @@ function _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 @@ -1128,7 +1319,7 @@ function _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 @@ -1151,18 +1342,18 @@ function _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 = _logScaleBases[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 @@ -1183,7 +1374,7 @@ function _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 @@ -1191,7 +1382,7 @@ function _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 @@ -1199,11 +1390,11 @@ function _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) @@ -1246,17 +1437,17 @@ expand_padding!(padding, bb, 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 _update_min_padding!(sp::Subplot{PythonPlotBackend}) - (ax = sp.o) === nothing && return sp.minpad +function PlotsBase._update_min_padding!(sp::Subplot{PythonPlotBackend}) + (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 ( + for bb ∈ ( _py_bbox_axis(ax, :x), _py_bbox_axis(ax, :y), _py_bbox_title(ax), @@ -1266,7 +1457,7 @@ function _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), @@ -1286,7 +1477,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((DPI / sp.plt[:dpi]) .* padding) end # ----------------------------------------------------------------- @@ -1298,8 +1489,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, @@ -1313,8 +1504,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, @@ -1326,31 +1517,33 @@ _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[legend_anchor_index(s)], xanchors[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) _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) - (leg = sp[:legend_position]) === :none && return + (leg = sp[:legend_position]) ≡ :none && return # gotta do this to ensure both axes are included labels, handles = [], [] 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 # 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) @@ -1425,7 +1618,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 @@ -1451,7 +1644,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(), @@ -1461,7 +1654,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]), @@ -1476,9 +1669,9 @@ 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}) - for sp in plt.subplots - (ax = sp.o) === nothing && return +function PlotsBase._update_plot_object(plt::Plot{PythonPlotBackend}) + for sp ∈ plt.subplots + (ax = sp.o) ≡ nothing && return figw, figh = sp.plt[:size] .* px # ax.set_position signature: `[left, bottom, width, height]` @@ -1504,9 +1697,9 @@ end # ----------------------------------------------------------------- # display/output -_display(plt::Plot{PythonPlotBackend}) = plt.o.show() +PlotsBase._display(plt::Plot{PythonPlotBackend}) = plt.o.show() -for (mime, fmt) in ( +for (mime, fmt) ∈ ( "application/eps" => "eps", "image/eps" => "eps", "application/pdf" => "pdf", @@ -1515,7 +1708,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, @@ -1528,4 +1725,8 @@ for (mime, fmt) in ( end end -closeall(::PythonPlotBackend) = PythonPlot.close("all") +PlotsBase.closeall(::PythonPlotBackend) = PythonPlot.close("all") + +PlotsBase.@precompile_backend PythonPlot + +end # module diff --git a/src/backends/unicodeplots.jl b/PlotsBase/ext/UnicodePlotsExt.jl similarity index 76% rename from src/backends/unicodeplots.jl rename to PlotsBase/ext/UnicodePlotsExt.jl index 057c68d09..c6c8bc728 100644 --- a/src/backends/unicodeplots.jl +++ b/PlotsBase/ext/UnicodePlotsExt.jl @@ -1,3 +1,101 @@ +module UnicodePlotsExt + +import PlotsBase: PlotsBase, PrecompileTools, texmath2unicode +import RecipesPipeline +import UnicodePlots + +using PlotsBase.Annotations +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 + +struct UnicodePlotsBackend <: PlotsBase.AbstractBackend end +PlotsBase.@extension_static UnicodePlotsBackend unicodeplots + +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] + # https://github.com/JuliaPlots/UnicodePlots.jl const _canvas_map = ( @@ -10,15 +108,15 @@ const _canvas_map = ( dot = UnicodePlots.DotCanvas, ) -should_warn_on_unsupported(::UnicodePlotsBackend) = false +PlotsBase.should_warn_on_unsupported(::UnicodePlotsBackend) = false -function _before_layout_calcs(plt::Plot{UnicodePlotsBackend}) +function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend}) plt.o = UnicodePlots.Plot[] up_width = UnicodePlots.DEFAULT_WIDTH[] 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] @@ -40,7 +138,7 @@ function _before_layout_calcs(plt::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 +147,7 @@ function _before_layout_calcs(plt::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 @@ -62,7 +160,7 @@ function _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) @@ -122,11 +220,11 @@ function _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, @@ -167,9 +265,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 +293,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 +316,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,12 +328,12 @@ 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] : "" - 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( @@ -249,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, @@ -266,7 +364,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,18 +374,18 @@ end # ------------------------------------------------------------------------------------------ -function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) +function PlotsBase._show(io::IO, ::MIME"image/png", plt::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 + for r ∈ 1:nr, c ∈ 1:nc if (l = plt.layout[r, c]) isa GridLayout && size(l) != (1, 1) unsupported_layout_error() else @@ -299,7 +397,7 @@ function _show(io::IO, ::MIME"image/png", plt::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) @@ -307,9 +405,9 @@ function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) 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] @@ -322,15 +420,16 @@ function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend}) end Base.show(plt::Plot{UnicodePlotsBackend}) = show(stdout, plt) -Base.show(io::IO, plt::Plot{UnicodePlotsBackend}) = _show(io, MIME("text/plain"), 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 _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) - prepare_output(plt) +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 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 @@ -342,9 +441,9 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) 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 @@ -366,9 +465,9 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) 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]) @@ -386,7 +485,11 @@ 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 PlotsBase._display(plt::Plot{UnicodePlotsBackend}) show(stdout, plt) println(stdout) end + +PlotsBase.@precompile_backend UnicodePlots + +end # module diff --git a/ext/UnitfulExt.jl b/PlotsBase/ext/UnitfulExt.jl similarity index 86% rename from ext/UnitfulExt.jl rename to PlotsBase/ext/UnitfulExt.jl index 34b2fc950..d91065703 100644 --- a/ext/UnitfulExt.jl +++ b/PlotsBase/ext/UnitfulExt.jl @@ -3,11 +3,25 @@ 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 +import Latexify + using UnitfulLatexify const MissingOrQuantity = Union{Missing,<:Quantity,<:LogScaled} @@ -19,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) @@ -46,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 @@ -129,10 +143,10 @@ 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 + if aspect_ratio ≡ :equal aspect_ratio = 1 end #======================================================================================= @@ -142,12 +156,12 @@ 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 + if axisletter ≡ :y attr[:aspect_ratio] = aspect_ratio * u - elseif axisletter === :x + elseif axisletter ≡ :x attr[:aspect_ratio] = aspect_ratio / u end nothing @@ -196,7 +210,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 +220,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,27 +232,27 @@ 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() - 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 == Plots.PGFPlotsXBackend() - attr[key] = UnitfulString( + attr[key] = if attr[:plot_object].backend == PlotsBase.backend_instance(:pgfplotsx) + UnitfulString( LaTeXString( format_unit_label( label, - latexify(u), + Latexify.latexify(u), get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round), ), ), u, ) else - attr[key] = UnitfulString( + UnitfulString( S( format_unit_label( label, @@ -287,7 +301,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 +311,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 +323,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.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.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 +352,7 @@ function _unit(x) unit(x) end -function Plots.pgfx_sanitize_string(s::UnitfulString) - UnitfulString(Plots.pgfx_sanitize_string(s.content), s.unit) -end +PlotsBase.pgfx_sanitize_string(s::UnitfulString) = + UnitfulString(PlotsBase.pgfx_sanitize_string(s.content), s.unit) end # module diff --git a/PlotsBase/src/Annotations.jl b/PlotsBase/src/Annotations.jl new file mode 100644 index 000000000..a96482f76 --- /dev/null +++ b/PlotsBase/src/Annotations.jl @@ -0,0 +1,285 @@ +# internal module +module Annotations + +export SeriesAnnotations, + EachAnn, + series_annotations, + series_annotations_shapes!, + process_annotation, + locate_annotation, + annotations, + assign_annotation_coord! + +import ..PlotsBase: Series, Subplot, is3d, discrete_value! + +using ..Commons +using ..Shapes +using ..Dates +using ..Fonts + +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 ∈ 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 ∈ 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._shapes, arg) + shp = Shapes._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 ∈ 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::Dates.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 ∈ 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 # 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 new file mode 100644 index 000000000..5d46785cd --- /dev/null +++ b/PlotsBase/src/Arrows.jl @@ -0,0 +1,68 @@ +module Arrows + +export Arrow, arrow, add_arrows + +using ..PlotsBase.Commons + +# 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 ∈ 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 ∈ 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 # module + +# ----------------------------------------------------------------------------- + +using .Arrows diff --git a/PlotsBase/src/Axes.jl b/PlotsBase/src/Axes.jl new file mode 100644 index 000000000..5737b0de9 --- /dev/null +++ b/PlotsBase/src/Axes.jl @@ -0,0 +1,475 @@ +module Axes + +export Axis, Extrema, tickfont, guidefont, widen_factor, scale_inverse_scale_func +export sort_3d_axes, axes_letters, process_axis_arg!, has_ticks, get_axis + +import ..PlotsBase +import ..PlotsBase: Subplot, DefaultsDict, attr! + +using ..RecipesPipeline +using ..Commons +using ..Ticks +using ..Fonts +using ..Dates + +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, Commons._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._match_map2, k) + axis.sps[1][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 = PlotsBase.plotarea(sp) + plot_ratio = PlotsBase.height(area) / PlotsBase.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 ∈ axis.sps, series ∈ 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 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 +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) + 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) = _has_ticks(get(axis, :ticks, nothing)) + +# update an Axis object with magic args and keywords +function PlotsBase.attr!(axis::Axis, args...; kw...) + # first process args + plotattributes = axis.plotattributes + foreach(arg -> process_axis_arg!(plotattributes, arg), args) + + # then preprocess keyword arguments + PlotsBase.Commons.preprocess_attributes!(KW(kw)) + + # then override for any keywords... only those keywords that already exists in plotattributes + 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 + elseif k ≡ :lims && isa(v, NTuple{2,Dates.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) = Commons.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 ∈ 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) + 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] = PlotsBase.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 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 && + 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]) + Commons.get_ticks(ticks, cvals, dvals, alims, axis[:scale], formatter) + end + end + axis.plotattributes[:optimized_ticks] +end + +function reset_extrema!(sp::Subplot) + for asym ∈ (:x, :y, :z) + sp[get_attr_symbol(asym, :axis)][:extrema] = Extrema() + end + for series ∈ sp.series_list + expand_extrema!(sp, series.plotattributes) + end +end + +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 + +PlotsBase.expand_extrema!(axis::Axis, v::Number) = expand_extrema!(axis[:extrema], v) + +# these shouldn't impact the extrema +PlotsBase.expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] +PlotsBase.expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] + +function PlotsBase.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 PlotsBase.expand_extrema!(axis::Axis, v::AVec{N}) where {N<:Number} + ex = axis[:extrema]::Extrema + foreach(vi -> expand_extrema!(ex, vi), v) + ex +end + +end # module + +# ----------------------------------------------------------------------------- + +using .Axes diff --git a/PlotsBase/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl new file mode 100644 index 000000000..705143e82 --- /dev/null +++ b/PlotsBase/src/BezierCurves.jl @@ -0,0 +1,26 @@ +module BezierCurves + +import ..PlotsBase + +"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 ∈ 0:n + p = p .+ bc.control_points[i + 1] .* binomial(n, i) .* (1 - t)^(n - i) .* t^i + end + p +end + +PlotsBase.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) = + map(curve, Base.range(first(range), stop = last(range), length = n)) + +end # module + +# ----------------------------------------------------------------------------- + +using .BezierCurves diff --git a/src/colorbars.jl b/PlotsBase/src/Colorbars.jl similarity index 77% rename from src/colorbars.jl rename to PlotsBase/src/Colorbars.jl index b6c4be499..f29708cbf 100644 --- a/src/colorbars.jl +++ b/PlotsBase/src/Colorbars.jl @@ -1,27 +1,42 @@ -# These functions return an operator for use in `get_clims(::Seres, op)` +module Colorbars + +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 # 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) 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] ∈ _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) @@ -52,7 +67,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`. @@ -61,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] ∈ _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) @@ -100,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 ∈ (: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 ∈ series_list(sp)) function get_colorbar_ticks(sp::Subplot; update = true, formatter = sp[:colorbar_formatter]) if update || !haskey(sp.attr, :colorbar_optimized_ticks) @@ -124,6 +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 # module + +# ----------------------------------------------------------------------------- + +using .Colorbars diff --git a/PlotsBase/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl new file mode 100644 index 000000000..150fcf7ae --- /dev/null +++ b/PlotsBase/src/Commons/Commons.jl @@ -0,0 +1,353 @@ +"Things that should be common to all backends and frontend modules" +module Commons + +export AVec, + AMat, KW, AKW, TicksArgs, PlotsBase, SEED, _haligns, _valigns, _cbar_width +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 +export istuple, isvector, ismatrix, isscalar, is_2tuple +export default, wraptuple, merge_with_base_supported + +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 +using ..Unzip + +const width = Measures.width +const height = Measures.height + +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 _haligns = :hcenter, :left, :right +const _valigns = :vcenter, :top, :bottom +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) + +# 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,) + +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 PlotsBase.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 + +macro ScopeModule(mod::Symbol, parent::Symbol, symbols...) + import_ex = Expr( + :import, + Expr( + :(:), + Expr(:., :., :., parent), + (Expr(:., s isa Expr ? s.args[1] : s) for s ∈ 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 + +"these should only be needed in frontend modules" +@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 + +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::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))] +_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 ∈ series_list(sp) + if series[:seriestype] ≡ :image + ar = :equal + end + end + end + ar isa Bool && (ar = Int(ar)) # NOTE: Bool <: ... <: Number + ar isa Rational && (ar = float(ar)) + 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 ∈ 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/PlotsBase/src/Commons/aliases.jl b/PlotsBase/src/Commons/aliases.jl new file mode 100644 index 000000000..f7a887e75 --- /dev/null +++ b/PlotsBase/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/PlotsBase/src/Commons/attrs.jl b/PlotsBase/src/Commons/attrs.jl new file mode 100644 index 000000000..9bc426a49 --- /dev/null +++ b/PlotsBase/src/Commons/attrs.jl @@ -0,0 +1,1272 @@ +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 ∈ 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 ∈ (: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) ∈ 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, + :diamond, + :hexagon, + :cross, + :xcross, + :utriangle, + :dtriangle, + :rtriangle, + :ltriangle, + :pentagon, + :heptagon, + :octagon, + :star4, + :star5, + :star6, + :star7, + :star8, + :vline, + :hline, + :+, + :x, + :uparrow, + :downarrow, +] + +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 ∈ (:x, :y, :z) + _axis_defaults_byletter[letter] = KW() + for (k, v) ∈ _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 ∈ (: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 + +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 ∈ v + if haskey(_axis_defaults, vi) + for letter ∈ (: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) = PlotsBase.is_attr_supported(k) + +# ----------------------------------------------------------------------------- +include("aliases.jl") +# ----------------------------------------------------------------------------- + +function parse_axis_kw(s::Symbol) + s = string(s) + for letter ∈ ('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 ∈ _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 ∈ _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) ∈ 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) + PlotsBase.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) <: 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) + + 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) + + elseif typeof(arg) <: PlotsBase.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) <: PlotsBase.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) <: PlotsBase.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) <: 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) + + 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) <: PlotsBase.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) <: PlotsBase.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 necessary while old and new font names coexist and should be standard after the transition + fontname = Symbol(fontname, :_) + end + if T <: PlotsBase.Font + Symbol(fontname, :family) --> arg.family + + # TODO: this is necessary 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) ∈ 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, PlotsBase, 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 ∈ 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 ∈ fieldnames(getproperty(PlotsBase, 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/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl new file mode 100644 index 000000000..aba10e409 --- /dev/null +++ b/PlotsBase/src/Commons/layouts.jl @@ -0,0 +1,179 @@ +make_measure_hor(n::Number) = n * Measures.w +make_measure_hor(m::Measure) = m + +make_measure_vert(n::Number) = n * Measures.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 ∈ 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, 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) + 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(::AbstractLayout) = 0mm +toppad(::AbstractLayout) = 0mm +rightpad(::AbstractLayout) = 0mm +bottompad(::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(::EmptyLayout) = (0, 0) +Base.length(::EmptyLayout) = 0 +Base.getindex(::EmptyLayout, ::Int, ::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(), + heights = nothing, + widths = nothing, + 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 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 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 + grid = Matrix{AbstractLayout}(undef, dims...) + layout = GridLayout( + parent, + DEFAULT_MINPAD[], + DEFAULT_BBOX[], + grid, + Measure[w * pct for w ∈ widths], + Measure[h * pct for h ∈ heights], + KW(kw), + ) + for i ∈ 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..57b1787fb --- /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 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 ∈ (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 new file mode 100644 index 000000000..76f6de869 --- /dev/null +++ b/PlotsBase/src/Commons/postprocess_attrs.jl @@ -0,0 +1,20 @@ + +# add all pluralized forms to the _keyAliases dict +foreach(arg -> add_aliases(arg, makeplural(arg)), _all_attrs) + +# fill symbol cache +for letter ∈ (:x, :y, :z) + new_attr_dict!(letter) + 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 ∈ (_magic_axis_attrs..., :(_discrete_indices)) + _attrsymbolcache[letter][keyword] = Symbol(letter, keyword) + end +end + +# add all non_underscored forms to the _keyAliases +add_non_underscore_aliases!(_keyAliases) diff --git a/PlotsBase/src/DataSeries.jl b/PlotsBase/src/DataSeries.jl new file mode 100644 index 000000000..b6d0422d1 --- /dev/null +++ b/PlotsBase/src/DataSeries.jl @@ -0,0 +1,349 @@ +module DataSeries + +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 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) ∈ 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) +Base.push!(series::Series, args...) = extend_series!(series, args...) +Base.append!(series::Series, args...) = extend_series!(series, args...) + +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, + ) + +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 + +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 ∈ plt.series_list, l ∈ (: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 ∈ (: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(!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 + + nextval:(nextnan - 1), nextnan +end + +Base.IteratorSize(::NaNSegmentsIterator) = Base.SizeUnknown() # COV_EXCL_LINE + +function iter_segments(args...) + tup = PlotsBase.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 ∈ PlotsBase.Commons._segmenting_vector_attributes + ) || any( + series[attr] isa AbstractArray for + attr ∈ PlotsBase.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) ∈ enumerate(args) + (scale = get(series, scales[n], :identity)) ∈ PlotsBase.Commons._log_scales || + continue + 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()) + 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 ∈ r) + else + (SeriesSegment(i:(i + 1), i) for i ∈ first(r):(last(r) - 1)) + end + end |> Iterators.flatten + else + (SeriesSegment(r, 1) for r ∈ 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 ∈ 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)) + @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 ∈ 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." + break + end + end +end + +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) ∈ 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 new file mode 100644 index 000000000..cff9ec2e4 --- /dev/null +++ b/PlotsBase/src/Fonts.jl @@ -0,0 +1,183 @@ +module Fonts + +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, PlotText, font, scalefontsizes, resetfontsizes, text, is_horizontal + +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 ∈ 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 ∈ 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 ∈ keys(merge(_initial_plt_fontsizes, _initial_sp_fontsizes)) + scalefontsize(k, factor) + end + + for letter ∈ (:x, :y, :z) + for k ∈ 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 ∈ 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 ∈ (: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] + 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 # module + +# ----------------------------------------------------------------------------- + +Reexport.@reexport using .Fonts diff --git a/PlotsBase/src/Plots.jl b/PlotsBase/src/Plots.jl new file mode 100644 index 000000000..59a3047c3 --- /dev/null +++ b/PlotsBase/src/Plots.jl @@ -0,0 +1,293 @@ +module Plots + +export Plot, + PlotOrSubplot, + plottitlefont, + ignorenan_extrema, + _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 + 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 = PlotsBase.backend() + new{typeof(be)}( + be, + 0, + DefaultsDict(KW(), PlotsBase._plot_defaults), + Series[], + nothing, + Subplot[], + SubplotMap(), + EmptyLayout(), + Subplot[], + false, + ) + end + + function Plot(osp::Subplot) + plt = Plot() + plt.layout = PlotsBase.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 + +const PlotOrSubplot = Union{Plot,Subplot} +# ----------------------------------------------------------- + +struct InputWrapper{T} + obj::T +end +protect(obj::T) where {T} = InputWrapper{T}(obj) +Base.isempty(::InputWrapper) = false +_cycle(wrapper::InputWrapper, ::Int) = wrapper.obj +_cycle(wrapper::InputWrapper, ::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 ∈ 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 ∈ 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 ∈ 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 ∈ plt.series_list +]) +"largest x in plot" +xmax(plt::Plot) = ignorenan_maximum([ + ignorenan_maximum(series.plotattributes[:x]) for series ∈ 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(::Plot) = 2 + +# clear out series list, but retain subplots +Base.empty!(plt::Plot) = foreach(sp -> empty!(sp.series_list), plt.subplots) +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(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!(::Plot, plotattributes::AKW) = + Commons.preprocess_attributes!(plotattributes) + +plottitlefont(plt::Plot) = font(; + family = plt[:plot_titlefontfamily], + pointsize = plt[:plot_titlefontsize], + valign = plt[:plot_titlefontvalign], + halign = plt[:plot_titlefonthalign], + rotation = plt[:plot_titlefontrotation], + color = plt[:plot_titlefontcolor], +) + +# update attr from an input dictionary +function _update_plot_attrs(plt::Plot, plotattributes_in::AKW) + for (k, v) ∈ PlotsBase._plot_defaults + PlotsBase.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 ∈ link + link_axes!(axis, get_axis(get_subplot(plt, other_sp), letter)) + end + axis.plotattributes[:link] = [] + nothing +end + +function 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) + + 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 + + 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 ∈ keys(_subplot_defaults) + PlotsBase.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 ∈ (:x, :y, :z) + 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 + 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 + + PlotsBase.Subplots._update_subplot_periphery(sp, anns) +end + +function Commons.scale_lims!(plt::Plot, letter, factor) + foreach(sp -> scale_lims!(sp, letter, factor), plt.subplots) + plt +end +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 # module + +# ----------------------------------------------------------------------------- + +using .Plots diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl new file mode 100644 index 000000000..b4def57bd --- /dev/null +++ b/PlotsBase/src/PlotsBase.jl @@ -0,0 +1,184 @@ +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 + +# multiple weakdeps triggers (keep in sync with Project.toml !) +const WEAKDEPS = Expr( + :block, + :(import UnitfulLatexify), + :(import LaTeXStrings), + :(import Latexify), + :(import Contour), + :(import Colors), +) + +using Base.Meta + +import PrecompileTools +import LinearAlgebra +import SparseArrays +import Preferences +import UnicodeFun +import Statistics +import StatsBase +import Downloads +import Reexport +import Measures +import NaNMath +import Showoff +import Random +import Base64 +import Printf +import Dates +import Unzip +import JLFzf +import JSON +import Pkg + +Reexport.@reexport using RecipesBase +Reexport.@reexport using PlotThemes +Reexport.@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 + +#! 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, + + plotattr, + scalefontsizes, + resetfontsizes + +#! format: on +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! + +include("Fonts.jl") +include("Ticks.jl") +include("DataSeries.jl") +include("Subplots.jl") +include("Axes.jl") +include("Surfaces.jl") +include("Colorbars.jl") +include("Plots.jl") +include("layouts.jl") +include("utils.jl") +include("axes_utils.jl") +include("legend.jl") +include("Shapes.jl") +include("Annotations.jl") +include("Arrows.jl") +include("Strokes.jl") +include("BezierCurves.jl") +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("alignment.jl") +include("output.jl") +include("shorthands.jl") +include("backends.jl") +include("web.jl") +include("plotly.jl") +include("init.jl") +include("users.jl") + +PlotsBase.@precompile_backend None + +end diff --git a/PlotsBase/src/Shapes.jl b/PlotsBase/src/Shapes.jl new file mode 100644 index 000000000..9a68df152 --- /dev/null +++ b/PlotsBase/src/Shapes.jl @@ -0,0 +1,240 @@ +module Shapes + +import ..PlotsBase + +using ..RecipesPipeline +using ..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 + +# 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)) + +"return the vertex points from a Shape or Segments object" +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_angle, end_angle, n = 20, r = 1) = + [(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 ∈ 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), + :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]) + +# ----------------------------------------------------------------------- + +# 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 ∈ 1:n + ip1 = i == n ? 1 : i + 1 + A += x[i] * y[ip1] - x[ip1] * y[i] + end + A *= 0.5 + 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 + 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 ∈ 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 ∈ 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 ∈ 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 # module + +# ----------------------------------------------------------------------------- + +using .Shapes diff --git a/PlotsBase/src/Strokes.jl b/PlotsBase/src/Strokes.jl new file mode 100644 index 000000000..605e8559f --- /dev/null +++ b/PlotsBase/src/Strokes.jl @@ -0,0 +1,86 @@ +module Strokes + +export Stroke, Brush, stroke, brush + +using ..Colors: Colorant +using ..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 ∈ 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 ∈ 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 # module + +# ----------------------------------------------------------------------------- + +using .Strokes diff --git a/PlotsBase/src/Subplots.jl b/PlotsBase/src/Subplots.jl new file mode 100644 index 000000000..1658514e8 --- /dev/null +++ b/PlotsBase/src/Subplots.jl @@ -0,0 +1,289 @@ +module Subplots + +export Subplot, + colorbartitlefont, + legendfont, + legendtitlefont, + titlefont, + get_series_color, + 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 + 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 + +# 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._match_map2, k) + sp.plt[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. +""" +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 + +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" + +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 + like_surface(seriestype) ? PlotsBase.cgrad() : _cycle(sp[:color_palette], n) + elseif isa(c, Int) + _cycle(sp[:color_palette], c) + else + c + end |> PlotsBase.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 ∈ vcat(anns, sp[:annotations]) + append!(newanns, PlotsBase.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] = 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) + + # 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 ∈ (: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]) + end + end + +needs_any_3d_axes(sp::Subplot) = any( + RecipesPipeline.needs_3d_axes( + _override_seriestype_check(s.plotattributes, s.plotattributes[:seriestype]), + ) for s ∈ series_list(sp) +) + +function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW) + + # first expand for the data + for letter ∈ (:x, :y, :z) + data = plotattributes[letter] + if ( + letter ≢ :z && + plotattributes[:seriestype] ≡ :straightline && + any(series[:seriestype] ≢ :straightline for series ∈ series_list(sp)) && + length(data) > 1 && + data[1] != data[2] + ) + data = [NaN] + end + axis = sp[get_attr_symbol(letter, :axis)] + + 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) + 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))] = + PlotsBase.discrete_value!(axis, data) + expand_extrema!(axis, plotattributes[letter]) + end + end + + # expand for fillrange + if (fr = plotattributes[:fillrange]) ≡ 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 = Commons._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 ∈ (:x, :y) + data = plotattributes[letter] + axis = sp[get_attr_symbol(letter, :axis)] + scale = get(plotattributes, get_attr_symbol(letter, :scale), :identity) + expand_extrema!(axis, PlotsBase.heatmap_edges(data, scale)) + end + end +end + +function PlotsBase.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) = 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) ∈ 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 new file mode 100644 index 000000000..dbcae93a1 --- /dev/null +++ b/PlotsBase/src/Surfaces.jl @@ -0,0 +1,28 @@ +module Surfaces + +export SurfaceFunction, Surface + +import PlotsBase: PlotsBase, expand_extrema! +using ..RecipesPipeline: AbstractSurface, Surface + +using ..Commons +using ..Axes + +function PlotsBase.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 # module + +# ----------------------------------------------------------------------------- + +using .Surfaces diff --git a/PlotsBase/src/Ticks.jl b/PlotsBase/src/Ticks.jl new file mode 100644 index 000000000..d1b6bcc17 --- /dev/null +++ b/PlotsBase/src/Ticks.jl @@ -0,0 +1,111 @@ +module Ticks + +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 + +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...) +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(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 ∈ 2:length(ticks) + lo = ticks[i - 1] + hi = ticks[i] + (isfinite(lo) && isfinite(hi) && hi > lo) || continue + if log_scaled + for e ∈ 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 # module + +# ----------------------------------------------------------------------------- + +using .Ticks diff --git a/PlotsBase/src/alignment.jl b/PlotsBase/src/alignment.jl new file mode 100644 index 000000000..5e54dd4e0 --- /dev/null +++ b/PlotsBase/src/alignment.jl @@ -0,0 +1,68 @@ +"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 ∈ 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/animation.jl b/PlotsBase/src/animation.jl similarity index 73% rename from src/animation.jl rename to PlotsBase/src/animation.jl index 2b0847963..7d75d1b97 100644 --- a/src/animation.jl +++ b/PlotsBase/src/animation.jl @@ -13,6 +13,8 @@ struct Animation frames::Vector{String} end +const ANIM_PATTERN = "plots-anim-%06d.png" + Animation(dir = convert(String, mktempdir())) = Animation(dir, String[]) """ @@ -21,17 +23,23 @@ Animation(dir = convert(String, mktempdir())) = Animation(dir, String[]) Add a plot (the current plot if not specified) to an existing animation """ function frame(anim::Animation, plt::P = current()) where {P<:AbstractPlot} - i = length(anim.frames) + 1 - filename = @sprintf "%06d.png" i + filename = Printf.format(Printf.Format(ANIM_PATTERN), length(anim.frames) + 1) png(plt, joinpath(anim.dir, filename)) push!(anim.frames, filename) end -giffn() = isijulia() ? "tmp.gif" : tempname() * ".gif" -movfn() = isijulia() ? "tmp.mov" : tempname() * ".mov" -mp4fn() = isijulia() ? "tmp.mp4" : tempname() * ".mp4" -webmfn() = isijulia() ? "tmp.webm" : tempname() * ".webm" -apngfn() = isijulia() ? "tmp.png" : tempname() * ".png" +anim_filename(ext, parent = nothing) = + if isijulia() + "tmp" + else + tempname(parent ≡ nothing ? tempdir() : parent) + end * ext + +giffn(parent = nothing) = anim_filename(".gif", parent) +movfn(parent = nothing) = anim_filename(".mov", parent) +mp4fn(parent = nothing) = anim_filename(".mp4", parent) +webmfn(parent = nothing) = anim_filename(".webm", parent) +apngfn(parent = nothing) = anim_filename(".png", parent) mutable struct FrameIterator itr @@ -45,7 +53,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) @@ -88,32 +96,34 @@ file_extension(fn) = Base.Filesystem.splitext(fn)[2][2:end] gif(animation[, filename]; fps=20, loop=0, variable_palette=false, verbose=false, show_msg=true) Creates an animated .gif-file from an `Animation` object. """ -gif(anim::Animation, fn = giffn(); kw...) = buildanimation(anim, fn; kw...) +gif(anim::Animation, fn = giffn(anim.dir); kw...) = build_animation(anim, fn; kw...) """ mov(animation[, filename]; fps=20, loop=0, verbose=false, show_msg=true) Creates an .mov-file from an `Animation` object. """ -mov(anim::Animation, fn = movfn(); kw...) = buildanimation(anim, fn, false; kw...) +mov(anim::Animation, fn = movfn(anim.dir); kw...) = build_animation(anim, fn, false; kw...) """ mp4(animation[, filename]; fps=20, loop=0, verbose=false, show_msg=true) Creates an .mp4-file from an `Animation` object. """ -mp4(anim::Animation, fn = mp4fn(); kw...) = buildanimation(anim, fn, false; kw...) +mp4(anim::Animation, fn = mp4fn(anim.dir); kw...) = build_animation(anim, fn, false; kw...) """ webm(animation[, filename]; fps=20, loop=0, verbose=false, show_msg=true) Creates an .webm-file from an `Animation` object. """ -webm(anim::Animation, fn = webmfn(); kw...) = buildanimation(anim, fn, false; kw...) +webm(anim::Animation, fn = webmfn(anim.dir); kw...) = + build_animation(anim, fn, false; kw...) """ apng(animation[, filename]; fps=20, loop=0, verbose=false, show_msg=true) Creates an animated .apng-file from an `Animation` object. """ -apng(anim::Animation, fn = apngfn(); kw...) = buildanimation(anim, fn, false; kw...) +apng(anim::Animation, fn = apngfn(anim.dir); kw...) = + build_animation(anim, fn, false; kw...) ffmpeg_framerate(fps) = "$fps" ffmpeg_framerate(fps::Rational) = "$(fps.num)/$(fps.den)" -function buildanimation( +function build_animation( anim::Animation, fn::AbstractString, is_animated_gif::Bool = true; @@ -126,30 +136,30 @@ function buildanimation( length(anim.frames) == 0 && throw(ArgumentError("Cannot build empty animations")) fn = abspath(expanduser(fn)) - animdir = anim.dir framerate = ffmpeg_framerate(fps) - verbose_level = (verbose isa Int ? verbose : verbose ? 32 : 16) # "error" - + verbose_level = (verbose isa Int ? verbose : verbose ? 32 : 16) # "error" + pattern = joinpath(anim.dir, ANIM_PATTERN) + palette = joinpath(anim.dir, "palette.bmp") if is_animated_gif if variable_palette # generate a colorpalette for each frame for highest quality, but larger filesize palette = "palettegen=stats_mode=single[pal],[0:v][pal]paletteuse=new=1" - `-v $verbose_level -framerate $framerate -i $animdir/%06d.png -lavfi "$palette" -loop $loop -y $fn` |> + `-v $verbose_level -framerate $framerate -i $pattern -lavfi "$palette" -loop $loop -y $fn` |> ffmpeg_exe else # generate a colorpalette first so ffmpeg does not have to guess it - `-v $verbose_level -i $animdir/%06d.png -vf "palettegen=stats_mode=full" -y "$animdir/palette.bmp"` |> + `-v $verbose_level -i $pattern -vf "palettegen=stats_mode=full" -y "$palette"` |> ffmpeg_exe # then apply the palette to get better results - `-v $verbose_level -framerate $framerate -i $animdir/%06d.png -i "$animdir/palette.bmp" -lavfi "paletteuse=dither=sierra2_4a" -loop $loop -y $fn` |> + `-v $verbose_level -framerate $framerate -i $pattern -i "$palette" -lavfi "paletteuse=dither=sierra2_4a" -loop $loop -y $fn` |> ffmpeg_exe end elseif file_extension(fn) in ("png", "apng") # FFMPEG specific command for APNG (Animated PNG) animations - `-v $verbose_level -framerate $framerate -i $animdir/%06d.png -plays $loop -f apng -y $fn` |> + `-v $verbose_level -framerate $framerate -i $pattern -plays $loop -f apng -y $fn` |> ffmpeg_exe else - `-v $verbose_level -framerate $framerate -i $animdir/%06d.png -vf format=yuv420p -loop $loop -y $fn` |> + `-v $verbose_level -framerate $framerate -i $pattern -vf format=yuv420p -loop $loop -y $fn` |> ffmpeg_exe end @@ -160,16 +170,15 @@ end # write out html to view the gif function Base.show(io::IO, ::MIME"text/html", agif::AnimatedGif) html = if (ext = file_extension(agif.filename)) == "gif" - "" + "" elseif ext == "apng" - "" + "" elseif ext in ("mov", "mp4", "webm") mimetype = ext == "mov" ? "video/quicktime" : "video/$ext" - "" + "" else error("Cannot show animation with extension $ext: $agif") end - write(io, html) nothing end @@ -199,7 +208,7 @@ function _animate(forloop::Expr, args...; type::Symbol = :none) freqassert = :() block = forloop.args[2] - animationsKwargs = Any[] + kw = Any[] filterexpr = true n = length(args) @@ -213,14 +222,14 @@ function _animate(forloop::Expr, args...; type::Symbol = :none) filterexpr == true || error("Can only specify one filterexpression (one of 'when' or 'every')") - filterexpr = # when every + filterexpr = # when every arg == :when ? args[i + 1] : :(mod1($countersym, $(args[i + 1])) == 1) i += 1 elseif arg isa Expr && arg.head == Symbol("=") - #specification of type = + # specification of type = lhs, rhs = arg.args - push!(animationsKwargs, :($lhs = $rhs)) + push!(kw, :($lhs = $rhs)) else error("Parameter specification not understood: $(arg)") end @@ -229,28 +238,28 @@ 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...))) - elseif type === :apng - :(Plots.apng($animsym; $(animationsKwargs...))) + retval = if type ≡ :gif + :(PlotsBase.gif($animsym; $(kw...))) + elseif type ≡ :apng + :(PlotsBase.apng($animsym; $(kw...))) else animsym end # full expression: quote - $freqassert # if filtering, check frequency is an Integer > 0 - $animsym = Plots.Animation() # init animation object - let $countersym = 1 # init iteration counter - $forloop # for loop, saving a frame after each iteration + $freqassert # if filtering, check frequency is an Integer > 0 + $animsym = PlotsBase.Animation() # init animation object + let $countersym = 1 # init iteration counter + $forloop # for loop, saving a frame after each iteration end - $retval # return the animation object, or the gif + $retval # return the animation object, or the gif end |> esc end diff --git a/src/arg_desc.jl b/PlotsBase/src/arg_desc.jl similarity index 95% rename from src/arg_desc.jl rename to PlotsBase/src/arg_desc.jl index 33eb6cd7c..1f425483b 100644 --- a/src/arg_desc.jl +++ b/PlotsBase/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)."), @@ -102,9 +101,9 @@ const _arg_desc = KW( :foreground_color_title => (ColorType, "Color of subplot title (`:match` matches :foreground_color_subplot`)."), :color_palette => (Union{AVec{ColorType},Symbol}, "Iterable (cycle through) or color gradient (generate list from gradient) or `:auto` (generate a color list using `Colors.distiguishable_colors` and custom seed colors chosen to contrast with the background). The color palette is a color list from which series colors are automatically chosen."), :legend_position => (Union{Bool,NTuple{2,Real},Symbol}, """ - Show the legend ? Can also be a (x,y) tuple or Symbol (legend position) or angle (angle,inout) tuple. Bottom left corner of legend is placed at (x,y). + Show the legend ? Can also be a (x,y) tuple or Symbol (legend position) or angle (angle,in-out) tuple. Bottom left corner of legend is placed at (x,y). Choose from (`:none`, `:best`, `:inline`, `:inside`, `:legend`) or any valid combination of `:(outer ?)(top/bottom ?)(right/left ?)`, i.e.: `:top`, `:topright`, `:outerleft`, `:outerbottomright` ... (note: only some may be supported in each backend)."""), - :legend_column => (Integer, "Number of columns in the legend. `-1` stands for maximum number of colums (horizontal legend)."), + :legend_column => (Integer, "Number of columns in the legend. `-1` stands for maximum number of columns (horizontal legend)."), :legend_title_font => (Font, "Font of the legend title."), :legend_font_family => (Union{AStr,Symbol}, "Font family of legend entries."), :legend_font_pointsize => (Integer, "Font pointsize of legend entries."), @@ -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/PlotsBase/src/axes_utils.jl b/PlotsBase/src/axes_utils.jl new file mode 100644 index 000000000..244e207a8 --- /dev/null +++ b/PlotsBase/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, ::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 + +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 + cvals, string.(dvals) + else + Δ = ceil(Int, n / 10) + rng = Δ:Δ:n + cvals[rng], string.(dvals[rng]) + end + else + optimal_ticks_and_labels(nothing, args...) + end + +Commons.get_ticks(ticks::AVec, cvals, dvals, args...) = + optimal_ticks_and_labels(ticks, args...) +Commons.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_labels(formatter::Symbol, scaled_ticks, scale) = + if formatter in (:auto, :plain, :scientific, :engineering) + 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 + String[] + 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) + fticks +end + +# Ticks getter functions +for l ∈ (: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 ∈ 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 ∈ 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 ∈ 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 ∈ 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/PlotsBase/src/backends.jl b/PlotsBase/src/backends.jl new file mode 100644 index 000000000..c94d56aa8 --- /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 NoneBackend <: AbstractBackend end + +backend_name(::NoneBackend) = :none +should_warn_on_unsupported(::NoneBackend) = false + +for sym ∈ _default_supported_syms + @eval begin + $(_f1_sym(sym))(::NoneBackend, $sym::Symbol) = true + $(_f2_sym(sym))(::NoneBackend) = Commons.$(Symbol("_all_$(sym)s")) + end +end + +_display(::Plot{NoneBackend}) = + @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}(NoneBackend => :none) +const _backendType = Dict{Symbol,DataType}(:none => NoneBackend) +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(::Plot) = nothing +_initialize_subplot(::Plot, ::Subplot) = nothing + +_series_added(::Plot, ::Series) = nothing +_series_updated(::Plot, ::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 ∈ _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 ∈ _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 ∈ _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()) # runtime init, incompatible with precompilation + @debug $("Initialized $be_type 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 ∈ 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 ∈ 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 ∈ (: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/src/examples.jl b/PlotsBase/src/examples.jl similarity index 90% rename from src/examples.jl rename to PlotsBase/src/examples.jl index 2fe65b1d6..75017dc06 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", @@ -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 @@ -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,7 +179,10 @@ const _examples = PlotExample[ PlotExample( # 13 "Marker types", quote - markers = filter(m -> m in Plots.supported_markers(), Plots._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)] @@ -231,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], @@ -243,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 @@ -265,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, @@ -283,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))`. @@ -294,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)), @@ -312,16 +319,16 @@ 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. + the size is expected to be roughly the area of the unit circle. """, quote verts = [ @@ -394,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, @@ -445,15 +452,15 @@ 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, ), PlotExample( # 29 "Layouts, margins, label rotation, title location", - :(using Plots.PlotMeasures), # for Measures, e.g. mm and px + :(using PlotsBase.Commons), # for Measures, e.g. mm and px quote plot( rand(100, 6), @@ -496,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 @@ -786,7 +793,7 @@ const _examples = PlotExample[ ylabel = "y", zlabel = "z", legend = :none, - margin = 2Plots.mm, + margin = 2PlotsBase.mm, ) end, ), @@ -830,7 +837,7 @@ const _examples = PlotExample[ z; projection = :polar, color = :cividis, - right_margin = 2Plots.mm, + right_margin = 2PlotsBase.mm, ) end, ), @@ -840,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 @@ -882,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, @@ -937,7 +944,7 @@ const _examples = PlotExample[ "3D axis flip / mirror", :(using LinearAlgebra), quote - Plots.with(scalefonts = 0.5) do + with(scalefonts = 0.5) do x, y = collect(-6:0.5:10), collect(-8:0.5:8) args = x, y, (x, y) -> sinc(norm([x, y]) / π) @@ -951,29 +958,29 @@ const _examples = PlotExample[ plots = [wireframe(args..., title = "wire"; kw...)] - for ax in (:x, :y, :z) + for ax ∈ (:x, :y, :z) push!( plots, 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..., ), ) end - for ax in (:x, :y, :z) + for ax ∈ (:x, :y, :z) push!( plots, 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..., ), ) @@ -982,7 +989,7 @@ const _examples = PlotExample[ plot( plots..., layout = (@layout [_ ° _; ° ° °; ° ° °]), - margin = 0Plots.px, + margin = 0PlotsBase.px, ) end end, @@ -1197,8 +1204,8 @@ const _examples = PlotExample[ ), legs, ) - w, h = Plots._plot_defaults[:size] - Plots.with(scalefonts = 0.5, size = (2w, 2h)) do + 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 end, @@ -1229,8 +1236,8 @@ const _examples = PlotExample[ ), legs, ) - w, h = Plots._plot_defaults[:size] - Plots.with(scalefonts = 0.5, size = (2w, 2h)) do + 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 end, @@ -1239,23 +1246,23 @@ const _examples = PlotExample[ "Specifying edges and missing values for barplots", "In `bar(x, y)`, `x` may be the same length as `y` to specify bar centers, or one longer to specify bar edges.", :(plot( - bar(-5:5, randn(10)), # bar edges at -5:5 - bar(-2:2, [2, -2, NaN, -1, 1], color = 1:5), # bar centers at -2:2, one missing value + bar(-5:5, randn(10)), # bar edges at -5:5 + bar(-2:2, [2, -2, NaN, -1, 1], color = 1:5), # bar centers at -2:2, one missing value legend = false, )), ), ] # Some constants for PlotDocs and PlotReferenceImages -_animation_examples = [2, 31] +_animation_examples = [02, 31] _backend_skips = Dict( - :gr => [], - :pyplot => [], + :none => Int[], + :hdf5 => Int[47], + :pythonplot => Int[], + :gr => Int[], :plotlyjs => [ 21, 24, - 25, - 30, 49, 50, 51, @@ -1268,7 +1275,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 @@ -1276,33 +1283,9 @@ _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 + 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 @@ -1326,11 +1309,14 @@ _backend_skips = Dict( 31, # animations - needs github.com/mbaz/Gaston.jl/pull/178 49, # TODO: support polar 60, # :perspective projection unsupported - 63, # FXIME: twin axes misalignement + 63, # FXIME: twin axes misalignment ], ) _backend_skips[:plotly] = _backend_skips[:plotlyjs] -_backend_skips[:pythonplot] = _backend_skips[:pyplot] +# 25 and 30 require StatsPlots, which doesn't support Plots v2 yet +for backend ∈ keys(_backend_skips) + append!(_backend_skips[backend], [25, 30]) +end # --------------------------------------------------------------------------------- # replace `f(args...)` with `f(rng, args...)` for `f ∈ (rand, randn)` @@ -1339,13 +1325,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 ∈ 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...) @@ -1364,21 +1370,21 @@ function test_examples( # prevent leaking variables (esp. functions) directly into Plots namespace Base.eval(m, quote using Random - using Plots - Plots.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.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 = 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() + callback ≡ nothing || callback(m, pkgname, i) + m.PlotsBase.current() end # generate all plots and create a dict mapping idx --> plt @@ -1398,7 +1404,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 @@ -1412,7 +1418,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/init.jl b/PlotsBase/src/init.jl new file mode 100644 index 000000000..29007fa39 --- /dev/null +++ b/PlotsBase/src/init.jl @@ -0,0 +1,187 @@ +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.3.0.min.js" # must match https://github.com/JuliaPlots/PlotlyJS.jl/blob/master/deps/plotly_cdn_version.jl + +const _use_local_dependencies = Ref(false) +const _use_local_plotlyjs = Ref(false) + +_plots_defaults() = + if isdefined(Main, :PLOTSBASE_DEFAULTS) + copy(Dict{Symbol,Any}(Main.PLOTSBASE_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 Base.get_bool_env("PLOTSBASE_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 + +# 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(Preferences.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 + Preferences.delete_preferences!(PlotsBase, "default_backend"; force, kw...) + else + # NOTE: `_check_installed` already throws a warning + if (value = lowercase(string(backend))) |> PlotsBase._check_installed ≢ nothing + Preferences.set_preferences!( + PlotsBase, + "default_backend" => value; + force, + kw..., + ) + end + end + nothing +end + +function diagnostics(io::IO = stdout) + origin = if Preferences.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 + +macro precompile_backend(backend_package) + abstract_backend = Symbol(backend_package, :Backend) + quote + PrecompileTools.@setup_workload begin + using PlotsBase # for extensions + backend($abstract_backend()) + __init__() # call extension module init !! + @debug PlotsBase.backend_package_name() + n = length(PlotsBase._examples) + imports = sizehint!(Expr[], n) + examples = sizehint!(Expr[], 10n) + scratch_dir = mktempdir() + for i ∈ 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 + if backend_name() ≡ :pythonplot + return # FIXME: __init__ failure with PythonPlot + end + @debug $i + $(PlotsBase._examples[i].exprs) + $i == 1 || return # trigger display only for one example + fn = tempname(scratch_dir) + pl = current() + show(devnull, pl) + if backend_name() ≡ :plotlyjs + return # FIXME: precompilation hang + end + if backend_name() ≡ :pgfplotsx + return # FIXME: `Colors` extension issue for PFPlotsX + end + if backend_name() ≡ :unicodeplots + savefig(pl, "$fn.txt") + return + end + if showable(MIME"image/png"(), pl) + savefig(pl, "$fn.png") + end + if showable(MIME"application/pdf"(), pl) + savefig(pl, "$fn.pdf") + end + 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 + PrecompileTools.@compile_workload begin + eval.(imports) + eval.(examples) + PlotsBase.CURRENT_PLOT.nullableplot = nothing + end + end + end + end |> esc +end diff --git a/src/layouts.jl b/PlotsBase/src/layouts.jl similarity index 54% rename from src/layouts.jl rename to PlotsBase/src/layouts.jl index 2eed33558..231f9f150 100644 --- a/src/layouts.jl +++ b/PlotsBase/src/layouts.jl @@ -1,127 +1,12 @@ -# NOTE: (0,0) is the top-left !!! - -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] -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) @@ -134,142 +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 = nothing, - heights = nothing, - kw..., -) - # Check the values for heights and widths if values are provided - if heights !== nothing - if sum(heights) != 1 - error("The sum of heights must be 1!") - end - if all(x -> 0 < x < 1, heights) == false - error("Values for heights must be in the range (0, 1)!") - end - else - heights = zeros(dims[1]) - end - if widths !== nothing - if sum(widths) != 1 - error("The sum of widths must be 1!") - end - if all(x -> 0 < x < 1, widths) == false - error("Values for widths must be in the range (0, 1)!") - end - else - widths = zeros(dims[2]) - end - - 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) @@ -288,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) @@ -309,12 +69,12 @@ 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 cnt = 0 - for vi in v + for vi ∈ v if vi == 0pct cnt += 1 else @@ -328,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!) @@ -369,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) @@ -403,10 +162,12 @@ 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 + 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 @@ -444,10 +205,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.", @@ -456,60 +217,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] + for ci ∈ CartesianIndices(m) + 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 @@ -517,10 +278,10 @@ end function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) nr, nc = size(layout) subplots = Subplot[] - spmap = SubplotMap() + 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 @@ -538,19 +299,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) @@ -558,7 +319,7 @@ function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot}) merge!(spmap, m) i += length(sps) elseif isa(l, Subplot) && empty - error("Subplot exists. Cannot re-use existing layout. Please make a new one.") + error("Subplot exists. Cannot reuse existing layout. Please make a new one.") end i ≥ n && break # only add n subplots end @@ -572,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, ignorenan_extrema(a2)) - for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) + expand_extrema!(a1, Axes.ignorenan_extrema(a2)) + 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 @@ -591,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) @@ -604,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 @@ -614,26 +375,20 @@ 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 ∈ 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 ∈ 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 ∈ sps + link_axes!(base_axis, sp[:xaxis]) + link_axes!(base_axis, sp[:yaxis]) end end - if link === :all + if link ≡ :all link_axes!(layout.grid, :xaxis) link_axes!(layout.grid, :yaxis) end @@ -645,11 +400,11 @@ 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 - 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/src/legend.jl b/PlotsBase/src/legend.jl similarity index 67% rename from src/legend.jl rename to PlotsBase/src/legend.jl index 74dfd66cd..8e0fbec11 100644 --- a/src/legend.jl +++ b/PlotsBase/src/legend.jl @@ -1,3 +1,18 @@ +@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) @@ -14,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 """ @@ -55,3 +68,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/PlotsBase/src/output.jl similarity index 85% rename from src/output.jl rename to PlotsBase/src/output.jl index ce7a3f244..eae03364e 100644 --- a/src/output.jl +++ b/PlotsBase/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") @@ -107,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. @@ -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) @@ -158,12 +159,10 @@ 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) -function inline end # for IJulia - function Base.display(::PlotsDisplay, plt::Plot) prepare_output(plt) _display(plt) @@ -171,33 +170,33 @@ 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 # --------------------------------------------------------- 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) 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, "", ) - 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))") @@ -212,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", @@ -240,16 +239,30 @@ 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) +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] - plt[:dpi] = get(io, :juno_dpi_ratio, 1) * Plots.DPI + plt[:dpi] = get(io, :juno_dpi_ratio, 1) * PlotsBase.DPI prepare_output(plt) try @@ -266,7 +279,8 @@ _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) + # COV_EXCL_STOP diff --git a/src/pipeline.jl b/PlotsBase/src/pipeline.jl similarity index 77% rename from src/pipeline.jl rename to PlotsBase/src/pipeline.jl index 3babfb5ab..6072cc09a 100644 --- a/src/pipeline.jl +++ b/PlotsBase/src/pipeline.jl @@ -9,8 +9,8 @@ function RecipesPipeline.warn_on_recipe_aliases!( @nospecialize(args) ) pkeys = keys(plotattributes) - for k in pkeys - if (dk = get(_keyAliases, k, nothing)) !== nothing + for k ∈ pkeys + if (dk = get(Commons._keyAliases, k, nothing)) ≢ nothing kv = RecipesPipeline.pop_kw!(plotattributes, k) dk ∈ pkeys || (plotattributes[dk] = kv) end @@ -31,20 +31,17 @@ 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 - letter = :x - end + treats_y_as_x(get(plotattributes, :seriestype, :path)) && (letter = :x) 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 +59,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 + 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 ∈ 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)) @@ -99,8 +96,8 @@ 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 - if get(kw, esym, nothing) !== nothing + for esym ∈ errors + if get(kw, esym, nothing) ≢ nothing # we make a copy of the KW and apply an errorbar recipe errkw = copy(kw) errkw[:seriestype] = esym @@ -140,7 +137,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(::Plot, st) = get(Commons._typeAliases, st, st) ## Plot setup @@ -150,10 +147,10 @@ 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!(::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) @@ -162,15 +159,15 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::Plots.Plot, kw_l # swap errors 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 + 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]) kw_list[ind - 1] = tmp end end - for kw in kw_list + for kw ∈ kw_list kw[:x_extrema] = xe kw[:y_extrema] = ye kw[:z_extrema] = ze @@ -178,35 +175,34 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::Plots.Plot, kw_l 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 + 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 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 + for kw ∈ kw_list, (k, v) ∈ kw haskey(_plot_defaults, k) && (plotattributes[k] = pop!(kw, k)) 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) # 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 @@ -215,9 +211,9 @@ 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 + for inset ∈ insets parent, bb = is_2tuple(inset) ? inset : (nothing, inset) parent = if (P = typeof(parent)) <: Integer plt.subplots[parent] @@ -243,39 +239,36 @@ 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( 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 # 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) + 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) 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)] 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]) @@ -285,13 +278,13 @@ 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 get(sp_attrs, sp, KW()) end - _update_subplot_args(plt, sp, attr, idx, false) + Plots._update_subplot_attrs(plt, sp, attr, idx, false) end # do we need to link any axes together? @@ -316,8 +309,8 @@ 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, NoneBackend) ? nothing : 0mm bot = 0mm plt[:force_minpad] = nothing, top, nothing, bot subplot[:subplot_index] = last(plt.subplots)[:subplot_index] + 1 @@ -331,7 +324,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 @@ -345,25 +338,25 @@ 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 -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) - 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] = @@ -383,16 +376,13 @@ 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 = 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,27 +394,9 @@ 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 + if st ≡ :image xmin, xmax = ignorenan_extrema(plotattributes[:x]) ymin, ymax = ignorenan_extrema(plotattributes[:y]) expand_extrema!(sp[:xaxis], (xmin, xmax)) @@ -441,16 +413,16 @@ 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()) 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")) @@ -458,9 +430,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/src/plot.jl b/PlotsBase/src/plot.jl similarity index 80% rename from src/plot.jl rename to PlotsBase/src/plot.jl index f16587489..02b8ab3a0 100644 --- a/src/plot.jl +++ b/PlotsBase/src/plot.jl @@ -1,10 +1,12 @@ +struct PlaceHolder end + mutable struct CurrentPlot nullableplot::Union{AbstractPlot,Nothing} end const CURRENT_PLOT = CurrentPlot(nothing) -isplotnull() = CURRENT_PLOT.nullableplot === nothing +isplotnull() = CURRENT_PLOT.nullableplot ≡ nothing """ current() @@ -19,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) @@ -31,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) ∈ pairs(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) ∈ pairs(ekwargs) do_show && println(io, " Series{$i}:") println(io, " "^4, key, ": ", value) do_show = false @@ -56,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 # --------------------------------------------------------- @@ -78,27 +82,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) + PlotsBase.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 @@ -110,6 +114,7 @@ plot( plts_tail::Union{PlaceHolder,Plot}...; kw..., ) = plot!(deepcopy(plt1), deepcopy(plt2), deepcopy.(plts_tail)...; kw...) + function plot!( plt1::Plot, plt2::Union{PlaceHolder,Plot}, @@ -118,7 +123,7 @@ function plot!( ) @nospecialize plotattributes = KW(kw) - Plots.preprocess_attributes!(plotattributes) + PlotsBase.Commons.preprocess_attributes!(plotattributes) # build our plot vector from the args plts = Plot[plt1] @@ -127,32 +132,32 @@ function plot!( n = length(plts) # compute the layout - layout = layout_args(plotattributes, n)[1] - num_sp = sum(length(p.subplots) for p in plts) + layout = layout_attrs(plotattributes, n)[1] + 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 # 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 + for p ∈ 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() - for (k, v) in plotattributes - is_series_attr(k) && (series_attr[k] = pop!(plotattributes, k)) + series_attrs = KW() + for (k, v) ∈ plotattributes + Commons.is_series_attrs(k) && (series_attrs[k] = pop!(plotattributes, k)) end # create the layout @@ -163,15 +168,15 @@ 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 - merge!(series.plotattributes, series_attr) - _slice_series_args!(series.plotattributes, plt, sp, cmdidx) + for series ∈ serieslist + merge!(series.plotattributes, series_attrs) + _slice_series_attrs!(series.plotattributes, plt, sp, cmdidx) push!(plt.series_list, series) _series_added(plt, series) cmdidx += 1 @@ -180,8 +185,14 @@ function plot!( ttl_idx = _add_plot_title!(plt) # 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) + for (idx, sp) ∈ enumerate(plt.subplots) + Plots._update_subplot_attrs( + plt, + sp, + idx == ttl_idx ? KW() : plotattributes, + idx, + false, + ) end # finish up @@ -208,8 +219,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) + PlotsBase.Commons.preprocess_attributes!(plotattributes) + # merge!(plt.user_attrs, plotattributes) _plot!(plt, plotattributes, args) end @@ -245,10 +256,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 @@ -289,7 +299,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 79% rename from src/plotattr.jl rename to PlotsBase/src/plotattr.jl index 69018dc64..3933c9c07 100644 --- a/src/plotattr.jl +++ b/PlotsBase/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,35 +30,35 @@ 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" end d = default(attr) - print(""" + """ # $letter$attr - $attrtype attribute - Default: `$(d isa Symbol ? string(':', d) : d)`. - $(_argument_description(attr)) - """) + """ |> print end # COV_EXCL_STOP @@ -69,8 +69,8 @@ end function plotattr(attribute::AbstractString) attribute = Symbol(attribute) - attribute = get(_keyAliases, attribute, attribute) - for (k, v) in _attribute_defaults + attribute = get(Commons._keyAliases, attribute, attribute) + for (k, v) ∈ _attribute_defaults attribute ∈ keys(v) && return plotattr(k, attribute) end error("There is no attribute named $attribute") @@ -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 = PlotsBase.Commons.aliases(attribute)) |> length > 0 "Aliases: " * string(Tuple(al)) * ".\n\n" else "" diff --git a/src/backends/plotly.jl b/PlotsBase/src/plotly.jl similarity index 81% rename from src/backends/plotly.jl rename to PlotsBase/src/plotly.jl index 12f81ad60..f927ace8f 100644 --- a/src/backends/plotly.jl +++ b/PlotsBase/src/plotly.jl @@ -1,22 +1,189 @@ # https://plot.ly/javascript/getting-started - -_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 - -# -------------------------------------------------------------------------------------- - -using UUIDs +module Plotly + +export PlotlyBackend, plotly_show_js, plotly_series, plotly_layout, html_head, html_body + +import RecipesPipeline +import Statistics +import UUIDs +import JSON + +using PlotUtils + +using PlotsBase.Colors: Colorant +using PlotsBase.Annotations +using PlotsBase.DataSeries +using PlotsBase.Colorbars +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) + +eval(PlotsBase.backend_defines(:PlotlyBackend, :plotly)) + +const _plotly_attrs = PlotsBase.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] + +PlotsBase.default_output_format(plt::Plot{PlotlyBackend}) = "html" # ---------------------------------------------------------------- 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") @@ -25,6 +192,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), @@ -49,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, ), ) @@ -67,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 @@ -81,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 @@ -104,7 +280,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]] @@ -121,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 @@ -157,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 - ticks = get_ticks(sp, axis) - ttype = ticksType(ticks) - if ttype === :ticks + if axis[:ticks] ≢ :native + ticks = PlotsBase.get_ticks(sp, axis) + ttype = PlotsBase.ticks_type(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 @@ -182,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) @@ -203,21 +379,21 @@ 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) : ("", "") + x_idx, y_idx = multiple_subplots ? plotly_link_indices(plt, sp) : ("", "") # add an annotation for the title 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 @@ -286,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( @@ -297,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( @@ -333,13 +509,14 @@ 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) - 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]), @@ -352,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)), ), ) @@ -412,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)] @@ -437,15 +614,18 @@ plotly_colorscale(c::AbstractVector{<:Colorant}, α = nothing) = [[0.0, rgba_string(plot_color(c[1], α))], [1.0, rgba_string(plot_color(c[1], α))]] else vals = range(0.0, stop = 1.0, length = length(c)) - map(i --> [vals[i], rgba_string(plot_color(c[i], α))], eachindex(c)) + map(i -> [vals[i], rgba_string(plot_color(c[i], α))], eachindex(c)) end 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]], 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 @@ -466,8 +646,8 @@ get_plotly_marker(k, def) = get( def, ) -# find indicies of axes to which the subplot links to -function plotly_link_indicies(plt::Plot, sp::Subplot) +# find indices of axes to which the subplot links to +function plotly_link_indices(plt::Plot, sp::Subplot) if plt[:link] in (:x, :y, :both) x_idx = sp[:xaxis].sps[1][:subplot_index] y_idx = sp[:yaxis].sps[1][:subplot_index] @@ -477,11 +657,11 @@ function plotly_link_indicies(plt::Plot, sp::Subplot) x_idx, y_idx end -# the Shape contructor will automatically close the shape. since we need it closed, +# the Shape constructor will automatically close the shape. since we need it closed, # 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 @@ -491,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 @@ -503,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) @@ -530,7 +710,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 @@ -543,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() @@ -555,28 +735,27 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:zaxis] = "z$spidx" plotattributes_out[:scene] = "scene$spidx" else - x_idx, y_idx = length(plt.subplots) > 1 ? plotly_link_indicies(plt, sp) : ("", "") + x_idx, y_idx = length(plt.subplots) > 1 ? plotly_link_indices(plt, sp) : ("", "") plotattributes_out[:xaxis] = "x$(x_idx)" plotattributes_out[:yaxis] = "y$(y_idx)" 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] 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] 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) && @@ -584,7 +763,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 @@ -592,16 +771,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 @@ -640,7 +819,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, @@ -655,16 +834,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] @@ -680,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 @@ -701,7 +878,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) @@ -748,7 +925,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], ) @@ -763,7 +940,7 @@ function plotly_series_shapes(plt::Plot, series::Series, clims) # x, y = series[:x], series[:y] # these are the axes that the series should be mapped to - x_idx, y_idx = plotly_link_indicies(plt, series[:subplot]) + x_idx, y_idx = plotly_link_indices(plt, series[:subplot]) plotattributes_base = KW( :xaxis => "x$(x_idx)", :yaxis => "y$(y_idx)", @@ -773,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), 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 @@ -808,11 +985,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), @@ -825,7 +1002,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) && @@ -836,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) @@ -845,7 +1022,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 @@ -884,7 +1061,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), @@ -926,11 +1103,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" @@ -967,7 +1144,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) @@ -981,11 +1158,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), @@ -1028,7 +1205,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" @@ -1042,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 @@ -1054,10 +1231,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[] - _plotly_data_url() + 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) @@ -1077,7 +1254,7 @@ function plotly_html_head(plt::Plot) "\n\t\t" end - if isijulia() + if PlotsBase.isijulia() mathjax_head else "$mathjax_head" @@ -1085,13 +1262,13 @@ 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 requirejs_prefix = requirejs_suffix = "" - if isijulia() + if PlotsBase.isijulia() # require.js adds .js automatically plotly_no_ext = plotly_url() |> splitext |> first @@ -1128,9 +1305,20 @@ 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) + +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 -_display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) +using .Plotly diff --git a/src/recipes.jl b/PlotsBase/src/recipes.jl similarity index 88% rename from src/recipes.jl rename to PlotsBase/src/recipes.jl index c82a0ff01..6ab4e240f 100644 --- a/src/recipes.jl +++ b/PlotsBase/src/recipes.jl @@ -12,8 +12,8 @@ function seriestype_supported(pkg::AbstractBackend, st::Symbol) haskey(_series_recipe_deps, st) || return :no supported = true - for dep in _series_recipe_deps[st] - if seriestype_supported(pkg, dep) === :no + for dep ∈ _series_recipe_deps[st] + if seriestype_supported(pkg, dep) ≡ :no supported = false break end @@ -22,15 +22,15 @@ 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 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 ∈ _initialized_backends + be = backend_instance(bsym) + sts = union(sts, Set{Symbol}(supported_seriestypes(be))) end sts |> collect |> sort 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,20 +196,20 @@ 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 + 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]) 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 @@ -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)) ∈ 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,32 @@ 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 + markershape := if plotattributes[:markershape] === :arrow + [isless(yi, 0.0) ? :downarrow : :uparrow for yi ∈ y] + else + plotattributes[:markershape] + end seriestype := :scatter x := x y := y - if z !== nothing + if z ≢ nothing z := z end primary := true @@ -356,7 +361,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 @@ -366,35 +371,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 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)) 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 () @@ -408,7 +413,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 @@ -422,9 +427,9 @@ end # compute half-width of bars bw = plotattributes[:bar_width] - hw = if bw === nothing - 0.5_bar_width * if nx > 1 - ignorenan_minimum(filter(x -> x > 0, diff(sort(procx)))) + hw = if bw ≡ nothing + 0.5Commons._bar_width * if nx > 1 + ignorenan_minimum(filter(>(0), diff(sort(procx)))) else 1 end @@ -433,15 +438,15 @@ 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 _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 @@ -451,7 +456,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] @@ -462,16 +467,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 @@ -481,7 +480,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,13 +507,13 @@ 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) 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]] @@ -552,11 +551,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 @@ -593,7 +592,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) @@ -622,8 +621,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 @@ -640,12 +639,12 @@ 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 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,17 +677,13 @@ 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 + if plotattributes[:markershape] ≢ :none @series begin seriestype := :scatter x := _bin_centers(edge) @@ -725,7 +720,8 @@ function _auto_binning_nbins( ) where {N} max_bins = 10_000 _cl(x) = min(ceil(Int, max(x, one(x))), max_bins) - _iqr(v) = (q = quantile(v, 0.75) - quantile(v, 0.25); q > 0 ? q : oftype(q, 1)) + _iqr(v) = (q = Statistics.quantile(v, 0.75) - Statistics.quantile(v, 0.25); + q > 0 ? q : oftype(q, 1)) _span(v) = maximum(v) - minimum(v) n_samples = length(LinearIndices(first(vs))) @@ -739,19 +735,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 - _cl(_span(v) / (3.5 * std(v) / nd)) - elseif mode === :fd # Freedman–Diaconis rule + elseif mode ≡ :scott # Scott's normal reference rule + _cl(_span(v) / (3.5 * Statistics.std(v) / nd)) + 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") @@ -778,7 +774,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 @@ -792,7 +788,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, @@ -802,7 +798,7 @@ function _make_hist( closed = :left, ), ) - normalize!(h, mode = _hist_norm_mode(normed)) + LinearAlgebra.normalize!(h, mode = _hist_norm_mode(normed)) end @nospecialize @@ -866,20 +862,20 @@ 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) 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 end @recipe f(hv::AbstractVector{H}) where {H<:StatsBase.Histogram} = # COV_EXCL_LINE - for h in hv + for h ∈ hv @series begin h end @@ -893,21 +889,21 @@ 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) + for (i, c) ∈ enumerate(float_weights) c == 0 && (float_weights[i] = NaN) 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( @@ -937,7 +933,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 @@ -958,7 +954,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 @@ -971,7 +967,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 @@ -987,7 +983,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] @@ -1041,7 +1037,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 @@ -1088,14 +1084,15 @@ end # --------------------------------------------------------------------------- # Error Bars -@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) +Commons.@attributes function error_style!(plotattributes::AKW) + # errorbar color should solely determined by markerstrokecolor + 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 + msc = if (msc = plotattributes[:markerstrokecolor]) ≡ :match plotattributes[:subplot][:foreground_color_subplot] - elseif msc === :auto + elseif msc ≡ :auto get_series_color( plotattributes[:linecolor], plotattributes[:subplot], @@ -1123,8 +1120,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 @@ -1143,16 +1140,16 @@ 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 + 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 () @@ -1160,16 +1157,16 @@ 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 + 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 () @@ -1177,14 +1174,14 @@ 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 + 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 () @@ -1211,7 +1208,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) @@ -1259,7 +1256,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) @@ -1312,7 +1309,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 @@ -1366,7 +1363,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 """ @@ -1381,7 +1378,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 @@ -1448,7 +1445,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) @@ -1465,7 +1462,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 @@ -1507,10 +1504,23 @@ end SliceIt, m, n, Surface(mat) end +@specialize + +find_nnz(A::SparseArrays.AbstractSparseMatrix) = SparseArrays.findnz(A) + +# fallback function for finding non-zero elements of non-sparse matrices +function find_nnz(A::AbstractMatrix) + keysnz = findall(!iszero, A) + rs = map(k -> k[1], keysnz) + cs = map(k -> k[2], keysnz) + zs = A[keysnz] + rs, cs, zs +end + @recipe function f(::Type{Val{:spy}}, x, y, z) # COV_EXCL_LINE yflip := true aspect_ratio := 1 - rs, cs, zs = findnz(z.surf) + rs, cs, zs = PlotsBase.find_nnz(z.surf) xlims := ignorenan_extrema(cs) ylims := ignorenan_extrema(rs) widen --> true @@ -1532,19 +1542,6 @@ end () end -@specialize - -findnz(A::AbstractSparseMatrix) = SparseArrays.findnz(A) - -# fallback function for finding non-zero elements of non-sparse matrices -function findnz(A::AbstractMatrix) - keysnz = findall(!iszero, A) - rs = map(k -> k[1], keysnz) - cs = map(k -> k[2], keysnz) - zs = A[keysnz] - rs, cs, zs -end - # ------------------------------------------------- @nospecialize @@ -1586,10 +1583,10 @@ 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 Plots.isvertical(plotattributes) ? (sx, sy) : (sy, sx) + @series (sx, sy) end end @@ -1598,7 +1595,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/src/shorthands.jl b/PlotsBase/src/shorthands.jl similarity index 90% rename from src/shorthands.jl rename to PlotsBase/src/shorthands.jl index 09fa9fd2b..835ce48d1 100644 --- a/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...) = @@ -520,7 +520,7 @@ for letter in ("x", "y", "z") $($letter)error(x, y [, z]; $($letter)error = vals) $($letter)error!(x, y [, z]; $($letter)error = vals) - Create or add a series of $($letter)errorbars at the positions defined by `x`, `y` and `z` with the lenghts defined in `vals`. + Create or add a series of $($letter)errorbars at the positions defined by `x`, `y` and `z` with the lengths defined in `vals`. Markerstrokecolor will color the whole errorbars if not specified otherwise. """ @@ -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/src/themes.jl b/PlotsBase/src/themes.jl similarity index 91% rename from src/themes.jl rename to PlotsBase/src/themes.jl index 67d3569f6..7aeabc7a3 100644 --- a/src/themes.jl +++ b/PlotsBase/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 @@ -57,10 +57,10 @@ _get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func 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(_keyAliases, k, k) + arg = get(Commons._keyAliases, k, k) plotattributes[arg] = if typeof(def) <: Colorant cfunc(RGB(def)) elseif eltype(def) <: Colorant @@ -78,7 +78,7 @@ _get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func 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/users.jl b/PlotsBase/src/users.jl new file mode 100644 index 000000000..9e7274cd6 --- /dev/null +++ b/PlotsBase/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/PlotsBase/src/utils.jl b/PlotsBase/src/utils.jl new file mode 100644 index 000000000..619e24c39 --- /dev/null +++ b/PlotsBase/src/utils.jl @@ -0,0 +1,962 @@ + +treats_y_as_x(seriestype) = + seriestype in (:vline, :vspan, :histogram, :barhist, :stephist, :scatterhist) + +function replace_image_with_heatmap(z::AbstractMatrix{<:Colorant}) + n, m = size(z) + colors = palette(vec(z)) + reshape(1:(n * m), n, m), colors +end + +"Build line segments for plotting" +mutable struct Segments{T} + pts::Vector{T} +end + +# Segments() = Segments{Float64}(zeros(0)) + +Segments() = Segments(Float64) +Segments(::Type{T}) where {T} = Segments(T[]) +Segments(p::Int) = Segments(NTuple{p,Float64}[]) + +# Segments() = Segments(zeros(0)) + +to_nan(::Type{Float64}) = NaN +to_nan(::Type{NTuple{2,Float64}}) = (NaN, NaN) +to_nan(::Type{NTuple{3,Float64}}) = (NaN, NaN, NaN) + +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)) +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} + isempty(segments.pts) || push!(segments.pts, to_nan(T)) + foreach(v -> push!(segments.pts, convert(T, v)), vs) + segments +end + +function Base.push!(segments::Segments{T}, vs::AVec) where {T} + isempty(segments.pts) || push!(segments.pts, to_nan(T)) + foreach(v -> push!(segments.pts, convert(T, v)), vs) + segments +end + +# 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 + +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, + ) + + # update alphas + for asym ∈ (: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 ∈ (: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 + + # 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( + 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) + 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 + + # 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] = Commons.label_to_string.(plotattributes[:label], globalIndex) + + Commons._replace_linewidth(plotattributes) + plotattributes +end +""" +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 + +""" +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 + +function _slice_series_attrs!( + plotattributes::AKW, + plt::Plot, + sp::Subplot, + commandIndex::Int, +) + for k ∈ 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] + isedges && return v + # `isedges = true` means that v is a vector which already describes edges + # and does not need to be extended. + vmin, vmax = ignorenan_extrema(v) + extra_min = ispolar ? min(v[1], 0.5(v[2] - v[1])) : 0.5(v[2] - v[1]) + extra_max = 0.5(v[n] - v[n - 1]) + vcat(vmin - extra_min, 0.5(v[1:(n - 1)] + v[2:n]), vmax + extra_max) +end + +_heatmap_edges(::Val{true}, v::AVec, ::Symbol, isedges::Bool, ispolar::Bool) = + __heatmap_edges(v, isedges, ispolar) + +function _heatmap_edges(::Val{false}, v::AVec, scale::Symbol, isedges::Bool, ispolar::Bool) + f, invf = scale_inverse_scale_func(scale) + invf.(__heatmap_edges(f.(v), isedges, ispolar)) +end + +"create an (n+1) list of the outsides of heatmap rectangles" +heatmap_edges( + v::AVec, + scale::Symbol = :identity, + isedges::Bool = false, + ispolar::Bool = false, +) = _heatmap_edges(Val(scale ≡ :identity), v, scale, isedges, ispolar) + +function heatmap_edges( + x::AVec, + xscale::Symbol, + y::AVec, + yscale::Symbol, + z_size::NTuple{2,Int}, + ispolar::Bool = false, +) + nx, ny = length(x), length(y) + # ismidpoints = z_size == (ny, nx) # This fails some tests, but would actually be + # the correct check, since (4, 3) != (3, 4) and a misleading plot is produced. + ismidpoints = prod(z_size) == (ny * nx) + isedges = z_size == (ny - 1, nx - 1) + (ismidpoints || isedges) || + """ + Length of x & y does not match the size of z. + Must be either `size(z) == (length(y), length(x))` (x & y define midpoints) + or `size(z) == (length(y)+1, length(x)+1))` (x & y define 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 + ) +end + +is_uniformly_spaced(v; tol = 1e-6) = + let dv = diff(v) + maximum(dv) - minimum(dv) < tol * mean(abs.(dv)) + end + +function convert_to_polar(theta, r, r_extrema = ignorenan_extrema(r)) + rmin, rmax = r_extrema + r = @. (r - rmin) / (rmax - rmin) + x = @. r * cos(theta) + y = @. r * sin(theta) + x, y +end + +fakedata(sz::Int...) = fakedata(Random.seed!(SEED), sz...) + +function fakedata(rng::Random.AbstractRNG, sz...) + y = zeros(sz...) + for r ∈ 2:size(y, 1) + y[r, :] = 0.95vec(y[r - 1, :]) + randn(rng, size(y, 2)) + end + y +end + +isijulia() = :IJulia in nameof.(collect(values(Base.loaded_modules))) +isatom() = :Atom in nameof.(collect(values(Base.loaded_modules))) + +limsType(lims::Tuple{<:Real,<:Real}) = :limits +limsType(lims::Symbol) = lims ≡ :auto ? :auto : :invalid +limsType(lims) = :invalid + +isautop(sp::Subplot) = sp[:projection_type] ≡ :auto +isortho(sp::Subplot) = sp[:projection_type] ∈ (:ortho, :orthographic) +ispersp(sp::Subplot) = sp[:projection_type] ∈ (:persp, :perspective) + +# recursively merge kw-dicts, e.g. for merging extra_kwargs / extra_plot_kwargs in plotly) +recursive_merge(x::AbstractDict...) = merge(recursive_merge, x...) +# if values are not AbstractDicts, take the last definition (as does merge) +recursive_merge(x...) = x[end] + +nanpush!(a::AbstractVector, b) = (push!(a, NaN); push!(a, b); nothing) +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 + # no nans + push!(vs, v) + break + elseif idx > 1 + push!(vs, v[1:(idx - 1)]) + end + v = v[(idx + 1):end] + end + vs +end + +function nanvcat(vs::AVec) + v_out = zeros(0) + foreach(v -> nanappend!(v_out, v), vs) + v_out +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) ∈ pairs(y) + frs[i] = yi + _cycle(rib, i) + end + frs +end + +# turn a ribbon into a fillrange +function make_fillrange_from_ribbon(kw::AKW) + y, rib = kw[:y], kw[:ribbon] + rib = wraptuple(rib) + 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) +end + +#turn tuple of fillranges to one path +function concatenate_fillrange(x, y::Tuple) + rib1, rib2 = collect(first(y)), collect(last(y)) # collect needed until https://github.com/JuliaLang/julia/pull/37629 is merged + vcat(x, reverse(x)), vcat(rib1, reverse(rib2)) # x, y +end + +get_sp_lims(sp::Subplot, letter::Symbol) = axis_limits(sp, letter) + +""" + xlims([plt]) + +Returns the x axis limits of the current plot or subplot +""" +xlims(sp::Subplot) = get_sp_lims(sp, :x) + +""" + ylims([plt]) + +Returns the y axis limits of the current plot or subplot +""" +ylims(sp::Subplot) = get_sp_lims(sp, :y) + +""" + zlims([plt]) + +Returns the z axis limits of the current plot or subplot +""" +zlims(sp::Subplot) = get_sp_lims(sp, :z) + +xlims(plt::Plot, sp_idx::Int = 1) = xlims(plt[sp_idx]) +ylims(plt::Plot, sp_idx::Int = 1) = ylims(plt[sp_idx]) +zlims(plt::Plot, sp_idx::Int = 1) = zlims(plt[sp_idx]) +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) + +"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases." +function Commons.preprocess_attributes!(plotattributes::AKW) + Commons.replaceAliases!(plotattributes, Commons._keyAliases) + + # handle axis args common to all axis + args = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :axis, ())) + showarg = wraptuple(RecipesPipeline.pop_kw!(plotattributes, :showaxis, ())) + for arg ∈ wraptuple((args..., showarg...)) + for letter ∈ (:x, :y, :z) + process_axis_arg!(plotattributes, arg, letter) + end + end + # handle axis args + for letter ∈ (:x, :y, :z) + asym = get_attr_symbol(letter, :axis) + args = RecipesPipeline.pop_kw!(plotattributes, asym, ()) + if !(typeof(args) <: Axis) + for arg ∈ 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 + processGridArg! = Commons.process_grid_attr! + args = RecipesPipeline.pop_kw!(plotattributes, :grid, ()) + for arg ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) + processGridArg!(plotattributes, arg, letter) + end + end + # handle individual axes grid args + for letter ∈ (:x, :y, :z) + gridsym = get_attr_symbol(letter, :grid) + args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) + 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 ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) + Commons.process_minor_grid_attr!(plotattributes, arg, letter) + end + end + # handle individual axes grid args + for letter ∈ (:x, :y, :z) + gridsym = get_attr_symbol(letter, :minorgrid) + args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ()) + for arg ∈ wraptuple(args) + Commons.process_minor_grid_attr!(plotattributes, arg, letter) + end + end + # handle font args common to all axes + for fontname ∈ (:tickfont, :guidefont) + args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) + for arg ∈ wraptuple(args) + for letter ∈ (:x, :y, :z) + Commons.process_font_attr!( + plotattributes, + get_attr_symbol(letter, fontname), + arg, + ) + end + end + end + # handle individual axes font args + for letter ∈ (:x, :y, :z) + for fontname ∈ (:tickfont, :guidefont) + args = RecipesPipeline.pop_kw!( + plotattributes, + get_attr_symbol(letter, fontname), + (), + ) + for arg ∈ wraptuple(args) + Commons.process_font_attr!( + plotattributes, + get_attr_symbol(letter, fontname), + arg, + ) + end + end + end + # handle axes args + for k ∈ Commons._axis_attrs + if haskey(plotattributes, k) && k ≢ :link + v = plotattributes[k] + for letter ∈ (:x, :y, :z) + lk = get_attr_symbol(letter, k) + if !is_explicit(plotattributes, lk) + plotattributes[lk] = v + end + end + end + end + + # fonts + for fontname ∈ + (:titlefont, :legend_title_font, :plot_titlefont, :colorbar_titlefont, :legend_font) + args = RecipesPipeline.pop_kw!(plotattributes, fontname, ()) + for arg ∈ wraptuple(args) + Commons.process_font_attr!(plotattributes, fontname, arg) + end + end + + # handle line args + for arg ∈ wraptuple(RecipesPipeline.pop_kw!(plotattributes, :line, ())) + Commons.process_line_attr(plotattributes, arg) + end + + if haskey(plotattributes, :seriestype) && + haskey(Commons._typeAliases, plotattributes[:seriestype]) + plotattributes[:seriestype] = Commons._typeAliases[plotattributes[:seriestype]] + end + + # handle marker args... default to ellipse if shape not set + anymarker = false + for arg ∈ 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 + + # handle fill + for arg ∈ wraptuple(get(plotattributes, :fill, ())) + Commons.process_fill_attr(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] = + Commons.convert_legend_value(plotattributes[:legend_position]) + end + if haskey(plotattributes, :colorbar) + plotattributes[:colorbar] = Commons.convert_legend_value(plotattributes[:colorbar]) + end + + # framestyle + if haskey(plotattributes, :framestyle) && + haskey(Commons._framestyle_aliases, plotattributes[:framestyle]) + plotattributes[:framestyle] = + Commons._framestyle_aliases[plotattributes[:framestyle]] + end + + # 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 PlotsBase. Settings apply only for the `do` block. Example: +``` +with(:gr, size=(400,400), type=:histogram) do + plot(rand(10)) + plot(rand(10)) +end +``` +""" +function with(f::Function, args...; scalefonts = nothing, kw...) + new_defs = KW(kw) + + if :canvas in args + 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 + old_defs = KW() + for k ∈ keys(new_defs) + old_defs[k] = default(k) + end + + # save the backend + old_backend = backend_name() + + for arg ∈ args + # 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 + # as in: with(:gr, :scatter, :legend, :grid) do; ...; end + # TODO: can we generalize this enough to also do something similar in the plot commands?? + + k = :legend + if arg in (k, :leg) + old_defs[k] = default(k) + new_defs[k] = true + end + + k = :grid + if arg == k + old_defs[k] = default(k) + new_defs[k] = true + end + end + + # now set all those defaults + default(; new_defs...) + scalefonts ≡ nothing || scalefontsizes(scalefonts) + + # call the function + ret = Base.invokelatest(f) + + # put the defaults back + scalefonts ≡ nothing || resetfontsizes() + default(; old_defs...) + + # revert the backend + 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) + for key ∈ keys(_convert_sci_unicode_dict) + label = replace(label, key => _convert_sci_unicode_dict[key]) + end + occursin("×10^{", label) && (label = string(label, "}")) + label +end + +function ___straightline_data(xl, yl, x, y, exp_fact) + x_vals, y_vals = if y[1] == y[2] + if x[1] == x[2] + error("Two identical points cannot be used to describe a straight line.") + else + [xl[1], xl[2]], [y[1], y[2]] + end + elseif x[1] == x[2] + [x[1], x[2]], [yl[1], yl[2]] + else + # get a and b from the line y = a * x + b through the points given by + # the coordinates x and x + b = y[1] - (y[1] - y[2]) * x[1] / (x[1] - x[2]) + 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 ∈ yl + ] + + xdata, a .* xdata .+ b + end + # expand the data outside the axis limits, by a certain factor too improve + # plotly(js) and interactive behaviour + ( + x_vals .+ (x_vals[2] - x_vals[1]) .* exp_fact, + y_vals .+ (y_vals[2] - y_vals[1]) .* exp_fact, + ) +end + +__straightline_data(xl, yl, x, y, exp_fact) = + if (n = length(x)) == 2 + ___straightline_data(xl, yl, x, y, exp_fact) + else + k, r = divrem(n, 3) + @assert r == 0 "Malformed 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 ∈ 1:k + inds = (3i - 2):(3i - 1) + xdata[inds], ydata[inds] = + ___straightline_data(xl, yl, x[inds], y[inds], exp_fact) + end + xdata, ydata + end + +_straightline_data(::Val{true}, ::Function, ::Function, ::Function, ::Function, args...) = + __straightline_data(args...) + +function _straightline_data( + ::Val{false}, + xf::Function, + xinvf::Function, + yf::Function, + yinvf::Function, + xl, + yl, + x, + y, + exp_fact, +) + xdata, ydata = __straightline_data(xf.(xl), yf.(yl), xf.(x), yf.(y), exp_fact) + xinvf.(xdata), yinvf.(ydata) +end + +function straightline_data(series, expansion_factor = 1) + sp = series[:subplot] + xl, yl = (xlims(sp), ylims(sp)) + + # handle axes scales + xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) + yf, yinvf, ynoop = scale_inverse_scale_func(sp[:yaxis][:scale]) + + _straightline_data( + Val(xnoop && ynoop), + xf, + xinvf, + yf, + yinvf, + xl, + yl, + series[:x], + series[:y], + [-expansion_factor, +expansion_factor], + ) +end + +function _shape_data!(::Val{false}, xf::Function, xinvf::Function, x, xl, exp_fact) + @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 + x[i] = xinvf(xf(xl[2]) + exp_fact * (xf(xl[2]) - xf(xl[1]))) + end + end + x +end + +function _shape_data!(::Val{true}, ::Function, ::Function, x, xl, exp_fact) + @inbounds for i ∈ eachindex(x) + if x[i] == -Inf + x[i] = xl[1] - exp_fact * (xl[2] - xl[1]) + elseif x[i] == +Inf + x[i] = xl[2] + exp_fact * (xl[2] - xl[1]) + end + end + x +end + +function shape_data(series, expansion_factor = 1) + sp = series[:subplot] + xl, yl = (xlims(sp), ylims(sp)) + + # handle axes scales + xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) + yf, yinvf, ynoop = scale_inverse_scale_func(sp[:yaxis][:scale]) + + ( + _shape_data!(Val(xnoop), xf, xinvf, copy(series[:x]), xl, expansion_factor), + _shape_data!(Val(ynoop), yf, yinvf, copy(series[:y]), yl, expansion_factor), + ) +end + +function _add_triangle!(I::Int, i::Int, j::Int, k::Int, x, y, z, X, Y, Z) + m = 4(I - 1) + 1 + n = m + 1 + o = m + 2 + p = m + 3 + X[m] = X[p] = x[i] + Y[m] = Y[p] = y[i] + Z[m] = Z[p] = z[i] + X[n] = x[j] + Y[n] = y[j] + Z[n] = z[j] + X[o] = x[k] + Y[o] = y[k] + Z[o] = z[k] + nothing +end + +function mesh3d_triangles(x, y, z, cns::Tuple{Array,Array,Array}) + ci, cj, ck = cns + length(ci) == length(cj) == length(ck) || + throw(ArgumentError("Argument connections must consist of equally sized arrays.")) + X = zeros(eltype(x), 4length(ci)) + Y = zeros(eltype(y), 4length(cj)) + Z = zeros(eltype(z), 4length(ck)) + @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 +end + +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 ∈ eachindex(cns) # connections are 1-based + _add_triangle!(I, cns[I]..., x, y, z, X, Y, Z) + end + X, Y, Z +end + +texmath2unicode(s::AbstractString, pat = r"\$([^$]+)\$") = + replace(s, pat => m -> UnicodeFun.to_latex(m[2:(length(m) - 1)])) + +_fmt_paragraph(paragraph::AbstractString; kw...) = + _fmt_paragraph(PipeBuffer(), paragraph, 0; kw...) + +function _fmt_paragraph( + io::IOBuffer, + remaining_text::AbstractString, + column_count::Integer; + fillwidth = 60, + leadingspaces = 0, +) + kw = (; fillwidth, leadingspaces) + + if (m = match(r"(.*?) (.*)", remaining_text)) isa Nothing + if column_count + length(remaining_text) ≤ fillwidth + print(io, remaining_text) + else + print(io, '\n', ' '^leadingspaces, remaining_text) + end + read(io, String) + else + if column_count + length(m[1]) ≤ fillwidth + print(io, m[1], ' ') + _fmt_paragraph(io, m[2], column_count + length(m[1]) + 1; kw...) + else + print(io, '\n', ' '^leadingspaces, m[1], ' ') + _fmt_paragraph(io, m[2], leadingspaces; kw...) + end + end +end + +_argument_description(s::Symbol) = + if s ∈ keys(_arg_desc) + aliases = if (al = PlotsBase.Commons.aliases(s)) |> length > 0 + " Aliases: " * string(Tuple(al)) * '.' + else + "" + end + "`$s::$(_arg_desc[s][1])`: $(rstrip(replace(_arg_desc[s][2], '\n' => ' '), '.'))." * + aliases + else + "" + end + +_document_argument(s::Symbol) = + _fmt_paragraph(_argument_description(s), leadingspaces = 6 + length(string(s))) + +# The following functions implement the guess of the optimal legend position, +# from the data series. +function d_point(x, y, lim, scale) + p_scaled = (x / scale[1], y / scale[2]) + d = sum(abs2, lim .- p_scaled) + isnan(d) && return 0.0 + d +end +# Function barrier because lims are type-unstable +function _guess_best_legend_position(xl, yl, plt, weight = 100) + scale = (maximum(xl) - minimum(xl), maximum(yl) - minimum(yl)) + u = zeros(4) # faster than tuple + # quadrants where the points will be tested + quadrants = ( + ((0.00, 0.25), (0.00, 0.25)), # bottomleft + ((0.75, 1.00), (0.00, 0.25)), # bottomright + ((0.00, 0.25), (0.75, 1.00)), # topleft + ((0.75, 1.00), (0.75, 1.00)), # topright + ) + for series ∈ plt.series_list + x = series[:x] + y = series[:y] + yoffset = firstindex(y) - firstindex(x) + for (i, lim) ∈ enumerate(Iterators.product(xl, yl)) + lim = lim ./ scale + 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 + xi > xl[1] + quadrants[i][1][2] * (xl[2] - xl[1]) && continue + yi < yl[1] + quadrants[i][2][1] * (yl[2] - yl[1]) && continue + yi > yl[1] + quadrants[i][2][2] * (yl[2] - yl[1]) && continue + u[i] += inv(1 + weight * d_point(xi, yi, lim, scale)) + end + end + end + # return in the preferred order in case of draws + ibest = findmin(u)[2] + u[ibest] ≈ u[4] && return :topright + u[ibest] ≈ u[3] && return :topleft + u[ibest] ≈ u[2] && return :bottomright + :bottomleft +end + +""" +Computes the distances of the plot limits to a sample of points at the extremes of +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 + _guess_best_legend_position(xlims(plt), ylims(plt), plt) +end + +_generate_doclist(attributes) = + replace(join(sort(collect(attributes)), "\n- "), "_" => "\\_") + +# for UnitfulExt - cannot reside in `UnitfulExt` (macro) +function protectedstring end # COV_EXCL_LINE + +""" + P_str(s) + +(Unitful extension only). +Creates a string that will be Protected from recipe passes. + +Example: +```julia +julia> using Unitful +julia> plot([0,1]u"m", [1,2]u"m/s^2", xlabel=P"This label will NOT display units") +julia> plot([0,1]u"m", [1,2]u"m/s^2", xlabel="This label will display units") +``` +""" +macro P_str(s) + protectedstring(s) +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/src/backends/web.jl b/PlotsBase/src/web.jl similarity index 90% rename from src/backends/web.jl rename to PlotsBase/src/web.jl index cfa7e454c..2061e988c 100644 --- a/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 @@ -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 @@ -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 @@ -68,6 +68,8 @@ function show_png_from_html(io::IO, plt::AbstractPlot) html_to_png(html_fn, png_fn, w, h) # now read that file data into io - pngdata = readall(png_fn) - write(io, pngdata) + write(io, readall(png_fn)) + rm(html_fn) + rm(png_fn) + nothing end 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..5646fdfe5 --- /dev/null +++ b/PlotsBase/test/runtests.jl @@ -0,0 +1,127 @@ +using Pkg +Pkg.status(; outdated = true, mode = Pkg.PKGMODE_MANIFEST) + +const TEST_PACKAGES = + let val = get( + ENV, + "PLOTSBASE_TEST_PACKAGES", + "GR,UnicodePlots,PythonPlot,PGFPlotsX,PlotlyJS,Gaston", + ) + Symbol.(strip.(split(val, ","))) + end +const TEST_BACKENDS = NamedTuple(p => Symbol(lowercase(string(p))) for p ∈ TEST_PACKAGES) + +get!(ENV, "MPLBACKEND", "agg") + +using PlotsBase +eval(PlotsBase.WEAKDEPS) + +# initialize all backends +for pkg ∈ TEST_PACKAGES + @eval begin + import $pkg # trigger extension + $(TEST_BACKENDS[pkg])() + end +end + +import Unitful: m, s, cm, DimensionError +import PlotsBase: SEED, Plot, with +import SentinelArrays: ChainedVector +import GeometryBasics +import OffsetArrays +import Downloads +import FreeType # for `unicodeplots` +import LibGit2 +import Aqua +import JSON + +using VisualRegressionTests +using RecipesPipeline +using FilePathsBase +using LaTeXStrings +using RecipesBase +using Preferences +using TestImages +using Unitful +using FileIO +using Dates +using Test + +function available_channels() + juliaup = "https://julialang-s3.julialang.org/juliaup" + for i ∈ 1:6 + buf = PipeBuffer() + Downloads.download("$juliaup/DBVERSION", buf) + dbversion = VersionNumber(readline(buf)) + dbversion.major == 1 || continue + buf = PipeBuffer() + Downloads.download( + "$juliaup/versiondb/versiondb-$dbversion-x86_64-unknown-linux-gnu.json", + buf, + ) + json = JSON.parse(buf) + haskey(json, "AvailableChannels") || continue + return json["AvailableChannels"] + sleep(10i) + end +end + +""" +julia> is_latest("lts") +julia> is_latest("release") +""" +function is_latest(variant) + channels = available_channels() + ver = VersionNumber(split(channels[variant]["Version"], '+') |> first) + dev = occursin("DEV", string(VERSION)) # or length(VERSION.prerelease) < 2 + !dev && + VersionNumber(ver.major, ver.minor, 0, ("",)) ≤ + VERSION < + VersionNumber(ver.major, ver.minor + 1) +end + +is_auto() = Base.get_bool_env("VISUAL_REGRESSION_TESTS_AUTO", false) +is_pkgeval() = Base.get_bool_env("JULIA_PKGEVAL", false) +is_ci() = Base.get_bool_env("CI", false) + +is_ci() || @eval using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 + +ref_name(i) = "ref" * lpad(i, 3, '0') + +const broken_examples = if Sys.isapple() + [50] # FIXME: https://github.com/jheinen/GR.jl/issues/550 +else + [] +end + +for name ∈ ( + "misc", + "utils", + "args", + "defaults", + "dates", + "axes", + "layouts", + "contours", + "components", + "shorthands", + "recipes", + "unitful", + "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) + if is_auto() || is_pkgeval() + name != "backends" && continue + end + include("test_$name.jl") + end +end diff --git a/test/test_animations.jl b/PlotsBase/test/test_animations.jl similarity index 85% rename from test/test_animations.jl rename to PlotsBase/test/test_animations.jl index cc1025053..a988ac3c9 100644 --- a/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,31 +33,31 @@ 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 @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 + @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( + @test_throws ErrorException 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 = Plots.@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 @@ -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 84% rename from test/test_args.jl rename to PlotsBase/test/test_args.jl index 30e337a02..99c52997e 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} @@ -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 @@ -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 @@ -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 @@ -84,16 +84,16 @@ 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) 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 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 53% rename from test/test_axes.jl rename to PlotsBase/test/test_axes.jl index 01df32ecd..9b9f7754d 100644 --- a/test/test_axes.jl +++ b/PlotsBase/test/test_axes.jl @@ -1,56 +1,59 @@ @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.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.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) + for lab ∈ ("foo", :foo) pl = plot(1:2, xlabel = lab, ylabel = lab, title = lab) 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) == - ["5.00×10^{2}", "1.00×10^{3}", "1.50×10^{3}"] - @test Plots.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\$"] - # GR is used during tests and it correctly overrides labelfunc(), but PGFPlotsX did not - Plots.with(:pgfplotsx) do - @test Plots.get_labels(:auto, 1:3, :log10) == ["10^{1}", "10^{2}", "10^{3}"] + # 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}"] + @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 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}\$"] 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) == - ["\$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 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(:auto, 1:3, :identity) == ["1", "2", "3"] + with(:gr) do # NOTE: GR overrides `labelfunc` + @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\$"] + @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}"] + end + @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._allShowaxisArgs + 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] @@ -66,9 +69,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 @@ -82,53 +85,54 @@ end end @testset "Axis limits" begin - default_widen(from, to) = Plots.scale_lims(from, to, Plots.default_widen_factor) + default_widen(from, to) = + 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]) + for x ∈ (1:3, -10:10), xlims ∈ ((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()) + 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 Plots.xlims( + @test_logs (:warn, r"Invalid limits for x axis") match_mode = :any PlotsBase.xlims( pl, ) @test plims == default_widen(1, 5) 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 Plots.ylims(pl) == (-5.0, 3.0) + @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 Plots.ylims(pl) == (-2.0, 4.0) + @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 Plots.xlims(pl) == (-3.0, 3.0) + @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 Plots.xlims(pl) == (-2.0, 4.0) + @test PlotsBase.xlims(pl) == (-2.0, 4.0) end end end @@ -139,23 +143,23 @@ 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._keyAliases, :xguideposition) - @test haskey(Plots._keyAliases, :x_guide_position) - @test !haskey(Plots._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" + @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) @@ -163,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 @@ -184,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) @@ -208,7 +212,7 @@ end @testset "scale_lims!" begin let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.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 @@ -216,7 +220,7 @@ end let pl = plot(1:2) xl, yl = xlims(pl), ylims(pl) - Plots.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) @@ -226,36 +230,36 @@ end @testset "reset_extrema!" begin pl = plot(1:2) - Plots.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 # 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 # 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 ? Plots.DEFAULT_MINOR_INTERVALS[] - 1 : 0 - elseif minor_intervals === :auto - Plots.DEFAULT_MINOR_INTERVALS[] - 1 - elseif minor_intervals === :none || minor_intervals isa Nothing + minor_intervals ? PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 : 0 + elseif minor_intervals ≡ :auto + PlotsBase.Ticks.DEFAULT_MINOR_INTERVALS[] - 1 + elseif minor_intervals ≡ :none || minor_intervals isa Nothing 0 else max(0, minor_intervals - 1) 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) + 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 = 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) @@ -263,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 new file mode 100644 index 000000000..633cda8a0 --- /dev/null +++ b/PlotsBase/test/test_backends.jl @@ -0,0 +1,73 @@ +@testset "UnicodePlots" begin + with(:unicodeplots) do + @test backend() == PlotsBase.backend_instance(:unicodeplots) + + io = IOContext(IOBuffer(), :color => true) + + # lets just make sure it runs without error + pl = plot(rand(10)) + @test show(io, pl) isa Nothing + + pl = bar(randn(10)) + @test show(io, pl) isa Nothing + + pl = plot([1, 2], [3, 4]) + 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, 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]) + annotate!(pl, [(Dates.Date(2019, 1, 15), 3.2, :auto)]) + hline!(pl, [3.1]) + @test show(io, pl) isa Nothing + + pl = plot(map(plot, 1:4)..., layout = (2, 2)) + @test show(io, pl) isa Nothing + + pl = plot(map(plot, 1:3)..., layout = (2, 2)) + @test show(io, pl) isa Nothing + + pl = plot(map(plot, 1:2)..., layout = @layout([° _; _ °])) + @test show(io, pl) isa Nothing + + redirect_stdout(devnull) do + show(plot(1:2)) + end + end +end + +(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 + display(pl) + end +end + +is_pkgeval() || @testset "Backends" begin + callback(mod, pkgname, i) = begin + save_func = (; pgfplotsx = mod.PlotsBase.pdf, unicodeplots = mod.PlotsBase.txt) # fastest `savefig` for each backend + pl = mod.PlotsBase.current() + fn = Base.invokelatest( + get(save_func, pkgname, mod.PlotsBase.png), + pl, + tempname() * ref_name(i), + ) + @test filesize(fn) > 1_000 + end + (Sys.islinux() && is_latest("release")) && for be ∈ TEST_BACKENDS + 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 +end diff --git a/test/test_components.jl b/PlotsBase/test/test_components.jl similarity index 70% rename from test/test_components.jl rename to PlotsBase/test/test_components.jl index 971f4e519..740ea053e 100644 --- a/test/test_components.jl +++ b/PlotsBase/test/test_components.jl @@ -1,9 +1,14 @@ +const Shapes = PlotsBase.Shapes + @testset "Shapes" begin + get_xs = Shapes.get_xs + get_ys = Shapes.get_ys + vertices = 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 +17,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 @@ -24,7 +29,7 @@ @testset "Center" begin square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)]) - @test Plots.center(square) == (0.5, 0.5) + @test Shapes.center(square) == (0.5, 0.5) end @testset "Translate" begin @@ -32,10 +37,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 Shapes.translate(square, 0, 1).x == squareUp.x + @test Shapes.translate(square, 0, 1).y == squareUp.y - @test Plots.center(translate!(square, 1)) == (1.5, 1.5) + @test Shapes.center(Shapes.translate!(square, 1)) == (1.5, 1.5) end @testset "Rotate" begin @@ -47,12 +52,12 @@ square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)]) # make a new, rotated square - square2 = Plots.rotate(square, -2) + square2 = Shapes.rotate(square, -2) @test square2.x ≈ coordsRotated2[1, :] @test square2.y ≈ coordsRotated2[2, :] # unrotate the new square in place - rotate!(square2, 2) + Shapes.rotate!(square2, 2) @test square2.x ≈ coords[1, :] @test square2.y ≈ coords[2, :] end @@ -60,30 +65,30 @@ @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) - @test pl[1][1][:seriestype] === :shape + @test pl[1][1][:seriestype] ≡ :shape 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 Shapes.weave([1, 3], [2, 4]) == collect(1:4) + @test Shapes.makeshape(3) isa Shape + @test Shapes.makestar(3) isa Shape + @test Shapes.makecross() isa Shape + @test Shapes.makearrowhead(10.0) isa Shape - @test Plots.rotate(1.0, 2.0, 5.0, (0, 0)) isa Tuple + @test Shapes.rotate(1.0, 2.0, 5.0, (0, 0)) isa Tuple - star = Plots.makestar(3) - star_scaled = Plots.scale(star, 0.5) + star = Shapes.makestar(3) + star_scaled = Shapes.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) + Shapes.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 @@ -99,7 +104,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 @@ -110,45 +115,45 @@ 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" @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 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)] @@ -166,19 +171,19 @@ end annotate!(sp = 2, (0.03, 0.95), text("Cats&Dogs", :left)) end - for scale in Plots._logScales + 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 @@ -198,26 +203,29 @@ end :zguidefontsize, ] # get initial font sizes - initialSizes = [Plots.default(s) for s in sizesToCheck] + initialSizes = [PlotsBase.default(s) for s ∈ 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 ∈ sizesToCheck] @test doubledSizes == initialSizes * 2 # reset font sizes resetfontsizes() - finalSizes = [Plots.default(s) for s in sizesToCheck] + finalSizes = [PlotsBase.default(s) for s ∈ sizesToCheck] @test finalSizes == initialSizes end end @testset "Series Annotations" begin + get_xs = Shapes.get_xs + get_ys = Shapes.get_ys + vertices = 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( @@ -245,14 +253,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) @@ -308,7 +316,7 @@ end end @testset "Bezier" begin - curve = Plots.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 55% rename from test/test_contours.jl rename to PlotsBase/test/test_contours.jl index 3c7672733..65a6e0b65 100644 --- a/test/test_contours.jl +++ b/PlotsBase/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 = 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_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 "Commons.preprocess_attributes!" begin function equal_after_pipeline(kw) kw′ = deepcopy(kw) - Plots.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.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 PlotsBase.Commons.preprocess_attributes!(KW(:levels => 1.0)) + @test_throws ArgumentError PlotsBase.Commons.preprocess_attributes!( + KW(:levels => (1, 2, 3)), + ) + @test_throws ArgumentError PlotsBase.Commons.preprocess_attributes!(KW(:levels => -3)) end @testset "contour[f]" begin @@ -35,14 +39,14 @@ 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 - @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 + @test attr[:seriestype] ≡ :contour @test attr[:levels] == n end end diff --git a/test/test_dates.jl b/PlotsBase/test/test_dates.jl similarity index 63% rename from test/test_dates.jl rename to PlotsBase/test/test_dates.jl index 68db3cfe4..cfba7351b 100644 --- a/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]] @@ -9,13 +9,13 @@ 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 - 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,12 +24,12 @@ 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) 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 69% rename from test/test_defaults.jl rename to PlotsBase/test/test_defaults.jl index d96f82152..90898c2bf 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() +const PLOTSBASE_DEFAULTS = Dict(:theme => :wong2, :fontfamily => :palantino) +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() +empty!(PLOTSBASE_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)) @@ -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) @@ -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 81% rename from test/test_hdf5plots.jl rename to PlotsBase/test/test_hdf5plots.jl index 8430acc9f..acab1535a 100644 --- a/test/test_hdf5plots.jl +++ b/PlotsBase/test/test_hdf5plots.jl @@ -1,14 +1,17 @@ +import HDF5 +const HDF5 = Base.get_extension(PlotsBase, :HDF5Ext).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/PlotsBase/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl new file mode 100644 index 000000000..660e3123c --- /dev/null +++ b/PlotsBase/test/test_layouts.jl @@ -0,0 +1,157 @@ +@testset "Plotting plots" begin + pl = @test_nowarn plot(plot(1:2), plot(1:2, size = (1_200, 400))) + @test pl[:size] == (1_200, 400) + pl = @test_nowarn plot(plot(1:2), plot(1:2), size = (1_200, 400)) + @test pl[:size] == (1_200, 400) +end + +@testset "Subplot slicing" begin + pl = @test_nowarn plot( + rand(4, 8), + 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 +end + +@testset "Plot title" begin + pl = plot( + rand(4, 8), + layout = 4, + plot_title = "My title", + background_color = :darkgray, + background_color_inside = :lightgray, + ) + @test pl.layout.heights == [0.05PlotsBase.pct, 0.95PlotsBase.pct] + @test pl[:plot_title] == "My title" + @test pl[:plot_titleindex] == 5 + + @test pl[5][:background_color_inside] == RGBA(colorant"darkgray") + + plot!(pl) + @test pl[:plot_title] == "My title" + @test pl[:plot_titleindex] == 5 + + plot!(pl, plot_title = "My new title") + @test pl[:plot_title] == "My new title" + @test pl[:plot_titleindex] == 5 +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 +end + +@testset "Allowed subplot counts" begin + pl = plot(plot(1:2); layout = grid(2, 2)) + @test length(pl) == 1 + + pl = plot(map(_ -> plot(1:2), 1:2)...; layout = grid(2, 2)) + @test length(pl) == 2 + + pl = plot(map(_ -> plot(1:2), 1:3)...; layout = grid(2, 2)) + @test length(pl) == 3 + @test length(plot!(pl, plot(1:2))) == 4 + + pl = plot(map(_ -> plot(1:2), 1:4)...; layout = grid(2, 2)) + @test length(pl) == 4 + + @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)) + show(devnull, pl) +end + +@testset "Coverage" begin + pl = plot(map(plot, 1:4)..., layout = (2, 2)) + + sp = pl[end] + @test sp isa PlotsBase.Subplot + @test size(sp) == (1, 1) + @test length(sp) == 1 + @test sp[1, 1] == sp + @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 PlotsBase.GridLayout + @test length(gl) == 1 + @test size(gl) == (1, 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 PlotsBase.Series + io = devnull + show(io, pl[1]) + + @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 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 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.Commons.parent_bbox(pl.layout)) + + rl = PlotsBase.RootLayout() + show(io, rl) + @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 ∈ (:left, :right, :hcenter), v_anchor ∈ (:top, :bottom, :vcenter) + @test PlotsBase.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa PlotsBase.BoundingBox + end + + el = PlotsBase.EmptyLayout() + @test PlotsBase.update_position!(el) ≡ nothing + @test size(el) == (0, 0) + @test length(el) == 0 + @test el[1, 1] ≡ nothing + + @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)) +end + +@testset "Link" begin + plot(map(plot, 1:4)..., link = :all) + plot(map(plot, 1:4)..., link = :square) +end diff --git a/test/test_misc.jl b/PlotsBase/test/test_misc.jl similarity index 68% rename from test/test_misc.jl rename to PlotsBase/test/test_misc.jl index 172aee38c..334eb7263 100644 --- a/test/test_misc.jl +++ b/PlotsBase/test/test_misc.jl @@ -2,31 +2,31 @@ @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[] - 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 + @test PlotsBase._plotly_local_file_path[] ≡ nothing + temp = PlotsBase._use_local_dependencies[] + withenv("PLOTSBASE_HOST_DEPENDENCY_LOCAL" => true) do + 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 - Plots.with(:unicodeplots) do - @test backend() == Plots.UnicodePlotsBackend() + with(:unicodeplots) do + @test backend() == PlotsBase.backend_instance(:unicodeplots) 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]), @@ -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 @@ -63,24 +63,17 @@ end 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") -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 +90,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 @@ -122,24 +117,17 @@ 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) 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) + @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 @@ -166,14 +154,14 @@ 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_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 @@ -211,21 +199,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._all_series_args)) + @test occursin( + "label", + PlotsBase._generate_doclist(PlotsBase.Commons._all_series_attrs), + ) end -@testset "wrap" begin +@testset "protect" begin # not sure what is intended here ... - wrapped = Plots.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 PlotsBase.Plot end @testset "group" begin @@ -234,19 +225,19 @@ 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 -Plots.with(:gr) do +with(:gr) do @testset "text" begin 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), ) @@ -259,19 +250,19 @@ Plots.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.find_nnz([0 1; 2 0]) == ([2, 1], [1, 2], [2, 1]) end @testset "mesh3d" begin @@ -299,7 +290,7 @@ Plots.with(:gr) do p4 = [0.5, 0.5, 1.0] pts = [p0, p1, p2, p3, p4] x, y, z = broadcast(i -> getindex.(pts, i), (1, 2, 3)) - # [x[i],y[i],z[i]] is the i-th vertix of the mesh + # [x[i],y[i],z[i]] is the i-th vertex of the mesh mesh3d( x, y, @@ -319,7 +310,7 @@ Plots.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 @@ -331,11 +322,12 @@ Plots.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 69% rename from test/test_output.jl rename to PlotsBase/test/test_output.jl index 86bfc75df..e221c1819 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 -Plots.with(:gr) do - @test Plots.defaultOutputFormat(plot()) == "png" - @test Plots.addExtension("foo", "bar") == "foo.bar" +with(:gr) do + @test PlotsBase.default_output_format(plot()) == "png" + @test PlotsBase.addExtension("foo", "bar") == "foo.bar" @test_save :png @test_save :pdf @@ -39,15 +39,16 @@ Plots.with(:gr) do @test_save :ps end -Plots.with(:unicodeplots) do +with(:unicodeplots) do @test_save :txt - if Plots.UnicodePlots.get_font_face() ≢ nothing + UnicodePlots = Base.get_extension(PlotsBase, :UnicodePlotsExt).UnicodePlots + if UnicodePlots.get_font_face() ≢ nothing @test_save :png end end if Sys.isunix() - Plots.with(:plotlyjs) do + with(:plotlyjs) do @test_save :html @test_save :json @test_save :pdf @@ -56,7 +57,7 @@ if Sys.isunix() # @test_save :eps end - Plots.with(:plotly) do + with(:plotly) do @test_save :pdf @test_save :png @test_save :svg @@ -65,13 +66,13 @@ if Sys.isunix() end if Sys.islinux() && Sys.which("pdflatex") ≢ nothing - Plots.with(:pgfplotsx) do + with(:pgfplotsx) do @test_save :tex @test_save :png @test_save :pdf end - Plots.with(:pythonplot) do + with(:pythonplot) do @test_save :pdf @test_save :png @test_save :svg @@ -80,34 +81,25 @@ if Sys.islinux() && Sys.which("pdflatex") ≢ nothing end end -#= -Plots.with(:gaston) do +Sys.islinux() && with(:gaston) do @test_save :png @test_save :pdf @test_save :eps @test_save :svg end -Plots.with(:inspectdr) do - @test_save :png - @test_save :pdf - @test_save :eps - @test_save :svg -end -=# - @testset "html" begin - Plots.with(:gr) do + 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 82% rename from test/test_pgfplotsx.jl rename to PlotsBase/test/test_pgfplotsx.jl index 12069b740..950549db1 100644 --- a/test/test_pgfplotsx.jl +++ b/PlotsBase/test/test_pgfplotsx.jl @@ -1,4 +1,5 @@ -using Test, Plots, Unitful, LaTeXStrings +using Test, PlotsBase, Unitful, LaTeXStrings +const PGFPlotsX = Base.get_extension(PlotsBase, :PGFPlotsXExt).PGFPlotsX function create_plot(args...; kwargs...) pl = plot(args...; kwargs...) @@ -11,19 +12,19 @@ function create_plot!(args...; kwargs...) end function get_pgf_axes(pl) - Plots._update_plot_object(pl) - Plots.pgfx_axes(pl.o) + PlotsBase._update_plot_object(pl) + PlotsBase.get_backend_module(:PGFPlotsX)[1].pgfx_axes(pl.o) end -Plots.with(:pgfplotsx) do +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", 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) @@ -51,7 +52,7 @@ Plots.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, @@ -59,7 +60,7 @@ Plots.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 @@ -77,7 +78,7 @@ Plots.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"], ) @@ -106,9 +107,12 @@ Plots.with(:pgfplotsx) do end @testset "Marker types" begin - markers = filter((m -> begin - m in Plots.supported_markers() - end), Plots._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)] @@ -121,22 +125,22 @@ Plots.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 @@ -172,25 +176,25 @@ Plots.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 - 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)) if @test_nowarn(haskey(axis.options.dict, "colorbar")) - @test axis["colorbar"] === nothing + @test axis["colorbar"] ≡ nothing @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 @@ -204,8 +208,8 @@ Plots.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 @@ -216,7 +220,7 @@ Plots.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 @@ -232,7 +236,7 @@ Plots.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"] @@ -252,7 +256,7 @@ Plots.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") @@ -261,7 +265,7 @@ Plots.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) @@ -275,8 +279,8 @@ Plots.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) @@ -299,7 +303,7 @@ Plots.with(:pgfplotsx) do "map", "to", "series", - Plots.text("data", :green), + PlotsBase.text("data", :green), ], ) axis_content = first(get_pgf_axes(pl)).contents @@ -332,8 +336,8 @@ Plots.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 @@ -410,14 +414,14 @@ Plots.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", @@ -425,41 +429,41 @@ Plots.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 - @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") == - 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("Interpolação polynomial") == + raw"Interpola$\textnormal{\c{c}}$$\tilde{a}$o polynomial" + @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 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 @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 b50c2323d..ee5fc6c75 100644 --- a/test/test_plotly.jl +++ b/PlotsBase/test/test_plotly.jl @@ -1,12 +1,12 @@ -using Plots, Test -Sys.isunix() && Plots.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 @@ Sys.isunix() && Plots.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 @@ Sys.isunix() && Plots.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 @@ Sys.isunix() && Plots.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 @@ Sys.isunix() && Plots.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/PlotsBase/test/test_preferences.jl b/PlotsBase/test/test_preferences.jl new file mode 100644 index 000000000..e439bf9e7 --- /dev/null +++ b/PlotsBase/test/test_preferences.jl @@ -0,0 +1,99 @@ +# 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() && is_latest("release")) && for pkg ∈ 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, + """ + $(PlotsBase.WEAKDEPS) + + 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 new file mode 100644 index 000000000..dde199a77 --- /dev/null +++ b/PlotsBase/test/test_quality.jl @@ -0,0 +1,13 @@ +@testset "Auto QUality Assurance" begin + # JuliaTesting/Aqua.jl/issues/77 + # :CondaPkg stale deps show up when running CI + Aqua.test_all( + PlotsBase; + stale_deps = (; ignore = [:CondaPkg]), + persistent_tasks = false, + ambiguities = false, + deps_compat = false, # FIXME: fails `CondaPkg` + piracies = false, + ) + 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 65% rename from test/test_recipes.jl rename to PlotsBase/test/test_recipes.jl index a7fe5007f..47d8a96c2 100644 --- a/test/test_recipes.jl +++ b/PlotsBase/test/test_recipes.jl @@ -7,42 +7,48 @@ 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 +@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) @test length(pl.series_list) == 4 - @test pl[2][:colorbar] === :none + @test pl[2][:colorbar] ≡ :none 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 +81,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) - for letter in (:x, :y, :z) - for framestyle in [:box :semi :origin :zerolines :grid :none] + defaultret = PlotsBase.axis_drawing_info_3d(sp, :x) + for letter ∈ (:x, :y, :z) + for framestyle ∈ [: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 @@ -93,18 +99,21 @@ end end @testset "coverage" begin - @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 + # 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.NoneBackend(), :line) ≡ :native end -Plots.with(:gr) do +with(:gr) do @testset "error bars" begin 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 @@ -114,8 +123,8 @@ Plots.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/PlotsBase/test/test_reference.jl b/PlotsBase/test/test_reference.jl new file mode 100644 index 000000000..114593d35 --- /dev/null +++ b/PlotsBase/test/test_reference.jl @@ -0,0 +1,154 @@ +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 PLOTSBASE_IMG_TOL = parse(Float64, get(ENV, "PLOTSBASE_IMG_TOL", is_ci() ? ci_tol() : "1e-5")) + +Base.eval(TESTS_MODULE, :(using Random, StableRNGs, PlotsBase)) + +reference_dir(args...) = + if (ref_dir = get(ENV, "PLOTSBASE_REFERENCE_DIR", nothing)) ≢ nothing + ref_dir + else + joinpath(homedir(), ".julia", "dev", "PlotReferenceImages.jl", args...) + end +reference_path(backend, version) = + reference_dir("PlotsBase", string(backend), string(version)) + +function checkout_reference_dir(dn::AbstractString) + mkpath(dn) + local repo + for i ∈ 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 = 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) + 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.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) + 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" + tol = 1e-2, # acceptable error (percent) +) + for i ∈ setdiff(1:length(PlotsBase._examples), skip) + if only ≡ nothing || i in only + 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 + +## Uncomment the following lines to update reference images for different backends +#= + +with(:gr) do + image_comparison_facts(:gr, tol = PLOTSBASE_IMG_TOL, skip = PlotsBase._backend_skips[:gr]) +end + +with(:plotlyjs) do + image_comparison_facts(:plotlyjs, tol = PLOTSBASE_IMG_TOL, skip = PlotsBase._backend_skips[:plotlyjs]) +end + +with(:pgfplotsx) do + image_comparison_facts(:pgfplotsx, tol = PLOTSBASE_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 = PLOTSBASE_IMG_TOL, + skip = vcat(PlotsBase._backend_skips[:gr]), + broken = broken_examples, + ) + end +end 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..69a9cdf28 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 81% rename from test/test_unitful.jl rename to PlotsBase/test/test_unitful.jl index b0d79fdee..e9b141b14 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,35 +151,35 @@ 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 @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) @@ -190,87 +190,86 @@ 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 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] - - @test plot(xs, ys) isa Plots.Plot + @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 Plots.Plot + @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] - @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( + @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] 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 +315,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 +325,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 +335,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,18 +388,18 @@ 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 @testset "pgfplotsx exponents" begin # github.com/JuliaPlots/Plots.jl/issues/4722 - Plots.with(:pgfplotsx) do + with(:pgfplotsx) do pl = plot([1u"s", 2u"s"], [1u"m/s^2", 2u"m/s^2"]) savefig(pl, tempname() * ".pdf") diff --git a/test/test_utils.jl b/PlotsBase/test/test_utils.jl similarity index 50% rename from test/test_utils.jl rename to PlotsBase/test/test_utils.jl index adf07c2a3..c973c743d 100644 --- a/test/test_utils.jl +++ b/PlotsBase/test/test_utils.jl @@ -11,80 +11,78 @@ [(missing, missing)], [(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) + for z ∈ zipped + @test isequal(collect(zip(PlotsBase.unzip(z)...)), z) + @test isequal(collect(zip(PlotsBase.unzip(GeometryBasics.Point.(z))...)), z) end - op1 = Plots.process_clims((1.0, 2.0)) - op2 = Plots.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.process_clims(nothing) == - Plots.process_clims(missing) == - Plots.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.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 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.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 @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 - - Plots.makekw(foo = 1, bar = 2) isa Dict + @test plot(-1:10, xscale = :log10) isa PlotsBase.Plot ###################### - Plots.debug!(true) + PlotsBase.Commons.debug!(true) io = PipeBuffer() - Plots.debugshow(io, nothing) - Plots.debugshow(io, [1]) + PlotsBase.Commons.debugshow(io, nothing) + PlotsBase.Commons.debugshow(io, [1]) pl = plot(1:2) - Plots.dumpdict(devnull, first(pl.series_list).plotattributes) + PlotsBase.Commons.dumpdict(devnull, first(pl.series_list).plotattributes) show(devnull, pl[1][:xaxis]) # bounding boxes - Plots.with(:gr) do + with(:gr) do show(devnull, plot(1:2)) end - Plots.debug!(false) + PlotsBase.Commons.debug!(false) ###################### let pl = plot(1) @@ -104,51 +102,51 @@ 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 PlotsBase.Plots.xmin(pl) == 1 + @test PlotsBase.Plots.xmax(pl) == 3 + @test PlotsBase.Commons.ignorenan_extrema(pl) == (1, 3) - @test Plots.get_attr_symbol(:x, "lims") === :xlims - @test Plots.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.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 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" - attr!(series; label) + PlotsBase.attr!(series; label) @test series[:label] == label - @test Plots.attr(series, :label) == label + @test PlotsBase.attr(series, :label) == label label = "another label" - attr!(series, label, :label) - @test Plots.attr(series, :label) == label + PlotsBase.attr!(series, label, :label) + @test PlotsBase.attr(series, :label) == label sp = first(pl.subplots) title = "fancy title" - attr!(sp; title) + PlotsBase.attr!(sp; title) @test sp[:title] == title end end @testset "NaN-separated Segments" begin - segments(args...) = collect(iter_segments(args...)) + segments(args...) = collect(PlotsBase.DataSeries.iter_segments(args...)) nan10 = fill(NaN, 10) @test segments(11:20) == [1:10] @@ -177,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 @@ -197,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, @@ -252,56 +250,56 @@ 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]) + y = vcat([0.0 for _ ∈ 1:100], [1.0 for _ ∈ 101:200], [0.5 for _ ∈ 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 - Plots.with(:gr) do + with(:gr) do 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(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 6c1a6ef3e..a338888b7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,133 +1,25 @@ name = "Plots" uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +desc = "Metapackage for PlotsBase + GR" author = ["Tom Breloff (@tbreloff)"] -version = "1.40.4" +license = "MIT" +version = "2.0.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" 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" -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" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" -Requires = "ae029012-a4dd-5104-9daa-d747884805df" -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" [compat] -Aqua = "0.8" -Contour = "0.5 - 0.6" -Downloads = "1" -FFMPEG = "0.3, 0.4" -FixedPointNumbers = "0.6 - 0.8" -GR = "0.69.5 - 0.73" -Gaston = "1" -HDF5 = "0.16 - 0.17" -InspectDR = "0.5" -JLFzf = "0.1" -JSON = "0.21, 1" -LaTeXStrings = "1" -Latexify = "0.14 - 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" -PyPlot = "2" -PythonPlot = "1" -RecipesBase = "1.3.1" -RecipesPipeline = "0.6.10" -Reexport = "0.2, 1" -RelocatableFolders = "0.3, 1" -Requires = "1" -Scratch = "1" -Showoff = "0.3.1, 1" -Statistics = "1" -StatsBase = "0.33 - 0.34" -UnicodeFun = "0.4" -UnicodePlots = "3.4" -UnitfulLatexify = "1" -Unzip = "0.1 - 0.2" -julia = "1.6" - -[extensions] -FileIOExt = "FileIO" -GeometryBasicsExt = "GeometryBasics" -IJuliaExt = "IJulia" -ImageInTerminalExt = "ImageInTerminal" -UnitfulExt = "Unitful" +GR = "0.73" +PlotsBase = "0.1" +Reexport = "1" +julia = "1.10" [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" -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" -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", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyBase", "PyPlot", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "StatsPlots", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] - -[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" +test = ["PythonPlot", "Test", "UnicodePlots"] diff --git a/README.md b/README.md index fec56f006..2f50f65c7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Plots -[gh-ci-img]: https://github.com/JuliaPlots/Plots.jl/workflows/ci/badge.svg?branch=master +[gh-ci-img]: https://github.com/JuliaPlots/Plots.jl/actions/workflows/ci.yml/badge.svg?branch=v2 [gh-ci-url]: https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci [pkgeval-img]: https://juliaci.github.io/NanosoldierReports/pkgeval_badges/P/Plots.svg 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/RecipesBase/src/RecipesBase.jl b/RecipesBase/src/RecipesBase.jl index 5f3410aff..987bacc83 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 @@ -110,9 +110,9 @@ function create_kw_body(func_signature::Expr) kw_body, cleanup_body = map(_ -> Expr(:block), 1:2) arg1 = args[1] if isa(arg1, Expr) && arg1.head ≡ :parameters - for kwpair in arg1.args + for kwpair ∈ 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. @@ -143,13 +143,13 @@ end # and we push this block onto the series_blocks list. # then at the end we push the main body onto the series list function process_recipe_body!(expr::Expr) - for (i, e) in enumerate(expr.args) + for (i, e) ∈ enumerate(expr.args) if isa(e, Expr) # process trailing flags, like: # a --> b, :quiet, :force quiet, require, force = false, false, false if _is_arrow_tuple(e) - for flag in e.args + for flag ∈ e.args if _equals_symbol(flag, :quiet) quiet = true elseif _equals_symbol(flag, :require) @@ -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, @@ -506,9 +506,9 @@ function create_grid_vcat(expr::Expr) nr = length(expr.args) nc = rmin body = Expr(:block) - for r in 1:nr + for r ∈ 1:nr if (arg = expr.args[r]) |> isrow - for (c, item) in enumerate(arg.args) + for (c, item) ∈ enumerate(arg.args) push!(body.args, :(cell[$r, $c] = $(create_grid(item)))) end else @@ -535,7 +535,7 @@ end function create_grid_curly(expr::Expr) kw = KW() - for (i, arg) in enumerate(expr.args[2:end]) + for (i, arg) ∈ enumerate(expr.args[2:end]) add_layout_pct!(kw, arg, i, length(expr.args) - 1) end s = expr.args[1] diff --git a/RecipesBase/test/runtests.jl b/RecipesBase/test/runtests.jl index 2dae4af67..8dd467495 100644 --- a/RecipesBase/test/runtests.jl +++ b/RecipesBase/test/runtests.jl @@ -9,7 +9,7 @@ const KW = Dict{Symbol,Any} RB.is_key_supported(k::Symbol) = true -for t in map(i -> Symbol(:T, i), 1:5) +for t ∈ map(i -> Symbol(:T, i), 1:5) @eval struct $t end end diff --git a/RecipesPipeline/Project.toml b/RecipesPipeline/Project.toml index 8c3062174..aed9a7a06 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" @@ -11,11 +11,11 @@ RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" [compat] -NaNMath = "0.3, 1" -PlotUtils = "0.6.5, 1" +NaNMath = "1" +PlotUtils = "1" RecipesBase = "1.3.1" PrecompileTools = "1" -julia = "1.6" +julia = "1.10" [extras] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" diff --git a/RecipesPipeline/src/RecipesPipeline.jl b/RecipesPipeline/src/RecipesPipeline.jl index d2cf52588..38ffdadc0 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 ∈ 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..62df9d193 100644 --- a/RecipesPipeline/src/api.jl +++ b/RecipesPipeline/src/api.jl @@ -20,7 +20,7 @@ function warn_on_recipe_aliases!( recipe_type::Symbol, @nospecialize(args) ) - for x in v + for x ∈ v warn_on_recipe_aliases!(plt, x, recipe_type, args) end end @@ -55,7 +55,7 @@ Select the proper indices from `val` for attribute `key`. split_attribute(plt, key, val::AbstractArray, indices) = val[indices, fill(Colon(), ndims(val) - 1)...] split_attribute(plt, key, val::Tuple, indices) = - Tuple(split_attribute(plt, key, v, indices) for v in val) + Tuple(split_attribute(plt, key, v, indices) for v ∈ val) # ## Preprocessing attributes @@ -87,41 +87,41 @@ 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) - for (k, v) in plotattributes +function preprocess_axis_attrs!(plt, plotattributes) + for (k, v) ∈ plotattributes is_axis_attribute(plt, k) || continue pop!(plotattributes, k) - for letter in (:x, :y, :z) + for letter ∈ (:x, :y, :z) get!(plotattributes, Symbol(letter, k), v) end end 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 + for (k, v) ∈ plotattributes is_axis_attribute(plt, k) || continue pop!(plotattributes, k) get!(plotattributes, Symbol(letter, k), v) diff --git a/RecipesPipeline/src/group.jl b/RecipesPipeline/src/group.jl index cfb5c5bf5..583ceff14 100644 --- a/RecipesPipeline/src/group.jl +++ b/RecipesPipeline/src/group.jl @@ -9,7 +9,7 @@ end # this is when given a vector-type of values to group by function _extract_group_attributes(v::AVec, args...; legend_entry = string) res = Dict{eltype(v),Vector{Int}}() - for (i, label) in enumerate(v) + for (i, label) ∈ enumerate(v) if haskey(res, label) push!(res[label], i) else @@ -31,7 +31,7 @@ function _extract_group_attributes(vs::Tuple, args...) end # allow passing NamedTuples for a named legend entry -legend_entry_from_tuple(ns::NamedTuple) = join(["$k = $v" for (k, v) in pairs(ns)], ", ") +legend_entry_from_tuple(ns::NamedTuple) = join(["$k = $v" for (k, v) ∈ pairs(ns)], ", ") function _extract_group_attributes(vs::NamedTuple, args...) isempty(vs) && return GroupBy([""], [axes(args[1], 1)]) @@ -42,7 +42,7 @@ end # expecting a mapping of "group label" to "group indices" function _extract_group_attributes(idxmap::Dict{T,V}, args...) where {T,V<:AVec{Int}} group_labels = (sort ∘ collect ∘ keys)(idxmap) - group_indices = Vector{Int}[collect(idxmap[k]) for k in group_labels] + group_indices = Vector{Int}[collect(idxmap[k]) for k ∈ group_labels] GroupBy(group_labels, group_indices) end @@ -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 ∈ (:x, :y, :z, :xerror, :yerror, :zerror) plotattributes[s] = filter_data(get(plotattributes, s, nothing), idxfilter) end end @@ -67,7 +67,7 @@ function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1]) length(groupby.group_labels), ) fill!(y_mat, def_val) - for i in eachindex(groupby.group_labels) + for i ∈ eachindex(groupby.group_labels) xi = x[groupby.group_indices[i]] yi = y[groupby.group_indices[i]] y_mat[getindex.(Ref(x_ind), xi), i] = yi @@ -76,7 +76,7 @@ function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1]) end groupedvec2mat(x_ind, x, y::Tuple, groupby) = - Tuple(groupedvec2mat(x_ind, x, v, groupby) for v in y) + Tuple(groupedvec2mat(x_ind, x, v, groupby) for v ∈ y) group_as_matrix(t) = false # used in `StatsPlots` @@ -85,11 +85,11 @@ group_as_matrix(t) = false # used in `StatsPlots` plt = plotattributes[:plot_object] group_length = maximum(union(groupby.group_indices...)) if !group_as_matrix(args[1]) - for (i, glab) in enumerate(groupby.group_labels) + for (i, glab) ∈ enumerate(groupby.group_labels) @series begin label --> string(glab) idxfilter --> groupby.group_indices[i] - for (key, val) in plotattributes + for (key, val) ∈ plotattributes if splittable_attribute(plt, key, val, group_length) :($key) := split_attribute(plt, key, val, groupby.group_indices[i]) end @@ -101,16 +101,16 @@ group_as_matrix(t) = false # used in `StatsPlots` g = args[1] if length(g.args) == 1 x = zeros(Int, group_length) - for indexes in groupby.group_indices + for indexes ∈ 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))) - for (key, val) in plotattributes + for (key, val) ∈ plotattributes if splittable_attribute(plt, key, val, group_length) :($key) := groupedvec2mat(x_ind, x, val, groupby) end @@ -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 ∈ last_attrs)..., )) end end diff --git a/RecipesPipeline/src/plot_recipe.jl b/RecipesPipeline/src/plot_recipe.jl index 8cddfbd70..97c490565 100644 --- a/RecipesPipeline/src/plot_recipe.jl +++ b/RecipesPipeline/src/plot_recipe.jl @@ -31,7 +31,7 @@ function _process_plotrecipe(plt, kw, kw_list, still_to_process) datalist = RecipesBase.apply_recipe(kw, Val{st}, plt) if !isnothing(datalist) warn_on_recipe_aliases!(plt, datalist, :plot, st) - for data in datalist + for data ∈ datalist preprocess_attributes!(plt, data.plotattributes) if data.plotattributes[:seriestype] == st error( diff --git a/RecipesPipeline/src/series.jl b/RecipesPipeline/src/series.jl index 08d7d40ec..29f222476 100644 --- a/RecipesPipeline/src/series.jl +++ b/RecipesPipeline/src/series.jl @@ -36,7 +36,7 @@ _prepare_series_data(v::Volume) = _series_data_vector(x, plotattributes) = [_prepare_series_data(x)] # fixed number of blank series -_series_data_vector(n::Integer, plotattributes) = [zeros(0) for i in 1:n] +_series_data_vector(n::Integer, plotattributes) = [zeros(0) for i ∈ 1:n] # vector of data points is a single series _series_data_vector(v::AVec{<:DataPoint}, plotattributes) = [_prepare_series_data(v)] @@ -48,7 +48,7 @@ function _series_data_vector(v::AVec, plotattributes) elseif all(x -> x isa MaybeString, v) _series_data_vector(Vector{MaybeString}(v), plotattributes) else - vcat((_series_data_vector(vi, plotattributes) for vi in v)...) + vcat((_series_data_vector(vi, plotattributes) for vi ∈ v)...) end end @@ -57,7 +57,7 @@ function _series_data_vector(v::AMat{<:DataPoint}, plotattributes) if is3d(plotattributes) [_prepare_series_data(Surface(v))] else - [_prepare_series_data(v[:, i]) for i in axes(v, 2)] + [_prepare_series_data(v[:, i]) for i ∈ axes(v, 2)] end end @@ -133,7 +133,7 @@ struct SliceIt end my = length(ys) mz = length(zs) if mx > 0 && my > 0 && mz > 0 - for i in 1:max(mx, my, mz) + for i ∈ 1:max(mx, my, mz) # add a new series di = copy(plotattributes) xi, yi, zi = xs[mod1(i, mx)], ys[mod1(i, my)], zs[mod1(i, mz)] diff --git a/RecipesPipeline/src/series_recipe.jl b/RecipesPipeline/src/series_recipe.jl index bb4275803..b97763fb4 100644 --- a/RecipesPipeline/src/series_recipe.jl +++ b/RecipesPipeline/src/series_recipe.jl @@ -8,14 +8,14 @@ Recursively apply series recipes until the backend supports the seriestype """ function _process_seriesrecipes!(plt, kw_list) - for kw in kw_list + for kw ∈ kw_list # in series attributes given as vector with one element per series, # select the value for current series slice_series_attributes!(plt, kw_list, kw) end process_sliced_series_attributes!(plt, kw_list) - for kw in kw_list - series_attr = DefaultsDict(kw, series_defaults(plt)) + for kw ∈ kw_list + 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 @@ -51,7 +51,7 @@ function _process_seriesrecipe(plt, plotattributes) warn_on_recipe_aliases!(plt, datalist, :series, st) # assuming there was no error, recursively apply the series recipes - for data in datalist + for data ∈ datalist if isa(data, RecipeData) preprocess_attributes!(plt, data.plotattributes) if data.plotattributes[:seriestype] == st 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/RecipesPipeline/src/user_recipe.jl b/RecipesPipeline/src/user_recipe.jl index 4c0fcbaaa..9e38e299b 100644 --- a/RecipesPipeline/src/user_recipe.jl +++ b/RecipesPipeline/src/user_recipe.jl @@ -5,7 +5,7 @@ Wrap input arguments in a `RecipeData' vector and recursively apply user recipes and type recipes on the first element. Prepend the returned `RecipeData` vector. If an element with -empy `args` is returned pop it from the vector, finish up, and it to vector of `Dict`s with +empty `args` is returned pop it from the vector, finish up, and it to vector of `Dict`s with processed series. When all arguments are processed return the series `Dict`. """ function _process_userrecipes!(plt, plotattributes, args) @@ -66,7 +66,7 @@ function _recipedata_vector(plt, plotattributes, args) # remove subplot and axis args from plotattributes... # they will be passed through in the kw_list - isempty(args) || for (k, v) in plotattributes + isempty(args) || for (k, v) ∈ plotattributes if is_subplot_attribute(plt, k) || is_axis_attribute(plt, k) reset_kw!(plotattributes, k) end @@ -81,7 +81,7 @@ function _expand_seriestype_array(plotattributes, args) if typeof(sts) <: AbstractArray reset_kw!(plotattributes, :seriestype) rd = Vector{RecipeData}(undef, size(sts, 1)) - for r in axes(sts, 1) + for r ∈ axes(sts, 1) dc = copy(plotattributes) dc[:seriestype] = sts[r:r, :] rd[r] = RecipeData(dc, args) @@ -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/RecipesPipeline/src/utils.jl b/RecipesPipeline/src/utils.jl index 682b91d01..b02ac17ad 100644 --- a/RecipesPipeline/src/utils.jl +++ b/RecipesPipeline/src/utils.jl @@ -81,11 +81,11 @@ struct Surface{M<:AMat} <: AbstractSurface surf::M end -Surface(f::Function, x, y) = Surface(Float64[f(xi, yi) for yi in y, xi in x]) +Surface(f::Function, x, y) = Surface(Float64[f(xi, yi) for yi ∈ y, xi ∈ x]) Base.Array(surf::Surface) = surf.surf -for f in (:length, :size, :axes, :iterate) +for f ∈ (:length, :size, :axes, :iterate) @eval Base.$f(surf::Surface, args...) = $f(surf.surf, args...) end Base.copy(surf::Surface) = Surface(copy(surf.surf)) @@ -110,7 +110,7 @@ function Volume( end Base.Array(vol::Volume) = vol.v -for f in (:length, :size, :axes, :iterate) +for f ∈ (:length, :size, :axes, :iterate) @eval Base.$f(vol::Volume, args...) = $f(vol.v, args...) end Base.copy(vol::Volume{T}) where {T} = @@ -138,7 +138,7 @@ end Returns `true` if `myseriestype` represents a 3D series, `false` otherwise. """ is3d(st) = false -for st in ( +for st ∈ ( :contour, :contourf, :contour3d, @@ -162,7 +162,7 @@ is3d(plotattributes::AbstractDict) = is3d(get(plotattributes, :seriestype, :path Returns `true` if `myseriestype` represents a surface series, `false` otherwise. """ is_surface(st) = false -for st in (:contour, :contourf, :contour3d, :image, :heatmap, :surface, :wireframe) +for st ∈ (:contour, :contourf, :contour3d, :image, :heatmap, :surface, :wireframe) @eval is_surface(::Type{Val{Symbol($(string(st)))}}) = true end is_surface(st::Symbol) = is_surface(Val{st}) @@ -175,7 +175,7 @@ is_surface(plotattributes::AbstractDict) = Returns `true` if `myseriestype` needs 3d axes, `false` otherwise. """ needs_3d_axes(st) = false -for st in (:contour3d, :path3d, :scatter3d, :surface, :volume, :wireframe, :mesh3d) +for st ∈ (:contour3d, :path3d, :scatter3d, :surface, :volume, :wireframe, :mesh3d) @eval needs_3d_axes(::Type{Val{Symbol($(string(st)))}}) = true end needs_3d_axes(st::Symbol) = needs_3d_axes(Val{st}) @@ -209,7 +209,7 @@ unzip(v::AVec{<:Tuple}) = map(x -> getfield.(v, x), fieldnames(eltype(v))) # -------------------------------- _map_funcs(f::Function, u::AVec) = map(f, u) -_map_funcs(fs::AVec{F}, u::AVec) where {F<:Function} = [map(f, u) for f in fs] +_map_funcs(fs::AVec{F}, u::AVec) where {F<:Function} = [map(f, u) for f ∈ fs] # -------------------------------- # ## Signature strings diff --git a/RecipesPipeline/test/test_group.jl b/RecipesPipeline/test/test_group.jl index 41f4c862a..66f5b9528 100644 --- a/RecipesPipeline/test/test_group.jl +++ b/RecipesPipeline/test/test_group.jl @@ -6,7 +6,7 @@ function _extract_group_attributes_old_slow_known_good_implementation( group_labels = collect(unique(sort(v))) n = length(group_labels) group_indices = - Vector{Int}[filter(i -> v[i] == glab, eachindex(v)) for glab in group_labels] + Vector{Int}[filter(i -> v[i] == glab, eachindex(v)) for glab ∈ group_labels] RecipesPipeline.GroupBy(map(legend_entry, group_labels), group_indices) end @@ -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 ∈ filtered_keys) + orig_akw[:idxfilter] = [1, 4, 10] + + akw = deepcopy(orig_akw) + RecipesPipeline._filter_input_data!(akw) + for k ∈ filtered_keys + @test akw[k] == orig_akw[k][orig_akw[:idxfilter]] + end + end end 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..ad7e87f95 --- /dev/null +++ b/StatsPlots/Project.toml @@ -0,0 +1,52 @@ +name = "StatsPlots" +uuid = "f3b207a7-027a-5e70-b257-86293d7955fd" +version = "1.0.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" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +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" +Clustering = "0.15" +DataStructures = "0.18" +Distributions = "0.25" +Interpolations = "0.15" +KernelDensity = "0.6" +MultivariateStats = "0.10" +NaNMath = "1" +Observables = "0.5" +RecipesBase = "1" +RecipesPipeline = "1" +Reexport = "1" +StatsBase = "0.34" +TableOperations = "1" +Tables = "1" +Widgets = "0.6" +julia = "1.10" + +[extras] +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["NaNMath", "GR", "PlotsBase", "StableRNGs", "Test"] diff --git a/StatsPlots/README.md b/StatsPlots/README.md new file mode 100644 index 000000000..d318a9465 --- /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) +name = repeat("G" .* string.(1:5), outer = 2) + +groupedbar(name, 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..6b9df5847 --- /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 PlotsBase +import PlotsBase.Commons: _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..f1de1eba3 --- /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 = PlotsBase.Segments(), 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..e2c986be5 --- /dev/null +++ b/StatsPlots/src/df.jl @@ -0,0 +1,228 @@ +""" + `@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]) +# get the appropriate name when passed an AbstractString +add_sym!(cols, str::AbstractString, names) = add_sym!(cols, Symbol(str), names) +# 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..2fb5ec706 --- /dev/null +++ b/StatsPlots/src/hist.jl @@ -0,0 +1,247 @@ + +# --------------------------------------------------------------------------- +# 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), t_run = true) + n = length(x) + minx, maxx = range_x + gpoints = range(minx, stop = maxx, length = gridsize) + gcounts = linbin(x, gpoints; t_run) + + 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; t_run) + + 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; t_run = 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 !t_run + lt < 1 && (gcnts[1] += 1) + li ≥ M && (gcnts[M] += 1) + 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..476d5d0f6 --- /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 = PlotsBase.Segments(), PlotsBase.Segments() + qxsegs, qysegs = PlotsBase.Segments(), PlotsBase.Segments() + mxsegs, mysegs = PlotsBase.Segments(), 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..a2229f5e0 --- /dev/null +++ b/StatsPlots/test/runtests.jl @@ -0,0 +1,494 @@ +using MultivariateStats +using Distributions +using StatsPlots +using StableRNGs +using Clustering +using PlotsBase +using NaNMath +using Test + +import GR; gr() + +@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/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/ci/build-docs.sh b/ci/build-docs.sh new file mode 100644 index 000000000..73ceb1904 --- /dev/null +++ b/ci/build-docs.sh @@ -0,0 +1,101 @@ +#!/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 MPLBACKEND=agg +export COLORTERM=truecolor # UnicodePlots.jl +export PLOTDOCS_ANSICOLOR=true +export JULIA_CONDAPKG_BACKEND=MicroMamba + +julia='xvfb-run -a julia --color=yes --project=docs' + +JULIA_PKG_PRECOMPILE_AUTO=0 $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", + v"3.4.32" => ">=14.1,<15.1", + v"3.4.33" => ">=15.1,<16.1", + v"3.4.34" => ">=16.1,<17.1", + # ... keep this up-to-date with gcc 18 + )[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 ==" +JULIA_PKG_PRECOMPILE_AUTO=0 $julia -e ' +using Pkg + +rev = split(ENV["GITHUB_REF"], "/", limit=3)[3] +println("rev=$rev") + +Pkg.develop([ + (; path="./RecipesBase"), + (; path="./RecipesPipeline"), + (; path="./PlotsBase"), + (; path="."), + (; path="./GraphRecipes"), + (; path="./StatsPlots"), +]) +Pkg.add(PackageSpec(; name="Plots", rev)) +Pkg.instantiate() +Pkg.precompile() +' +$julia docs/make.jl diff --git a/ci/downstream.jl b/ci/downstream.jl new file mode 100644 index 000000000..02b3c9d36 --- /dev/null +++ b/ci/downstream.jl @@ -0,0 +1,84 @@ +using Pkg + +const LibGit2 = Pkg.GitTools.LibGit2 +const TOML = Pkg.TOML + +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; branch) + 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) + + 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 + +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") + 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) + Pkg.test(pkg) + end + nothing +end + +test_stable.(ARGS) 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/docs/Project.toml b/docs/Project.toml new file mode 100644 index 000000000..b258e3b86 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,49 @@ +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +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" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +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" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +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" +StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" +TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" +UnitfulLatexify = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" 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..1811340ef --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,858 @@ +using Pkg +Base.get_bool_env("PLOTS_DOCS_DEV", false) && Pkg.develop([ + (; path="../RecipesBase"), + (; path="../RecipesPipeline"), + (; path="../PlotThemes"), + (; path="../PlotsBase"), + (; path="../GraphRecipes"), + (; path="../StatsPlots"), + (; path=".."), +]) +# oneliner debug PLOTS_DOCS_DEV=1 PLOTDOCS_PACKAGES='GR' PLOTDOCS_EXAMPLES=1 julia --project -e 'include("make.jl")' + +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 PlotsBase = Plots.PlotsBase +eval(PlotsBase.WEAKDEPS) + +const SRC_DIR = joinpath(@__DIR__, "src") +const WORK_DIR = joinpath(@__DIR__, "work") +const GEN_DIR = joinpath(WORK_DIR, "generated") +const BRANCH = ("master", "v2")[2] # transition to v2 + +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(dctx::DCtx) = begin + ctx, navnode = dctx.ctx, dctx.navnode + return map(getpage(ctx, navnode).mdast.children) do node + rec = SearchRecord(ctx, navnode, node, node.element) + ############################################################ + # 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(dctx, node, node.element) + end +end + +@eval DemoCards get_logopath() = $(joinpath(SRC_DIR, "assets", "axis_logo_600x400.png")) + +# ---------------------------------------------------------------------- + +edit_url(args...) = + "https://github.com/JuliaPlots/Plots.jl/blob/$BRANCH/docs/" * if length(args) == 0 + "make.jl" + else + joinpath("src", args...) + end + +autogenerated() = "(Automatically generated: " * Dates.format(now(), RFC1123Format) * ')' + +author() = "[Plots.jl](https://github.com/JuliaPlots/Plots.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(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(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 ∈ 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, """ + PlotsBase.Commons.reset_defaults() #hide + using StableRNGs #hide + rng = StableRNG($(PlotsBase.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 = 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 ∈ 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 = 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(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 + 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 == PlotsBase.supported_seriestypes + stype = PlotsBase.seriestype_supported(PlotsBase.backend_instance(be), val) + stype ≡ :native ? "✅" : (stype ≡ :no ? "" : "🔼") + else + val ∈ func(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" => (PlotsBase.Commons._all_attrs, PlotsBase.supported_attrs), + "Markers" => (PlotsBase.Commons._all_markers, PlotsBase.supported_markers), + "Line Styles" => (PlotsBase.Commons._all_styles, PlotsBase.supported_styles), + "Scales" => (PlotsBase.Commons._all_scales, 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(PlotsBase.all_seriestypes(), 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(PlotsBase._arg_desc, k, (Any, "")) + + aliases = sort(collect(keys(filter(p -> p.second == k, PlotsBase.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 => PlotsBase.Commons._series_defaults, + :Plot => PlotsBase.Commons._plot_defaults, + :Subplot => PlotsBase.Commons._subplot_defaults, + :Axis => PlotsBase.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 palette. 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() + + 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) + 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" + + 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"), + ) + 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{Symbol,Any}() + + 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", edit_branch=BRANCH) + push!(gallery, string(pkg) => joinpath("gallery", path)) + push!(gallery_callbacks, cb) + push!(gallery_assets, assets) + end + end + user_gallery, cb, assets = makedemos(joinpath("user_gallery"); src=work, edit_branch=BRANCH) + push!(gallery_callbacks, cb) + push!(gallery_assets, assets) + unique!(gallery_assets) + @show user_gallery gallery_assets + + + pages = if (debug = length(packages) ≤ 1) # debug + ["Home" => "index.md", "Gallery" => gallery, "User Gallery" => user_gallery] + else # release + [ + "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", + "GraphRecipes" => [ + "Introduction" => "GraphRecipes/introduction.md", + "Examples" => "GraphRecipes/examples.md", + "Attributes" => "generated/graph_attributes.md", + ], + "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", + ] + end + + # 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 debug selected_pages length(gallery) pages + + 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 = Base.get_bool_env("PLOTDOCS_ANSICOLOR", true) + @info "makedocs ansicolor=$ansicolor" + failed = false + try + @time makedocs(; + root = @__DIR__, + source = work, + format = Documenter.HTML(; + size_threshold = nothing, + prettyurls = Base.get_bool_env("CI", false), + assets = ["assets/favicon.ico", gallery_assets...], + collapselevel = 2, + ansicolor, + ), + # pagesonly = true, # fails DemoCards, see github.com/JuliaDocs/DemoCards.jl/issues/162 + sitename = "Plots", + authors = "Thomas Breloff", + warnonly = true, + pages, + ) + catch e + failed = true + e isa InterruptException || rethrow() + end + + @info "gallery_callbacks" + foreach(gallery_callbacks) do cb + cb() # URL redirection for DemoCards-generated gallery + end + + failed && return # don't deploy and post-process on failure + + @info "post-process 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 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)) + 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 + + @info "post-process temporary work dir" + src = basename(SRC_DIR) + for file ∈ glob("*/index.html", joinpath(@__DIR__, "build")) + lines = readlines(file; keep=true) + any(line -> occursin("blob/$BRANCH/docs", line), lines) || continue + @info "fixing $file" + open(file, "w") do io + for line ∈ lines + write(io, replace(line, "blob/$BRANCH/docs/$work" => "blob/$BRANCH/docs/$src")) + end + end + end + + @info "deploydocs" + repo = "github.com/JuliaPlots/PlotDocs.jl.git" # see https://documenter.juliadocs.org/stable/man/hosting/#Out-of-repo-deployment + withenv("GITHUB_REPOSITORY" => repo) do + deploydocs(; + versions = ["stable" => "v^", "v#.#", "dev" => "dev", "latest" => "dev"], + push_preview = true, + forcepush = true, + repo, + ) + end + @info "done !" +end + +main() diff --git a/docs/src/GraphRecipes/examples.md b/docs/src/GraphRecipes/examples.md new file mode 100644 index 000000000..ba9ad0c51 --- /dev/null +++ b/docs/src/GraphRecipes/examples.md @@ -0,0 +1,183 @@ +```@setup graphexamples +using Plots, GraphRecipes, Graphs, LinearAlgebra, SparseArrays, AbstractTrees; gr() +Plots.Commons.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..dcd334c1a --- /dev/null +++ b/docs/src/GraphRecipes/introduction.md @@ -0,0 +1,25 @@ +```@setup graphintro +using Plots, GraphRecipes; gr() +Plots.Commons.reset_defaults() +``` +# GraphRecipes +[GraphRecipes](https://github.com/JuliaPlots/Plots.jl/tree/v2/GraphRecipes) 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 function `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..ed872257c --- /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/Plots.jl/tree/v2/StatsPlots) and [GraphRecipes.jl](https://github.com/JuliaPlots/Plots.jl/tree/v2/GraphRecipes) 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..80e6c7851 --- /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); PlotsBase.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); PlotsBase.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 accessing 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); PlotsBase.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..e654431ca --- /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/))! + +--- + +### Omissions, 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..10e9d38b5 --- /dev/null +++ b/docs/src/UnitfulExt/unitfulext_examples.jl @@ -0,0 +1,212 @@ +#--------------------------------------------------------- +# # [Simple Examples](@id 1_Examples) +#--------------------------------------------------------- + +#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.PlotsBase, :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..b9220dd8c --- /dev/null +++ b/docs/src/UnitfulExt/unitfulext_plots.jl @@ -0,0 +1,193 @@ +#--------------------------------------------------------- +# # [Plots.jl examples](@id 2_Plots) +#--------------------------------------------------------- + +#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(PlotsBase.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], PlotsBase.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(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" +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(PlotsBase.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..3ec758a3c --- /dev/null +++ b/docs/src/animations.md @@ -0,0 +1,76 @@ +```@setup animations +using Plots; gr() +Plots.Commons.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 000000000..7a5e516eb Binary files /dev/null and b/docs/src/assets/axis_logo.png differ 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 000000000..2d212c3d2 Binary files /dev/null and b/docs/src/assets/axis_logo_600x400.png differ diff --git a/docs/src/assets/favicon.ico b/docs/src/assets/favicon.ico new file mode 100644 index 000000000..73f818ed2 Binary files /dev/null and b/docs/src/assets/favicon.ico differ diff --git a/docs/src/assets/hdf5_samplestruct.png b/docs/src/assets/hdf5_samplestruct.png new file mode 100644 index 000000000..00316cdaf Binary files /dev/null and b/docs/src/assets/hdf5_samplestruct.png differ diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100755 index 000000000..7a5e516eb Binary files /dev/null and b/docs/src/assets/logo.png differ diff --git a/docs/src/assets/old_batman_logo.png b/docs/src/assets/old_batman_logo.png new file mode 100644 index 000000000..1eb3b1050 Binary files /dev/null and b/docs/src/assets/old_batman_logo.png differ diff --git a/docs/src/attributes.md b/docs/src/attributes.md new file mode 100644 index 000000000..64d55aef4 --- /dev/null +++ b/docs/src/attributes.md @@ -0,0 +1,141 @@ + +# [Attributes](@id attributes) + +```@setup attr +using Plots +``` + +### Introduction to Attributes + +In Plots, input data is passed positionally (for example, the `y` in `plot(y)`), and attributes are passed as keywords (for example, `plot(y, color = :blue)`). +Most of the information on this page is available from your Julia REPL. +After one executes, `using Plots` in the REPL, one can use the function `plotattr()` to print a list of all attributes for either series, plots, subplots, or axes. + +```julia +# Valid Operations +plotattr(:Plot) +plotattr(:Series) +plotattr(:Subplot) +plotattr(:Axis) +``` + +Once you acquire the list of attributes, you can either use the aliases of a specific attribute or investigate a specific attribute to print that attribute's aliases and its description. + +```@repl attr +# Specific Attribute Example +plotattr("size") +``` + +!!! note + Do not forget to enclose the attribute you are attempting to use with double quotes! + +--- + +### [Aliases](@id aliases) + +Keywords can take a range of values through the **alias mechanic**. For example, `plot(y, color = :blue)` is really interpreted as `plot(y, seriescolor = :blue)`. Each attribute has a number of aliases (see the charts below), which are available to avoid the pain of constantly looking up plotting API documentation because you forgot the argument name. `c`, `color`, and `seriescolor` all mean the same thing, and in fact those are eventually converted into the more precise attributes `linecolor`, `markercolor`, `markerstrokecolor`, and `fillcolor` (which you can then override if desired). + + +!!! tip + Use aliases for one-off analysis and visualization, but use the true keyword name for long-lived library code to avoid confusion. + +--- + +### [Magic Arguments](@id magic-arguments) + + +Some arguments encompass smart shorthands for setting many related arguments at the same time. Plots uses type checking and multiple dispatch to smartly "figure out" which values apply to which argument. Pass in a tuple of values. Single values will be first wrapped in a tuple before processing. + +##### axis (and xaxis/yaxis/zaxis) + +Passing a tuple of settings to the `xaxis` argument will allow the quick definition +of `xlabel`, `xlims`, `xticks`, `xscale`, `xflip`, and `xtickfont`. The following are equivalent: + +```julia +plot(y, xaxis = ("my label", (0,10), 0:0.5:10, :log, :flip, font(20, "Courier"))) + +plot(y, + xlabel = "my label", + xlims = (0,10), + xticks = 0:0.5:10, + xscale = :log, + xflip = true, + xtickfont = font(20, "Courier") +) +``` + +Note that `yaxis` and `zaxis` work similarly, and `axis` will apply to all. + +Passing a tuple to `xticks` (and similarly to `yticks` and `zticks`) changes +the position of the ticks and the labels: + +```julia +plot!(xticks = ([0:π:3*π;], ["0", "\\pi", "2\\pi"])) +yticks!([-1:1:1;], ["min", "zero", "max"]) +``` + +##### line + +Set attributes corresponding to a series line. Aliases: `l`. The following are equivalent: + +```julia +plot(y, line = (:steppre, :dot, :arrow, 0.5, 4, :red)) + +plot(y, + seriestype = :steppre, + linestyle = :dot, + arrow = :arrow, + linealpha = 0.5, + linewidth = 4, + linecolor = :red +) +``` + +##### fill + +Set attributes corresponding to a series fill area. Aliases: `f`, `area`. The following are equivalent: + +```julia +plot(y, fill = (0, 0.5, :red)) + +plot(y, + fillrange = 0, + fillalpha = 0.5, + fillcolor = :red +) +``` + +##### marker + +Set attributes corresponding to a series marker. Aliases: `m`, `mark`. The following are equivalent: + +```julia +scatter(y, marker = (:hexagon, 20, 0.6, :green, stroke(3, 0.2, :black, :dot))) + +scatter(y, + markershape = :hexagon, + markersize = 20, + markeralpha = 0.6, + markercolor = :green, + markerstrokewidth = 3, + markerstrokealpha = 0.2, + markerstrokecolor = :black, + markerstrokestyle = :dot +) +``` + +### [Notable Arguments](@id notable-arguments) +This is a collection of some notable arguments that are not well-known: + +```julia +scatter(y, thickness_scaling = 2) # increases fontsizes and linewidth by factor 2 +# good for presentations and posters +# If backend does not support this, use the function `scalefontsizes(2)` that scales +# the default fontsizes. + + +scatter(y, ticks=:native) # Tells backends to calculate ticks by itself. +# Good idea if you use interactive backends where you perform mouse zooming + +scatter(rand(100), smooth=true) # Adds a regression line to your plots +``` diff --git a/docs/src/backends.md b/docs/src/backends.md new file mode 100644 index 000000000..fe2ed57ae --- /dev/null +++ b/docs/src/backends.md @@ -0,0 +1,524 @@ +```@setup backends +using StatsPlots +using Plots, RecipesBase, Statistics; gr() +Plots.Commons.reset_defaults() + +@userplot BackendPlot + +@recipe function f(bp::BackendPlot; n = 4) + t = range(0, 3π, length = 100) + d = rand(3, 3) + + layout := n + + @series begin + subplot := 1 + f = s -> -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") + ), +) +PlotsBase.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 datasets 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 supersded 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..f9127858e --- /dev/null +++ b/docs/src/colorschemes.md @@ -0,0 +1,84 @@ +```@setup colors +using Plots; gr() +Plots.Commons.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..f12ea8233 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,317 @@ +```@setup contributing +using Plots; gr() +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: + +- [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 `PR`ing 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/ecosystem.md b/docs/src/ecosystem.md new file mode 100644 index 000000000..d03cf30aa --- /dev/null +++ b/docs/src/ecosystem.md @@ -0,0 +1,135 @@ +```@setup ecosystem +using StatsPlots, Plots, RDatasets, Distributions; gr() +Plots.Commons.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/Plots.jl/tree/v2/RecipesBase)) 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..52e03362d --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,143 @@ +```@setup index +using Plots; gr() +Plots.Commons.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..e76d03681 --- /dev/null +++ b/docs/src/input_data.md @@ -0,0 +1,263 @@ +```@setup input_data +using Plots; gr() +Plots.Commons.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 address 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/Plots.jl/tree/v2/StatsPlots) 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(PlotsBase.BezierCurves.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 = PlotsBase.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 = PlotsBase.scale(make_batman(), 0.07, 0.07, (0, 0)) +batman = PlotsBase.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..272f9ad3e --- /dev/null +++ b/docs/src/install.md @@ -0,0 +1,78 @@ + +### Install + +First, add the package: + +```julia +import Pkg +Pkg.add("Plots") # ≡ `PlotsBase` + `GR` backend + +Pkg.add("PlotBase", "PythonPlot") # `PlotsBase` + `PythonPlot` backend, avoids installing the `GR` backend + +# 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("UnicodePlots") # simplest terminal based backend (guaranteed to work from a cluster, e.g. without X forwarding) + +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("Gaston") # Gnuplot based backend +``` + +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 + +# or alternatively +import PythonPlot # select installed backend (triggered by packages extensions: https://docs.julialang.org/en/v1/manual/code-loading/#man-extensions) +using PlotsBase +``` + +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 +unicodeplots() # plot in terminal +pgfplotsx() +plotly(ticks=:native) # plotlyjs for richer saving options +pythonplot() # backends are selected with lowercase names +``` + +!!! 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["PLOTSBASE_DEFAULT_BACKEND"] = "UnicodePlots"`. + +!!! tip + You can override standard default values in your `~/.julia/config/startup.jl` file, for example `PLOTSBASE_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..4baa4a09c --- /dev/null +++ b/docs/src/layouts.md @@ -0,0 +1,120 @@ +```@setup layouts +using Plots; gr() +Plots.Commons.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 +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, 200Plots.px, 100Plots.px, :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..512c23ba6 --- /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://julia.quantecon.org/intro.html) has many examples. Search for Plots. +- [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..ebf8ae024 --- /dev/null +++ b/docs/src/pipeline.md @@ -0,0 +1,168 @@ +```@setup pipeline +using Plots; gr() +Plots.Commons.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.Commons.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 attributes (`: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..bed8bc958 --- /dev/null +++ b/docs/src/recipes.md @@ -0,0 +1,493 @@ +```@setup recipes +using Plots; gr() +Plots.Commons.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/Plots.jl/tree/v2/RecipesBase), 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/Plots.jl/tree/v2/GraphRecipes). + + +### 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 desirable, 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/Plots.jl/tree/v2/StatsPlots) 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/Plots.jl/tree/v2/StatsPlots). 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..de478ee5d --- /dev/null +++ b/docs/src/series_types/contour.md @@ -0,0 +1,146 @@ +```@setup contour +using Plots +Plots.Commons.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..f844ee996 --- /dev/null +++ b/docs/src/series_types/histogram.md @@ -0,0 +1,125 @@ +```@setup histogram +using Plots; gr() +Plots.Commons.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..3ebb98db2 --- /dev/null +++ b/docs/src/tutorial.md @@ -0,0 +1,538 @@ +```@setup tutorial +using Plots; gr() +Plots.Commons.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 [GraphRecipes.jl](https://github.com/JuliaPlots/Plots.jl/tree/v2/GraphRecipes) and +[StatsPlots.jl](https://github.com/JuliaPlots/Plots.jl/tree/v2/StatsPlots). 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/Plots.jl/tree/v2/StatsPlots) 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 diff --git a/ext/IJuliaExt.jl b/ext/IJuliaExt.jl deleted file mode 100644 index 5322c155b..000000000 --- a/ext/IJuliaExt.jl +++ /dev/null @@ -1,79 +0,0 @@ -module IJuliaExt - -import Plots: @ext_imp_use, Plots, Plot -using Base64 - -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[]) - - 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{Plots.PlotlyJSBackend}, out::Dict) - out["application/vnd.plotly.v1+json"] = - Dict(:data => Plots.plotly_series(plt), :layout => Plots.plotly_layout(plt)) - out -end - -function _ijulia__extra_mime_info!(plt::Plot{Plots.PlotlyBackend}, out::Dict) - out["application/vnd.plotly.v1+json"] = - Dict(:data => Plots.plotly_series(plt), :layout => Plots.plotly_layout(plt)) - out -end - -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) - end - out = Dict() - if output_type === :txt - mime = "text/plain" - out[mime] = sprint(show, MIME(mime), plt) - elseif output_type === :png - mime = "image/png" - out[mime] = base64encode(show, MIME(mime), plt) - elseif output_type === :svg - mime = "image/svg+xml" - out[mime] = sprint(show, MIME(mime), plt) - elseif output_type === :html - mime = "text/html" - out[mime] = sprint(show, MIME(mime), plt) - _ijulia__extra_mime_info!(plt, out) - elseif output_type === :pdf - mime = "application/pdf" - out[mime] = base64encode(show, MIME(mime), plt) - else - error("Unsupported output type $output_type") - end - out -end - -if IJulia.inited - _init_ijulia_plotting() - IJulia.display_dict(plt::Plot) = _ijulia_display_dict(plt) -end - -# IJulia only... inline display -function Plots.inline(plt::Plot = Plots.current()) - IJulia.clear_output(true) - display(IJulia.InlineDisplay(), plt) -end - -end # module diff --git a/ext/ImageInTerminalExt.jl b/ext/ImageInTerminalExt.jl deleted file mode 100644 index d03dc4aea..000000000 --- a/ext/ImageInTerminalExt.jl +++ /dev/null @@ -1,32 +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.PyPlotBackend, - 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/src/Plots.jl b/src/Plots.jl index e3610d9bd..db6cbcd2e 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -1,178 +1,17 @@ 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 -end - -using Pkg, Dates, Printf, Statistics, Base64, LinearAlgebra, SparseArrays, Random -using PrecompileTools, 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 Requires: @require -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, - Surface, - 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, - - wrap, - theme, - - plot, - plot!, - attr!, - - current, - default, - with, - twinx, - twiny, - - pie, - pie!, - plot3d, - plot3d!, - - title!, - annotate!, - - xlims, - ylims, - zlims, - - savefig, - png, - gui, - inline, - closeall, +import Reexport +Reexport.@reexport using PlotsBase - backend, - backends, - backend_name, - backend_object, - aliases, - - Shape, - text, - font, - stroke, - brush, - Surface, - OHLC, - arrow, - Segments, - Formatted, - - Animation, - frame, - gif, - mov, - mp4, - webm, - animate, - @animate, - @gif, - @P_str, - - test_examples, - iter_segments, - coords, - - translate, - translate!, - rotate, - rotate!, - center, - BezierCurve, - - plotattr, - scalefontsize, - scalefontsizes, - resetfontsizes -#! format: on -# --------------------------------------------------------- - -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) - -# --------------------------------------------------------- -import Measures -include("plotmeasures.jl") -using .PlotMeasures -import .PlotMeasures: Length, AbsoluteLength, Measure, width, height -# --------------------------------------------------------- - -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") -include("legend.jl") -include("consts.jl") -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("output.jl") -include("shorthands.jl") -include("backends/web.jl") -include("init.jl") +if PlotsBase.DEFAULT_BACKEND == "gr" + @debug "loading default GR" + import GR +end +function __init__() + ccall(:jl_generating_output, Cint, ()) == 1 && return + PlotsBase.default_backend() + nothing end + +end # module 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/backends.jl b/src/backends.jl deleted file mode 100644 index 1a75a940a..000000000 --- a/src/backends.jl +++ /dev/null @@ -1,1772 +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)) - -# --------------------------------------------------------- -const PLOTS_DEFAULT_BACKEND = "gr" - -function load_default_backend() - CURRENT_BACKEND.sym = :gr - backend(CURRENT_BACKEND.sym) -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 - const mpl_toolkits = PythonPlot.pyimport("mpl_toolkits") - const mpl = PythonPlot.pyimport("matplotlib") - const numpy = PythonPlot.pyimport("numpy") - - PythonPlot.pyimport("mpl_toolkits.axes_grid1") - numpy.seterr(invalid = "ignore") - - const pyisnone = if isdefined(PythonCall, :pyisnone) - PythonCall.pyisnone - else - PythonCall.Core.pyisnone - end - - 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/inspectdr.jl b/src/backends/inspectdr.jl deleted file mode 100644 index 07a435fa0..000000000 --- a/src/backends/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 = ticksType(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/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/backends/plotlyjs.jl b/src/backends/plotlyjs.jl deleted file mode 100644 index b5021087f..000000000 --- a/src/backends/plotlyjs.jl +++ /dev/null @@ -1,54 +0,0 @@ -# https://github.com/JuliaPlots/PlotlyJS.jl - -# ------------------------------------------------------------------------------ -include(_path(:plotly)) - -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/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/init.jl b/src/init.jl deleted file mode 100644 index a3f955be5..000000000 --- a/src/init.jl +++ /dev/null @@ -1,151 +0,0 @@ -using Scratch -using REPL -import Base64 - -""" -Reference to hold path of local plotly temp file. Initialized to `nothing`. -""" -const _plotly_local_file_path = Ref{Union{Nothing,String}}(nothing) - -""" -Reference to hold cached plotly data URL. Initialized to `nothing`. -""" -const _plotly_data_url_cached = Ref{Union{Nothing,String}}(nothing) - -_plotly_data_url() = - if _plotly_data_url_cached[] === nothing - _plotly_data_url_cached[] = "data:text/javascript;base64,$(Base64.base64encode(read(_plotly_local_file_path)))" - else - _plotly_data_url_cached[] - end - -""" -use fixed version of Plotly instead of the latest one for stable dependency -""" -const _plotly_min_js_filename = "plotly-2.6.3.min.js" - -""" -Whether to use local embedded or local dependencies instead of CDN. -""" -const _use_local_dependencies = Ref(false) - -""" -Whether to use local plotly.js files instead of CDN. -""" -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 - - @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) - 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 # only for one example - fn = joinpath(scratch_dir, tempname()) - pl = current() - show(devnull, pl) - 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/plotmeasures.jl b/src/plotmeasures.jl deleted file mode 100644 index 7c9278493..000000000 --- a/src/plotmeasures.jl +++ /dev/null @@ -1,21 +0,0 @@ -module PlotMeasures - -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) - -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) - -end 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/utils.jl b/src/utils.jl deleted file mode 100644 index faf7b1880..000000000 --- a/src/utils.jl +++ /dev/null @@ -1,1274 +0,0 @@ -# --------------------------------------------------------------- -bool_env(x, default)::Bool = - try - return parse(Bool, get(ENV, x, default)) - catch e - @warn e - return false - end - -treats_y_as_x(seriestype) = - seriestype in (:vline, :vspan, :histogram, :barhist, :stephist, :scatterhist) - -function replace_image_with_heatmap(z::AbstractMatrix{<:Colorant}) - n, m = size(z) - colors = palette(vec(z)) - reshape(1:(n * m), n, m), colors -end - -# --------------------------------------------------------------- - -"Build line segments for plotting" -mutable struct Segments{T} - pts::Vector{T} -end - -# Segments() = Segments{Float64}(zeros(0)) - -Segments() = Segments(Float64) -Segments(::Type{T}) where {T} = Segments(T[]) -Segments(p::Int) = Segments(NTuple{p,Float64}[]) - -# Segments() = Segments(zeros(0)) - -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}}) = - (map(p -> p[1], segs.pts), map(p -> p[2], segs.pts)) -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} - isempty(segments.pts) || push!(segments.pts, to_nan(T)) - foreach(v -> push!(segments.pts, convert(T, v)), vs) - segments -end - -function Base.push!(segments::Segments{T}, vs::AVec) where {T} - isempty(segments.pts) || push!(segments.pts, to_nan(T)) - foreach(v -> push!(segments.pts, convert(T, v)), vs) - 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 - -"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))) - -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 - -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 - 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 - 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 _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 - 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 - 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 - - nextval:(nextnan - 1), nextnan -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) - -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))) - -scale_inverse_scale_func(scale::Symbol) = ( - RecipesPipeline.scale_func(scale), - RecipesPipeline.inverse_scale_func(scale), - scale === :identity, -) - -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] - isedges && return v - # `isedges = true` means that v is a vector which already describes edges - # and does not need to be extended. - vmin, vmax = ignorenan_extrema(v) - extra_min = ispolar ? min(v[1], 0.5(v[2] - v[1])) : 0.5(v[2] - v[1]) - extra_max = 0.5(v[n] - v[n - 1]) - vcat(vmin - extra_min, 0.5(v[1:(n - 1)] + v[2:n]), vmax + extra_max) -end - -_heatmap_edges(::Val{true}, v::AVec, ::Symbol, isedges::Bool, ispolar::Bool) = - __heatmap_edges(v, isedges, ispolar) - -function _heatmap_edges(::Val{false}, v::AVec, scale::Symbol, isedges::Bool, ispolar::Bool) - f, invf = scale_inverse_scale_func(scale) - invf.(__heatmap_edges(f.(v), isedges, ispolar)) -end - -"create an (n+1) list of the outsides of heatmap rectangles" -heatmap_edges( - v::AVec, - scale::Symbol = :identity, - isedges::Bool = false, - ispolar::Bool = false, -) = _heatmap_edges(Val(scale === :identity), v, scale, isedges, ispolar) - -function heatmap_edges( - x::AVec, - xscale::Symbol, - y::AVec, - yscale::Symbol, - z_size::NTuple{2,Int}, - ispolar::Bool = false, -) - nx, ny = length(x), length(y) - # ismidpoints = z_size == (ny, nx) # This fails some tests, but would actually be - # the correct check, since (4, 3) != (3, 4) and a missleading plot is produced. - ismidpoints = prod(z_size) == (ny * nx) - isedges = z_size == (ny - 1, nx - 1) - (ismidpoints || isedges) || - """ - Length of x & y does not match the size of z. - Must be either `size(z) == (length(y), length(x))` (x & y define midpoints) - or `size(z) == (length(y)+1, length(x)+1))` (x & y define 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 - ) -end - -is_uniformly_spaced(v; tol = 1e-6) = - let dv = diff(v) - maximum(dv) - minimum(dv) < tol * mean(abs.(dv)) - end - -function convert_to_polar(theta, r, r_extrema = ignorenan_extrema(r)) - rmin, rmax = r_extrema - r = @. (r - rmin) / (rmax - rmin) - x = @. r * cos(theta) - y = @. r * sin(theta) - x, y -end - -fakedata(sz::Int...) = fakedata(Random.seed!(PLOTS_SEED), sz...) - -function fakedata(rng::AbstractRNG, sz...) - y = zeros(sz...) - for r in 2:size(y, 1) - y[r, :] = 0.95vec(y[r - 1, :]) + randn(rng, size(y, 2)) - end - y -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 - -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 - -limsType(lims::Tuple{<:Real,<:Real}) = :limits -limsType(lims::Symbol) = lims === :auto ? :auto : :invalid -limsType(lims) = :invalid - -isautop(sp::Subplot) = sp[:projection_type] === :auto -isortho(sp::Subplot) = sp[:projection_type] ∈ (:ortho, :orthographic) -ispersp(sp::Subplot) = sp[:projection_type] ∈ (:persp, :perspective) - -# recursively merge kw-dicts, e.g. for merging extra_kwargs / extra_plot_kwargs in plotly) -recursive_merge(x::AbstractDict...) = merge(recursive_merge, x...) -# if values are not AbstractDicts, take the last definition (as does merge) -recursive_merge(x...) = x[end] - -nanpush!(a::AbstractVector, b) = (push!(a, NaN); push!(a, b); nothing) -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 - # no nans - push!(vs, v) - break - elseif idx > 1 - push!(vs, v[1:(idx - 1)]) - end - v = v[(idx + 1):end] - end - vs -end - -function nanvcat(vs::AVec) - v_out = zeros(0) - foreach(v -> nanappend!(v_out, v), vs) - 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)) - for (i, yi) in pairs(y) - frs[i] = yi + _cycle(rib, i) - end - frs -end - -# turn a ribbon into a fillrange -function make_fillrange_from_ribbon(kw::AKW) - y, rib = kw[:y], kw[:ribbon] - rib = wraptuple(rib) - 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) -end - -#turn tuple of fillranges to one path -function concatenate_fillrange(x, y::Tuple) - rib1, rib2 = collect(first(y)), collect(last(y)) # collect needed until https://github.com/JuliaLang/julia/pull/37629 is merged - vcat(x, reverse(x)), vcat(rib1, reverse(rib2)) # x, y -end - -get_sp_lims(sp::Subplot, letter::Symbol) = axis_limits(sp, letter) - -""" - xlims([plt]) - -Returns the x axis limits of the current plot or subplot -""" -xlims(sp::Subplot) = get_sp_lims(sp, :x) - -""" - ylims([plt]) - -Returns the y axis limits of the current plot or subplot -""" -ylims(sp::Subplot) = get_sp_lims(sp, :y) - -""" - zlims([plt]) - -Returns the z axis limits of the current plot or subplot -""" -zlims(sp::Subplot) = get_sp_lims(sp, :z) - -xlims(plt::Plot, sp_idx::Int = 1) = xlims(plt[sp_idx]) -ylims(plt::Plot, sp_idx::Int = 1) = ylims(plt[sp_idx]) -zlims(plt::Plot, sp_idx::Int = 1) = zlims(plt[sp_idx]) -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 - -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 - -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 - 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 - -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 - -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) - -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) - -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 - -# 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 - 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) - -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]) - -# --------------------------------------------------------------- -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) - -# --------------------------------------------------------------- - -""" -Allows temporary setting of backend and defaults for Plots. Settings apply only for the `do` block. Example: -``` -Plots.with(:gr, size=(400,400), type=:histogram) do - plot(rand(10)) - plot(rand(10)) -end -``` -""" -function with(f::Function, args...; scalefonts = nothing, kw...) - newdefs = KW(kw) - - if :canvas in args - newdefs[:xticks] = nothing - newdefs[:yticks] = nothing - newdefs[:grid] = false - newdefs[: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) - end - - # save the backend - CURRENT_BACKEND.sym === :none && _pick_default_backend() - oldbackend = CURRENT_BACKEND.sym - - for arg in args - # change backend? - arg in backends() && backend(arg) - - # TODO: generalize this strategy to allow args as much as possible - # as in: with(:gr, :scatter, :legend, :grid) do; ...; end - # TODO: can we generalize this enough to also do something similar in the plot commands?? - - k = :legend - if arg in (k, :leg) - olddefs[k] = default(k) - newdefs[k] = true - end - - k = :grid - if arg == k - olddefs[k] = default(k) - newdefs[k] = true - end - end - - # now set all those defaults - default(; newdefs...) - scalefonts ≡ nothing || scalefontsizes(scalefonts) - - # call the function - ret = f() - - # put the defaults back - scalefonts ≡ nothing || resetfontsizes() - default(; olddefs...) - - # revert the backend - CURRENT_BACKEND.sym != oldbackend && backend(oldbackend) - - # return the result of the function - ret -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). - -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]) - end - if occursin("×10^{", label) - label = string(label, "}") - end - label -end - -function ___straightline_data(xl, yl, x, y, exp_fact) - x_vals, y_vals = if y[1] == y[2] - if x[1] == x[2] - error("Two identical points cannot be used to describe a straight line.") - else - [xl[1], xl[2]], [y[1], y[2]] - end - elseif x[1] == x[2] - [x[1], x[2]], [yl[1], yl[2]] - else - # get a and b from the line y = a * x + b through the points given by - # the coordinates x and x - b = y[1] - (y[1] - y[2]) * x[1] / (x[1] - x[2]) - 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 - ] - - xdata, a .* xdata .+ b - end - # expand the data outside the axis limits, by a certain factor too improve - # plotly(js) and interactive behaviour - ( - x_vals .+ (x_vals[2] - x_vals[1]) .* exp_fact, - y_vals .+ (y_vals[2] - y_vals[1]) .* exp_fact, - ) -end - -__straightline_data(xl, yl, x, y, exp_fact) = - if (n = length(x)) == 2 - ___straightline_data(xl, yl, x, y, exp_fact) - else - 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 - inds = (3i - 2):(3i - 1) - xdata[inds], ydata[inds] = - ___straightline_data(xl, yl, x[inds], y[inds], exp_fact) - end - xdata, ydata - end - -_straightline_data(::Val{true}, ::Function, ::Function, ::Function, ::Function, args...) = - __straightline_data(args...) - -function _straightline_data( - ::Val{false}, - xf::Function, - xinvf::Function, - yf::Function, - yinvf::Function, - xl, - yl, - x, - y, - exp_fact, -) - xdata, ydata = __straightline_data(xf.(xl), yf.(yl), xf.(x), yf.(y), exp_fact) - xinvf.(xdata), yinvf.(ydata) -end - -function straightline_data(series, expansion_factor = 1) - sp = series[:subplot] - xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) - - # handle axes scales - xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) - yf, yinvf, ynoop = scale_inverse_scale_func(sp[:yaxis][:scale]) - - _straightline_data( - Val(xnoop && ynoop), - xf, - xinvf, - yf, - yinvf, - xl, - yl, - series[:x], - series[:y], - [-expansion_factor, +expansion_factor], - ) -end - -function _shape_data!(::Val{false}, xf::Function, xinvf::Function, x, xl, exp_fact) - @inbounds for i in eachindex(x) - if x[i] == -Inf - x[i] = xinvf(xf(xl[1]) - exp_fact * (xf(xl[2]) - xf(xl[1]))) - elseif x[i] == +Inf - x[i] = xinvf(xf(xl[2]) + exp_fact * (xf(xl[2]) - xf(xl[1]))) - end - end - x -end - -function _shape_data!(::Val{true}, ::Function, ::Function, x, xl, exp_fact) - @inbounds for i in eachindex(x) - if x[i] == -Inf - x[i] = xl[1] - exp_fact * (xl[2] - xl[1]) - elseif x[i] == +Inf - x[i] = xl[2] + exp_fact * (xl[2] - xl[1]) - end - end - x -end - -function shape_data(series, expansion_factor = 1) - sp = series[:subplot] - xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) - - # handle axes scales - xf, xinvf, xnoop = scale_inverse_scale_func(sp[:xaxis][:scale]) - yf, yinvf, ynoop = scale_inverse_scale_func(sp[:yaxis][:scale]) - - ( - _shape_data!(Val(xnoop), xf, xinvf, copy(series[:x]), xl, expansion_factor), - _shape_data!(Val(ynoop), yf, yinvf, copy(series[:y]), yl, expansion_factor), - ) -end - -function _add_triangle!(I::Int, i::Int, j::Int, k::Int, x, y, z, X, Y, Z) - m = 4(I - 1) + 1 - n = m + 1 - o = m + 2 - p = m + 3 - X[m] = X[p] = x[i] - Y[m] = Y[p] = y[i] - Z[m] = Z[p] = z[i] - X[n] = x[j] - Y[n] = y[j] - Z[n] = z[j] - X[o] = x[k] - Y[o] = y[k] - Z[o] = z[k] - nothing -end - -function mesh3d_triangles(x, y, z, cns::Tuple{Array,Array,Array}) - ci, cj, ck = cns - length(ci) == length(cj) == length(ck) || - throw(ArgumentError("Argument connections must consist of equally sized arrays.")) - 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 - _add_triangle!(I, ci[I] + 1, cj[I] + 1, ck[I] + 1, x, y, z, X, Y, Z) - end - X, Y, Z -end - -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 - _add_triangle!(I, cns[I]..., x, y, z, X, Y, Z) - end - 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)])) - -_fmt_paragraph(paragraph::AbstractString; kw...) = - _fmt_paragraph(PipeBuffer(), paragraph, 0; kw...) - -function _fmt_paragraph( - io::IOBuffer, - remaining_text::AbstractString, - column_count::Integer; - fillwidth = 60, - leadingspaces = 0, -) - kw = (; fillwidth, leadingspaces) - - if (m = match(r"(.*?) (.*)", remaining_text)) isa Nothing - if column_count + length(remaining_text) ≤ fillwidth - print(io, remaining_text) - else - print(io, '\n', ' '^leadingspaces, remaining_text) - end - read(io, String) - else - if column_count + length(m[1]) ≤ fillwidth - print(io, m[1], ' ') - _fmt_paragraph(io, m[2], column_count + length(m[1]) + 1; kw...) - else - print(io, '\n', ' '^leadingspaces, m[1], ' ') - _fmt_paragraph(io, m[2], leadingspaces; kw...) - end - end -end - -_argument_description(s::Symbol) = - if s ∈ keys(_arg_desc) - aliases = if (al = Plots.aliases(s)) |> length > 0 - " Aliases: " * string(Tuple(al)) * '.' - else - "" - end - "`$s::$(_arg_desc[s][1])`: $(rstrip(replace(_arg_desc[s][2], '\n' => ' '), '.'))." * - aliases - else - "" - end - -_document_argument(s::Symbol) = - _fmt_paragraph(_argument_description(s), leadingspaces = 6 + length(string(s))) - -# The following functions implement the guess of the optimal legend position, -# from the data series. -function d_point(x, y, lim, scale) - p_scaled = (x / scale[1], y / scale[2]) - d = sum(abs2, lim .- p_scaled) - isnan(d) && return 0.0 - d -end -# Function barrier because lims are type-unstable -function _guess_best_legend_position(xl, yl, plt, weight = 100) - scale = (maximum(xl) - minimum(xl), maximum(yl) - minimum(yl)) - u = zeros(4) # faster than tuple - # Quadrants where the points will be tested - quadrants = ( - ((0.00, 0.25), (0.00, 0.25)), # bottomleft - ((0.75, 1.00), (0.00, 0.25)), # bottomright - ((0.00, 0.25), (0.75, 1.00)), # topleft - ((0.75, 1.00), (0.75, 1.00)), # topright - ) - for series in plt.series_list - x = series[:x] - y = series[:y] - yoffset = firstindex(y) - firstindex(x) - for (i, lim) in enumerate(Iterators.product(xl, yl)) - lim = lim ./ scale - for ix in 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 - xi > xl[1] + quadrants[i][1][2] * (xl[2] - xl[1]) && continue - yi < yl[1] + quadrants[i][2][1] * (yl[2] - yl[1]) && continue - yi > yl[1] + quadrants[i][2][2] * (yl[2] - yl[1]) && continue - u[i] += inv(1 + weight * d_point(xi, yi, lim, scale)) - end - end - end - # return in the preferred order in case of draws - ibest = findmin(u)[2] - u[ibest] ≈ u[4] && return :topright - u[ibest] ≈ u[3] && return :topleft - u[ibest] ≈ u[2] && return :bottomright - return :bottomleft -end - -""" -Computes the distances of the plot limits to a sample of points at the extremes of -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 - _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 - -# for UnitfulExt - cannot reside in `UnitfulExt` (macro) -function protectedstring end # COV_EXCL_LINE - -""" - P_str(s) - -(Unitful extension only). -Creates a string that will be Protected from recipe passes. - -Example: -```julia -julia> using Unitful -julia> plot([0,1]u"m", [1,2]u"m/s^2", xlabel=P"This label will NOT display units") -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) -end - -# for `PGFPlotsx` together with `UnitfulExt` -function pgfx_sanitize_string end # COV_EXCL_LINE diff --git a/test/runtests.jl b/test/runtests.jl index 72b7b4e77..f6da926aa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,68 +1,28 @@ -import Unitful: m, s, cm, DimensionError -import Plots: PLOTS_SEED, Plot, with -import SentinelArrays: ChainedVector -import GeometryBasics -import OffsetArrays -import ImageMagick -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 Plots -using Dates -using Test -using Gtk # see JuliaPlots/VisualRegressionTests.jl/issues/30 +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 ∈ TEST_PACKAGES) -# 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 +using PlotsBase -# initial load - required for `should_warn_on_unsupported` -unicodeplots() -pgfplotsx() -plotlyjs() -hdf5() +# initialize all backends +for pkg ∈ TEST_PACKAGES + @eval begin + import $pkg # trigger extension + $(TEST_BACKENDS[pkg])() + end +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") +using Plots +using Test -for name in ( - "quality", - "misc", - "utils", - "args", - "defaults", - "dates", - "axes", - "layouts", - "contours", - "components", - "shorthands", - "recipes", - "unitful", - "hdf5plots", - "pgfplotsx", - "plotly", - "animations", - "output", - "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") +for pkg ∈ 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 diff --git a/test/test_backends.jl b/test/test_backends.jl deleted file mode 100644 index e335cd6c8..000000000 --- a/test/test_backends.jl +++ /dev/null @@ -1,231 +0,0 @@ -ci_tol() = - if Sys.islinux() - is_pkgeval() ? "1e-2" : "5e-4" - elseif Sys.isapple() - "1e-3" - else - "1e-1" - end - -const TESTS_MODULE = Module(:PlotsTestsModule) -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)) - -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 = Plots._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) - ver > version && continue - 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 = Plots._examples[idx] - @info "Testing plot: $pkg:$idx:$(example.header)" - - ver = Plots._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.debug!($debug) - backend($(QuoteNode(pkg))) - theme(:default) - rng = StableRNG(Plots.PLOTS_SEED) - $(Plots.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(Plots._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 -#= - -Plots.with(:gr) do - image_comparison_facts(:gr, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:gr]) -end - -Plots.with(:plotlyjs) do - image_comparison_facts(:plotlyjs, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:plotlyjs]) -end - -Plots.with(:pyplot) do - image_comparison_facts(:pyplot, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:pyplot]) -end - -Plots.with(:pgfplotsx) do - image_comparison_facts(:pgfplotsx, tol = PLOTS_IMG_TOL, skip = Plots._backend_skips[:pgfplotsx]) -end -=# - -@testset "UnicodePlots" begin - Plots.with(:unicodeplots) do - @test backend() == Plots.UnicodePlotsBackend() - - io = IOContext(IOBuffer(), :color => true) - - # lets just make sure it runs without error - pl = plot(rand(10)) - @test show(io, pl) isa Nothing - - pl = bar(randn(10)) - @test show(io, pl) isa Nothing - - pl = plot([1, 2], [3, 4]) - annotate!(pl, [(1.5, 3.2, Plots.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))]) - @test show(io, pl) isa Nothing - - pl = plot([Dates.Date(2019, 1, 1), Dates.Date(2019, 2, 1)], [3, 4]) - annotate!(pl, [(Dates.Date(2019, 1, 15), 3.2, :auto)]) - hline!(pl, [3.1]) - @test show(io, pl) isa Nothing - - pl = plot(map(plot, 1:4)..., layout = (2, 2)) - @test show(io, pl) isa Nothing - - pl = plot(map(plot, 1:3)..., layout = (2, 2)) - @test show(io, pl) isa Nothing - - pl = plot(map(plot, 1:2)..., layout = @layout([° _; _ °])) - @test show(io, pl) isa Nothing - - redirect_stdout(devnull) do - show(plot(1:2)) - end - end -end - -const blacklist = if VERSION.major == 1 && VERSION.minor ∈ (9, 10) - [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 - Plots.with(:gr) do - # NOTE: use `ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true;` to automatically replace reference images - @test backend() == Plots.GRBackend() - @test backend_name() === :gr - image_comparison_facts( - :gr, - tol = PLOTS_IMG_TOL, - skip = vcat(Plots._backend_skips[:gr], blacklist), - ) - end -end - -is_pkgeval() || @testset "PlotlyJS" begin - Plots.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 "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 - fn = Base.invokelatest( - get(save_func, pkgname, m.Plots.png), - pl, - tempname() * ref_name(i), - ) - @test filesize(fn) > 1_000 - end - Sys.islinux() && for be in 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() - end -end diff --git a/test/test_layouts.jl b/test/test_layouts.jl deleted file mode 100644 index 182720e92..000000000 --- a/test/test_layouts.jl +++ /dev/null @@ -1,147 +0,0 @@ -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) - pl = @test_nowarn plot(plot(1:2), plot(1:2), size = (1_200, 400)) - @test pl[:size] == (1_200, 400) -end - -@testset "Subplot slicing" begin - pl = @test_nowarn plot( - rand(4, 8), - 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 -end - -@testset "Plot title" begin - pl = plot( - rand(4, 8), - layout = 4, - plot_title = "My title", - background_color = :darkgray, - background_color_inside = :lightgray, - ) - @test pl.layout.heights == [0.05Plots.pct, 0.95Plots.pct] - @test pl[:plot_title] == "My title" - @test pl[:plot_titleindex] == 5 - - @test pl[5][:background_color_inside] == RGBA(colorant"darkgray") - - plot!(pl) - @test pl[:plot_title] == "My title" - @test pl[:plot_titleindex] == 5 - - plot!(pl, plot_title = "My new title") - @test pl[:plot_title] == "My new title" - @test pl[:plot_titleindex] == 5 -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 -end - -@testset "Allowed subplot counts" begin - pl = plot(plot(1:2); layout = grid(2, 2)) - @test length(pl) == 1 - - pl = plot(map(_ -> plot(1:2), 1:2)...; layout = grid(2, 2)) - @test length(pl) == 2 - - pl = plot(map(_ -> plot(1:2), 1:3)...; layout = grid(2, 2)) - @test length(pl) == 3 - @test length(plot!(pl, plot(1:2))) == 4 - - pl = plot(map(_ -> plot(1:2), 1:4)...; layout = grid(2, 2)) - @test length(pl) == 4 - - @test_throws ErrorException plot(map(_ -> plot(1:2), 1:5)...; layout = grid(2, 2)) -end - -@testset "Invalid viewport" begin - # github.com/JuliaPlots/Plots.jl/issues/2804 - pl = plot(1, layout = (10, 2)) - show(devnull, pl) -end - -@testset "Coverage" begin - pl = plot(map(plot, 1:4)..., layout = (2, 2)) - - sp = pl[end] - @test sp isa Plots.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 - - gl = pl[2, 2] - @test gl isa Plots.GridLayout - @test length(gl) == 1 - @test size(gl) == (1, 1) - @test Plots.layout_args(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 - 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 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[]) - show(io, pl.layout) - - @test Plots.make_measure_hor(1Plots.mm) == 1Plots.mm - @test Plots.make_measure_vert(1Plots.mm) == 1Plots.mm - - @test Plots.parent(pl.layout) isa Plots.RootLayout - show(io, Plots.parent_bbox(pl.layout)) - - rl = Plots.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) - 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 - end - - el = Plots.EmptyLayout() - @test Plots.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 - - plot(map(plot, 1:4)..., layout = (2, :)) - plot(map(plot, 1:4)..., layout = (:, 2)) -end - -@testset "Link" begin - plot(map(plot, 1:4)..., link = :all) - plot(map(plot, 1:4)..., link = :square) -end diff --git a/test/test_quality.jl b/test/test_quality.jl deleted file mode 100644 index d38c2b331..000000000 --- a/test/test_quality.jl +++ /dev/null @@ -1,23 +0,0 @@ -@testset "Auto QUality Assurance" begin - # JuliaTesting/Aqua.jl/issues/77 - # TODO: fix :Contour, :Latexify and :LaTeXStrings stale imports in Plots 2.0 - # :Conda stale deps show up when running CI - Aqua.test_all( - Plots; - stale_deps = (; - ignore = [ - :GR, - :CondaPkg, - :Contour, - :Latexify, - :LaTeXStrings, - :Requires, - :UnitfulLatexify, - ] - ), - ambiguities = false, - deps_compat = false, # FIXME: fails `CondaPkg` - piracies = false, - ) - Aqua.test_ambiguities(Plots; exclude = [RecipesBase.apply_recipe]) # FIXME: remaining ambiguities -end