diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml
deleted file mode 100644
index b338f8d9b4..0000000000
--- a/.JuliaFormatter.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-always_for_in = true
-import_to_using = false
-align_pair_arrow = true
-align_assignment = true
-align_conditional = true
-always_use_return = false
-conditional_to_if = false
-whitespace_in_kwargs = true
-remove_extra_newlines = true
-whitespace_ops_in_indices = true
-long_to_short_function_def = true
-short_to_long_function_def = false
-annotate_untyped_fields_with_any = false
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000000..dc46984e10
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,3 @@
+comment: false
+github_checks:
+ annotations: false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index e4c1835a7e..a0bb4443f6 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -2,4 +2,3 @@
github: JuliaPlots
open_collective: plotsjl
-
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 4e5e151924..71b5493689 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -1,8 +1,7 @@
---
name: Bug report
about: Create a bug report
-title: "[BUG]"
-labels: bug
+type: bug
assignees: ''
---
@@ -19,11 +18,11 @@ This bug occurs on ( insert `x` below )
Backend | yes | no | untested
-------------|-----|-----|---------
gr (default) | | |
+unicodeplots | | |
pythonplot | | |
-plotlyjs | | |
pgfplotsx | | |
-unicodeplots | | |
-inspectdr | | |
+plotlyjs | | |
+plotly | | |
gaston | | |
### Versions
diff --git a/.github/ISSUE_TEMPLATE/feature request.md b/.github/ISSUE_TEMPLATE/feature request.md
index df245765fa..27f3f3244d 100644
--- a/.github/ISSUE_TEMPLATE/feature request.md
+++ b/.github/ISSUE_TEMPLATE/feature request.md
@@ -1,8 +1,7 @@
---
name: Feature request
about: Suggest a feature or enhancement
-title: "[FR]"
-labels: feature request
+type: feature
assignees: ''
---
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 18b49b51d3..2b8eb79c05 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,8 +1,7 @@
-
## Description
## Attribution
-- [ ] I am listed in [.zenodo.json](https://github.com/JuliaPlots/Plots.jl/blob/2463eb9f8065c52ed8314f6e541664c5b9db88d2/.zenodo.json) (see https://github.com/JuliaPlots/Plots.jl/issues/3503)
+- [ ] I am listed in the appropriate version of `.zenodo.json` for [PRs against v2](https://github.com/JuliaPlots/Plots.jl/blob/v2/.zenodo.json) or [PRs against master](https://github.com/JuliaPlots/Plots.jl/blob/master/.zenodo.json) (see https://github.com/JuliaPlots/Plots.jl/issues/3503)
## Things to consider
- [ ] Does it work on log scales?
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index ef72f78bfe..ddb8cbbf74 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -1,9 +1,11 @@
name: benchmarks
on:
+ workflow_dispatch:
pull_request:
+ branches: [master]
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
@@ -12,9 +14,9 @@ jobs:
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: julia-actions/setup-julia@latest
-
+
- name: Ubuntu TESTCMD
run: echo "TESTCMD=xvfb-run --auto-servernum julia" >> $GITHUB_ENV
- name: Install Plots dependencies
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2e7445d068..0af6097c2c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,144 +1,119 @@
name: ci
on:
+ workflow_dispatch:
pull_request:
push:
- branches: [master]
+ branches: [v2]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
+# needed to allow julia-actions/cache to delete old caches that it has created
+permissions:
+ actions: write
+ contents: read
+
defaults:
run:
shell: bash
jobs:
ci:
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
+ if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.pull_request.labels.*.name, 'skip ci') }}
env:
- GKS_ENCODING: "utf8"
- GKSwstype: "nul"
- JULIA_CONDAPKG_BACKEND: "MicroMamba"
- MPLBACKEND: "agg"
- name: Julia ${{ matrix.version }} - ${{ matrix.os }}
+ JULIA_PROJECT_CMD: julia --project=@. --check-bounds=yes --color=yes
+ JULIA_CONDAPKG_LIBSTDCXX_NG_VERSION : 'ignore' # handled ourselves in ci/matplotlib.jl
+ 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 }}
strategy:
fail-fast: false
matrix:
+ experimental: [false]
version:
- - '1.6' # LTS (minimal declared julia compat in `Project.toml`)
- - '1' # latest stable
- experimental:
- - false
- os: [ubuntu-latest, windows-latest, macos-latest]
+ - 'lts' # minimal declared julia compat in `Project.toml`
+ - '1' # latest stable
+ 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
+ # NOTE: macos-latest resolves to macos-14 (only aarch64), use macos-13 for x86.
+ - os: macos-latest
experimental: false
- prefix: xvfb-run
- version: '1.8' # only test intermediate release on `ubuntu` to spare resources
- - os: ubuntu-latest
+ arch: aarch64
+ version: 'lts'
+ - os: macos-latest
experimental: false
- prefix: xvfb-run
- version: '1.9' # only test intermediate release on `ubuntu` to spare resources
+ arch: aarch64
+ version: '1'
- 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`)
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- - name: Ubuntu LaTeX dependencies
+ - name: setup ubuntu dependencies
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} # LaTeX
sudo fc-cache -vr
-
- - name: Set LD_PRELOAD
- if: startsWith(matrix.os, 'ubuntu')
- run: echo "LD_PRELOAD=$(g++ --print-file-name=libstdc++.so)" >> $GITHUB_ENV
+ echo "LD_PRELOAD=$(g++ --print-file-name=libstdc++.so)" >>$GITHUB_ENV
- 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}
- run: |
- using Pkg
- foreach(("RecipesBase", "RecipesPipeline")) do name
- Pkg.develop(path=name); Pkg.test(name; coverage=true)
- end
+ - uses: julia-actions/cache@v2
- - name: Install conda based matplotlib
- shell: julia --project=@. --color=yes {0}
+ - name: dev downstream pkgs and install mpl deps
+ env:
+ JULIA_PKG_PRECOMPILE_AUTO: 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_PROJECT_CMD }} -e '
+ using Pkg
+ Pkg.develop([
+ (; path="./RecipesBase"), # compat for LTS [sources], remove later
+ (; path="./RecipesPipeline"), # compat for LTS [sources], remove later
+ (; path="./PlotThemes"), # compat for LTS [sources], remove later
+ (; path="./PlotsBase"), # compat for LTS [sources], remove later
+ (; path="./GraphRecipes"),
+ (; path="./StatsPlots"),
+ ])
+ include(joinpath(@__DIR__, "ci", "matplotlib.jl"))
+ Pkg.precompile() # // precompilation
+ '
- - uses: julia-actions/julia-runtest@latest
+ - name: test Plots stack
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=(${{ env.JULIA_PROJECT_CMD }} --depwarn=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
- if: startsWith(matrix.os, 'ubuntu')
+ directories: RecipesBase/src,RecipesPipeline/src,PlotsBase/src,PlotsBase/ext,src,PlotThemes/src,GraphRecipes/src,StatsPlots/src
+ - uses: codecov/codecov-action@v5
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: false
file: lcov.info
-
- Skip:
- if: contains(github.event.head_commit.message, '[skip ci]')
- runs-on: ubuntu-latest
- steps:
- - name: Skip CI 🚫
- run: echo skip ci
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
new file mode 100644
index 0000000000..7ce38d7c37
--- /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@v5
+ - uses: codespell-project/actions-codespell@v2
+ with:
+ ignore_words_list: nd,nax,namin,namax,linez,ist
+ only_warn: 0
diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/compathelper.yml
similarity index 91%
rename from .github/workflows/CompatHelper.yml
rename to .github/workflows/compathelper.yml
index d4d64580ac..58dfe39213 100644
--- a/.github/workflows/CompatHelper.yml
+++ b/.github/workflows/compathelper.yml
@@ -1,6 +1,7 @@
-name: CompatHelper
+name: compathelper
on:
+ workflow_dispatch:
schedule:
- cron: '00 00 * * *'
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index b74399ab37..724b924483 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,33 +1,35 @@
name: docs
on:
- workflow_dispatch:
- push:
- branches: [master]
- tags: '*'
+ workflow_dispatch:
+ push:
+ branches: [v2]
+ tags: '*'
+ release:
+ types: [published]
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
+ cancel-in-progress: true
jobs:
- Build_docs:
+ build:
+ if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'documentation') }}
+ permissions:
+ actions: write
+ contents: write
+ pull-requests: read
+ statuses: write
runs-on: ubuntu-latest
+ env:
+ PYTHON: ""
+ DOCUMENTER_KEY: ${{secrets.DOCUMENTER_KEY}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
steps:
- - uses: actions/checkout@v4
- with:
- repository: JuliaPlots/PlotDocs.jl
+ - uses: actions/checkout@v5
- uses: julia-actions/setup-julia@latest
- - name: Cache artifacts
- uses: actions/cache@v4
- env:
- cache-name: cache-artifacts
- with:
- path: ~/.julia/artifacts
- key: ${{runner.os}}-test-${{env.cache-name}}-${{hashFiles('**/Project.toml')}}
- restore-keys: |
- ${{runner.os}}-test-${{env.cache-name}}-
- ${{runner.os}}-test-
- ${{runner.os}}-
- - name: Build documentation
- env:
- PYTHON: ""
- DOCUMENTER_KEY: ${{secrets.DOCUMENTER_KEY}}
- GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- run: bash docs/ci_build.sh
+ - uses: julia-actions/cache@v2
+ - run: bash -c '. ci/build-docs.sh; install_ubuntu_deps'
+ - run: bash -c '. ci/build-docs.sh; install_and_precompile_julia_deps'
+ - run: bash -c '. ci/build-docs.sh; build_documenter_docs'
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 0000000000..d910b03859
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,21 @@
+name: format
+
+on:
+ push:
+ branches: ['release-', 'master', 'v2']
+ tags:
+ - '*'
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ runic:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: julia-actions/setup-julia@latest
+ - uses: julia-actions/cache@v2
+ - uses: fredrikekre/runic-action@v1
diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml
deleted file mode 100644
index 951358a7dc..0000000000
--- a/.github/workflows/format_check.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: format
-
-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: actions/checkout@v4
- - uses: julia-actions/setup-julia@latest
- - name: Install dependencies
- 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
- uses: reviewdog/action-suggester@v1
- with:
- tool_name: JuliaFormatter
- fail_on_error: true
-
- # reviewdog/action-suggester not using `cleanup` flag?
- - 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
deleted file mode 100644
index 14979d97e6..0000000000
--- a/.github/workflows/format_pr.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: format
-
-on:
- schedule:
- - cron: '0 0 1 * *'
-
-jobs:
- pr:
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Install JuliaFormatter and format
- run: |
- julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter"))'
- julia -e 'using JuliaFormatter; [format(["src", "test"]) for _ in 1:2]'
- git diff --exit-code
-
- - name: Create Pull Request
- if: ${{ failure() }}
- id: cpr
- uses: peter-evans/create-pull-request@v6
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- commit-message: "Format .jl files [skip ci]"
- title: 'Automatic JuliaFormatter.jl run'
- branch: auto-juliaformatter-pr
- delete-branch: true
- labels: formatting, automated pr, no changelog
-
- - name: Check outputs
- if: ${{ failure() }}
- run: |
- echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
- echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml
deleted file mode 100644
index 98d42f9cfb..0000000000
--- 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/.github/workflows/reference_images.yml b/.github/workflows/reference_images.yml
new file mode 100644
index 0000000000..e75a39eabf
--- /dev/null
+++ b/.github/workflows/reference_images.yml
@@ -0,0 +1,49 @@
+name: Update reference images
+
+on:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ repository: "JuliaPlots/PlotReferenceImages.jl"
+ token: ${{ secrets.PLOTS_REFIMAGES_TOKEN }}
+ path: PlotReferenceImages
+ - uses: actions/checkout@v5
+ with:
+ path: Plots
+ - uses: julia-actions/setup-julia@latest
+ with:
+ version: 1
+ - uses: julia-actions/cache@v2
+ - name: Run Plots tests
+ env:
+ PLOTSBASE_TEST_PACKAGES: GR
+ VISUAL_REGRESSION_TESTS_AUTO: true
+ shell: julia --color=yes --project=Plots {0}
+ run: |
+ using Pkg
+ Pkg.test("PlotsBase")
+ - name: Diagnostics
+ run: |
+ ls
+ cd PlotReferenceImages
+ cp -r ~/.julia/dev/PlotReferenceImages.jl/* .
+ ls
+ git status
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ title: "Update Plot reference images from branch ${{ github.ref_name}} by the action ${{ github.action }}"
+ body: "Review changes thoroughly and only merge when no unwanted changes are present."
+ path: PlotReferenceImages
+ token: ${{ secrets.PLOTS_REFIMAGES_TOKEN }}
+ branch: "${{ github.ref_name}}"
+ base: "master"
diff --git a/.github/workflows/TagBot.yml b/.github/workflows/tagbot.yml
similarity index 96%
rename from .github/workflows/TagBot.yml
rename to .github/workflows/tagbot.yml
index e98273ee71..b96d26052f 100644
--- a/.github/workflows/TagBot.yml
+++ b/.github/workflows/tagbot.yml
@@ -1,4 +1,4 @@
-name: TagBot
+name: tagbot
on:
issue_comment:
types:
@@ -21,7 +21,7 @@ permissions:
security-events: read
statuses: read
jobs:
- TagBot:
+ tagbot:
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
runs-on: ubuntu-latest
steps:
diff --git a/.gitignore b/.gitignore
index 39b6f9a2d9..3efdb58a67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +1,28 @@
+# coverage / test files
*.jl.cov
*.jl.*.cov
*.jl.mem
.DS_Store
+
examples/.ipynb_checkpoints/*
examples/meetup/.ipynb_checkpoints/*
+
deps/plotly-*
deps/build.log
deps/deps.jl
+
+Manifest-v*.toml
Manifest.toml
-dev/
+
test/tmpplotsave.hdf5
-/.benchmarkci
/benchmark/*.json
+/.benchmarkci
+dev/
+
+.CondaPkg/
.vscode/
-.CondaPkg/
\ No newline at end of file
+
+# docs
+**/generated*/
+docs/build*/
+docs/work*/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000..a1046ed2d4
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,15 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-toml
+ - id: check-yaml
+ # - id: check-added-large-files
+- repo: 'https://github.com/fredrikekre/runic-pre-commit'
+ rev: v2.0.1
+ hooks:
+ - id: runic
diff --git a/.zenodo.json b/.zenodo.json
index 539036d03b..687427c751 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -768,7 +768,51 @@
},
{
"name": "Syver Døving Agdestein",
- "orcid": "0000-0002-1589-2916",
+ "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"
+ },
+ {
+ "name": "Finn Eisenach",
+ "type": "Other"
+ },
+ {
+ "affiliation": "Purdue University",
+ "name": "Isaac Wheeler",
+ "orcid": "0000-0002-9717-073X",
+ "type": "Other"
+ },
+ {
+ "affiliation": "European XFEL",
+ "name": "James Wrigley",
+ "orcid": "0009-0003-6525-7413",
+ "type": "Other"
+ },
+ {
+ "affiliation": "@JuliaComputing",
+ "name": "Παναγιώτης Γεωργακόπουλος",
"type": "Other"
}
],
diff --git a/CITATION.bib b/CITATION.bib
index 4feba307e0..4487678bf1 100644
--- a/CITATION.bib
+++ b/CITATION.bib
@@ -8,4 +8,3 @@ @article{PlotsJL
year = {2023},
copyright = {Creative Commons Attribution 4.0 International}
}
-
diff --git a/GraphRecipes/LICENSE.md b/GraphRecipes/LICENSE.md
new file mode 100644
index 0000000000..a15a28ae28
--- /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 0000000000..402f2a28a3
--- /dev/null
+++ b/GraphRecipes/Project.toml
@@ -0,0 +1,34 @@
+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"
+
+[sources]
+PlotsBase = {path = "../PlotsBase"}
+RecipesBase = {path = "../RecipesBase"}
+
+[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"
diff --git a/GraphRecipes/README.md b/GraphRecipes/README.md
new file mode 100644
index 0000000000..bd81d4b6eb
--- /dev/null
+++ b/GraphRecipes/README.md
@@ -0,0 +1,52 @@
+# GraphRecipes
+The repository formerly known as PlotRecipes
+
+[](
+ https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci
+)
+[](
+ https://docs.juliaplots.org/stable/GraphRecipes/introduction
+)
+[](
+ https://julialang.zulipchat.com/#narrow/stream/236493-plots
+)
+
+## Summary
+In this repository, a graph is a network of connected nodes (although sometimes people use the same word to refer to a plot). If you want to do plotting, then use [Plots.jl](https://github.com/JuliaPlots/Plots.jl).
+
+For a given graph, there are many legitimate ways to display and visualize the graph. However, some graph layouts will convey the structure of the underlying graph much more clearly than other layouts. GraphRecipes provides many options for producing graph layouts including (un)directed graphs, tree graphs and arc/chord diagrams. For each layout type the `graphplot` function will try to create a default layout that optimizes visual clarity. However, the user can tweak the default layout through a large number of powerful keyword arguments, see the [documentation](https://docs.juliaplots.org/stable/GraphRecipes/introduction) for more details and some examples.
+
+## Installation
+```julia
+] add GraphRecipes
+```
+
+## An example
+```julia
+using GraphRecipes
+using Plots
+
+# or, for an alternate backend:
+# julia> import PythonPlot
+# julia> using GraphRecipes, PlotsBase
+# julia> pythonplot()
+
+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)
+```
+
+
+
+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 0000000000..76a8e65f85
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 0000000000..9448dabfeb
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 0000000000..1430d0e044
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 0000000000..7277664ab7
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 0000000000..d12812a45e
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 0000000000..02658ea829
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 0000000000..c04befbdc8
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 0000000000..6cc7407a08
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 0000000000..0666a6e2e8
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 0000000000..fee0e9c61b
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 0000000000..4bdb84d0c8
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 0000000000..01b6bd396e
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 0000000000..80291f7aa0
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 0000000000..a4681c71a9
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 0000000000..d9a471c0ce
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 0000000000..44b663f220
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 0000000000..9147c81cc3
--- /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 0000000000..a4d4c18663
--- /dev/null
+++ b/GraphRecipes/src/graph_layouts.jl
@@ -0,0 +1,488 @@
+# -----------------------------------------------------
+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 points(dim::Integer, positions)
+ one, two = [p[1] for p in positions], [p[2] for p in positions]
+ return if dim == 2
+ (one, two, nothing)
+ else
+ (one, two, [p[3] for p in positions])
+ end
+end
+
+function spectral_graph(
+ adjmat::AbstractMatrix;
+ node_weights::AbstractVector = ones(size(adjmat, 1)),
+ kw...,
+ )
+ positions =
+ NetworkLayout.spectral(adjmat; nodeweights = convert(Vector{Float64}, node_weights))
+
+ return points(3, positions)
+end
+
+spectral_graph(
+ source::AbstractVector{Int},
+ destiny::AbstractVector{Int},
+ weights::AbstractVector;
+ kw...,
+) = spectral_graph(get_adjacency_matrix(source, destiny, weights); kw...)
+
+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, 3)
+ T = Float64
+ adjmat = make_symmetric(adjmat)
+ startpostions = if dim == 2
+ [Point(T(x[i]), T(y[i])) for i in 1:length(x)]
+ elseif dim == 3
+ [Point(T(x[i]), T(y[i]), T(z[i])) for i in 1:length(x)]
+ end
+
+ positions = NetworkLayout.spring(
+ adjmat;
+ dim,
+ Ptype = T,
+ iterations = maxiter,
+ initialtemp = initialtemp,
+ C = C,
+ initialpos = startpostions,
+ )
+
+ return points(dim, positions)
+end
+
+spring_graph(
+ source::AbstractVector{Int},
+ destiny::AbstractVector{Int},
+ weights::AbstractVector;
+ kw...,
+) = spring_graph(get_adjacency_matrix(source, destiny, weights); kw...)
+
+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 = 1.0e-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 in 1:length(x)]
+ elseif dim == 3
+ [Point(T(x[i]), T(y[i]), T(z[i])) for i in 1:length(x)]
+ end
+
+ positions = NetworkLayout.sfdp(
+ adjmat;
+ dim,
+ Ptype = T,
+ iterations = maxiter,
+ tol = tol,
+ C = C,
+ K = K,
+ initialpos = startpostions,
+ )
+ return points(dim, positions)
+end
+
+sfdp_graph(
+ source::AbstractVector{Int},
+ destiny::AbstractVector{Int},
+ weights::AbstractVector;
+ kw...,
+) = sfpd_graph(get_adjacency_matrix(source, destiny, weights); kw...)
+
+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)
+
+ return points(dim, positions)
+end
+
+shell_graph(
+ source::AbstractVector{Int},
+ destiny::AbstractVector{Int},
+ weights::AbstractVector;
+ kw...,
+) = shell_graph(get_adjacency_matrix(source, destiny, weights); kw...)
+
+# -----------------------------------------------------
+
+# 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 in 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 in 1:size(X, 1), j in 1:(i - 1)
+ tot += stress(X, dist, w, i, j)
+ end
+ return 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 in 1:maxiter
+ for p in free_dims
+ for i in 1:n
+ num, den = 0.0, 0.0
+ for j in 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) < 1.0e-6
+ # info("converged. numiter=$k last=$laststress this=$thisstress")
+ break
+ end
+ laststress = thisstress
+ end
+
+ return dim == 2 ? (X..., nothing) : X
+end
+
+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...)
+
+# -----------------------------------------------------
+
+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))
+ return points(dim, positions)
+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_layers(source, destiny, n)
+ end
+
+ # reverse direction?
+ if root in (:top, :right)
+ layers = -layers
+ end
+
+ # add noise
+ add_noise && (layers += 0.6rand(rng_from_rng_or_seed(rng, nothing), size(layers)...))
+
+ # 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)
+ return by_axis_local_stress_graph(
+ get_adjacency_matrix(source, destiny, weights);
+ node_weights,
+ rng,
+ dim,
+ extrakw...,
+ )
+end
+
+function adjlist_and_degrees(source, destiny, n)
+ # build a list of children (adjacency list)
+ alist = Vector{Int}[Int[] for i in 1:n]
+ indeg, outdeg = zeros(Int, n), zeros(Int, n)
+ for (si, di) in zip(source, destiny)
+ push!(alist[si], di)
+ indeg[di] += 1
+ outdeg[si] += 1
+ end
+ return alist, indeg, outdeg
+end
+
+#=
+function compute_tree_layers_old(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_layers(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 in 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 in 1:n
+ if !(isempty(alist[parent]))
+ minidx = minimum(layers[child] for child in alist[parent])
+ if layers[parent] < minidx - 1
+ shifted = true
+ layers[parent] = minidx - 1
+ end
+ end
+ end
+ shifted || break
+ end
+
+ return layers
+end
+
+function shift_children!(layers, alist, placed, parent)
+ for idx in alist[parent]
+ if !(idx in placed) && layers[idx] ≤ layers[parent]
+ layers[idx] = layers[parent] + 1
+ end
+ end
+ for idx in alist[parent]
+ if idx != parent && !(idx in placed)
+ push!(placed, idx)
+ shift_children!(layers, alist, placed, idx)
+ end
+ end
+ return
+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)
+ return 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 in 1:N
+ v = (i - 1) * δ
+ x[i] = sin(v)
+ y[i] = cos(v)
+ end
+
+ return x, y, zero(x)
+end
diff --git a/GraphRecipes/src/graphs.jl b/GraphRecipes/src/graphs.jl
new file mode 100644
index 0000000000..2f1d2f1a57
--- /dev/null
+++ b/GraphRecipes/src/graphs.jl
@@ -0,0 +1,1164 @@
+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)
+ return 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 in 1:nrow, j in 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
+ return 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
+ return 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
+ return 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) in enumerate(adjlist)
+ for j in l
+ push!(source, i)
+ push!(destiny, j)
+ end
+ end
+ return 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 in 1:n]
+ for (s, d) in zip(source, destiny)
+ push!(adjlist[s], d)
+ end
+ return adjlist
+end
+
+get_adjacency_list(adjlist::AbstractVector{<:AbstractVector{Int}}) = adjlist
+
+# -----------------------------------------------------
+
+function make_symmetric(A::AbstractMatrix)
+ A = copy(A)
+ for i in 1:size(A, 1), j in (i + 1):size(A, 2)
+ A[i, j] = A[j, i] = A[i, j] + A[j, i]
+ end
+ return 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 in 1:n, j in 1:n]
+
+ return 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) in enumerate(dists)
+ if d < 1.0e10
+ tot += d
+ cnt += 1
+ end
+ end
+ avg = cnt > 0 ? tot / cnt : 1.0
+ for (i, d) in enumerate(dists)
+ if d > 1.0e10
+ dists[i] = 3avg
+ end
+ end
+ return 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 in Graphs.edges(g)
+ push!(source, Graphs.src(e))
+ push!(destiny, Graphs.dst(e))
+ end
+ return 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)
+ return 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](https://docs.juliaplots.org/stable/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 in 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) in 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 in 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 in source]
+ destiny = Int[findfirst(names, di) for di in 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 in 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 in 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 in 1:size(edgelabel)[1], j in 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 in keys(edgelabel)
+ if length(key) == 2
+ edgelabel[(key..., 1)] = edgelabel[key]
+ end
+ end
+ end
+ edge_has_been_seen = Dict()
+ for edge in 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)) in 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 in 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 in xpts
+ push!(xseg, x)
+ push!(l_wg, wi)
+ end
+ # push!(xseg, NaN)
+ for y in 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 in tfine], [itp(t, 2) for t in 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 in tfine], [itp(t, 2) for t in 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 in tfine], [itp(t, 2) for t in 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 in 1:size(edges_list[1], 2)],
+ [edges_list[2][:, j] for j in 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 in zip(source, destiny)
+ edge_has_been_seen[edge] = 0
+ end
+ index = 0
+ if edge_label_box && !isnothing(edgelabel)
+ for (edge_num, (si, di, wi)) in 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 in eachindex(x)]
+ end
+ @series begin
+ seriestype := :shape
+ N = length(x)
+ angles = Vector{Float64}(undef, N)
+ for i in 1:N
+ angles[i] = if y[i] > 0
+ acos(x[i])
+ else
+ 2pi - acos(x[i])
+ end
+ end
+ δ = 0.4 * (angles[2] - angles[1])
+ vec_vec_xy = [arcshape(Θ - δ, Θ + δ) for Θ in angles] # Shape
+ [[xy[1] for xy in vec_xy] for vec_xy in vec_vec_xy],
+ [[xy[2] for xy in vec_xy] for vec_xy in 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 in node_vec_vec_xy
+ push!(nodeperimeters[1], [xy[1] for xy in vec_xy])
+ push!(nodeperimeters[2], [xy[2] for xy in 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
+ append!(
+ edge_label_array, (
+ x[i],
+ y[i],
+ names[
+ ifelse(
+ i % length(names) == 0,
+ length(names),
+ i % length(names),
+ ),
+ ],
+ fontsize,
+ ) for i in eachindex(x)
+ )
+ colorbar_entry --> false
+ markersize := 0
+ markeralpha := 0
+ markerstrokesize := 0
+ annotations --> edge_label_array
+ 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 0000000000..be8e6c99f3
--- /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 in (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
+ return 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)
+
+ return 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 in subtypes(T)
+ push!(nodes, sub)
+ subidx = length(nodes)
+ push!(source, supidx)
+ push!(destiny, subidx)
+ add_subs!(nodes, source, destiny, sub, subidx)
+ end
+ return
+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 0000000000..a18dfece57
--- /dev/null
+++ b/GraphRecipes/src/trees.jl
@@ -0,0 +1,61 @@
+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 in 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
+ return
+end
+
+function string_from_node(node)
+ io = IOBuffer()
+ AbstractTrees.printnode(io, node)
+ return 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 0000000000..0200d14fec
--- /dev/null
+++ b/GraphRecipes/src/utils.jl
@@ -0,0 +1,380 @@
+"""
+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
+ return x, y
+end
+
+function shorten_segment(x1, y1, x2, y2, shorten)
+ xshort = shorten * (x2 - x1)
+ yshort = shorten * (y2 - y1)
+ return 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 in 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
+ return 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(α)
+
+ return 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
+ return (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
+ return (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]
+
+ return [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 in 1:length(x)]
+ for i in 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 in 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.
+ return 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) in zip(source, destiny)][:] |> permutedims
+ elseif attr isa Function
+ attr =
+ [
+ attr(si, di, wi) for
+ (i, (si, di, wi)) in enumerate(zip(source, destiny, weights))
+ ][:] |> permutedims
+ elseif attr isa Dict
+ attr = [attr[(si, di)] for (si, di) in zip(source, destiny)][:] |> permutedims
+ elseif all(size(attr) .!= 1)
+ attr = [attr[si, di] for (si, di) in zip(source, destiny)][:] |> permutedims
+ end
+ return 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)
+ return Tuple{Float64, Float64}[
+ (r * cos(u), r * sin(u)) for u in range(start_θ, stop = end_θ, length = n)
+ ]
+end
+
+function partialcircle(start_θ, end_θ, circle_center::Array{T, 1}, n = 20, r = 1) where {T}
+ return Tuple{Float64, Float64}[
+ (r * cos(u) + circle_center[1], r * sin(u) + circle_center[2]) for
+ u in range(start_θ, stop = end_θ, length = n)
+ ]
+end
+
+function partialellipse(start_θ, end_θ, n = 20, major_axis = 2, minor_axis = 1)
+ return Tuple{Float64, Float64}[
+ (major_axis * cos(u), minor_axis * sin(u)) for
+ u in 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}
+ return Tuple{Float64, Float64}[
+ (major_axis * cos(u) + ellipse_center[1], minor_axis * sin(u) + ellipse_center[2])
+ for u in range(start_θ, stop = end_θ, length = n)
+ ]
+end
+
+# for chord diagrams:
+function arcshape(θ1, θ2)
+ return 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
+ return (xmin - margin, xmax + margin), ylims
+end
+
+function islabel(item)
+ ismissing(item) && return false
+ ((item isa AbstractFloat) && isnan(item)) && return false
+ return !in(item, (nothing, false, ""))
+end
+
+function replacement_kwarg(sym, name, plotattributes, graph_aliases)
+ replacement = name
+ for alias in graph_aliases[sym]
+ if haskey(plotattributes, alias)
+ replacement = plotattributes[alias]
+ end
+ end
+ return 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 in attributes
+ ]
+ return ex
+end
+
+remove_aliases!(sym, plotattributes, graph_aliases) =
+ for alias in 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
+ return vmin - buffer, vmax + buffer
+end
diff --git a/GraphRecipes/test/Project.toml b/GraphRecipes/test/Project.toml
new file mode 100644
index 0000000000..88f9953221
--- /dev/null
+++ b/GraphRecipes/test/Project.toml
@@ -0,0 +1,22 @@
+[deps]
+AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
+GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
+GraphRecipes = "bd48cda9-67a9-57be-86fa-5b3c104eda73"
+Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
+Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44"
+ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
+Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
+Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92"
+
+[compat]
+Plots = "2"
+
+[sources]
+Plots = {path = "../.."}
diff --git a/GraphRecipes/test/functions.jl b/GraphRecipes/test/functions.jl
new file mode 100644
index 0000000000..88377a5cc8
--- /dev/null
+++ b/GraphRecipes/test/functions.jl
@@ -0,0 +1,278 @@
+function random_labelled_graph()
+ n = 15
+ rng = StableRNG(1)
+ A = Float64[rand(rng) < 0.5 ? 0 : rand(rng) for i in 1:n, j in 1:n]
+ for i in 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,
+ )
+ return p, n, A, x, y, z
+end
+
+function random_3d_graph()
+ n, A, x, y, z = random_labelled_graph()[2:end]
+ return 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)
+ return graphplot(g, curves = false, rng = StableRNG(1))
+end
+
+function directed()
+ g = [
+ 0 1 1
+ 0 0 1
+ 0 1 0
+ ]
+ return 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 in 1:n
+ for j in 1:n
+ edgelabel_dict[(i, j)] = string("edge ", i, " to ", j)
+ end
+ end
+
+ return 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
+ ]
+ return 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)))
+ return plot(
+ graphplot(
+ adjmat;
+ method = :chorddiagram,
+ names = [text(string(i), 8) for i in 1:8],
+ linecolor = :black,
+ fillcolor = :lightgray,
+ rng
+ ),
+ graphplot(
+ adjmat;
+ method = :arcdiagram,
+ markersize = 0.5,
+ markershape = :circle,
+ linecolor = :black,
+ markercolor = :black,
+ 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 in 1:nv(g)]
+ return 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 in list
+ out += value
+ end
+ return out
+ end
+ )
+ return 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 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], ": ")
+ return print(io, str)
+end
+
+function julia_dict_tree()
+ d = Dict(:a => 2, :d => Dict(:b => 4, :c => "Hello"), :e => 5.0)
+ return 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) in [(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)]
+ return 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 in 1:5
+ g[i, i] = 0
+ end
+ return 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 in 1:5
+ g[i, i] = 0
+ end
+ return 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 in 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]
+
+ return 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 0000000000..dbf30f9abb
--- /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 in 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 in 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 0000000000..a2a05d5502
--- /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 in all_pkgs)
+@show deplists
+
+const childlists = Dict(pkg => Set{String}() for pkg in all_pkgs)
+for pkg in all_pkgs
+ for dep in 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 in deplists[pkg]
+ if !(dep in deps)
+ push!(deps, dep)
+ add_deps(dep, deps)
+ end
+ end
+ return deps
+end
+
+function add_children(pkg, children = Set([pkg]))
+ for child in childlists[pkg]
+ if !(child in children)
+ push!(children, child)
+ add_children(child, children)
+ end
+ end
+ return children
+end
+
+function plotdeps(pkg)
+ pkgs = unique(union(add_deps(pkg), add_children(pkg)))
+ idxmap = Dict(p => i for (i, p) in enumerate(pkgs))
+
+ source, destiny = Int[], Int[]
+ for pkg in pkgs
+ i = idxmap[pkg]
+ for dep in 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
+ return 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 0000000000..69702a0837
--- /dev/null
+++ b/GraphRecipes/test/runtests.jl
@@ -0,0 +1,195 @@
+using VisualRegressionTests
+using AbstractTrees
+using LinearAlgebra
+using GraphRecipes
+using GraphRecipes
+using SparseArrays
+using ImageMagick
+using StableRNGs
+using Logging
+using Graphs
+using Plots
+using Test
+using Gtk # for popup
+
+const PlotsBase = Plots.PlotsBase
+
+isci() = get(ENV, "CI", "false") == "true"
+itol(tol = nothing) = something(tol, isci() ? 1.0e-3 : 1.0e-5)
+
+include("functions.jl")
+include("parse_readme.jl")
+
+default(show = false, reuse = true)
+
+@testset "functions" begin
+ rng = StableRNG(1)
+ for method in keys(GraphRecipes._graph_funcs)
+ method ≡ :spectral && continue # FIXME
+ dat = if (inp = GraphRecipes._graph_inputs[method]) ≡ :adjmat
+ [
+ 0 1 1
+ 1 0 1
+ 1 1 0
+ ]
+ elseif inp ≡ :sourcedestiny
+ Symmetric(sparse(rand(rng, 0:1, 8, 8)))
+ elseif inp ≡ :adjlist
+ dat = [
+ 0 1 1 0 0 0 0 0 0 0
+ 0 0 0 0 1 1 0 0 0 0
+ 0 0 0 1 0 0 1 0 1 0
+ 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 1 0 1
+ 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0
+ ]
+ else
+ @error "wrong input $inp"
+ end
+ pl = graphplot(dat; method)
+ @test pl isa PlotsBase.Plot
+ 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)
+ end
+end
+
+@testset "utils.jl" begin
+ rng = StableRNG(1)
+ @test GraphRecipes.directed_curve(0.0, 1.0, 0.0, 1.0; rng) ==
+ GraphRecipes.directed_curve(0, 1, 0, 1; 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) in [(rand(rng), rand(rng)) for i in 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)
+ graphplot(A; nodeweights = 1:4, rng)
+ graphplot(A; curvaturescalar = 0, rng)
+ graphplot(A; el = Dict((1, 2) => ""), elb = true, rng)
+ graphplot(A; ew = (s, d, w) -> 3, rng)
+ graphplot(A; ses = 0.5, rng)
+ end
+end
+
+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(2.0e-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
diff --git a/NEWS.md b/NEWS.md
index fcb363c42a..eb57a24ccc 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,6 +1,32 @@
# 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`
+- The default `label` is now `:none`, use `:auto` to restore the previous behaviour
+- UnitfulExt changes:
+ - The `P_str` macro is no longer provided as API. The intended goal was simply to not put
+ units on axis labels which were "protected strings". To achieve this now, pass `unitformat=:nounit`.
+ - The axis option `unitformat=:none`, `unitformat=nothing`, and `unitformat=false` previously
+ printed the axis guide as `string(label, " ", unit)`. That behavior is now moved to `unitformat=:space`,
+ and `:none`, `nothing`, and `false` print the axis guide without units.
+
+
+---
+
#### 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 +72,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 +172,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 +578,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 0000000000..08738ae50f
--- /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 0000000000..d295b7a505
--- /dev/null
+++ b/PlotThemes/Project.toml
@@ -0,0 +1,10 @@
+name = "PlotThemes"
+uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a"
+version = "3.3.0"
+
+[deps]
+PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043"
+
+[compat]
+PlotUtils = "1"
+julia = "1.10"
diff --git a/PlotThemes/README.md b/PlotThemes/README.md
new file mode 100644
index 0000000000..5ce6439f94
--- /dev/null
+++ b/PlotThemes/README.md
@@ -0,0 +1,114 @@
+# PlotThemes
+
+[](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`
+
+
+### `: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`
+
+
+## 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 0000000000..75dcdf7aae
--- /dev/null
+++ b/PlotThemes/src/PlotThemes.jl
@@ -0,0 +1,78 @@
+module PlotThemes
+
+using PlotUtils
+
+export add_theme, theme_palette, PlotTheme
+
+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"
+theme_palette(s::Symbol) =
+if haskey(_themes, s) && haskey(_themes[s].defaults, :palette)
+ _themes[s].defaults[:palette]
+else
+ palette(:default)
+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
diff --git a/PlotThemes/src/boxed.jl b/PlotThemes/src/boxed.jl
new file mode 100644
index 0000000000..c626045efd
--- /dev/null
+++ b/PlotThemes/src/boxed.jl
@@ -0,0 +1,19 @@
+const _boxed = [
+ :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],
+ ),
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/dao.jl b/PlotThemes/src/dao.jl
new file mode 100644
index 0000000000..db814ae7fb
--- /dev/null
+++ b/PlotThemes/src/dao.jl
@@ -0,0 +1,28 @@
+const dao_palette = [
+ colorant"#d77255",
+ colorant"#009afa",
+ colorant"#707070",
+ colorant"#21ab74",
+ colorant"#ba3030",
+ colorant"#9467bd",
+]
+
+const _dao = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/dark.jl b/PlotThemes/src/dark.jl
new file mode 100644
index 0000000000..ab9d8bb086
--- /dev/null
+++ b/PlotThemes/src/dark.jl
@@ -0,0 +1,23 @@
+#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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/dracula.jl b/PlotThemes/src/dracula.jl
new file mode 100644
index 0000000000..d1fe76bf10
--- /dev/null
+++ b/PlotThemes/src/dracula.jl
@@ -0,0 +1,28 @@
+# 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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/ggplot2.jl b/PlotThemes/src/ggplot2.jl
new file mode 100644
index 0000000000..5b6c7d0484
--- /dev/null
+++ b/PlotThemes/src/ggplot2.jl
@@ -0,0 +1,62 @@
+# # 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 = [
+ ## 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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/gruvbox.jl b/PlotThemes/src/gruvbox.jl
new file mode 100644
index 0000000000..f608ec0e76
--- /dev/null
+++ b/PlotThemes/src/gruvbox.jl
@@ -0,0 +1,91 @@
+# 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 = [
+ :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 in _gruvbox_dark_palette],
+ ),
+ :colorgradient => cgrad(:YlOrRd, rev = true),
+] |> Dict |> PlotTheme
+
+
+const _gruvbox_light = [
+ :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 in _gruvbox_light_palette],
+ ),
+ :colorgradient => cgrad(:YlOrRd, rev = true),
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/juno.jl b/PlotThemes/src/juno.jl
new file mode 100644
index 0000000000..50bb70ec06
--- /dev/null
+++ b/PlotThemes/src/juno.jl
@@ -0,0 +1,21 @@
+#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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/lime.jl b/PlotThemes/src/lime.jl
new file mode 100644
index 0000000000..66c4371f57
--- /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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/orange.jl b/PlotThemes/src/orange.jl
new file mode 100644
index 0000000000..047616ad4c
--- /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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/rose_pine.jl b/PlotThemes/src/rose_pine.jl
new file mode 100644
index 0000000000..59218ee4f2
--- /dev/null
+++ b/PlotThemes/src/rose_pine.jl
@@ -0,0 +1,47 @@
+# 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 = [
+ :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),
+] |> Dict |> PlotTheme
+
+
+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 = [
+ :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),
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/sand.jl b/PlotThemes/src/sand.jl
new file mode 100644
index 0000000000..7c1f27354d
--- /dev/null
+++ b/PlotThemes/src/sand.jl
@@ -0,0 +1,22 @@
+#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 = [
+ :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,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/sheet.jl b/PlotThemes/src/sheet.jl
new file mode 100644
index 0000000000..25ecf04824
--- /dev/null
+++ b/PlotThemes/src/sheet.jl
@@ -0,0 +1,141 @@
+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 0000000000..16927003e6
--- /dev/null
+++ b/PlotThemes/src/solarized.jl
@@ -0,0 +1,52 @@
+# 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 = [
+ :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 in _solarized_palette],
+ ),
+ :colorgradient => :YlOrRd,
+] |> Dict |> PlotTheme
+
+
+const _solarized_light = [
+ :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 in _solarized_palette],
+ ),
+ :colorgradient => cgrad(:YlOrRd, rev = true),
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/src/wong.jl b/PlotThemes/src/wong.jl
new file mode 100644
index 0000000000..d34b59b10a
--- /dev/null
+++ b/PlotThemes/src/wong.jl
@@ -0,0 +1,31 @@
+# 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 = [
+ :palette => expand_palette(
+ colorant"white",
+ wong_palette;
+ lchoices = [57],
+ cchoices = [100],
+ ),
+ :colorgradient => cgrad(:viridis).colors,
+] |> Dict |> PlotTheme
+
+const _wong2 = [
+ :palette => expand_palette(
+ colorant"white",
+ [RGB(0, 0, 0); wong_palette];
+ lchoices = [57],
+ cchoices = [100],
+ ),
+ :colorgradient => :viridis,
+] |> Dict |> PlotTheme
diff --git a/PlotThemes/test/Project.toml b/PlotThemes/test/Project.toml
new file mode 100644
index 0000000000..04c84780e5
--- /dev/null
+++ b/PlotThemes/test/Project.toml
@@ -0,0 +1,3 @@
+[deps]
+PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/PlotThemes/test/runtests.jl b/PlotThemes/test/runtests.jl
new file mode 100644
index 0000000000..b9c80a04dc
--- /dev/null
+++ b/PlotThemes/test/runtests.jl
@@ -0,0 +1,15 @@
+using PlotThemes
+using Test, PlotUtils
+
+@testset "basics" begin
+ @test :sand ∈ keys(PlotThemes._themes)
+
+ palette = theme_palette(:wong)
+ @test palette isa ColorPalette
+
+ thm = PlotTheme(PlotThemes._dark; palette)
+ @test thm isa PlotTheme
+
+ add_theme(:custom, thm)
+ @test theme_palette(:custom) == palette
+end
diff --git a/PlotsBase/Project.toml b/PlotsBase/Project.toml
new file mode 100644
index 0000000000..fafedaa909
--- /dev/null
+++ b/PlotsBase/Project.toml
@@ -0,0 +1,119 @@
+name = "PlotsBase"
+uuid = "c52230a3-c5da-43a3-9e85-260fcdfdc737"
+version = "0.1"
+
+[deps]
+Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
+Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
+Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
+Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a"
+FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
+JLFzf = "1019f520-868f-41f5-a6de-eb00f4b6a39c"
+JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
+Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
+LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
+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"
+TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87"
+UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1"
+Unzip = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d"
+
+[sources]
+PlotThemes = {path = "../PlotThemes"}
+RecipesBase = {path = "../RecipesBase"}
+RecipesPipeline = {path = "../RecipesPipeline"}
+
+[weakdeps]
+FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
+GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
+Gaston = "4b11ee91-296f-5714-9832-002c20994614"
+GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
+HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
+IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a"
+ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254"
+PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925"
+PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
+PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c"
+PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9"
+UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
+Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
+
+[extensions]
+FileIOExt = "FileIO"
+GRExt = "GR"
+GastonExt = "Gaston"
+GeometryBasicsExt = "GeometryBasics"
+HDF5Ext = "HDF5"
+IJuliaExt = "IJulia"
+ImageInTerminalExt = "ImageInTerminal"
+PGFPlotsXExt = "PGFPlotsX"
+PlotlyJSExt = "PlotlyJS"
+PlotlyKaleidoExt = "PlotlyKaleido"
+PythonPlotExt = "PythonPlot"
+UnicodePlotsExt = "UnicodePlots"
+UnitfulExt = "Unitful"
+
+[compat]
+Base64 = "1"
+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, 1"
+LaTeXStrings = "1"
+Latexify = "0.16.8"
+Measures = "0.3"
+NaNMath = "1"
+PGFPlotsX = "1"
+Pkg = "1"
+PlotThemes = "3"
+PlotUtils = "1"
+PlotlyJS = "0.18.16"
+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"
+TableOperations = "1"
+UnicodeFun = "0.4"
+UnicodePlots = "3"
+Unitful = "1.25"
+Unzip = "0.2"
+UUIDs = "1"
+julia = "1.10"
diff --git a/PlotsBase/ext/FileIOExt.jl b/PlotsBase/ext/FileIOExt.jl
new file mode 100644
index 0000000000..86fba6f9a3
--- /dev/null
+++ b/PlotsBase/ext/FileIOExt.jl
@@ -0,0 +1,43 @@
+module FileIOExt
+
+import PlotsBase: PlotsBase, Plot
+import FileIO
+
+_fileio_load(@nospecialize(filename::AbstractString)) =
+ FileIO.load(filename::AbstractString)
+_fileio_save(@nospecialize(filename::AbstractString), @nospecialize(x)) =
+ FileIO.save(filename::AbstractString, x)
+
+function _show_pdfbackends(io::IO, ::MIME"image/png", plt::Plot)
+ fn = tempname()
+
+ # first save a pdf file
+ PlotsBase.pdf(plt, fn)
+
+ # load that pdf into a FileIO Stream
+ s = _fileio_load("$fn.pdf")
+
+ # save a png
+ pngfn = "$fn.png"
+ _fileio_save(pngfn, s)
+
+ # now write from the file
+ write(io, read(open(pngfn), String))
+
+ # cleanup
+ rm("$fn.pdf")
+ rm("$fn.png")
+ return 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
diff --git a/src/backends/gr.jl b/PlotsBase/ext/GRExt.jl
similarity index 75%
rename from src/backends/gr.jl
rename to PlotsBase/ext/GRExt.jl
index 896e978fb0..d6dee2a80c 100644
--- a/src/backends/gr.jl
+++ b/PlotsBase/ext/GRExt.jl
@@ -1,3 +1,186 @@
+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 +229,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,29 +306,29 @@ 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))
gr_color(c, ::Type{<:AbstractRGB}) = UInt32(
round(UInt, clamp(255alpha(c), 0, 255)) << 24 +
- round(UInt, clamp(255blue(c), 0, 255)) << 16 +
- round(UInt, clamp(255green(c), 0, 255)) << 8 +
- round(UInt, clamp(255red(c), 0, 255)),
+ round(UInt, clamp(255blue(c), 0, 255)) << 16 +
+ round(UInt, clamp(255green(c), 0, 255)) << 8 +
+ round(UInt, clamp(255red(c), 0, 255)),
)
gr_color(c, ::Type{<:AbstractGray}) =
- let g = round(UInt, clamp(255gray(c), 0, 255)),
+let g = round(UInt, clamp(255gray(c), 0, 255)),
α = round(UInt, clamp(255alpha(c), 0, 255))
- UInt32(α << 24 + g << 16 + g << 8 + g)
- end
+ UInt32(α << 24 + g << 16 + g << 8 + g)
+end
set_RGBA_alpha(alpha, c::RGBA) = RGBA(red(c), green(c), blue(c), alpha)
set_RGBA_alpha(alpha::Nothing, c::RGBA) = c
function gr_getcolorind(c)
gr_set_transparency(float(alpha(c)))
- convert(Int, GR.inqcolorfromrgb(red(c), green(c), blue(c)))
+ return convert(Int, GR.inqcolorfromrgb(red(c), green(c), blue(c)))
end
gr_set_linecolor(c) = GR.setlinecolorind(gr_getcolorind(_cycle(c, 1)))
@@ -164,7 +348,7 @@ gr_set_fillstyle(::Nothing) = GR.setfillintstyle(GR.INTSTYLE_SOLID)
function gr_set_fillstyle(s::Symbol)
GR.setfillintstyle(GR.INTSTYLE_HATCH)
GR.setfillstyle(get(gr_fill_styles, s, 9))
- nothing
+ return nothing
end
# https://gr-framework.org/python-gr.html?highlight=setprojectiontype#gr.setprojectiontype
@@ -178,11 +362,19 @@ gr_set_projectiontype(sp) = GR.setprojectiontype(gr_projections[sp[:projection_t
# draw line segments, splitting x/y into contiguous/finite segments
# note: this can be used for shapes by passing func `GR.fillarea`
-function gr_polyline(x, y, func = GR.polyline; arrowside = :none, arrowstyle = :simple)
+function gr_polyline(
+ x,
+ y,
+ func = GR.polyline;
+ arrowside = :none,
+ arrowstyle = :simple,
+ arrowsize = 1,
+ )
draw_head = arrowside in (:head, :both)
draw_tail = arrowside in (:tail, :both)
n = length(x)
iend = 0
+ GR.setarrowsize(arrowsize)
while iend < n - 1
istart = -1 # set istart to the first index that is finite
for j in (iend + 1):n
@@ -216,6 +408,7 @@ function gr_polyline(x, y, func = GR.polyline; arrowside = :none, arrowstyle = :
break
end
end
+ return
end
function gr_polyline3d(x, y, z, func = GR.polyline3d)
@@ -246,25 +439,26 @@ function gr_polyline3d(x, y, z, func = GR.polyline3d)
break
end
end
+ return
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
- GR.inqtextext(x, y, s)
- else
- GR.inqtext(x, y, s)
- end
+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)
+end
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
- GR.textext(x, y, s)
- else
- GR.text(x, y, s)
- end
+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)
+end
function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot)
GR.savestate()
@@ -327,7 +521,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot)
(r ≤ 1 && r ≥ 0) && gr_text(GR.wctondc(0.05, r)..., _cycle(rtick_labels, i))
end
GR.restorestate()
- nothing
+ return nothing
end
# using the axis extrema and limit overrides, return the min/max value for this axis
@@ -348,7 +542,7 @@ function gr_fill_viewport(vp::GRViewport, c)
GR.fillrect(vp.xmin, vp.xmax, vp.ymin, vp.ymax)
GR.selntran(1)
GR.restorestate()
- nothing
+ return nothing
end
gr_fill_plotarea(sp, vp::GRViewport) =
@@ -361,7 +555,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)
@@ -384,7 +578,7 @@ function gr_draw_marker(series, xi, yi, zi, clims, i, msize, strokewidth, shape:
gr_set_line(strokewidth, :solid, msc, series)
gr_set_transparency(msc, get_markerstrokealpha(series, i))
GR.polyline(xs, ys)
- nothing
+ return nothing
end
# draw ONE symbol marker
@@ -395,12 +589,12 @@ 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])
end
- nothing
+ return nothing
end
# ---------------------------------------------------------
@@ -409,7 +603,7 @@ function gr_set_line(lw, style, c, s) # s can be Subplot or Series
GR.setlinetype(gr_linetypes[style])
GR.setlinewidth(get_thickness_scaling(s) * max(0, lw / gr_nominal_size(s)))
gr_set_linecolor(c)
- nothing
+ return nothing
end
gr_set_fill(c) = (gr_set_fillcolor(c); GR.setfillintstyle(GR.INTSTYLE_SOLID); nothing)
@@ -420,13 +614,13 @@ gr_point_mult(s) = 1.5get_thickness_scaling(s) * px / pt / maximum(get_size(s))
# set the font attributes.
function gr_set_font(
- f::Font,
- s;
- halign = f.halign,
- valign = f.valign,
- color = f.color,
- rotation = f.rotation,
-)
+ f::Font,
+ s;
+ halign = f.halign,
+ valign = f.valign,
+ color = f.color,
+ rotation = f.rotation,
+ )
family = lowercase(f.family)
GR.setcharheight(gr_point_mult(s) * f.pointsize)
GR.setcharup(sincosd(-rotation)...)
@@ -439,12 +633,12 @@ function gr_set_font(
)
gr_set_textcolor(plot_color(color))
GR.settextalign(gr_haligns[halign], gr_valigns[valign])
- nothing
+ return nothing
end
function gr_w3tondc(x, y, z)
xw, yw, _ = GR.wc3towc(x, y, z)
- GR.wctondc(xw, yw) # x, y
+ return GR.wctondc(xw, yw) # x, y
end
# --------------------------------------------------------------------------------------
@@ -458,7 +652,7 @@ function gr_viewport_from_bbox(sp::Subplot{GRBackend}, bb::BoundingBox, w, h, vp
vp_canvas.ymax * (1 - top(bb) / h),
)
hascolorbar(sp) && (viewport.xmax -= 0.1(1 + 0.5gr_is3d(sp)))
- viewport
+ return viewport
end
# change so we're focused on the viewport area
@@ -471,7 +665,7 @@ function gr_set_viewport_cmap(sp::Subplot, vp::GRViewport)
offset = gr_cbar_offsets[][gr_is3d(sp) ? 2 : 1]
args = vp.xmax + offset, vp.xmax + offset + gr_cbar_width[], vp.ymin, vp.ymax
GR.setviewport(args...)
- GRViewport(args...)
+ return GRViewport(args...)
end
function gr_set_viewport_polar(vp)
@@ -481,7 +675,7 @@ function gr_set_viewport_polar(vp)
r = 0.5NaNMath.min(width(vp), dist - vp.ymin)
GR.setviewport(x_ctr - r, x_ctr + r, y_ctr - r, y_ctr + r)
GR.setwindow(-1, 1, -1, 1)
- r
+ return r
end
struct GRColorbar
@@ -492,19 +686,20 @@ 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.")
- push!(list, series)
+ style == Colorbars.cbar_gradient ? cbar.gradients :
+ style == Colorbars.cbar_fill ? cbar.fills :
+ style == Colorbars.cbar_lines ? cbar.lines :
+ error("Unknown colorbar style: $style.")
+ return push!(list, series)
end
function gr_contour_levels(series::Series, clims)
levels = collect(contour_levels(series, clims))
# GR implicitly uses the maximal z value as the highest level
isfilledcontour(series) && pop!(levels)
- levels
+ return levels
end
function gr_colorbar_colors(series::Series, clims)
@@ -519,18 +714,18 @@ function gr_colorbar_colors(series::Series, clims)
else
1_000:1_255 # 256 values
end
- round.(Int, colors)
+ return round.(Int, colors)
end
function _cbar_unique(values, propname)
out = last(values)
if any(x != out for x in values)
- @warn """
+ @maxlog_warn """
Multiple series with different $propname share a colorbar.
Colorbar may not reflect all series correctly.
"""
end
- out
+ return out
end
const gr_colorbar_tick_size = Ref(0.005)
@@ -542,12 +737,12 @@ function gr_colorbar_title(sp::Subplot)
text(ttl, colorbartitlefont(sp))
end
title.font.rotation += 90 # default rotated by 90° (vertical)
- title
+ return title
end
function gr_colorbar_info(sp::Subplot)
clims = gr_clims(sp)
- maximum(first.(gr_text_size.(clims))), clims
+ return maximum(first.(gr_text_size.(clims))), clims
end
# add the colorbar
@@ -574,7 +769,7 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport)
levels = _cbar_unique(contour_levels.(series, Ref(clims)), "levels")
# GR implicitly uses the maximal z value as the highest level
if last(levels) < z_max
- @warn "GR: highest contour level less than maximal z value is not supported."
+ @maxlog_warn "GR: highest contour level less than maximal z value is not supported."
# replace levels, rather than assign to last(levels), to ensure type
# promotion in case levels is an integer array
pop!(levels)
@@ -608,7 +803,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
@@ -618,28 +813,29 @@ function gr_draw_colorbar(cbar::GRColorbar, sp::Subplot, vp::GRViewport)
gr_text(vp.xmax + 0.1, ycenter(vp), title.str)
GR.restorestate()
- nothing
+ return nothing
end
position(symb) =
- if symb === :top || symb === :right
- 0.95
- elseif symb === :left || symb === :bottom
- 0.05
- else
- 0.5
- end
+if symb ≡ :top || symb ≡ :right
+ 0.95
+elseif symb ≡ :left || symb ≡ :bottom
+ 0.05
+else
+ 0.5
+end
alignment(symb) =
- if symb === :top || symb === :right
- :right
- elseif symb === :left || symb === :bottom
- :left
- else
- :center
- end
+if symb ≡ :top || symb ≡ :right
+ :right
+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)
@@ -647,11 +843,11 @@ function gr_set_gradient(c)
c = grad[z]
GR.setcolorrep(999 + i, red(c), green(c), blue(c))
end
- grad
+ return grad
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)
@@ -688,7 +884,7 @@ function gr_display(plt::Plot, dpi_factor = 1)
foreach(sp -> gr_display(sp, w * px, h * px, vp_canvas), plt.subplots)
GR.updatews()
- nothing
+ return nothing
end
gr_set_tickfont(sp, ax::Axis; kw...) = gr_set_font(
@@ -701,7 +897,7 @@ gr_set_tickfont(sp, ax::Axis; kw...) = gr_set_font(
function gr_set_tickfont(sp, letter::Symbol; kw...)
axis = sp[get_attr_symbol(letter, :axis)]
- gr_set_font(
+ return gr_set_font(
tickfont(axis),
sp;
rotation = axis[:rotation],
@@ -717,7 +913,7 @@ function gr_text_size(str)
GR.setcharup(0, 1)
(l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(str)))
GR.restorestate()
- r - l, t - b # w, h
+ return r - l, t - b # w, h
end
# size of the text with rotation applied
@@ -727,7 +923,7 @@ function gr_text_size(str, rot)
GR.setcharup(0, 1)
(l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(str)))
GR.restorestate()
- text_box_width(r - l, t - b, rot), text_box_height(r - l, t - b, rot) # w, h
+ return text_box_width(r - l, t - b, rot), text_box_height(r - l, t - b, rot) # w, h
end
text_box_width(w, h, rot) = abs(cosd(rot)) * w + abs(cosd(rot + 90)) * h
@@ -738,7 +934,7 @@ function gr_get_3d_axis_angle(cvs, nt, ft, letter)
tickpoints = map(cv -> gr_w3tondc(sort_3d_axes(cv, nt, ft, letter)...), cvs)
dx = tickpoints[2][1] - tickpoints[1][1]
dy = tickpoints[2][2] - tickpoints[1][2]
- atand(dy, dx)
+ return atand(dy, dx)
end
function gr_get_ticks_size(ticks, rot)
@@ -748,13 +944,7 @@ function gr_get_ticks_size(ticks, rot)
w = NaNMath.max(w, wi)
h = NaNMath.max(h, hi)
end
- w, h
-end
-
-function labelfunc(scale::Symbol, backend::GRBackend)
- texfunc = labelfunc_tex(scale)
- # replace dash with \minus (U+2212)
- label -> replace(texfunc(label), "-" => "−")
+ return w, h
end
function gr_axis_height(sp, axis)
@@ -763,14 +953,14 @@ function gr_axis_height(sp, axis)
gr_set_font(tickfont(axis), sp)
h = (
ticks in (nothing, false, :none) ? 0 :
- last(gr_get_ticks_size(ticks, axis[:rotation]))
+ last(gr_get_ticks_size(ticks, axis[:rotation]))
)
- if (guide = axis[:guide]) != ""
+ if (guide = PlotsBase.get_guide(axis)) |> !isempty
gr_set_font(guidefont(axis), sp)
h += last(gr_text_size(guide))
end
GR.restorestate()
- h
+ return h
end
function gr_axis_width(sp, axis)
@@ -779,17 +969,23 @@ function gr_axis_width(sp, axis)
gr_set_font(tickfont(axis), sp)
w = (
ticks in (nothing, false, :none) ? 0 :
- first(gr_get_ticks_size(ticks, axis[:rotation]))
+ first(gr_get_ticks_size(ticks, axis[:rotation]))
)
- if (guide = axis[:guide]) != ""
+ if (guide = PlotsBase.get_guide(axis)) |> !isempty
gr_set_font(guidefont(axis), sp)
w += last(gr_text_size(guide))
end
GR.restorestate()
- w
+ return w
+end
+
+function PlotsBase.labelfunc(scale::Symbol, backend::GRBackend)
+ texfunc = PlotsBase.labelfunc_tex(scale)
+ # replace dash with \minus (U+2212)
+ return label -> replace(texfunc(label), "-" => "−")
end
-function _update_min_padding!(sp::Subplot{GRBackend})
+function PlotsBase._update_min_padding!(sp::Subplot{GRBackend})
dpi = sp.plt[:thickness_scaling]
width, height = sp_size = get_size(sp)
@@ -802,7 +998,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
)
# Add margin for title
- if (title = sp[:title]) != ""
+ if (title = sp[:title]) |> !isempty
gr_set_font(titlefont(sp), sp)
l = last(gr_text_size(title))
padding.top[] += 1mm + height * l * px
@@ -846,7 +1042,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
# Add margin for x or y label
m = 0mm
for ax in (xaxis, yaxis)
- (guide = ax[:guide] == "") && continue
+ (guide = PlotsBase.get_guide(ax)) |> isempty && continue
gr_set_font(guidefont(ax), sp)
l = last(gr_text_size(guide))
m = max(m, 1mm + height * l * px)
@@ -856,30 +1052,30 @@ function _update_min_padding!(sp::Subplot{GRBackend})
padding[mirrored(xaxis, :top) ? :top : :bottom][] += m
end
# Add margin for z label
- if (guide = zaxis[:guide]) != ""
+ if (guide = PlotsBase.get_guide(zaxis)) |> !isempty
gr_set_font(guidefont(zaxis), sp)
l = last(gr_text_size(guide))
- padding[mirrored(zaxis, :right) ? :right : :left][] += 1mm + height * l * px # NOTE: why `height` here ?
+ padding[mirrored(zaxis, :right) ? :right : :left][] += 1mm + height * l * px # NOTE: why `height` here ?
end
else
# Add margin for x/y ticks & labels
for (ax, tc, (a, b)) in
((xaxis, xticks, (:top, :bottom)), (yaxis, yticks, (:right, :left)))
if !isempty(first(tc))
- isy = ax[:letter] === :y
+ isy = ax[:letter] ≡ :y
gr_set_tickfont(sp, ax)
ts = gr_get_ticks_size(tc, ax[:rotation])
l = 0.01 + (isy ? first(ts) : last(ts))
padding[ax[:mirror] ? a : b][] += 1mm + sp_size[isy ? 1 : 2] * l * px
end
- if (guide = ax[:guide]) != ""
+ if (guide = PlotsBase.get_guide(ax)) |> !isempty
gr_set_font(guidefont(ax), sp)
l = last(gr_text_size(guide))
padding[mirrored(ax, a) ? a : b][] += 1mm + height * l * px # NOTE: using `height` is arbitrary
end
end
end
- if (title = gr_colorbar_title(sp)).str != ""
+ if (title = gr_colorbar_title(sp)).str |> !isempty
padding.right[] += @static if false
sz = gr_text_size(title)
l = is_horizontal(title) ? first(sz) : last(sz)
@@ -889,7 +1085,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
end
end
- sp.minpad = (
+ return sp.minpad = (
dpi * padding.left[],
dpi * padding.top[],
dpi * padding.right[],
@@ -901,7 +1097,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
@@ -912,7 +1108,7 @@ function gr_clims(sp, args...)
lo = zero(lo)
end
end
- lo, hi
+ return lo, hi
end
function gr_viewport_bbox(vp, sp, color)
@@ -923,11 +1119,11 @@ function gr_viewport_bbox(vp, sp, color)
GR.drawrect(vp.xmin, vp.xmax, vp.ymin, vp.ymax)
GR.selntran(1)
GR.restorestate()
- nothing
+ return nothing
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)
@@ -973,7 +1169,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport)
# add annotations
for ann in sp[:annotations]
- x, y = if is3d(sp)
+ x, y = if PlotsBase.is3d(sp)
x, y, z, val = locate_annotation(sp, ann...)
GR.setwindow(-1, 1, -1, 1)
gr_w3tondc(x, y, z)
@@ -984,6 +1180,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, vp_canvas::GRViewport)
gr_set_font(val.font, sp)
gr_text(x, y, val.str)
end
+ return
end
## Legend
@@ -1006,7 +1203,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 +1215,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)
@@ -1053,10 +1249,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,21 +1265,22 @@ 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)
+ mfac = 0.8 * lfps / (msz + 0.5 * msw + 1.0e-20)
gr_draw_marker(
series,
xpos - 2leg.base_factor,
@@ -1096,7 +1290,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
@@ -1114,11 +1308,11 @@ function gr_add_legend(sp, leg, viewport_area)
end
GR.selntran(1)
GR.restorestate()
- nothing
+ return nothing
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]
@@ -1126,8 +1320,8 @@ function gr_legend_pos(sp::Subplot, leg, vp)
ymirror = mirrored(yaxis, :right)
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
+ elseif lp isa Tuple{<:Real, Symbol}
+ axisclearance = if lp[2] ≡ :outer
[
!ymirror * gr_axis_width(sp, yaxis),
ymirror * gr_axis_width(sp, yaxis),
@@ -1142,7 +1336,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 +1354,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
@@ -1175,10 +1369,10 @@ function gr_legend_pos(sp::Subplot, leg, vp)
# adding min y to shift legend pos to correct graph (#2377)
vp.ymin + 0.5height(vp) + 0.5leg.h - leg.yoffset
end
- xpos, ypos
+ return xpos, ypos
end
-gr_legend_pos(v::NTuple{2,Real}, vp) =
+gr_legend_pos(v::NTuple{2, Real}, vp) =
(vp.xmin + v[1] * (vp.xmax - vp.xmin), vp.ymin + v[2] * (vp.ymax - vp.ymin))
function gr_legend_pos(theta::Real, leg, vp; axisclearance = nothing)
@@ -1194,7 +1388,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)
+ return 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,13 +1398,13 @@ function gr_get_legend_geometry(vp, sp)
textw = texth = 0.0
has_title = false
nseries = 0
- if sp[:legend_position] !== :none
+ if sp[:legend_position] ≢ :none
GR.savestate()
GR.selntran(0)
GR.setcharup(0, 1)
GR.setscale(0)
ttl = sp[:legend_title]
- if (has_title = ttl !== nothing)
+ if (has_title = ttl ≢ nothing)
gr_set_font(legendtitlefont(sp), sp)
(l, r), (b, t) = extrema.(gr_inqtext(0, 0, string(ttl)))
texth = t - b
@@ -1232,10 +1426,10 @@ function gr_get_legend_geometry(vp, sp)
column_layout = if legend_column == -1
(1, has_title + nseries)
elseif legend_column > nseries && nseries != 0 # catch plot_title here
- @warn "n° of legend_column=$legend_column is larger than n° of series=$nseries"
+ @maxlog_warn "n° of legend_column=$legend_column is larger than n° of series=$nseries"
(1 + has_title, nseries)
elseif legend_column == 0
- @warn "n° of legend_column=$legend_column. Assuming vertical layout."
+ @maxlog_warn "n° of legend_column=$legend_column. Assuming vertical layout."
vertical = true
(has_title + nseries, 1)
else
@@ -1274,7 +1468,7 @@ function gr_get_legend_geometry(vp, sp)
w = dx * column_layout[2] - space - !vertical * span_hspace
h = dy * column_layout[1]
- (
+ return (
yoffset = height(vp) / 30,
xoffset = width(vp) / 30,
base_markersize,
@@ -1301,7 +1495,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,65 +1516,65 @@ 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
vp.xmax -= leg.textw
end
end
- nothing
+ return nothing
end
gr_update_viewport_ratio!(vp, sp) =
- 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
- if window_ratio < viewport_ratio
- viewport_center = xcenter(vp)
- viewport_size = width(vp) * window_ratio / viewport_ratio
- vp.xmin = viewport_center - 0.5viewport_size
- vp.xmax = viewport_center + 0.5viewport_size
- elseif window_ratio > viewport_ratio
- viewport_center = ycenter(vp)
- viewport_size = height(vp) * viewport_ratio / window_ratio
- vp.ymin = viewport_center - 0.5viewport_size
- vp.ymax = viewport_center + 0.5viewport_size
- end
+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
+ if window_ratio < viewport_ratio
+ viewport_center = xcenter(vp)
+ viewport_size = width(vp) * window_ratio / viewport_ratio
+ vp.xmin = viewport_center - 0.5viewport_size
+ vp.xmax = viewport_center + 0.5viewport_size
+ elseif window_ratio > viewport_ratio
+ viewport_center = ycenter(vp)
+ viewport_size = height(vp) * viewport_ratio / window_ratio
+ vp.ymin = viewport_center - 0.5viewport_size
+ vp.ymax = viewport_center + 0.5viewport_size
end
+end
gr_set_window(sp, vp) =
- if ispolar(sp)
- gr_set_viewport_polar(vp)
+if ispolar(sp)
+ gr_set_viewport_polar(vp)
+else
+ x_min, x_max, y_min, y_max = gr_xy_axislims(sp)
+ zok = if (needs_3d = needs_any_3d_axes(sp))
+ z_min, z_max = gr_z_axislims(sp)
+ z_max > z_min
else
- x_min, x_max, y_min, y_max = gr_xy_axislims(sp)
- zok = if (needs_3d = needs_any_3d_axes(sp))
- z_min, z_max = gr_z_axislims(sp)
- z_max > z_min
- else
- true
+ true
+ end
+ if x_max > x_min && y_max > y_min && zok
+ scaleop = 0
+ if (xscale = sp[:xaxis][:scale]) ∈ _log_scales
+ scaleop |= gr_x_log_scales[xscale]
end
- if x_max > x_min && y_max > y_min && zok
- scaleop = 0
- if (xscale = sp[:xaxis][:scale]) ∈ _logScales
- scaleop |= gr_x_log_scales[xscale]
- end
- if (yscale = sp[:yaxis][:scale]) ∈ _logScales
- scaleop |= gr_y_log_scales[yscale]
- end
- if needs_3d && (zscale = sp[:zaxis][:scale] ∈ _logScales)
- scaleop |= gr_z_log_scales[zscale]
- end
- sp[:xaxis][:flip] && (scaleop |= GR.OPTION_FLIP_X)
- sp[:yaxis][:flip] && (scaleop |= GR.OPTION_FLIP_Y)
- (needs_3d && sp[:zaxis][:flip]) && (scaleop |= GR.OPTION_FLIP_Z)
- # NOTE: setwindow sets the "data coordinate" limits of the current "viewport"
- GR.setwindow(x_min, x_max, y_min, y_max)
- GR.setscale(scaleop)
+ if (yscale = sp[:yaxis][:scale]) ∈ _log_scales
+ scaleop |= gr_y_log_scales[yscale]
+ end
+ if needs_3d && (zscale = sp[:zaxis][:scale]) ∈ _log_scales
+ scaleop |= gr_z_log_scales[zscale]
end
+ sp[:xaxis][:flip] && (scaleop |= GR.OPTION_FLIP_X)
+ sp[:yaxis][:flip] && (scaleop |= GR.OPTION_FLIP_Y)
+ (needs_3d && sp[:zaxis][:flip]) && (scaleop |= GR.OPTION_FLIP_Z)
+ # NOTE: setwindow sets the "data coordinate" limits of the current "viewport"
+ GR.setwindow(x_min, x_max, y_min, y_max)
+ GR.setscale(scaleop)
end
+end
## Axes
@@ -1394,8 +1588,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 +1601,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
+ return 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)]
+ return 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)]
+ return 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)]
+ return 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)]
+ return 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)
@@ -1434,16 +1654,14 @@ function gr_draw_axis(sp, letter, vp)
# labels
gr_label_ticks(sp, letter, ax.ticks)
gr_label_axis(sp, letter, vp)
- nothing
+ return nothing
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)
@@ -1453,67 +1671,67 @@ function gr_draw_axis_3d(sp, letter, vp)
gr_label_ticks_3d(sp, letter, ax.ticks)
gr_label_axis_3d(sp, letter)
gr_set_window(sp, vp)
- nothing
+ return nothing
end
gr_draw_grid(sp, axis, segments, func = gr_polyline) =
- if axis[:grid]
- gr_set_line(
- axis[:gridlinewidth],
- axis[:gridstyle],
- axis[:foreground_color_grid],
- sp,
- )
- gr_set_transparency(axis[:foreground_color_grid], axis[:gridalpha])
- func(coords(segments)...)
- end
+if axis[:grid]
+ gr_set_line(
+ axis[:gridlinewidth],
+ axis[:gridstyle],
+ axis[:foreground_color_grid],
+ sp,
+ )
+ gr_set_transparency(axis[:foreground_color_grid], axis[:gridalpha])
+ func(coords(segments)...)
+end
gr_draw_minorgrid(sp, axis, segments, func = gr_polyline) =
- if axis[:minorgrid]
- gr_set_line(
- axis[:minorgridlinewidth],
- axis[:minorgridstyle],
- axis[:foreground_color_minor_grid],
- sp,
- )
- gr_set_transparency(axis[:foreground_color_minor_grid], axis[:minorgridalpha])
- func(coords(segments)...)
- end
+if axis[:minorgrid]
+ gr_set_line(
+ axis[:minorgridlinewidth],
+ axis[:minorgridstyle],
+ axis[:foreground_color_minor_grid],
+ sp,
+ )
+ gr_set_transparency(axis[:foreground_color_minor_grid], axis[:minorgridalpha])
+ func(coords(segments)...)
+end
gr_draw_spine(sp, axis, segments, func = gr_polyline) =
- if axis[:showaxis]
- gr_set_line(1, :solid, axis[:foreground_color_border], sp)
- gr_set_transparency(1.0)
- GR.setclip(0)
- func(coords(segments)...)
- GR.setclip(1)
- end
+if axis[:showaxis]
+ gr_set_line(1, :solid, axis[:foreground_color_border], sp)
+ gr_set_transparency(1.0)
+ GR.setclip(0)
+ func(coords(segments)...)
+ GR.setclip(1)
+end
gr_draw_border(sp, axis, segments, func = gr_polyline) =
- if sp[:framestyle] in (:box, :semi)
- 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)
- func(coords(segments)...)
- GR.setclip(1)
- end
+if sp[:framestyle] in (:box, :semi)
+ 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)
+ func(coords(segments)...)
+ GR.setclip(1)
+end
gr_draw_ticks(sp, axis, segments, func = gr_polyline) =
- if axis[:showaxis]
- if sp[:framestyle] in (:zerolines, :grid)
- gr_set_line(1, :solid, axis[:foreground_color_grid], sp)
- gr_set_transparency(
- axis[:foreground_color_grid],
- axis[:tick_direction] === :out ? axis[:gridalpha] : 0,
- )
- else
- gr_set_line(1, :solid, axis[:foreground_color_axis], sp)
- end
- GR.setclip(0)
- func(coords(segments)...)
- GR.setclip(1)
+if axis[:showaxis]
+ if sp[:framestyle] in (:zerolines, :grid)
+ gr_set_line(1, :solid, axis[:foreground_color_grid], sp)
+ gr_set_transparency(
+ axis[:foreground_color_grid],
+ axis[:tick_direction] ≡ :out ? axis[:gridalpha] : 0,
+ )
+ else
+ gr_set_line(1, :solid, axis[:foreground_color_axis], sp)
end
+ GR.setclip(0)
+ func(coords(segments)...)
+ GR.setclip(1)
+end
function gr_label_ticks(sp, letter, ticks)
letters = axes_letters(sp, letter)
@@ -1522,14 +1740,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
@@ -1554,6 +1772,7 @@ function gr_label_ticks(sp, letter, ticks)
end
gr_text(x + sgn * x_off, y + sgn * y_off, dv)
end
+ return
end
gr_label_ticks(sp, letter, ticks::Nothing) = nothing
@@ -1564,12 +1783,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 +1798,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ϕ)
@@ -1619,45 +1838,47 @@ function gr_label_ticks_3d(sp, letter, ticks)
y_off = y_offset + 0.5(sgn2b * last(sz_rot) + sgn3 * last(sz) * cosd(rot))
gr_text(xi + sgn * x_off, yi + sgn * y_off, dv)
end
+ return
end
-gr_label_axis(sp, letter, vp) =
- if (ax = sp[get_attr_symbol(letter, :axis)])[:guide] != ""
+function gr_label_axis(sp, letter, vp)
+ ax = sp[get_attr_symbol(letter, :axis)]
+ return if PlotsBase.get_guide(ax) |> !isempty
mirror = ax[:mirror]
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])
+ gr_text(xpos, ypos, PlotsBase.get_guide(ax))
GR.restorestate()
end
+end
-gr_label_axis_3d(sp, letter) =
- if (ax = sp[get_attr_symbol(letter, :axis)])[:guide] != ""
+function gr_label_axis_3d(sp, letter)
+ ax = sp[get_attr_symbol(letter, :axis)]
+ return if PlotsBase.get_guide(ax) |> !isempty
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,37 +1895,38 @@ 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_text(x + sgn * x_offset, y + sgn * y_offset, PlotsBase.get_guide(ax))
GR.restorestate()
end
+end
gr_add_title(sp, vp_plt, vp_sp) =
- if (title = sp[:title]) != ""
- GR.savestate()
- xpos, ypos, halign, valign = if (loc = sp[:titlelocation]) === :left
- vp_plt.xmin, vp_sp.ymax, :left, :top
- elseif loc === :center
- xcenter(vp_plt), vp_sp.ymax, :center, :top
- elseif loc === :right
- vp_plt.xmax, vp_sp.ymax, :right, :top
- else
- xposition(vp_plt, loc[1]),
+if (title = sp[:title]) |> !isempty
+ GR.savestate()
+ xpos, ypos, halign, valign = if (loc = sp[:titlelocation]) ≡ :left
+ vp_plt.xmin, vp_sp.ymax, :left, :top
+ elseif loc ≡ :center
+ xcenter(vp_plt), vp_sp.ymax, :center, :top
+ elseif loc ≡ :right
+ vp_plt.xmax, vp_sp.ymax, :right, :top
+ else
+ xposition(vp_plt, loc[1]),
yposition(vp_plt, loc[2]),
sp[:titlefonthalign],
sp[:titlefontvalign]
- end
- gr_set_font(titlefont(sp), sp; halign, valign)
- gr_text(xpos, ypos, title)
- GR.restorestate()
end
+ gr_set_font(titlefont(sp), sp; halign, valign)
+ gr_text(xpos, ypos, title)
+ GR.restorestate()
+end
## Series
@@ -1719,12 +1941,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,33 +1958,34 @@ function gr_add_series(sp, series)
# draw the series
clims = gr_clims(sp, series)
if (st = series[:seriestype]) in (:path, :scatter, :straightline)
- if st === :straightline
- 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
@@ -1772,7 +1995,7 @@ function gr_add_series(sp, series)
gr_text(GR.wctondc(xi, yi)..., str)
end
- if sp[:legend_position] === :inline && should_add_to_legend(series)
+ if sp[:legend_position] ≡ :inline && should_add_to_legend(series)
gr_set_textcolor(plot_color(sp[:legend_font_color]))
offset, halign, valign = if sp[:yaxis][:mirror]
_, i = sp[:xaxis][:flip] ? findmax(x) : findmin(x)
@@ -1786,12 +2009,12 @@ function gr_add_series(sp, series)
gr_text(x_l + offset, y_l, series[:label])
end
GR.restorestate()
- nothing
+ return nothing
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
@@ -1801,9 +2024,9 @@ function gr_draw_segments(series, x, y, z, fillrange, clims)
for segment in series_segments(series, st; check = true)
i, rng = segment.attr_index, segment.range
isempty(rng) && continue
- is3d = st === :path3d && z !== nothing
- is2d = st === :path || st === :straightline
- if is2d && fillrange !== nothing
+ is3d = st ≡ :path3d && z ≢ nothing
+ is2d = st ≡ :path || st ≡ :straightline
+ if is2d && fillrange ≢ nothing
(fc = get_fillcolor(series, clims, i)) |> gr_set_fillcolor
gr_set_fillstyle(get_fillstyle(series, i))
fx = _cycle(x, vcat(rng, reverse(rng)))
@@ -1817,28 +2040,30 @@ function gr_draw_segments(series, x, y, z, fillrange, clims)
if is3d
GR.polyline3d(x[rng], y[rng], z[rng])
elseif is2d
- arrowside, arrowstyle = if (arrow = series[:arrow]) isa Arrow
- arrow.side, arrow.style
- else
- :none, :simple
- end
- gr_polyline(x[rng], y[rng]; arrowside = arrowside, arrowstyle = arrowstyle)
+ arrow =
+ series[:arrow] isa Arrow ? series[:arrow] : Arrow(:none, :simple, 1.0, 1.0)
+
+ arrowside, arrowstyle, arrowsize =
+ arrow.side, arrow.style, (arrow.headlength + arrow.headwidth) / 2
+
+ gr_polyline(x[rng], y[rng]; arrowside, arrowstyle, arrowsize)
end
end
+ return
end
function gr_draw_markers(
- series::Series,
- x,
- y,
- z,
- clims,
- msize = series[:markersize],
- strokewidth = series[:markerstrokewidth],
-)
+ series::Series,
+ x,
+ y,
+ z,
+ clims,
+ msize = series[:markersize],
+ strokewidth = series[:markerstrokewidth],
+ )
isempty(x) && return
GR.setfillintstyle(GR.INTSTYLE_SOLID)
- (shapes = series[:markershape]) === :none && return
+ (shapes = series[:markershape]) ≡ :none && return
for segment in series_segments(series, :scatter)
rng = intersect(eachindex(IndexLinear(), x), segment.range)
isempty(rng) && continue
@@ -1846,6 +2071,9 @@ function gr_draw_markers(
ms = get_thickness_scaling(series) * _cycle(msize, i)
msw = get_thickness_scaling(series) * _cycle(strokewidth, i)
shape = _cycle(shapes, i)
+ if !(shape isa Shape)
+ shape = gr_get_markershape.(shape)
+ end
for j in rng
gr_draw_marker(
series,
@@ -1860,10 +2088,11 @@ function gr_draw_markers(
)
end
end
+ return
end
function gr_draw_shapes(series, clims)
- x, y = shape_data(series)
+ x, y = PlotsBase.shape_data(series)
for segment in series_segments(series, :shape)
i, rng = segment.attr_index, segment.range
if length(rng) > 1
@@ -1888,6 +2117,7 @@ function gr_draw_shapes(series, clims)
GR.polyline(xseg, yseg)
end
end
+ return
end
function gr_draw_contour(series, x, y, z, clims)
@@ -1896,19 +2126,19 @@ 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)
coff = plot_color(series[:linecolor]) in (black, [black]) ? 0 : 1_000
GR.contour(x, y, h, z, coff + Int(series[:contour_labels] == true))
end
- nothing
+ return nothing
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,30 +2157,30 @@ 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
- if series[:connections] isa AbstractVector{<:AbstractVector{Int}}
+ elseif st ≡ :mesh3d
+ if series[:connections] isa AbstractVector{<:AbstractVector{<:Integer}}
# Combination of any polygon types
cns = map(cns -> [length(cns), cns...], series[:connections])
- elseif series[:connections] isa AbstractVector{NTuple{N,Int}} where {N}
+ elseif series[:connections] isa AbstractVector{NTuple{N, <:Integer}} where {N}
# Only N-gons - connections have to be 1-based (indexing)
N = length(series[:connections][1])
cns = map(cns -> [N, cns...], series[:connections])
- elseif series[:connections] isa NTuple{3,<:AbstractVector{Int}}
+ elseif series[:connections] isa NTuple{3, <:AbstractVector{<:Integer}}
# 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
+ ArgumentError |>
+ throw
end
cns = map(i -> ([3, ci[i] + 1, cj[i] + 1, ck[i] + 1]), eachindex(ci))
else
"Unsupported `:connections` type $(typeof(series[:connections])) for seriestype=$st" |>
- ArgumentError |>
- throw
+ ArgumentError |>
+ throw
end
facecolor = if series[:fillcolor] isa AbstractArray
series[:fillcolor]
@@ -1965,7 +2195,7 @@ function gr_draw_surface(series, x, y, z, clims)
else
throw(ArgumentError("Not handled !"))
end
- nothing
+ return nothing
end
function gr_z_normalized_log_scaled(scale, z, clims)
@@ -1978,10 +2208,10 @@ 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...)
+ return z_log, get_z_normalized.(z_log, loglims...)
end
function gr_draw_heatmap(series, x, y, z, clims)
@@ -1990,15 +2220,17 @@ 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
@@ -2008,11 +2240,11 @@ function gr_draw_heatmap(series, x, y, z, clims)
GR.drawimage(first(x), last(x), last(y), first(y), w, h, gr_color.(colors))
else
if something(series[:fillalpha], 1) < 1
- @warn "GR: transparency not supported in non-uniform heatmaps. Alpha values ignored."
+ @maxlog_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)
@@ -2021,7 +2253,7 @@ function gr_draw_heatmap(series, x, y, z, clims)
isnan(_z[i]) && (rgba[i] = bg_rgba)
end
if ispolar(series)
- y[1] < 0 && @warn "'y[1] < 0' (rmin) is not yet supported."
+ y[1] < 0 && @maxlog_warn "'y[1] < 0' (rmin) is not yet supported."
rad_max = gr_y_axislims(sp)[2]
GR.setwindow(-rad_max, rad_max, -rad_max, rad_max) # square ar
# nonuniformpolarcellarray(θ, ρ, nx, ny, color)
@@ -2030,26 +2262,26 @@ function gr_draw_heatmap(series, x, y, z, clims)
GR.nonuniformcellarray(x, y, w, h, rgba)
end
end
- nothing
+ return nothing
end
function gr_draw_image(series, x, y, z, clims)
x_min, x_max = ignorenan_extrema(x)
y_min, y_max = ignorenan_extrema(y)
GR.drawimage(x_min, x_max, y_max, y_min, size(z)..., gr_color.(z))
- nothing
+ return nothing
end
# ----------------------------------------------------------------
for (mime, fmt) in (
- "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
+ "application/pdf" => "pdf",
+ "image/png" => "png",
+ "application/postscript" => "ps",
+ "image/svg+xml" => "svg",
+ )
+ @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)
@@ -2063,12 +2295,12 @@ for (mime, fmt) in (
end
GR.emergencyclosegks()
write(io, read(filepath, String))
- rm(filepath)
+ return rm(filepath)
end
end
-function _display(plt::Plot{GRBackend})
- if plt[:display_type] === :inline
+function PlotsBase._display(plt::Plot{GRBackend})
+ return if plt[:display_type] ≡ :inline
filepath = tempname() * ".pdf"
GR.emergencyclosegks()
withenv(
@@ -2081,7 +2313,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 +2324,8 @@ function _display(plt::Plot{GRBackend})
end
end
-closeall(::GRBackend) = GR.emergencyclosegks()
+PlotsBase.closeall(::GRBackend) = GR.emergencyclosegks()
+
+PlotsBase.@precompile_backend GR
+
+end
diff --git a/src/backends/gaston.jl b/PlotsBase/ext/GastonExt.jl
similarity index 66%
rename from src/backends/gaston.jl
rename to PlotsBase/ext/GastonExt.jl
index 10b055ca75..55eb59bc4d 100644
--- a/src/backends/gaston.jl
+++ b/PlotsBase/ext/GastonExt.jl
@@ -1,14 +1,134 @@
+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)
+ return 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[]
@@ -24,7 +144,7 @@ function _before_layout_calcs(plt::Plot{GastonBackend})
foreach(series -> gaston_add_series(plt, series), plt.series_list)
for sp in plt.subplots
- sp === nothing && continue
+ sp ≡ nothing && continue
for ann in sp[:annotations]
x, y, val = locate_annotation(sp, ann...)
sp.o.axesconf *= "; set label '$(val.str)' at $x,$y $(gaston_font(val.font))"
@@ -35,50 +155,51 @@ function _before_layout_calcs(plt::Plot{GastonBackend})
foreach(x -> println("== n°$(x[1]) ==\n", x[2].conf), enumerate(sp.o.curves))
end
end
- nothing
+ return 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
+ return nothing
end
for (mime, term) in (
- "application/eps" => "epscairo",
- "image/eps" => "epslatex",
- "application/pdf" => "pdfcairo",
- "application/postscript" => "postscript",
- "image/png" => "png",
- "image/svg+xml" => "svg",
- "text/latex" => "tikz",
- "application/x-tex" => "epslatex",
- "text/plain" => "dumb",
-)
- @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend})
+ "application/eps" => "epscairo",
+ "image/eps" => "epscairo",
+ "application/pdf" => "pdfcairo",
+ "application/postscript" => "postscript",
+ "image/png" => "png",
+ "image/svg+xml" => "svg",
+ "text/latex" => "tikz",
+ "application/x-tex" => "cairolatex",
+ "text/plain" => "dumb",
+ )
+ @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
+ return 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 +208,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,
@@ -104,7 +225,7 @@ function gaston_saveopts(plt::Plot{GastonBackend})
"fontscale $scaling lw $scaling dl $scaling", # ps $scaling
)
- join(saveopts, " ")
+ return join(saveopts, " ")
end
function gaston_get_subplots(n, plt_subplots, layout)
@@ -123,27 +244,27 @@ function gaston_get_subplots(n, plt_subplots, layout)
end
end
end
- n, sps
+ return n, sps
end
function gaston_init_subplots(plt, sps)
sz = nr, nc = size(sps)
for c in 1:nc, r in 1:nr # NOTE: row major
- if (sp = sps[r, c]) isa Subplot || sp === nothing
+ if (sp = sps[r, c]) isa Subplot || sp ≡ nothing
gaston_init_subplot(plt, sp)
else
gaston_init_subplots(plt, sp)
sz = max.(sz, size(sp))
end
end
- sz
+ return sz
end
function gaston_init_subplot(
- plt::Plot{GastonBackend},
- sp::Union{Nothing,Subplot{GastonBackend}},
-)
- obj = if sp === nothing
+ plt::Plot{GastonBackend},
+ sp::Union{Nothing, Subplot{GastonBackend}},
+ )
+ obj = if sp ≡ nothing
sp
else
dims =
@@ -155,11 +276,11 @@ function gaston_init_subplot(
end
any_label |= should_add_to_legend(series)
end
- axesconf = gaston_parse_axes_args(plt, sp, dims, any_label)
+ axesconf = gaston_parse_axes_attrs(plt, sp, dims, any_label)
sp.o = Gaston.Plot(; dims, curves = [], axesconf)
end
push!(plt.o.subplots, obj)
- nothing
+ return nothing
end
function gaston_multiplot_pos_size(layout, parent_xy_wh)
@@ -178,8 +299,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
@@ -188,7 +309,7 @@ function gaston_multiplot_pos_size(layout, parent_xy_wh)
end
end
end
- dat
+ return dat
end
function gaston_multiplot_pos_size!(dat)
@@ -198,24 +319,23 @@ function gaston_multiplot_pos_size!(dat)
gaston_multiplot_pos_size!(xy_wh_sp)
elseif xy_wh_sp isa Tuple
x, y, w, h, sp = xy_wh_sp
- sp === nothing && continue
- sp.o === nothing && continue
+ sp ≡ nothing && continue
+ sp.o ≡ nothing && continue
# gnuplot screen coordinates: bottom left at 0,0 and top right at 1,1
gx, gy = x, 1 - y - h
- # @show gx, gy w, h
sp.o.axesconf = "set origin $gx, $gy; set size $w, $h; " * sp.o.axesconf
end
end
- nothing
+ return nothing
end
function gaston_add_series(plt::Plot{GastonBackend}, series::Series)
sp = series[:subplot]
- (gsp = sp.o) === nothing && return
+ (gsp = sp.o) ≡ nothing && return
x, y, z = series[:x], series[:y], series[:z]
st = series[:seriestype]
curves = Gaston.Curve[]
- if gsp.dims == 2 && z === nothing
+ if gsp.dims == 2 && z ≡ nothing
for (n, seg) in enumerate(series_segments(series, st; check = true))
i, rng = seg.attr_index, seg.range
fr = _cycle(series[:fillrange], 1:length(x[rng]))
@@ -227,7 +347,7 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series)
supp = nothing # supplementary column
if z isa Surface
z = z.surf
- if st === :image
+ if st ≡ :image
z = reverse(Float32.(Gray.(z)), dims = 1) # flip y axis
nr, nc = size(z)
if (ly = length(y)) == 2 && ly != nr
@@ -240,9 +360,9 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series)
length(x) == size(z, 2) + 1 && (x = (x[1:(end - 1)] + x[2:end]) / 2)
length(y) == size(z, 1) + 1 && (y = (y[1:(end - 1)] + y[2:end]) / 2)
end
- if st === :mesh3d
- 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)
@@ -259,15 +379,15 @@ function gaston_add_series(plt::Plot{GastonBackend}, series::Series)
push!(gsp.curves, c)
Gaston.write_data(c, gsp.dims, gsp.datafile; append)
end
- nothing
+ return nothing
end
function gaston_seriesconf!(
- sp::Subplot{GastonBackend},
- series::Series,
- add_to_legend::Bool,
- i::Int,
-)
+ sp::Subplot{GastonBackend},
+ series::Series,
+ add_to_legend::Bool,
+ i::Int,
+ )
#=
gnuplot abbreviations (see gnuplot/src/set.c)
---------------------------------------------
@@ -303,31 +423,31 @@ function gaston_seriesconf!(
fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i))
fs = gaston_fillstyle(get_fillstyle(series, i))
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
- curveconf *= if fr !== nothing # filled curves, but not filled curves with markers
+ curveconf *= if fr ≢ nothing # filled curves, but not filled curves with markers
"w filledcurves fc $fc fs $fs border lc $lc lw $lw dt $dt,'' w lines lc $lc lw $lw dt $dt"
- elseif series[:markershape] === :none # simplepath
+ elseif series[:markershape] ≡ :none # simplepath
"w lines lc $lc dt $dt lw $lw"
else
pt, ps, mc = gaston_mk_ms_mc(series, clims, i)
"w lp lc $mc dt $dt lw $lw pt $pt ps $ps"
end
- elseif st === :shape
+ elseif st ≡ :shape
fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i))
fs = gaston_fillstyle(get_fillstyle(series, i))
lc, = gaston_lc_ls_lw(series, clims, i)
curveconf *= "w filledcurves fc $fc fs $fs border lc $lc"
elseif st ∈ (:steppre, :stepmid, :steppost)
- step = if st === :steppre
+ step = if st ≡ :steppre
"fsteps"
- elseif st === :stepmid
+ elseif st ≡ :stepmid
"histeps"
- elseif st === :steppost
+ elseif st ≡ :steppost
"steps"
end
curveconf *= "w $step"
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
push!(extra_curves, "w points lc $lc dt $dt lw $lw notitle")
- elseif st === :image
+ elseif st ≡ :image
gsp.axesconf *= gaston_palette_conf(series)
curveconf *= "w image pixels"
elseif st ∈ (:contour, :contour3d)
@@ -338,7 +458,7 @@ function gaston_seriesconf!(
push!(extra_curves, "w labels notitle")
end
levels = collect(contour_levels(series, clims))
- if st === :contour # 2D
+ if st ≡ :contour # 2D
gsp.axesconf *= if filled
"; set view map; set palette maxcolors $(length(levels))"
else
@@ -349,33 +469,33 @@ 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"
+ @maxlog_warn "PlotsBase(Gaston): $st is not implemented yet"
end
- [curveconf, extra_curves...]
+ return [curveconf, extra_curves...]
end
const gp_borders = (
- bottom_left_front = 1 << 0,
- bottom_left_back = 1 << 1,
+ bottom_left_front = 1 << 0,
+ bottom_left_back = 1 << 1,
bottom_right_front = 1 << 2,
- bottom_right_back = 1 << 3,
- left_vertical = 1 << 4,
- back_vertical = 1 << 5,
- right_vertical = 1 << 6,
- front_vertical = 1 << 7,
- top_left_back = 1 << 8,
- top_right_back = 1 << 9,
- top_left_front = 1 << 10,
- top_right_front = 1 << 11,
- polar = 1 << 11,
+ bottom_right_back = 1 << 3,
+ left_vertical = 1 << 4,
+ back_vertical = 1 << 5,
+ right_vertical = 1 << 6,
+ front_vertical = 1 << 7,
+ top_left_back = 1 << 8,
+ top_right_back = 1 << 9,
+ top_left_front = 1 << 10,
+ top_right_front = 1 << 11,
+ polar = 1 << 11,
)
const gp_fillstyle = Dict(
@@ -386,18 +506,18 @@ const gp_fillstyle = Dict(
)
gaston_fillstyle(x) =
- if haskey(gp_fillstyle, x)
- "pattern $(gp_fillstyle[x])"
- else
- "solid"
- end
+if haskey(gp_fillstyle, x)
+ "pattern $(gp_fillstyle[x])"
+else
+ "solid"
+end
-function gaston_parse_axes_args(
- plt::Plot{GastonBackend},
- sp::Subplot{GastonBackend},
- dims::Int,
- any_label::Bool,
-)
+function gaston_parse_axes_attrs(
+ plt::Plot{GastonBackend},
+ sp::Subplot{GastonBackend},
+ dims::Int,
+ any_label::Bool,
+ )
# axesconf = ["set margins 2, 2, 2, 2"] # left, right, bottom, top
axesconf = String[]
@@ -405,7 +525,7 @@ function gaston_parse_axes_args(
fs = sp[:framestyle]
for letter in (:x, :y, :z)
- (letter === :z && dims == 2) && continue
+ (letter ≡ :z && dims == 2) && continue
axis = sp[get_attr_symbol(letter, :axis)]
# NOTE: there is no `z2tics` concept in gnuplot (only 2D)
@@ -418,28 +538,28 @@ 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!(
axesconf,
- "set $(letter)$(I)label '$(axis[:guide])' $(gaston_font(guide_font))",
+ "set $(letter)$(I)label '$(PlotsBase.get_guide(axis))' $(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 +569,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
@@ -457,7 +577,7 @@ function gaston_parse_axes_args(
end
push!(axesconf, "set $(letter)$(I)range [$lo:$hi]")
- offset = if dims == 2 && letter == :y
+ offset = if dims == 2 && letter ≡ :y
# ticks appear too close to the border, offset them by 1 character
"offset " * string(axis[:mirror] ? 1 : -1)
else
@@ -468,7 +588,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 +598,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 +608,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
@@ -512,14 +632,14 @@ function gaston_parse_axes_args(
gp_borders[:polar]
elseif dims == 2
bottom = gp_borders[:bottom_left_front]
- left = gp_borders[:bottom_left_back]
- top = gp_borders[:bottom_right_front]
- right = gp_borders[:bottom_right_back]
- if fs === :box
+ left = gp_borders[:bottom_left_back]
+ top = gp_borders[:bottom_right_front]
+ right = gp_borders[:bottom_right_back]
+ 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
@@ -527,10 +647,10 @@ function gaston_parse_axes_args(
else # 3D
(
gp_borders[:bottom_left_front] +
- gp_borders[:bottom_left_back] +
- gp_borders[:bottom_right_front] +
- gp_borders[:bottom_right_back] +
- gp_borders[:left_vertical]
+ gp_borders[:bottom_left_back] +
+ gp_borders[:bottom_right_front] +
+ gp_borders[:bottom_right_back] +
+ gp_borders[:left_vertical]
)
end
push!(axesconf, border > 0 ? "set border $border back" : "unset border")
@@ -545,7 +665,7 @@ function gaston_parse_axes_args(
push!(axesconf, "unset colorbox")
end
- if sp[:title] != ""
+ if sp[:title] |> !isempty
# NOTE: `set title` is hard centered, cannot use `sp[:titlelocation]`
# on `set label` takes `right`, `center` or `left` justification
push!(axesconf, "set title '$(sp[:title])' $(gaston_font(titlefont(sp)))")
@@ -556,9 +676,9 @@ 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
+ elseif ttype ≡ :ticks_and_labels
["'$l' $t" for (t, l) in zip(rticks...)]
end
push!(
@@ -571,7 +691,7 @@ function gaston_parse_axes_args(
)
end
- join(axesconf, "; ")
+ return join(axesconf, "; ")
end
function gaston_fix_ticks_overflow(ticks::AbstractVector)
@@ -584,23 +704,23 @@ function gaston_fix_ticks_overflow(ticks::AbstractVector)
end
any(t -> abs(t) > of, ticks) && return float.(ticks)
end
- ticks
+ return ticks
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,15 +728,15 @@ 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
+ return nothing
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)
@@ -635,15 +755,15 @@ function gaston_set_legend!(axesconf, sp, any_label)
end
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
+ push!(axesconf, "set key $(gaston_font(legendfont(sp), rot = false, align = false))")
+ if sp[:legend_title] ≢ nothing
# NOTE: cannot use legendtitlefont(sp) as it will override legendfont
push!(axesconf, "set key title '$(sp[:legend_title])'")
end
else
push!(axesconf, "set key off")
end
- nothing
+ return nothing
end
# --------------------------------------------
@@ -656,7 +776,7 @@ gaston_valign(k) = (top = :top, vcenter = :center, bottom = :bottom)[k]
# from the gnuplot docs:
# - an alpha value of 0 represents a fully opaque color; i.e., "#00RRGGBB" is the same as "#RRGGBB".
# - an alpha value of 255 (FF) represents full transparency
-gaston_alpha(alpha) = alpha === nothing ? 0 : alpha
+gaston_alpha(alpha) = alpha ≡ nothing ? 0 : alpha
gaston_lc_ls_lw(series::Series, clims, i::Int) = (
gaston_color(get_linecolor(series, clims, i), get_linealpha(series, i)),
@@ -675,52 +795,57 @@ function gaston_font(f; rot = true, align = true, color = true, scale = 1)
align && (font *= " $(gaston_halign(f.halign))")
rot && (font *= " rotate by $(f.rotation)")
color && (font *= " textcolor $(gaston_color(f.color))")
- font
+ return font
end
gaston_palette(gradient) =
- let palette = ["$(n - 1) $(c.r) $(c.g) $(c.b)" for (n, c) in enumerate(gradient)]
- '(' * join(palette, ", ") * ')'
- end
+let palette = ["$(n - 1) $(c.r) $(c.g) $(c.b)" for (n, c) in enumerate(gradient)]
+ '(' * join(palette, ", ") * ')'
+end
-gaston_palette_conf(series) =
- "; set palette model RGB defined $(gaston_palette(series[:seriescolor]))"
+gaston_palette_conf(
+ series,
+) = "; set palette model RGB defined $(gaston_palette(series[:seriescolor]))"
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"
- 1
+ 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"
+ return 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
+ return "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
- 1
+ style ≡ :solid && return 1
+ style ≡ :dash && return 2
+ style ≡ :dot && return 3
+ style ≡ :dashdot && return 4
+ style ≡ :dashdotdot && return 5
+ return 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}"
+ return "$base^{$power}"
+end
+
+PlotsBase.@precompile_backend Gaston
+
end
diff --git a/ext/GeometryBasicsExt.jl b/PlotsBase/ext/GeometryBasicsExt.jl
similarity index 71%
rename from ext/GeometryBasicsExt.jl
rename to PlotsBase/ext/GeometryBasicsExt.jl
index a46d2a65b7..871132a06f 100644
--- a/ext/GeometryBasicsExt.jl
+++ b/PlotsBase/ext/GeometryBasicsExt.jl
@@ -1,20 +1,20 @@
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} =
- isbitstype(T) && sizeof(T) > 0 ? Unzip.unzip(reinterpret(NTuple{N,T}, points)) :
+RecipesPipeline.unzip(points::AbstractVector{GeometryBasics.Point{N, T}}) where {N, T} =
+ isbitstype(T) && sizeof(T) > 0 ? Unzip.unzip(reinterpret(NTuple{N, T}, points)) :
Unzip.unzip(Tuple.(points))
# -----------------------------------------
# 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
+end
diff --git a/src/backends/hdf5.jl b/PlotsBase/ext/HDF5Ext.jl
similarity index 59%
rename from src/backends/hdf5.jl
rename to PlotsBase/ext/HDF5Ext.jl
index c81c5690bf..d6b33b577e 100644
--- a/src/backends/hdf5.jl
+++ b/PlotsBase/ext/HDF5Ext.jl
@@ -1,71 +1,133 @@
-#=
-
-# 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}
+const HDF5_SupportedTypes = Union{Number, String}
# Dispatch types:
struct CplxTuple end # Identifies a "complex" tuple structure (not merely numbers)
@@ -77,7 +139,7 @@ if length(HDF5PLOT_MAP_TELEM2STR) < 1
# Possible element types of high-level data types:
# (Used to add type information as an HDF5 string attribute)
# (Also used to dispatch appropriate read function through _read_typed())
- _telem2str = Dict{String,Type}(
+ _telem2str = Dict{String, Type}(
"NOTHING" => Nothing,
"SYMBOL" => Symbol,
"RGBA" => Colorant, # Write out any Colorant to an #RRGGBBAA string
@@ -101,9 +163,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,50 +173,50 @@ 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) in 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
- if isa(v, Axis)
- _hdf5_merge!(dest[k].plotattributes, v.plotattributes)
- else
- dest[k] = v
- end
+ if isa(v, Axis)
+ _hdf5_merge!(dest[k].plotattributes, v.plotattributes)
+ else
+ dest[k] = v
end
+end
# _type_for_map returns the type to use with HDF5PLOT_MAP_TELEM2STR[], in case it is not concrete:
_type_for_map(::Type{T}) where {T} = T # Catch-all
-_type_for_map(::Type{T}) where {T<:BoundingBox} = BoundingBox
-_type_for_map(::Type{T}) where {T<:ColorScheme} = ColorScheme
-_type_for_map(::Type{T}) where {T<:Surface} = Surface
+_type_for_map(::Type{T}) where {T <: BoundingBox} = BoundingBox
+_type_for_map(::Type{T}) where {T <: ColorScheme} = ColorScheme
+_type_for_map(::Type{T}) where {T <: Surface} = Surface
# Read/write things like type name in attributes
-_write_datatype_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"])]
+ return 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,27 +228,27 @@ _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)
+ return _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} =
+_write_typed(grp::Group, name::String, v::Array{T}) where {T <: Number} =
(set_value!(grp, name, v); nothing) # No need to _write_datatype_attr
_write_typed(grp::Group, name::String, v::AbstractRange) =
@@ -206,24 +268,24 @@ function _write_harray(grp::Group, name::String, v::Array)
_write_typed(sgrp, "v$idxstr", elem)
end
- _write_size_attr(sgrp, v)
+ return _write_size_attrs(sgrp, v)
end
# Write Dict without tagging with type:
_write(grp::Group, name::String, d::AbstractDict) =
- let sgrp = HDF5.create_group(grp, name)
- for (k, v) in d
- kstr = string(k)
- _write_typed(sgrp, kstr, v)
- end
+let sgrp = HDF5.create_group(grp, name)
+ for (k, v) in d
+ kstr = string(k)
+ _write_typed(sgrp, kstr, v)
end
+end
# Write out arbitrary `struct`s:
_writestructgeneric(grp::Group, obj::T) where {T} =
for fname in fieldnames(T)
- v = getfield(obj, fname)
- _write_typed(grp, String(fname), v)
- end
+ v = getfield(obj, fname)
+ _write_typed(grp, String(fname), v)
+end
# _write_typed(): More complex structures. (Labels with type name.)
@@ -234,28 +296,28 @@ function _write_typed(grp::Group, name::String, v::T) where {T}
try # Check to see if type is supported
typestr = HDF5PLOT_MAP_TELEM2STR[MT]
catch
- @warn "HDF5Plots does not yet support structs of type `$MT`\n\n$grp"
+ @maxlog_warn "HDF5Plots does not yet support structs of type `$MT`\n\n$grp"
return
end
# If attribute is supported and no writer is defined, then this should work:
objgrp = HDF5.create_group(grp, name)
- _write_datatype_attr(objgrp, MT)
- _writestructgeneric(objgrp, v)
+ _write_datatype_attrs(objgrp, MT)
+ return _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
+ return _write_datatype_attrs(grp[name], Array) # Any
end
-function _write_typed(grp::Group, name::String, v::Tuple, ::Type{ELT}) where {ELT<:Number} # Basic Tuple
+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)
+ return _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)
+ return _write_datatype_attrs(grp[name], CplxTuple)
end
_write_typed(grp::Group, name::String, v::Tuple) = _write_typed(grp, name, v, eltype(v))
@@ -263,21 +325,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)
+ return _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)
+ return _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,34 +352,24 @@ function _write(grp::Group, sp::Subplot{HDF5Backend})
_write_typed(grp, "attr", sp.attr)
listgrp = HDF5.create_group(grp, "series_list")
- _write_length_attr(listgrp, sp.series_list)
+ _write_length_attrs(listgrp, sp.series_list)
for (i, series) in enumerate(sp.series_list)
# Just write .plotattributes part:
_write(listgrp, "$i", series.plotattributes)
end
+ return
end
function _write(grp::Group, plt::Plot{HDF5Backend})
_write_typed(grp, "attr", plt.attr)
listgrp = HDF5.create_group(grp, "subplots")
- _write_length_attr(listgrp, plt.subplots)
+ _write_length_attrs(listgrp, plt.subplots)
for (i, sp) in enumerate(plt.subplots)
sgrp = HDF5.create_group(listgrp, "$i")
_write(sgrp, sp)
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
+ return
end
# _read(): Read data, but not type information.
@@ -337,13 +389,13 @@ _read(::Type{Symbol}, ds::Dataset) = Symbol(HDF5.read(ds))
_read(::Type{Colorant}, ds::Dataset) = parse(Colorant, HDF5.read(ds))
_read(::Type{Tuple}, ds::Dataset) = tuple(HDF5.read(ds)...)
_read(::Type{Extrema}, ds::Dataset) =
- let v = HDF5.read(ds)
- Extrema(v[1], v[2])
- end
+let v = HDF5.read(ds)
+ 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)
+ return Length{TUNIT, typeof(v)}(v)
end
_read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter
@@ -352,7 +404,7 @@ _read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter
# When type is unknown, _read_typed() figures it out:
function _read_typed(grp::Group, name::String)
ds = grp[name]
- _read(_read_datatype_attr(ds), ds)
+ return _read(_read_datatype_attrs(ds), ds)
end
# _readstructgeneric: Needs object values to be written out with _write_typed():
@@ -361,7 +413,7 @@ function _readstructgeneric(::Type{T}, grp::Group) where {T}
for (i, fname) in enumerate(fieldnames(T))
vlist[i] = _read_typed(grp, String(fname))
end
- T(vlist...)
+ return T(vlist...)
end
# Read KW from group:
@@ -373,10 +425,10 @@ function _read(::Type{KW}, grp::Group)
v = _read_typed(grp, k)
d[Symbol(k)] = v
catch e
- @warn "Could not read field $k" e grp
+ @maxlog_warn "Could not read field $k" e grp
end
end
- d
+ return d
end
# _read(): More complex structures.
@@ -385,7 +437,7 @@ end
_read(T::Type, grp::Group) = _readstructgeneric(T, grp)
function _read(::Type{Array}, grp::Group) # Array{Any}
- sz = _read_size_attr(Array, grp)
+ sz = _read_size_attrs(Array, grp)
tuple(0) == sz && return []
result = Array{Any}(undef, sz)
lidx = LinearIndices(sz)
@@ -399,7 +451,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
- reshape(result, sz)
+ return reshape(result, sz)
end
_read(::Type{CplxTuple}, grp::Group) = tuple(_read(Array, grp)...)
@@ -414,19 +466,19 @@ function _read(::Type{GridLayout}, grp::Group)
heights = _read_typed(grp, "heights")
attr = KW() # TODO support attr: _read_typed(grp, "attr")
- GridLayout(parent, minpad, bbox, grid, widths, heights, attr)
+ return GridLayout(parent, minpad, bbox, grid, widths, heights, attr)
end
# Defaults depends on context. So: user must constructs with defaults, then read.
function _read(::Type{DefaultsDict}, grp::Group)
# User should set DefaultsDict.defaults to one of:
# _plot_defaults, _subplot_defaults, _axis_defaults, _series_defaults
path = HDF5.name(ds)
- @warn "Cannot yet read DefaultsDict using _read_typed():\n $path\nCannot fully reconstruct plot."
+ return @maxlog_warn "Cannot yet read DefaultsDict using _read_typed():\n $path\nCannot fully reconstruct plot."
end
# 1st arg appears to be ref to subplots. Seems to work without it.
_read(::Type{Axis}, grp::Group) =
- Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), _axis_defaults))
+ 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 +488,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
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,10 +507,10 @@ 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")
@@ -469,61 +521,69 @@ function _read_plot(grp::Group)
_read(sgrp, sp)
end
- plt
+ return plt
end
-hdf5plot_read(path::AbstractString; name::String = "_unnamed") =
- HDF5.h5open(path, "r") do file
- grp = HDF5.open_group(file, h5plotpath("_unnamed"))
- return _read_plot(grp)
- end
-
-end # module _hdf5_implementation
-
-# Implement Plots.jl backend interface for HDF5Backend
+# Implement PlotsBase.jl backend interface for HDF5Backend
-is_marker_supported(::HDF5Backend, shape::Shape) = true
+PlotsBase.is_marker_supported(::HDF5Backend, shape::Shape) = true
# Create the window/figure for this backend.
-function _create_backend_figure(plt::Plot{HDF5Backend}) end
+function PlotsBase._create_backend_figure(plt::Plot{HDF5Backend}) end
# Set up the subplot within the backend object.
-function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end
+function PlotsBase._initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) end
# Add one series to the underlying backend object.
# Called once per series
# NOTE: Seems to be called when user calls plot()... even if backend
# plot, sp.o has not yet been constructed...
-function _series_added(plt::Plot{HDF5Backend}, series::Series) end
+function PlotsBase._series_added(plt::Plot{HDF5Backend}, series::Series) end
# When series data is added/changed, this callback can do dynamic updates to the backend object.
# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here.
-function _series_updated(plt::Plot{HDF5Backend}, series::Series) end
+function PlotsBase._series_updated(plt::Plot{HDF5Backend}, series::Series) end
# called just before updating layout bounding boxes... in case you need to prep
# for the calcs
-function _before_layout_calcs(plt::Plot{HDF5Backend}) end
+function PlotsBase._before_layout_calcs(plt::Plot{HDF5Backend}) end
# Set the (left, top, right, bottom) minimum padding around the plot area
# to fit ticks, tick labels, guides, colorbars, etc.
-function _update_min_padding!(sp::Subplot{HDF5Backend}) end
+function PlotsBase._update_min_padding!(sp::Subplot{HDF5Backend}) end
# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations])
-function _update_plot_object(plt::Plot{HDF5Backend}) end
+function PlotsBase._update_plot_object(plt::Plot{HDF5Backend}) end
# ----------------------------------------------------------------
# Display/show the plot (open a GUI window, or browser page, for example).
-function _display(plt::Plot{HDF5Backend})
+function PlotsBase._display(plt::Plot{HDF5Backend})
msg = "HDF5 interface does not support `display()` function."
- msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead."
- @warn msg
+ msg *= "\nUse `PlotsBase.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead."
+ @maxlog_warn msg
return
end
# Interface actually required to use HDF5Backend
+PlotsBase.hdf5plot_write(path::AbstractString; kw...) =
+ PlotsBase.hdf5plot_write(current(), path; kw...)
-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)
+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
+
+end
diff --git a/PlotsBase/ext/IJuliaExt.jl b/PlotsBase/ext/IJuliaExt.jl
new file mode 100644
index 0000000000..3306260ce6
--- /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[])
+
+ return 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
+ return 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)
+ return display(IJulia.InlineDisplay(), plt)
+end
+
+end
diff --git a/PlotsBase/ext/ImageInTerminalExt.jl b/PlotsBase/ext/ImageInTerminalExt.jl
new file mode 100644
index 0000000000..ea2a30e202
--- /dev/null
+++ b/PlotsBase/ext/ImageInTerminalExt.jl
@@ -0,0 +1,30 @@
+module ImageInTerminalExt
+
+import ImageInTerminal
+import PlotsBase
+
+if ImageInTerminal.ENCODER_BACKEND[] ≡ :Sixel
+ get!(ENV, "GKSwstype", "nul") # disable `gr` output, we display in the terminal instead
+ for be in (
+ PlotsBase.GRBackend,
+ PlotsBase.PythonPlotBackend,
+ # PlotsBase.UnicodePlotsBackend, # better and faster as MIME("text/plain") in terminal
+ PlotsBase.PGFPlotsXBackend,
+ PlotsBase.PlotlyJSBackend,
+ PlotsBase.PlotlyBackend,
+ PlotsBase.GastonBackend,
+ )
+ @eval function Base.display(::PlotsBase.PlotsDisplay, plt::PlotsBase.Plot{$be})
+ PlotsBase.prepare_output(plt)
+ buf = PipeBuffer()
+ show(buf, MIME("image/png"), plt)
+ return display(
+ ImageInTerminal.TerminalGraphicDisplay(stdout),
+ MIME("image/png"),
+ read(buf),
+ )
+ end
+ end
+end
+
+end
diff --git a/src/backends/pgfplotsx.jl b/PlotsBase/ext/PGFPlotsXExt.jl
similarity index 76%
rename from src/backends/pgfplotsx.jl
rename to PlotsBase/ext/PGFPlotsXExt.jl
index 28479b1c82..3fe8c39140 100644
--- a/src/backends/pgfplotsx.jl
+++ b/PlotsBase/ext/PGFPlotsXExt.jl
@@ -1,5 +1,207 @@
+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
+const Table = PGFPlotsX.Table
Base.@kwdef mutable struct PGFPlotsXPlot
is_created::Bool = false
@@ -43,17 +245,17 @@ 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
fulltext = String(repr("application/x-tex", pgfx_plot))
preamble = fulltext[1:(first(findfirst("\\begin{document}", fulltext)) - 1)]
pgfx_plot.attr[:tex_output_standalone] = old_flag
- preamble
+ return preamble
end
-function surface_to_vecs(x::AVec, y::AVec, s::Union{AMat,Surface})
+function surface_to_vecs(x::AVec, y::AVec, s::Union{AMat, Surface})
a = Array(s)
xn = Vector{eltype(x)}(undef, length(a))
yn = Vector{eltype(y)}(undef, length(a))
@@ -73,7 +275,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 +283,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,12 +294,14 @@ 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
- :background_color
- else
- :background_color_outside
- end]
+ 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
+ end,
+ ]
if bgc isa Colors.Colorant
cstr = plot_color(bgc)
push!(
@@ -163,7 +368,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend})
sp_w > 0mm && push!(axis_opt, "width" => string(sp_w - (rpad + lpad)))
sp_h > 0mm && push!(axis_opt, "height" => string(sp_h - (tpad + bpad)))
for letter in (:x, :y, :z)
- if letter !== :z || RecipesPipeline.is3d(sp)
+ if letter ≢ :z || RecipesPipeline.is3d(sp)
pgfx_axis!(axis_opt, sp, letter)
end
end
@@ -196,7 +401,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 +410,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 +436,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 +450,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)),
@@ -266,8 +468,8 @@ 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()
+ # 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]
@@ -277,20 +479,20 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend})
"name path" => string(series_id),
)
series_func =
- if (
+ if (
RecipesPipeline.is3d(series) ||
- st in (:heatmap, :contour) ||
- (st === :quiver && opt[:z] !== nothing)
+ st in (:heatmap, :contour) ||
+ (st ≡ :quiver && opt[:z] ≢ nothing)
)
- PGFPlotsX.Plot3
- else
- PGFPlotsX.Plot
- end
+ PGFPlotsX.Plot3
+ else
+ PGFPlotsX.Plot
+ end
if (
- series[:fillrange] !== nothing &&
- series[:ribbon] === nothing &&
- !isfilledcontour(series)
- )
+ series[:fillrange] ≢ nothing &&
+ series[:ribbon] ≡ nothing &&
+ !isfilledcontour(series)
+ )
push!(series_opt, "area legend" => nothing)
end
pgfx_add_series!(Val(st), axis, series_opt, series, series_func, opt)
@@ -298,7 +500,7 @@ function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend})
axis.contents[end] isa PGFPlotsX.LegendEntry ? axis.contents[end - 1] :
axis.contents[end]
merge!(last_plot.options, Options(extra_series_opt...))
- if extra_series !== nothing
+ if extra_series ≢ nothing
push!(axis.contents[end], wraptuple(extra_series)...)
end
# add series annotations
@@ -339,7 +541,7 @@ end
## seriestype specifics
function pgfx_add_series!(axis, series_opt, series, series_func, opt)
series_opt = series_func(series_opt, Table(pgfx_series_arguments(series, opt)...))
- pgfx_add_legend!(push!(axis, series_opt), series, opt)
+ return pgfx_add_legend!(push!(axis, series_opt), series, opt)
end
function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, opt)
@@ -348,7 +550,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o
for (k, segment) in enumerate(segments)
i, rng = segment.attr_index, segment.range
segment_opt = pgfx_linestyle(opt, i)
- if opt[:markershape] !== :none
+ if opt[:markershape] ≢ :none
if (marker = _cycle(opt[:markershape], i)) isa Shape
scale_factor = 0.00125
msize = opt[:markersize] * scale_factor
@@ -369,10 +571,10 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o
segment_opt = merge(segment_opt, pgfx_marker(opt, i))
end
# add fillrange
- if (sf = opt[:fillrange]) !== nothing && !isfilledcontour(series)
+ if (sf = opt[:fillrange]) ≢ nothing && !isfilledcontour(series)
if sf isa Number || sf isa AVec
pgfx_fillrange_series!(axis, series, series_func, i, _cycle(sf, rng), rng)
- elseif sf isa Tuple && series[:ribbon] !== nothing
+ elseif sf isa Tuple && series[:ribbon] ≢ nothing
for sfi in sf
pgfx_fillrange_series!(
axis,
@@ -385,10 +587,10 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o
end
end
if (
- i == 1 &&
- series[:subplot][:legend_position] !== :none &&
- pgfx_should_add_to_legend(series)
- )
+ i == 1 &&
+ series[:subplot][:legend_position] ≢ :none &&
+ pgfx_should_add_to_legend(series)
+ )
pgfx_filllegend!(series_opt, opt)
end
end
@@ -407,19 +609,21 @@ 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)],
- ])
+ 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)],
+ ]
+ )
arrow_plot = series_func(merge(series_opt, arrow_opt), coords)
push!(series_opt, "forget plot" => nothing)
push!(axis, arrow_plot)
@@ -429,7 +633,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!(
@@ -456,7 +660,7 @@ function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, o
),
)
end
- nothing
+ return nothing
end
pgfx_add_series!(::Val{:straightline}, args...) = pgfx_add_series!(Val(:path), args...)
@@ -464,12 +668,12 @@ pgfx_add_series!(::Val{:path3d}, args...) = pgfx_add_series!(Val(:path), args...
function pgfx_add_series!(::Val{:scatter}, axis, series_opt, args...)
push!(series_opt, "only marks" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:scatter3d}, axis, series_opt, args...)
push!(series_opt, "only marks" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:surface}, axis, series_opt, series, series_func, opt)
@@ -481,12 +685,12 @@ function pgfx_add_series!(::Val{:surface}, axis, series_opt, series, series_func
"z buffer" => "sort",
"opacity" => something(get_fillalpha(series), 1.0),
)
- pgfx_add_series!(axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:wireframe}, axis, series_opt, series, series_func, opt)
push!(series_opt, "mesh" => nothing, "mesh/rows" => length(opt[:x]))
- pgfx_add_series!(axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:heatmap}, axis, series_opt, series, series_func, opt)
@@ -504,28 +708,30 @@ function pgfx_add_series!(::Val{:heatmap}, axis, series_opt, series, series_func
for arg in args
arg[(!isfinite).(arg)] .= 0
end
- table = Table([
- "x" => ispolar(series) ? rad2deg.(args[1]) : args[1],
- "y" => args[2],
- "z" => args[3],
- "meta" => meta,
- ])
+ table = Table(
+ [
+ "x" => ispolar(series) ? rad2deg.(args[1]) : args[1],
+ "y" => args[2],
+ "z" => args[3],
+ "meta" => meta,
+ ]
+ )
push!(axis, series_func(series_opt, table))
- pgfx_add_legend!(axis, series, opt)
+ return pgfx_add_legend!(axis, series, opt)
end
function pgfx_add_series!(::Val{:mesh3d}, axis, series_opt, series, series_func, opt)
- ptable = if (cns = opt[:connections]) isa Tuple{Array,Array,Array} # 0-based indexing
+ ptable = if (cns = opt[:connections]) isa Tuple{Array, Array, Array} # 0-based indexing
map((i, j, k) -> "$i $j $k\\\\", cns...)
- elseif typeof(cns) <: AVec{NTuple{3,Int}} # 1-based indexing
+ elseif typeof(cns) <: AVec{NTuple{3, Int}} # 1-based indexing
map(c -> "$(c[1] - 1) $(c[2] - 1) $(c[3] - 1)\\\\", cns)
else
"""
Argument connections has to be either a tuple of three arrays (0-based indexing)
or an AbstractVector{NTuple{3,Int}} (1-based indexing).
""" |>
- ArgumentError |>
- throw
+ ArgumentError |>
+ throw
end
push!(
series_opt,
@@ -533,7 +739,7 @@ function pgfx_add_series!(::Val{:mesh3d}, axis, series_opt, series, series_func,
"table/row sep" => "\\\\",
"patch table" => join(ptable, "\n "),
)
- pgfx_add_series!(axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:contour}, axis, series_opt, series, series_func, opt)
@@ -542,7 +748,7 @@ function pgfx_add_series!(::Val{:contour}, axis, series_opt, series, series_func
pgfx_add_series!(Val(:filledcontour), axis, series_opt, series, series_func, opt)
return nothing
end
- pgfx_add_series!(Val(:contour3d), axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(Val(:contour3d), axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:filledcontour}, axis, series_opt, series, series_func, opt)
@@ -557,7 +763,7 @@ function pgfx_add_series!(::Val{:filledcontour}, axis, series_opt, series, serie
elseif levels isa AVec
push!(series_opt["contour filled"], "levels" => levels)
end
- pgfx_add_series!(axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:contour3d}, axis, series_opt, series, series_func, opt)
@@ -569,11 +775,11 @@ function pgfx_add_series!(::Val{:contour3d}, axis, series_opt, series, series_fu
Table(Contour.contours(pgfx_series_arguments(series, opt)..., opt[:levels])),
),
)
- pgfx_add_legend!(axis, series, opt)
+ return pgfx_add_legend!(axis, series, opt)
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 +789,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]]
@@ -592,41 +798,41 @@ function pgfx_add_series!(::Val{:quiver}, axis, series_opt, series, series_func,
end
pgfx_add_legend!(push!(axis, series_func(series_opt, Table(table))), series, opt)
end
- nothing
+ return nothing
end
function pgfx_add_series!(::Val{:shape}, axis, series_opt, series, series_func, opt)
series_opt = merge(push!(series_opt, "area legend" => nothing), pgfx_fillstyle(opt))
- pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt)
+ return pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt)
end
function pgfx_add_series!(::Val{:steppre}, axis, series_opt, args...)
push!(series_opt, "const plot mark right" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:stepmid}, axis, series_opt, args...)
push!(series_opt, "const plot mark mid" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:steppost}, axis, series_opt, args...)
push!(series_opt, "const plot" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:ysticks}, axis, series_opt, args...)
push!(series_opt, "const plot" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), axis, series_opt, args...)
end
function pgfx_add_series!(::Val{:xsticks}, axis, series_opt, args...)
push!(series_opt, "const plot" => nothing)
- pgfx_add_series!(Val(:path), axis, series_opt, args...)
+ return pgfx_add_series!(Val(:path), 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
@@ -640,28 +846,28 @@ function pgfx_add_legend!(axis, series, opt, i = 1)
push!(axis, PGFPlotsX.LegendEntry(Options(), leg_entry, false))
end
end
- nothing
+ return nothing
end
pgfx_series_arguments(series, opt, range) =
map(a -> a[range], pgfx_series_arguments(series, opt))
pgfx_series_arguments(series, opt) =
- if (st = series[:seriestype]) in (:contour, :contour3d)
- opt[:x], opt[:y], handle_surface(opt[:z])
- elseif st in (:heatmap, :surface, :wireframe)
- 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 ispolar(series)
- theta, r = opt[:x], opt[:y]
- rad2deg.(theta), r
- else
- opt[:x], opt[:y]
- end
+if (st = series[:seriestype]) in (:contour, :contour3d)
+ opt[:x], opt[:y], handle_surface(opt[:z])
+elseif st in (:heatmap, :surface, :wireframe)
+ surface_to_vecs(opt[:x], opt[:y], opt[:z])
+elseif RecipesPipeline.is3d(st)
+ opt[:x], opt[:y], opt[:z]
+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
+else
+ opt[:x], opt[:y]
+end
pgfx_get_linestyle(k::AbstractString) = pgfx_get_linestyle(Symbol(k))
pgfx_get_linestyle(k::Symbol) = get(
@@ -724,7 +930,7 @@ pgfx_get_yguide_pos(k::Symbol) = get(
)
pgfx_get_legend_pos(k::AbstractString) = pgfx_get_legend_pos(Symbol(k))
-pgfx_get_legend_pos(t::Tuple{<:Real,<:Real}) = ("at" => curly(t), "anchor" => "north west")
+pgfx_get_legend_pos(t::Tuple{<:Real, <:Real}) = ("at" => curly(t), "anchor" => "north west")
pgfx_get_legend_pos(nt::NamedTuple) = ("at" => curly(nt.at), "anchor" => string(nt.anchor))
pgfx_get_legend_pos(theta::Real) = pgfx_get_legend_pos((theta, :inner))
pgfx_get_legend_pos(k::Symbol) = get(
@@ -749,7 +955,7 @@ pgfx_get_legend_pos(k::Symbol) = get(
k,
("at" => "(1.02, 1)", "anchor" => "north west"),
)
-function pgfx_get_legend_pos(v::Tuple{<:Real,Symbol})
+function pgfx_get_legend_pos(v::Tuple{<:Real, Symbol})
s, c = sincosd(first(v))
anchors = [
"south west" "south" "south east"
@@ -757,12 +963,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 +1026,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 +1040,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,27 +1070,27 @@ 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
function pgfx_filllegend!(series_opt, opt)
style = strip(sprint(PGFPlotsX.print_tex, pgfx_fillstyle(opt)), ['[', ']', ' '])
- push!(
+ return push!(
series_opt,
"legend image code/.code" => "{\n\\draw[$style] (0cm,-0.1cm) rectangle (0.6cm,0.1cm);\n}",
)
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)",
@@ -897,23 +1104,23 @@ pgfx_colormap(cg::ColorGradient) = join(
)
pgfx_framestyle(style::Symbol) =
- if style in (:box, :axes, :origin, :zerolines, :grid, :none)
- style
- else
- 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
+if style in (:box, :axes, :origin, :zerolines, :grid, :none)
+ style
+else
+ default_style = style ≡ :semi ? :box : :axes
+ @maxlog_warn "Framestyle :$style is not (yet) supported by the PGFPlotsX backend. :$default_style was chosen instead."
+ default_style
+end
pgfx_thickness_scaling(plt::Plot) = plt[:thickness_scaling]
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)
+ return Options("fill" => get_fillcolor(plotattributes, i), "fill opacity" => a)
end
function pgfx_linestyle(linewidth::Real, color, α = 1, linestyle = :solid)
@@ -926,7 +1133,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)
@@ -950,17 +1157,17 @@ pgfx_font(fontsize::Nothing, thickness_scaling = 1, font = "\\selectfont") = cur
pgfx_should_add_to_legend(series::Series) =
series.plotattributes[:primary] &&
series.plotattributes[:seriestype] ∉ (
- :hexbin,
- :bins2d,
- :histogram2d,
- :hline,
- :vline,
- :contour,
- :contourf,
- :contour3d,
- :heatmap,
- :image,
- )
+ :hexbin,
+ :bins2d,
+ :histogram2d,
+ :hline,
+ :vline,
+ :contour,
+ :contourf,
+ :contour3d,
+ :heatmap,
+ :image,
+)
function pgfx_marker(plotattributes, i = 1)
shape = _cycle(plotattributes[:markershape], i)
@@ -992,11 +1199,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
@@ -1008,13 +1215,13 @@ function pgfx_marker(plotattributes, i = 1)
end
function pgfx_add_annotation!(
- o,
- pos,
- val,
- thickness_scaling = 1;
- cs = "axis cs:",
- options = Options(),
-)
+ o,
+ pos,
+ val,
+ thickness_scaling = 1;
+ cs = "axis cs:",
+ options = Options(),
+ )
# Construct the style string.
cstr = val.font.color
ann_opt = merge(
@@ -1030,7 +1237,7 @@ function pgfx_add_annotation!(
),
options,
)
- push!(
+ return push!(
o,
"\\node$(sprint(PGFPlotsX.print_tex, ann_opt)) at ($(cs)$(join(pos, ','))) {$(val.str)};",
)
@@ -1049,22 +1256,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]]
@@ -1077,7 +1284,7 @@ pgfx_sanitize_string(s::LaTeXString) = LaTeXString(replace(s, r"\\?([#%])" => s"
function pgfx_sanitize_string(s::AbstractString)
# regular latex text with the following special characters won't compile if not sanitized (escaped)
sanitized = replace(s, r"\\?([#%_&\{\}\$])" => s"\\\1")
- map(collect(sanitized)) do c
+ return map(collect(sanitized)) do c
if isascii(c)
c
else
@@ -1088,24 +1295,24 @@ end
function pgfx_sanitize_plot!(plt)
for (key, value) in plt.attr
- if value isa Union{AbstractString,AVec{<:AbstractString}}
+ 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
+ if key ≡ :annotations && subplot.attr[:annotations] ≢ nothing
old_ann = subplot.attr[key]
for i in eachindex(old_ann)
# [1:end-1] is a tuple of coordinates, [end] - text
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}}
+ elseif value isa Union{AbstractString, AVec{<:AbstractString}}
subplot.attr[key] = pgfx_sanitize_string.(value)
- elseif value isa Axis
+ elseif value isa PlotsBase.Axis
for (k, v) in value.plotattributes
- if v isa Union{AbstractString,AVec{<:AbstractString}}
+ if v isa Union{AbstractString, AVec{<:AbstractString}}
value.plotattributes[k] = pgfx_sanitize_string.(v)
end
end
@@ -1114,22 +1321,23 @@ function pgfx_sanitize_plot!(plt)
end
for series in plt.series_list
for (key, value) in series.plotattributes
- if key === :series_annotations &&
- series.plotattributes[:series_annotations] !== nothing
+ if key ≡ :series_annotations &&
+ series.plotattributes[:series_annotations] ≢ nothing
old_ann = series.plotattributes[key].strs
for i in eachindex(old_ann)
series.plotattributes[key].strs[i] = pgfx_sanitize_string(old_ann[i])
end
- elseif value isa Union{AbstractString,AVec{<:AbstractString}}
+ elseif value isa Union{AbstractString, AVec{<:AbstractString}}
series.plotattributes[key] = pgfx_sanitize_string.(value)
end
end
end
+ return
end
pgfx_is_inline_math(lab) = (
(startswith(lab, '$') && endswith(lab, '$')) ||
- (startswith(lab, "\\(") && endswith(lab, "\\)"))
+ (startswith(lab, "\\(") && endswith(lab, "\\)"))
)
# surround the power part of label with curly braces
@@ -1137,7 +1345,7 @@ function wrap_power_label(label::AbstractString)
pgfx_is_inline_math(label) && return label # already in `mathmode` form
occursin('^', label) || return label
base, power = split(label, '^')
- "$base^$(curly(power))"
+ return "$base^$(curly(power))"
end
wrap_power_labels(labels::AVec{LaTeXString}) = labels
@@ -1146,7 +1354,7 @@ function wrap_power_labels(labels::AVec{<:AbstractString})
for (i, label) in enumerate(labels)
new_labels[i] = wrap_power_label(label)
end
- new_labels
+ return new_labels
end
# --------------------------------------------------------------------------------------
@@ -1158,7 +1366,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter)
push!(
opt,
"scaled $(letter) ticks" => "false",
- "$(letter)label" => axis[:guide],
+ "$(letter)label" => PlotsBase.get_guide(axis),
"$(letter) tick style" =>
Options("color" => color(tick_color), "opacity" => alpha(tick_color)),
"$(letter) tick label style" => Options(
@@ -1172,9 +1380,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 +1410,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 +1449,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 +1463,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 +1483,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 +1514,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 +1523,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,
@@ -1332,56 +1540,67 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter)
opt,
"$letter axis line style" =>
if !axis[:showaxis] || framestyle in (:zerolines, :grid, :none)
- "{draw opacity = 0}"
- else
- pgfx_linestyle(
- pgfx_thickness_scaling(sp),
- axis[:foreground_color_border],
- 1,
- )
- end,
+ "{draw opacity = 0}"
+ else
+ pgfx_linestyle(
+ pgfx_thickness_scaling(sp),
+ axis[:foreground_color_border],
+ 1,
+ )
+ end,
)
- nothing
+ return nothing
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})
- sp.minpad =
- if (leg = sp[:legend_position]) in
- (:best, :outertopright, :outerright, :outerbottomright) ||
- (leg isa Tuple && leg[1] >= 1)
- (0mm, 0mm, 5mm, 0mm)
- else
- (0mm, 0mm, 0mm, 0mm)
- end
+PlotsBase._update_min_padding!(sp::Subplot{PGFPlotsXBackend}) = sp.minpad =
+if (leg = sp[:legend_position]) in
+ (:best, :outertopright, :outerright, :outerbottomright) ||
+ (leg isa Tuple && leg[1] ≥ 1)
+ (0mm, 0mm, 5mm, 0mm)
+else
+ (0mm, 0mm, 0mm, 0mm)
end
-_create_backend_figure(plt::Plot{PGFPlotsXBackend}) = plt.o = PGFPlotsXPlot()
+PlotsBase._create_backend_figure(plt::Plot{PGFPlotsXBackend}) = plt.o = PGFPlotsXPlot()
-_series_added(plt::Plot{PGFPlotsXBackend}, series::Series) = plt.o.is_created = false
+PlotsBase._series_added(plt::Plot{PGFPlotsXBackend}, series::Series) =
+ plt.o.is_created = false
-_update_plot_object(plt::Plot{PGFPlotsXBackend}) = plt.o(plt)
+PlotsBase._update_plot_object(plt::Plot{PGFPlotsXBackend}) = plt.o(plt)
for mime in ("application/pdf", "image/svg+xml", "image/png")
- @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{PGFPlotsXBackend})
+ @eval function PlotsBase._show(
+ io::IO,
+ mime::MIME{Symbol($mime)},
+ plt::Plot{PGFPlotsXBackend},
+ )
plt.o.was_shown = true
- show(io, mime, plt.o.the_plot)
+ return 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(
+ return PGFPlotsX.print_tex(
io,
plt.o.the_plot,
include_preamble = plt.attr[:tex_output_standalone],
)
end
-function _display(plt::Plot{PGFPlotsXBackend})
+function PlotsBase._display(plt::Plot{PGFPlotsXBackend})
plt.o.was_shown = true
- display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot)
+ return display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot)
+end
+
+PlotsBase.@precompile_backend PGFPlotsX
+
end
diff --git a/PlotsBase/ext/PlotlyJSExt.jl b/PlotsBase/ext/PlotlyJSExt.jl
new file mode 100644
index 0000000000..91ab445ffb
--- /dev/null
+++ b/PlotsBase/ext/PlotlyJSExt.jl
@@ -0,0 +1,87 @@
+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
+
+function PlotsBase.extension_init(::PlotlyJSBackend)
+ return if Base.get_bool_env("PLOTSBASE_PLOTLYJS_UNSAFE_ELECTRON", false)
+ (Sys.islinux() && isdefined(PlotlyJS, :unsafe_electron)) &&
+ PlotlyJS.unsafe_electron()
+ end
+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 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)
+ return plt.o
+end
+
+# ------------------------------------------------------------------------------
+
+for (mime, fmt) in (
+ "application/pdf" => "pdf",
+ "image/png" => "png",
+ "image/svg+xml" => "svg",
+ "image/eps" => "eps",
+ )
+ @eval PlotsBase._show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{PlotlyJSBackend}) =
+ PlotlyJS.savefig(io, plotlyjs_syncplot(plt), format = $fmt)
+end
+
+# Use the Plotly implementation for json and html:
+PlotsBase._show(
+ io::IO,
+ mime::MIME"application/vnd.plotly.v1+json",
+ plt::Plot{PlotlyJSBackend},
+) = plotly_show_js(io, plt)
+
+PlotsBase.html_head(plt::Plot{PlotlyJSBackend}) = PlotsBase.Plotly.plotly_html_head(plt)
+PlotsBase.html_body(plt::Plot{PlotlyJSBackend}) = PlotsBase.Plotly.plotly_html_body(plt)
+
+PlotsBase._show(io::IO, ::MIME"text/html", plt::Plot{PlotlyJSBackend}) =
+ write(io, PlotsBase.embeddable_html(plt))
+
+PlotsBase._display(plt::Plot{PlotlyJSBackend}) = display(plotlyjs_syncplot(plt))
+
+WebIO.render(plt::Plot{PlotlyJSBackend}) = WebIO.render(plotlyjs_syncplot(plt))
+
+PlotsBase.closeall(::PlotlyJSBackend) =
+if !PlotsBase.isplotnull() && isa(PlotsBase.current().o, PlotlyJS.SyncPlot)
+ close(PlotsBase.current().o)
+end
+
+Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyJSBackend}) = true
+
+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))
+ return out
+end
+
+PlotsBase.@precompile_backend PlotlyJS
+
+end
diff --git a/PlotsBase/ext/PlotlyKaleidoExt.jl b/PlotsBase/ext/PlotlyKaleidoExt.jl
new file mode 100644
index 0000000000..9431dcb5dc
--- /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()
+ return atexit() do
+ PlotlyKaleido.kill_kaleido()
+ end
+end
+
+for (mime, fmt) in (
+ "application/pdf" => "pdf",
+ "image/svg+xml" => "svg",
+ "image/png" => "png",
+ "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
diff --git a/src/backends/pythonplot.jl b/PlotsBase/ext/PythonPlotExt.jl
similarity index 68%
rename from src/backends/pythonplot.jl
rename to PlotsBase/ext/PythonPlotExt.jl
index 38ef525748..4b039a7524 100644
--- a/src/backends/pythonplot.jl
+++ b/PlotsBase/ext/PythonPlotExt.jl
@@ -1,28 +1,228 @@
-# 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 in (:linthresh, :base, :label)
+ Commons.new_attr_dict!(keyword)
+ for letter in (:x, :y, :z, Symbol(), :top, :bottom, :left, :right)
+ Commons.set_attr_symbol!(keyword, string(letter))
+ end
+ end
+ if PythonPlot.version < v"3.4"
+ @maxlog_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
+ return 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,18 +232,18 @@ _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)
- cm
+ return cm
end
function _py_colormap(cg::PlotUtils.CategoricalColorGradient)
r = range(0, 1; length = 256)
pyvals = collect(zip(r, _py_color(cg[r])))
cm = mpl.colors.LinearSegmentedColormap.from_list("tmp", pyvals)
cm.set_bad(color = (0, 0, 0, 0.0), alpha = 0.0)
- cm
+ return cm
end
_py_colormap(c) = _py_colormap(_as_gradient(c))
@@ -56,13 +256,13 @@ _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 "-."
- @warn "Unknown linestyle $linestyle"
- "-"
+ seriestype ≡ :none && return " "
+ linestyle ≡ :solid && return "-"
+ linestyle ≡ :dash && return "--"
+ linestyle ≡ :dot && return ":"
+ linestyle ≡ :dashdot && return "-."
+ @maxlog_warn "Unknown linestyle $linestyle"
+ return "-"
end
function _py_marker(marker::Shape)
@@ -74,56 +274,57 @@ function _py_marker(marker::Shape)
mat[i, 2] = y[i]
end
mat[n + 1, :] = @view mat[1, :]
- mpl.path.Path(mat)
+ return mpl.path.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"
- "o"
+ 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
+ @maxlog_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 PythonPlot: $markers"
- markers |> first |> _py_marker
+ @maxlog_warn "Vectors of markers are currently unsupported in PythonPlot: $markers"
+ return markers |> first |> _py_marker
end
# pass through
function _py_marker(marker::AbstractString)
@assert length(marker) == 1
- marker
+ return marker
end
function _py_stepstyle(seriestype::Symbol)
- seriestype === :steppost && return "steps-post"
- seriestype === :stepmid && return "steps-mid"
- seriestype === :steppre && return "steps-pre"
- "default"
+ 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"
- nothing
+ seriestype ≡ :steppost && return "post"
+ seriestype ≡ :stepmid && return "mid"
+ seriestype ≡ :steppre && return "pre"
+ return nothing
end
_py_fillstyle(::Nothing) = nothing
@@ -132,17 +333,17 @@ _py_fillstyle(fillstyle::Symbol) = string(fillstyle)
function _py_get_matching_math_font(parent_fontfamily)
# matplotlib supported math fonts according to
# matplotlib.org/stable/tutorials/text/mathtext.html
- _py_math_supported_fonts = Dict{String,String}(
- "serif" => "dejavuserif",
+ _py_math_supported_fonts = Dict{String, String}(
+ "serif" => "dejavuserif",
"sans-serif" => "dejavusans",
- "stixsans" => "stixsans",
- "stix" => "stix",
- "cm" => "cm",
+ "stixsans" => "stixsans",
+ "stix" => "stix",
+ "cm" => "cm",
)
# 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))
+ return get(_py_math_supported_fonts, parent_fontfamily, matching_font(parent_fontfamily))
end
get_locator_and_formatter(vals::AVec) =
@@ -156,7 +357,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
+ return if (x = series[:x]) ≢ nothing
y = series[:y]
nx, ny = length(x), length(y)
if !(get(series.plotattributes, :z, nothing) isa Surface || nx == ny)
@@ -177,23 +378,23 @@ is_valid_cgrad_color(::Symbol) = true
is_valid_cgrad_color(::Any) = false
_py_linecolormap(series::Series) =
- if (color = get(series, :linecolor, nothing)) |> is_valid_cgrad_color
- _py_colormap(cgrad(color, alpha = get_linealpha(series)))
- else
- nothing
- end
+if (color = get(series, :linecolor, nothing)) |> is_valid_cgrad_color
+ _py_colormap(cgrad(color, alpha = get_linealpha(series)))
+else
+ nothing
+end
_py_fillcolormap(series::Series) =
- if (color = get(series, :fillcolor, nothing)) |> is_valid_cgrad_color
- _py_colormap(cgrad(color, alpha = get_fillalpha(series)))
- else
- nothing
- end
+if (color = get(series, :fillcolor, nothing)) |> is_valid_cgrad_color
+ _py_colormap(cgrad(color, alpha = get_fillalpha(series)))
+else
+ nothing
+end
_py_markercolormap(series::Series) =
- if (color = get(series, :markercolor, nothing)) |> is_valid_cgrad_color
- _py_colormap(cgrad(color, alpha = get_markeralpha(series)))
- else
- nothing
- end
+if (color = get(series, :markercolor, nothing)) |> is_valid_cgrad_color
+ _py_colormap(cgrad(color, alpha = get_markeralpha(series)))
+else
+ nothing
+end
# ---------------------------------------------------------------------------
# Figure utils -- F*** matplotlib for making me work so hard to figure this crap out
@@ -220,9 +421,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
+ return BoundingBox(x0, y0, w, h)
end
_py_bbox(::Nothing) = BoundingBox(0mm, 0mm)
@@ -233,19 +434,19 @@ function _py_bbox(v::AVec)
for obj in v
bbox_union += _py_bbox(obj)
end
- bbox_union
+ return bbox_union
end
-get_axis(l::Symbol, x::Union{Symbol,AbstractString}) = Symbol(:get_, l, x)
-set_axis(l::Symbol, x::Union{Symbol,AbstractString}) = Symbol(:set_, l, x)
+get_axis(l::Symbol, x::Union{Symbol, AbstractString}) = Symbol(:get_, l, x)
+set_axis(l::Symbol, x::Union{Symbol, AbstractString}) = Symbol(:set_, l, x)
# bounding box: union of axis tick labels
_py_bbox_ticks(ax, letter) =
- if to_str(ax.name) == "3d"
- _py_bbox(nothing) # FIXME: broken in `3d` (huge extents)
- else
- getproperty(ax, get_axis(letter, :ticklabels))() |> to_vec |> _py_bbox
- end
+if to_str(ax.name) == "3d"
+ _py_bbox(nothing) # FIXME: broken in `3d` (huge extents)
+else
+ getproperty(ax, get_axis(letter, :ticklabels))() |> to_vec |> _py_bbox
+end
# bounding box: axis guide
_py_bbox_axislabel(ax, letter) =
@@ -255,7 +456,7 @@ _py_bbox_axislabel(ax, letter) =
function _py_bbox_axis(ax, letter)
ticks = _py_bbox_ticks(ax, letter)
labels = _py_bbox_axislabel(ax, letter)
- ticks + labels
+ return ticks + labels
end
# bounding box: axis title
@@ -264,7 +465,7 @@ function _py_bbox_title(ax)
for s in (:title, :_left_title, :_right_title)
bb += _py_bbox(getproperty(ax, s))
end
- bb
+ return bb
end
# bounding box: legend
@@ -274,10 +475,10 @@ _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?
- plt[:overwrite_figure] ? PythonPlot.gcf() : PythonPlot.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 ?
+ return plt[:overwrite_figure] ? PythonPlot.gcf() : PythonPlot.figure()
end
# Set up the subplot within the backend object.
@@ -296,13 +497,13 @@ function _py_init_subplot(plt::Plot{PythonPlotBackend}, sp::Subplot{PythonPlotBa
orthographic = "ortho",
persp = "persp",
perspective = "persp",
- )[sp[:projection_type]]
+ )[sp[:projection_type]],
)
else
(;)
end
# add a new axis, and force it to create a new one by setting a distinct label
- sp.o = fig.add_subplot(; label = string(gensym()), projection, kw...)
+ return sp.o = fig.add_subplot(; label = string(gensym()), projection, kw...)
end
# ---------------------------------------------------------------------------
@@ -323,10 +524,10 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
# ax = getAxis(plt, series)
x, y, z = (_py_handle_surface(series[letter]) for letter in (:x, :y, :z))
- if st === :straightline
- x, y = straightline_data(series)
- elseif st === :shape
- x, y = shape_data(series)
+ 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)
@@ -337,14 +538,14 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
end
end
- xyargs = st ∈ _3dTypes ? (x, y, z) : (x, y)
+ xyargs = st ∈ Commons._3dTypes ? (x, y, z) : (x, y)
# handle zcolor and get c/cmap
needs_colorbar = hascolorbar(sp)
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()
@@ -368,11 +569,11 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
cbar_scale = sp[:colorbar_scale]
linewidths = linewidth = _py_thickness_scale(plt, get_linewidth(series))
linestyles = linestyle = _py_linestyle(st, get_linestyle(series))
- edgecolor = edgecolors = _py_color(get_linecolor(series, 1, cbar_scale))
- facecolor = facecolors = _py_color(series[:fillcolor])
- zorder = series[:series_plotindex]
- alpha = get_fillalpha(series)
- label = series[:label]
+ edgecolor = edgecolors = _py_color(get_linecolor(series, 1, cbar_scale))
+ facecolor = facecolors = _py_color(series[:fillcolor])
+ zorder = series[:series_plotindex]
+ alpha = get_fillalpha(series)
+ label = series[:label]
# add lines ?
if st ∈ _py_line_series && maximum(series[:linewidth]) > 0
@@ -394,18 +595,18 @@ 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))"
+ @maxlog_warn "Unexpected type for arrow: $(typeof(a))"
else
arrowprops = Dict(
"arrowstyle" => "simple,head_length=$(a.headlength),head_width=$(a.headwidth)",
- "edgecolor" => edgecolor,
- "facecolor" => edgecolor,
- "linewidth" => linewidth,
- "linestyle" => linestyle,
- "shrinkA" => 0,
- "shrinkB" => 0,
+ "edgecolor" => edgecolor,
+ "facecolor" => edgecolor,
+ "linewidth" => linewidth,
+ "linestyle" => linestyle,
+ "shrinkA" => 0,
+ "shrinkB" => 0,
)
add_arrows(x, y) do xyprev, xy
ax.annotate(
@@ -421,14 +622,10 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
end
# add markers ?
- if series[:markershape] !== :none && st ∈ _py_marker_series
+ if series[:markershape] ≢ :none && st ∈ _py_marker_series
for segment in series_segments(series, :scatter)
i, rng = segment.attr_index, segment.range
- args = if st === :bar && !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 +647,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 in series_segments(series, st)
i, rng = segment.attr_index, segment.range
if length(rng) > 1
lc = get_linecolor(series, clims, i, cbar_scale)
@@ -463,8 +660,6 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
has_fs = !isnothing(fs)
path = mpl.path.Path(hcat(x[rng], y[rng]))
- # FIXME: path can be un-filled e.g. ex 56,
- # where rectangles are created using 4 paths instead of a single one
# shape outline (and potentially solid fill)
mpl.patches.PathPatch(
@@ -477,8 +672,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
zorder,
label,
) |>
- ax.add_patch |>
- push_h
+ ax.add_patch |>
+ push_h
# shape hatched fill - hatch color/alpha are controlled by edge (not face) color/alpha
if has_fs
mpl.patches.PathPatch(
@@ -492,19 +687,19 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
fill = false,
zorder,
) |>
- ax.add_patch |>
- push_h
+ ax.add_patch |>
+ push_h
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)
z = if eltype(z) <: Colors.AbstractGray
float(z)
elseif eltype(z) <: Colorant
- rgba = Array{Float64,3}(undef, size(z)..., 4)
+ rgba = Array{Float64, 3}(undef, size(z)..., 4)
rgba[:, :, 1] = Colors.red.(z)
rgba[:, :, 2] = Colors.green.(z)
rgba[:, :, 3] = Colors.blue.(z)
@@ -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,17 +737,17 @@ 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
map(inds -> map(i -> [x[i], y[i], z[i]], inds), cns)
- elseif cns isa AbstractVector{NTuple{N,<:Integer}} where {N}
+ elseif cns isa AbstractVector{NTuple{N, <:Integer}} where {N}
# Only N-gons - connections have to be 1-based (indexing)
map(inds -> map(i -> [x[i], y[i], z[i]], inds), cns)
- elseif cns isa NTuple{3,<:AbstractVector{<:Integer}}
+ 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
@@ -569,8 +764,8 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
polys
else
"Unsupported `:connections` type $(typeof(series[:connections])) for seriestype=$st" |>
- ArgumentError |>
- throw
+ ArgumentError |>
+ throw
end
mpl_toolkits.mplot3d.art3d.Poly3DCollection(
polygons;
@@ -580,9 +775,9 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
zorder,
alpha,
) |>
- ax.add_collection3d |>
- push_h
- elseif st === :hexbin
+ ax.add_collection3d |>
+ push_h
+ 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,27 +817,27 @@ 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,
- z,
- levelargs...;
- zorder = zorder + 0.5,
- label,
- alpha,
- extrakw...,
- ) |> push_h
+ x,
+ y,
+ z,
+ levelargs...;
+ zorder = zorder + 0.5,
+ label,
+ alpha,
+ extrakw...,
+ ) |> push_h
elseif st ∈ (: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
+ 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]))
@@ -702,16 +897,12 @@ 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 in series_segments(series, st)
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}
+ 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)
@@ -743,19 +934,20 @@ function _py_add_series(plt::Plot{PythonPlotBackend}, series::Series)
for (xi, yi, str, fnt) in EachAnn(series[:series_annotations], x, y)
_py_add_annotations(sp, xi, yi, PlotText(str, fnt))
end
+ return
end
# --------------------------------------------------------------------------
_py_set_lims(ax, sp::Subplot, axis::Axis) =
- let letter = axis[:letter]
- getproperty(ax, set_axis(letter, :lim))(axis_limits(sp, letter)...)
- end
+let letter = axis[:letter]
+ getproperty(ax, set_axis(letter, :lim))(axis_limits(sp, letter)...)
+end
function _py_set_ticks(sp, ax, ticks, letter)
- ticks === :auto && return
+ ticks ≡ :auto && return
axis = getproperty(ax, get_attr_symbol(letter, :axis))
- if ticks === :none || ticks === nothing || ticks == false
+ if ticks ≡ :none || ticks ≡ nothing || ticks == false
kw = KW()
for dir in (:top, :bottom, :left, :right)
kw[dir] = kw[get_attr_symbol(:label, dir)] = false
@@ -764,16 +956,16 @@ function _py_set_ticks(sp, ax, ticks, letter)
return
end
- tick_values, tick_labels = if (ttype = ticksType(ticks)) === :ticks
+ tick_values, tick_labels = if (ttype = ticks_type(ticks)) ≡ :ticks
ticks, []
- elseif ttype === :ticks_and_labels
+ elseif ttype ≡ :ticks_and_labels
ticks
else
error("Invalid input for $(letter)ticks: $ticks")
end
length(tick_values) > 0 && axis.set_ticks(tick_values)
length(tick_labels) > 0 && axis.set_ticklabels(tick_labels)
- nothing
+ return nothing
end
function _py_compute_axis_minval(sp::Subplot, axis::Axis)
@@ -786,24 +978,25 @@ function _py_compute_axis_minval(sp::Subplot, axis::Axis)
# 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))
+ return NaNMath.min(minval, abs(vmin), abs(vmax))
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 @maxlog_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(:linthresh, Symbol()) => NaNMath.max(
- 1e-16,
- _py_compute_axis_minval(sp, sp[get_attr_symbol(letter, :axis)]),
- ),
- )
+ KW(
+ get_attr_symbol(:base, Symbol()) => Commons._log_scale_bases[scale],
+ get_attr_symbol(:linthresh, Symbol()) => NaNMath.max(
+ 1.0e-16,
+ _py_compute_axis_minval(sp, sp[get_attr_symbol(letter, :axis)]),
+ ),
+ )
end
- getproperty(ax, set_axis(letter, :scale))(scl; kw...)
+ return getproperty(ax, set_axis(letter, :scale))(scl; kw...)
end
_py_set_scale(ax, sp::Subplot, axis::Axis) =
@@ -816,8 +1009,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)
+ return if hasproperty(ax, axis_sym)
tickcolor =
sp[:framestyle] ∈ (:zerolines, :grid) ?
_py_color(plot_color(a[:foreground_color_grid], a[:gridalpha])) :
@@ -828,7 +1021,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 +1029,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
@@ -856,7 +1049,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
# update subplots
for sp in plt.subplots
- (ax = sp.o) === nothing && continue
+ (ax = sp.o) ≡ nothing && continue
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
# add the annotations
@@ -866,15 +1059,17 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
)
# title
- if (title = sp[:title]) != "" # support symbols
+ if (title = sp[:title]) |> !isempty # support symbols
loc = lowercase(string(sp[:titlelocation]))
- func = getproperty(ax, if loc == "left"
- :_left_title
- elseif loc == "right"
- :_right_title
- else
- :title
- end)
+ func = getproperty(
+ ax, if loc == "left"
+ :_left_title
+ elseif loc == "right"
+ :_right_title
+ else
+ :title
+ end
+ )
func.set_text(string(title))
func.set_fontsize(_py_thickness_scale(plt, sp[:titlefontsize]))
func.set_family(sp[:titlefontfamily])
@@ -890,33 +1085,33 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
cbar_series = slist[findfirst(hascolorbar, slist)]
kw = KW()
handle =
- if !isempty(sp[:zaxis][:discrete_values]) &&
- cbar_series[:seriestype] === :heatmap
- kw[:ticks], kw[:format] =
- get_locator_and_formatter(sp[:zaxis][:discrete_values])
- # kw[:values] = eachindex(sp[:zaxis][:discrete_values])
- kw[:values] = sp[:zaxis][:continuous_values]
- 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)
+ if !isempty(sp[:zaxis][:discrete_values]) &&
+ cbar_series[:seriestype] ≡ :heatmap
+ kw[:ticks], kw[:format] =
+ get_locator_and_formatter(sp[:zaxis][:discrete_values])
+ # kw[:values] = eachindex(sp[:zaxis][:discrete_values])
+ kw[:values] = sp[:zaxis][:continuous_values]
+ 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)
)
- cmin, cmax = get_clims(sp)
- 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
- end
- c_map = mpl.cm.ScalarMappable(; cmap, norm)
- c_map.set_array(PythonPlot.pylist([]))
- c_map
+ cmin, cmax = get_clims(sp)
+ norm = if cbar_scale ≡ :identity
+ mpl.colors.Normalize(vmin = cmin, vmax = cmax)
else
- cbar_series[:serieshandle][end]
+ 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
end
+ c_map = mpl.cm.ScalarMappable(; cmap, norm)
+ c_map.set_array(PythonPlot.pylist([]))
+ c_map
+ else
+ cbar_series[:serieshandle][end]
+ end
kw[:spacing] = "proportional"
cb_sym = sp[:colorbar]
@@ -927,22 +1122,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 +1154,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,8 +1164,7 @@ 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()
lab.set_fontsize(_py_thickness_scale(plt, sp[:colorbar_tickfontsize]))
@@ -984,10 +1178,10 @@ 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 :
- 5_py_thickness_scale(plt, intensity),
+ length = axis[:tick_direction] ≡ :none ? 0 :
+ 5_py_thickness_scale(plt, intensity),
)
cbar.outline.set_linewidth(_py_thickness_scale(plt, 1))
@@ -1004,7 +1198,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
end
# Then set visible some of them
- if framestyle === :semi
+ if framestyle ≡ :semi
intensity = 0.5
pyspine = getproperty(ax.spines, yaxis[:mirror] ? "left" : "right")
@@ -1014,7 +1208,7 @@ 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)
@@ -1022,13 +1216,13 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
(xaxis[:mirror] ? "bottom" : "top", yaxis[:mirror] ? "left" : "right")
getproperty(ax.spines, loc).set_visible(false)
end
- if framestyle === :origin
+ if framestyle ≡ :origin
ax.spines.bottom.set_position("zero")
ax.spines.left.set_position("zero")
end
elseif framestyle ∈ (:grid, :none, :zerolines)
_py_hide_spines(ax)
- if framestyle === :zerolines
+ if framestyle ≡ :zerolines
ax.axhline(
y = 0,
color = _py_color(xaxis[:foreground_color_axis]),
@@ -1044,37 +1238,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)
+ 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,16 +1281,16 @@ 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())
elseif has_major_ticks
fontProperties = Dict(
"math_fontfamily" => _py_get_matching_math_font(axis[:tickfontfamily]),
- "size" => _py_thickness_scale(plt, axis[:tickfontsize]),
- "rotation" => axis[:tickfontrotation],
- "family" => axis[:tickfontfamily],
+ "size" => _py_thickness_scale(plt, axis[:tickfontsize]),
+ "rotation" => axis[:tickfontrotation],
+ "family" => axis[:tickfontfamily],
)
positions = getproperty(ax, get_axis(letter, :ticks))()
pyaxis.set_major_locator(mpl.ticker.FixedLocator(positions))
@@ -1109,16 +1303,16 @@ 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_factor * _py_thickness_scale(plt, intensity),
+ length = axis[:tick_direction] ≡ :none ? 0 :
+ length_factor * _py_thickness_scale(plt, intensity),
)
else
pyaxis.set_major_locator(mpl.ticker.NullLocator())
end
- getproperty(ax, set_axis(letter, :label))(axis[:guide])
+ getproperty(ax, set_axis(letter, :label))(PlotsBase.get_guide(axis))
pyaxis.label.set_fontsize(_py_thickness_scale(plt, axis[:guidefontsize]))
pyaxis.label.set_family(axis[:guidefontfamily])
pyaxis.label.set_math_fontfamily(
@@ -1128,7 +1322,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,19 +1345,19 @@ 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 :
- 0.5length_factor * _py_thickness_scale(plt, intensity),
+ direction = axis[:tick_direction] ≡ :out ? "out" : "in",
+ length = axis[:tick_direction] ≡ :none ? 0 :
+ 0.5length_factor * _py_thickness_scale(plt, intensity),
)
end
@@ -1199,11 +1393,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)
@@ -1235,21 +1429,21 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
ax.sharex(x_ax_link)
end
end # for sp in pl.subplots
- _py_drawfig(fig)
+ return _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
+if ispositive(width(bb)) && ispositive(height(bb))
+ padding[1] = max(padding[1], left(plotbb) - left(bb))
+ padding[2] = max(padding[2], top(plotbb) - top(bb))
+ padding[3] = max(padding[3], right(bb) - right(plotbb))
+ padding[4] = max(padding[4], bottom(bb) - bottom(plotbb))
+end
-# Set the (left, top, right, bottom) minimum padding around the plot area
+# set the (left, top, right, bottom) minimum padding around the plot area
# to fit ticks, tick labels, guides, colorbars, etc.
-function _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
@@ -1257,20 +1451,20 @@ function _update_min_padding!(sp::Subplot{PythonPlotBackend})
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),
- )
+ _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),
- )
+ _py_bbox_axis(cbar_ax, :x),
+ _py_bbox_axis(cbar_ax, :y),
+ _py_bbox_title(cbar_ax),
+ )
expand_padding!(padding, bb, plotbb)
end
end
@@ -1286,7 +1480,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)
+ return sp.minpad = Tuple((DPI / sp.plt[:dpi]) .* padding)
end
# -----------------------------------------------------------------
@@ -1298,8 +1492,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 +1507,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,
@@ -1323,22 +1517,24 @@ _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"
+_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)
+function _py_legend_pos(pos::Tuple{<:Real, Symbol})
+ s, c = sincosd(pos[1]) .* (pos[2] ≡ :outer ? -1 : 1)
yanchors = "lower", "center", "upper"
xanchors = "left", "center", "right"
- join([yanchors[legend_anchor_index(s)], xanchors[legend_anchor_index(c)]], ' ')
+ return 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)
+_py_legend_bbox(pos::Tuple{<:Real, Symbol}) =
+ 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 = [], []
@@ -1350,7 +1546,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax)
clims = get_clims(sp, series)
nseries += 1
# add a line/marker and a label
- if series[:seriestype] === :shape || series[:fillrange] !== nothing
+ if series[:seriestype] ≡ :shape || series[:fillrange] ≢ nothing
lc = get_linecolor(series, clims)
fc = get_fillcolor(series, clims)
la = get_linealpha(series)
@@ -1375,13 +1571,13 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax)
# hatch color/alpha are controlled by edge (not face) color/alpha
has_fs &&
mpl.patches.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",
- ) |> push_h
+ 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",
+ ) |> push_h
elseif series[:seriestype] ∈ _py_legend_series
has_line = get_linewidth(series) > 0
PythonPlot.pyplot.Line2D(
@@ -1413,7 +1609,7 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax)
markeredgewidth = _py_thickness_scale(
plt,
0.8get_markerstrokewidth(series) * sp[:legend_font_pointsize] /
- first(series[:markersize]),
+ first(series[:markersize]),
), # retain the markersize/markerstroke ratio from the markers on the plot
) |> push_h
else
@@ -1425,12 +1621,12 @@ 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
lc == nseries ||
- @warn "n° of legend_column=$lc is not compatible with n° of series=$nseries"
+ @maxlog_warn "n° of legend_column=$lc is not compatible with n° of series=$nseries"
nseries
else
1
@@ -1451,7 +1647,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(),
@@ -1469,16 +1665,16 @@ function _py_add_legend(plt::Plot, sp::Subplot, ax)
fontsize = _py_thickness_scale(plt, sp[:legend_font_pointsize]),
)
end
- nothing
+ return nothing
end
# -----------------------------------------------------------------
# Use the bounding boxes (and methods left/top/right/bottom/width/height) `sp.bbox` and `sp.plotarea` to
# position the subplot in the backend.
-function _update_plot_object(plt::Plot{PythonPlotBackend})
+function PlotsBase._update_plot_object(plt::Plot{PythonPlotBackend})
for sp in plt.subplots
- (ax = sp.o) === nothing && return
+ (ax = sp.o) ≡ nothing && return
figw, figh = sp.plt[:size] .* px
# ax.set_position signature: `[left, bottom, width, height]`
@@ -1495,29 +1691,33 @@ function _update_plot_object(plt::Plot{PythonPlotBackend})
height(sp.bbox) - 2pad, # height
)
get(sp[:extra_kwargs], "3d_colorbar_axis", bbox_to_pcts(cb_bbox, figw, figh)) |>
- sp.attr[:cbar_ax].set_position
+ sp.attr[:cbar_ax].set_position
end
end
- PythonPlot.draw()
+ return PythonPlot.draw()
end
# -----------------------------------------------------------------
# display/output
-_display(plt::Plot{PythonPlotBackend}) = plt.o.show()
+PlotsBase._display(plt::Plot{PythonPlotBackend}) = plt.o.show()
for (mime, fmt) in (
- "application/eps" => "eps",
- "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{PythonPlotBackend})
+ "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 PlotsBase._show(
+ io::IO,
+ ::MIME{Symbol($mime)},
+ plt::Plot{PythonPlotBackend},
+ )
fig = plt.o
- fig.canvas.print_figure(
+ return fig.canvas.print_figure(
io,
format = $fmt,
# bbox_inches = "tight",
@@ -1528,4 +1728,8 @@ for (mime, fmt) in (
end
end
-closeall(::PythonPlotBackend) = PythonPlot.close("all")
+PlotsBase.closeall(::PythonPlotBackend) = PythonPlot.close("all")
+
+PlotsBase.@precompile_backend PythonPlot
+
+end
diff --git a/src/backends/unicodeplots.jl b/PlotsBase/ext/UnicodePlotsExt.jl
similarity index 74%
rename from src/backends/unicodeplots.jl
rename to PlotsBase/ext/UnicodePlotsExt.jl
index 057c68d093..c0bce96a0d 100644
--- a/src/backends/unicodeplots.jl
+++ b/PlotsBase/ext/UnicodePlotsExt.jl
@@ -1,3 +1,103 @@
+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,9 +110,9 @@ const _canvas_map = (
dot = UnicodePlots.DotCanvas,
)
-should_warn_on_unsupported(::UnicodePlotsBackend) = false
+PlotsBase.should_warn_on_unsupported(::UnicodePlotsBackend) = false
-function _before_layout_calcs(plt::Plot{UnicodePlotsBackend})
+function PlotsBase._before_layout_calcs(plt::Plot{UnicodePlotsBackend})
plt.o = UnicodePlots.Plot[]
up_width = UnicodePlots.DEFAULT_WIDTH[]
up_height = UnicodePlots.DEFAULT_HEIGHT[]
@@ -40,7 +140,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 +149,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
@@ -97,8 +197,8 @@ function _before_layout_calcs(plt::Plot{UnicodePlotsBackend})
kw = (
compact = true,
title = texmath2unicode(sp[:title]),
- xlabel = texmath2unicode(xaxis[:guide]),
- ylabel = texmath2unicode(yaxis[:guide]),
+ xlabel = texmath2unicode(PlotsBase.get_guide(xaxis)),
+ ylabel = texmath2unicode(PlotsBase.get_guide(yaxis)),
labels = !plot_3d, # guide labels and limits do not make sense in 3d
xscale = xaxis[:scale],
yscale = yaxis[:scale],
@@ -141,6 +241,7 @@ function _before_layout_calcs(plt::Plot{UnicodePlotsBackend})
push!(plt.o, o) # save the object
end
+ return
end
up_color(col::UnicodePlots.UserColorType) = col
@@ -155,21 +256,21 @@ up_cmap(series) = map(
# add a single series
function addUnicodeSeries!(
- sp::Subplot{UnicodePlotsBackend},
- up::UnicodePlots.Plot,
- kw,
- series,
- addlegend::Bool,
- plot_3d::Bool,
-)
+ sp::Subplot{UnicodePlotsBackend},
+ up::UnicodePlots.Plot,
+ kw,
+ series,
+ addlegend::Bool,
+ plot_3d::Bool,
+ )
st = series[:seriestype]
se_kw = series[:extra_kwargs]
# 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 +296,7 @@ function addUnicodeSeries!(
z = Array(series[:z])
if st ≡ :contour
isfilledcontour(series) &&
- @warn "Plots(UnicodePlots): filled contour is not implemented"
+ @maxlog_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 +319,7 @@ function addUnicodeSeries!(
elseif st ≡ :mesh3d
return UnicodePlots.lineplot!(
up,
- mesh3d_triangles(x, y, series[:z], series[:connections])...,
+ PlotsBase.mesh3d_triangles(x, y, series[:z], series[:connections])...,
)
end
@@ -230,7 +331,7 @@ function addUnicodeSeries!(
func = UnicodePlots.scatterplot!
series_kw = (; marker = series[:markershape])
else
- throw(ArgumentError("Plots(UnicodePlots): series type $st not supported"))
+ throw(ArgumentError("PlotsBase(UnicodePlots): series type $st not supported"))
end
label = addlegend ? series[:label] : ""
@@ -261,27 +362,27 @@ function addUnicodeSeries!(
)
end
- up
+ return up
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 |>
- throw
- nothing
+ ArgumentError |>
+ throw
+ return nothing
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
@@ -299,7 +400,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)."
+ @maxlog_warn "PlotsBase(UnicodePlots) failed to render `png` from plot (font issue)."
else
m1 = maximum(s1; dims = 2)
m2 = maximum(s2; dims = 1)
@@ -318,15 +419,16 @@ function _show(io::IO, ::MIME"image/png", plt::Plot{UnicodePlotsBackend})
end
UnicodePlots.save_image(io, img)
end
- nothing
+ return nothing
end
Base.show(plt::Plot{UnicodePlotsBackend}) = show(stdout, plt)
-Base.show(io::IO, plt::Plot{UnicodePlotsBackend}) = _show(io, MIME("text/plain"), plt)
+Base.show(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)
@@ -335,9 +437,9 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend})
i < n && println(io)
end
else
- have_color = Base.get_have_color()
- lines_colored = Array{Union{Nothing,Vector{String}}}(nothing, nr, nc)
- lines_uncolored = have_color ? similar(lines_colored) : lines_colored
+ color = get(io, :color, false)
+ lines_colored = Array{Union{Nothing, Vector{String}}}(nothing, nr, nc)
+ lines_uncolored = color ? similar(lines_colored) : lines_colored
l_max = zeros(Int, nr)
w_max = zeros(Int, nc)
nsp = length(plt.o)
@@ -353,9 +455,9 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend})
elseif (sps += 1) > nsp
continue
end
- colored = string(plt.o[sps]; color = have_color)
+ colored = string(plt.o[sps]; color)
lines_colored[r, c] = lu = lc = split(colored, '\n')
- if have_color
+ if color
uncolored = UnicodePlots.no_ansi_escape(colored)
lines_uncolored[r, c] = lu = split(uncolored, '\n')
end
@@ -382,11 +484,15 @@ function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend})
r < nr && println(io)
end
end
- nothing
+ return nothing
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)
+ return println(stdout)
+end
+
+PlotsBase.@precompile_backend UnicodePlots
+
end
diff --git a/PlotsBase/ext/UnitfulExt.jl b/PlotsBase/ext/UnitfulExt.jl
new file mode 100644
index 0000000000..a2c6274c16
--- /dev/null
+++ b/PlotsBase/ext/UnitfulExt.jl
@@ -0,0 +1,345 @@
+# previously https://github.com/jw3126/UnitfulRecipes.jl
+# authors: Benoit Pasquier (@briochemc) - David Gustavsson (@gustaphe) - Jan Weidner (@jw3126)
+
+module UnitfulExt
+
+import Unitful:
+ Unitful,
+ Quantity,
+ unit,
+ ustrip,
+ dimension,
+ Units,
+ NoUnits,
+ LogScaled,
+ logunit,
+ MixedUnits,
+ Level,
+ Gain,
+ uconvert
+import PlotsBase: PlotsBase, @recipe, PlotText, Subplot, AVec, AMat, Axis
+using Latexify: latexify
+using LaTeXStrings
+import PlotsBase.Axes: format_unit_label
+import RecipesBase
+
+const MissingOrQuantity = Union{Missing, <:Quantity, <:LogScaled}
+
+#==========
+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
+ u = get(plotattributes, :zunit, _unit(eltype(x)))
+ ustripattribute!(plotattributes, :clims, u)
+ append_cbar_unit_if_needed!(plotattributes, u)
+ end
+ fixaxis!(plotattributes, x, axisletter)
+end
+
+function fixaxis!(attr, x, axisletter)
+ # Attribute keys
+ err = Symbol(axisletter, :error) # xerror, yerror, zerror
+ axisunit = Symbol(axisletter, :unit) # xunit, yunit, zunit
+ axis = Symbol(axisletter, :axis) # xaxis, yaxis, zaxis
+ u = get!(attr, axisunit, _unit(eltype(x))) # get the unit
+ # if the subplot already exists with data, get its unit
+ sp = get(attr, :subplot, 1)
+ if sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0
+ spu = getaxisunit(attr[:plot_object][sp][axis])
+ if !isnothing(spu)
+ u = spu
+ end
+ attr[axisunit] = u # update the unit in the attributes
+ # get!(attr, axislabel, label) # if label was not given as an argument, reuse
+ end
+ # fix the attributes: labels, lims, ticks, marker/line stuff, etc.
+ ustripattribute!(attr, err, u)
+ if axisletter ≡ :y
+ ustripattribute!(attr, :ribbon, u)
+ ustripattribute!(attr, :fillrange, u)
+ end
+ fixaspectratio!(attr, u, axisletter)
+ fixseriescolor!(attr, :marker_z)
+ fixseriescolor!(attr, :line_z)
+ fixmarkersize!(attr)
+ return _ustrip.(u, x) # strip the unit
+end
+
+# Recipe for (x::AVec, y::AVec, z::Surface) types
+@recipe function f(x::AVec, y::AVec, z::AMat{T}) where {T <: Quantity} # COV_EXCL_LINE
+ u = get(plotattributes, :zunit, _unit(eltype(z)))
+ ustripattribute!(plotattributes, :clims, u)
+ z = fixaxis!(plotattributes, z, :z)
+ append_cbar_unit_if_needed!(plotattributes, u)
+ x, y, z
+end
+
+# Recipe for vectors of vectors
+@recipe function f(::Type{T}, x::T) where {T <: AVec{<:AVec{<:MissingOrQuantity}}} # COV_EXCL_LINE
+ axisletter = plotattributes[:letter] # x, y, or z
+ unitsymbol = Symbol(axisletter, :unit)
+ axisunit = pop!(plotattributes, unitsymbol, _unit(eltype(first(x))))
+ map(
+ x -> (
+ plotattributes[unitsymbol] = axisunit;
+ fixaxis!(plotattributes, x, axisletter)
+ ),
+ x,
+ )
+end
+
+# Recipe for bare units
+@recipe function f(::Type{T}, x::T) where {T <: Units} # COV_EXCL_LINE
+ primary := false
+ Float64[] * x
+end
+
+# Recipes for functions
+@recipe f(f::Function, x::T) where {T <: AVec{<:MissingOrQuantity}} = x, f.(x)
+@recipe f(x::T, f::Function) where {T <: AVec{<:MissingOrQuantity}} = x, f.(x)
+@recipe f(x::T, y::AVec, f::Function) where {T <: AVec{<:MissingOrQuantity}} = x, y, f.(x', y)
+@recipe f(x::AVec, y::T, f::Function) where {T <: AVec{<:MissingOrQuantity}} = x, y, f.(x', y)
+@recipe function f( # COV_EXCL_LINE
+ x::T1,
+ y::T2,
+ f::Function,
+ ) where {T1 <: AVec{<:MissingOrQuantity}, T2 <: AVec{<:MissingOrQuantity}}
+ x, y, f.(x', y)
+end
+@recipe function f(f::Function, u::Units) # COV_EXCL_LINE
+ uf = UnitFunction(f, [u])
+ recipedata = RecipesBase.apply_recipe(plotattributes, uf)
+ _, xmin, xmax = recipedata[1].args
+ f, xmin * u, xmax * u
+end
+
+"""
+```julia
+UnitFunction
+```
+A function, bundled with the assumed units of each of its inputs.
+
+```julia
+f(x, y) = x^2 + y
+uf = UnitFunction(f, u"m", u"m^2")
+uf(3, 2) == f(3u"m", 2u"m"^2) == 7u"m^2"
+```
+"""
+struct UnitFunction <: Function
+ f::Function
+ u::Vector{Units}
+end
+(f::UnitFunction)(args...) = f.f((args .* f.u)...)
+
+#===============
+Attribute fixing
+===============#
+# Aspect ratio
+function fixaspectratio!(attr, u, axisletter)
+ aspect_ratio = get(attr, :aspect_ratio, :auto)
+ if aspect_ratio in (:auto, :none)
+ # Keep the default behavior (let PlotsBase figure it out)
+ return
+ end
+ if aspect_ratio ≡ :equal
+ aspect_ratio = 1
+ end
+ #=======================================================================================
+ Implementation example:
+
+ Consider an x axis in `u"m"` and a y axis in `u"s"`, and an `aspect_ratio` in `u"m/s"`.
+ 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 PlotsBase throws a `DimensionError` as it tries
+ to compare `0 < 1u"m"`.
+ =======================================================================================#
+ if axisletter ≡ :y
+ attr[:aspect_ratio] = aspect_ratio * u
+ elseif axisletter ≡ :x
+ attr[:aspect_ratio] = aspect_ratio / u
+ end
+ return nothing
+end
+
+# Markers / lines
+function fixseriescolor!(attr, key)
+ sp = get(attr, :subplot, 1)
+ if haskey(attr, :zunit)
+ # Precedence to user-passed zunit
+ u = attr[:zunit]
+ ustripattribute!(attr, key, u)
+ elseif sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0
+ # Then to an existing subplot's colorbar title
+ cbar_title = get(attr[:plot_object][sp], :colorbar_title, nothing)
+ spu = (cbar_title isa UnitfulString ? cbar_title.unit : nothing)
+ if !isnothing(spu)
+ u = spu
+ ustripattribute!(attr, key, u)
+ else
+ u = ustripattribute!(attr, key)
+ end
+ else
+ # Otherwise, get from the attribute
+ u = ustripattribute!(attr, key)
+ end
+ ustripattribute!(attr, :clims, u)
+ # fixmarkercolor! is called for each axis, so after the first pass,
+ # u will be NoUnits and we don't want to append unit again
+ return u == NoUnits || append_cbar_unit_if_needed!(attr, u)
+end
+fixmarkersize!(attr) = ustripattribute!(attr, :markersize)
+
+# strip unit from attribute[key]
+ustripattribute!(attr, key) =
+if haskey(attr, key)
+ v = attr[key]
+ u = _unit(eltype(v))
+ attr[key] = _ustrip.(u, v)
+ u
+else
+ NoUnits
+end
+
+# if supplied, use the unit (optional 3rd argument)
+function ustripattribute!(attr, key, u)
+ if haskey(attr, key)
+ v = attr[key]
+ if eltype(v) <: Quantity
+ attr[key] = _ustrip.(u, v)
+ elseif v isa Tuple
+ attr[key] = Tuple([(eltype(vi) <: Quantity ? _ustrip.(u, vi) : vi) for vi in v])
+ end
+ end
+ return u
+end
+
+#=======================================
+Label string containing unit information
+Used only for colorbars, etc., which don't
+have a better place for storing units
+=======================================#
+
+struct UnitfulString{S, U} <: AbstractString
+ content::S
+ unit::U
+end
+# Minimum required AbstractString interface to work with PlotsBase
+Base.iterate(n::UnitfulString) = iterate(n.content)
+Base.iterate(n::UnitfulString, i::Integer) = iterate(n.content, i)
+Base.codeunit(n::UnitfulString) = codeunit(n.content)
+Base.ncodeunits(n::UnitfulString) = ncodeunits(n.content)
+Base.isvalid(n::UnitfulString, i::Integer) = isvalid(n.content, i)
+Base.pointer(n::UnitfulString) = pointer(n.content)
+Base.pointer(n::UnitfulString, i::Integer) = pointer(n.content, i)
+
+#=====================================
+Append unit to labels when appropriate
+This is needed for colorbars, etc., since axes have
+distinct unit handling
+=====================================#
+
+append_cbar_unit_if_needed!(attr, u) =
+ append_cbar_unit_if_needed!(attr, get(attr, :colorbar_title, nothing), u)
+# dispatch on the type of `label`
+append_cbar_unit_if_needed!(attr, label::UnitfulString, u) = nothing
+function append_cbar_unit_if_needed!(attr, label::Nothing, u)
+ unitformat = get(attr, Symbol(:z, :unitformat), :round)
+ if unitformat ∈ [:nounit, :none, false, nothing]
+ return attr[:colorbar_title] = UnitfulString("", u)
+ end
+ return attr[:colorbar_title] = if PlotsBase.backend_name() ≡ :pgfplotsx
+ UnitfulString(LaTeXString(latexify(u)), u)
+ else
+ UnitfulString(string(u), u)
+ end
+end
+function append_cbar_unit_if_needed!(attr, label::S, u) where {S <: AbstractString}
+ isempty(label) && return attr[:colorbar_title] = UnitfulString(label, u)
+ return attr[:colorbar_title] = if PlotsBase.backend_name() ≡ :pgfplotsx
+ UnitfulString(
+ LaTeXString(
+ format_unit_label(
+ label,
+ latexify(u),
+ get(attr, :zunitformat, :round),
+ ),
+ ),
+ u,
+ )
+ else
+ UnitfulString(S(format_unit_label(label, u, get(attr, :zunitformat, :round))), u)
+ end
+end
+
+#=============================================
+Surround unit string with specified delimiters
+=============================================#
+
+getaxisunit(::Nothing) = nothing
+getaxisunit(u) = u
+getaxisunit(a::Axis) = getaxisunit(a[:unit])
+
+#==============
+Fix annotations
+===============#
+function PlotsBase.locate_annotation(
+ sp::Subplot,
+ x::MissingOrQuantity,
+ y::MissingOrQuantity,
+ label::PlotText,
+ )
+ xunit = getaxisunit(sp.attr[:xaxis])
+ yunit = getaxisunit(sp.attr[:yaxis])
+ return (_ustrip(xunit, x), _ustrip(yunit, y), label)
+end
+function PlotsBase.locate_annotation(
+ sp::Subplot,
+ x::MissingOrQuantity,
+ y::MissingOrQuantity,
+ z::MissingOrQuantity,
+ label::PlotText,
+ )
+ xunit = getaxisunit(sp.attr[:xaxis])
+ yunit = getaxisunit(sp.attr[:yaxis])
+ zunit = getaxisunit(sp.attr[:zaxis])
+ return (_ustrip(xunit, x), _ustrip(yunit, y), _ustrip(zunit, z), label)
+end
+function PlotsBase.locate_annotation(
+ sp::Subplot,
+ rel::NTuple{N, <:MissingOrQuantity},
+ label,
+ ) where {N}
+ units = getaxisunit(sp.attr[:xaxis]),
+ getaxisunit(sp.attr[:yaxis]),
+ getaxisunit(sp.attr[:zaxis])
+ return PlotsBase.locate_annotation(sp, _ustrip.(zip(units, rel)), label)
+end
+
+# ticks and limits
+
+PlotsBase._transform_ticks(ticks::AbstractArray{T}, axis) where {T <: Quantity} =
+ _ustrip.(getaxisunit(axis), ticks)
+PlotsBase.Axes.process_limits(lims::AbstractArray{T}, axis) where {T <: Quantity} =
+ _ustrip.(getaxisunit(axis), lims)
+PlotsBase.Axes.process_limits(lims::Tuple{S, T}, axis) where {S <: Quantity, T <: Quantity} =
+ _ustrip.(getaxisunit(axis), lims)
+
+function _ustrip(u, x)
+ u isa MixedUnits && return ustrip(uconvert(u, x))
+ return ustrip(u, x)
+end
+
+function _unit(x)
+ (t = eltype(x)) <: LogScaled && return logunit(t)
+ return unit(x)
+end
+
+PlotsBase.pgfx_sanitize_string(s::UnitfulString) =
+ UnitfulString(PlotsBase.pgfx_sanitize_string(s.content), s.unit)
+
+end
diff --git a/PlotsBase/src/Annotations.jl b/PlotsBase/src/Annotations.jl
new file mode 100644
index 0000000000..ac396f8a2e
--- /dev/null
+++ b/PlotsBase/src/Annotations.jl
@@ -0,0 +1,281 @@
+module Annotations
+
+export SeriesAnnotations,
+ EachAnn,
+ series_annotations,
+ series_annotations_shapes!,
+ process_annotation,
+ locate_annotation,
+ annotations,
+ assign_annotation_coord!
+
+import ..PlotsBase: Series, Subplot, PlotOrSubplot, is3d, discrete_value!, plot!
+
+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"
+ return anns
+end
+
+function series_annotations(anns::AMat, outer_attrs...)
+ # Types that represent annotations for an entire series
+ whole_series = Union{AVec, Tuple{AVec, Vararg{Any}}}
+
+ # whole_series types can only be in a row vector
+ if size(anns, 1) > 1
+ for ann in Iterators.filter(ann -> ann isa whole_series, anns)
+ "Given series annotation must be the only element in its column:\n$ann" |>
+ ArgumentError |>
+ throw
+ end
+ end
+
+ ann_vec = map(eachcol(anns)) do col
+ ann = first(col) isa whole_series ? first(col) : col
+
+ # Override arguments from outer tuple with args from inner tuple
+ strs, inner_attrs = Iterators.peel(wraptuple(ann))
+ series_annotations(strs, outer_attrs..., inner_attrs...)
+ end
+
+ return 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._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
+ @maxlog_warn "Unused SeriesAnnotations arg: $arg ($(typeof(arg)))"
+ end
+ end
+ return 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
+ return 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
+ return (_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)
+ return _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)
+ return _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)
+ return [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) ?
+ return 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)
+
+
+"""
+ 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)"])
+```
+"""
+PlotsBase.annotate!(anns...; kw...) = plot!(; annotation = anns, kw...)
+PlotsBase.annotate!(anns::Tuple...; kw...) = plot!(; annotation = collect(anns), kw...)
+PlotsBase.annotate!(anns::AVec{<:Tuple}; kw...) = plot!(; annotation = anns, kw...)
+PlotsBase.annotate!(plt::PlotOrSubplot, anns...; kw...) = plot!(plt; annotations = anns, kw...)
+PlotsBase.annotate!(plt::PlotOrSubplot, anns::Tuple...; kw...) = plot!(plt; annotations = collect(anns), kw...)
+PlotsBase.annotate!(plt::PlotOrSubplot, anns::AVec{<:Tuple}; kw...) = plot!(plt; annotations = anns, kw...)
+
+end
diff --git a/PlotsBase/src/Arrows.jl b/PlotsBase/src/Arrows.jl
new file mode 100644
index 0000000000..6f0a131bda
--- /dev/null
+++ b/PlotsBase/src/Arrows.jl
@@ -0,0 +1,65 @@
+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 = 1
+ 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
+ @maxlog_warn "Skipped arrow arg $arg"
+ end
+ end
+ return 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
+ return
+end
+
+end
diff --git a/PlotsBase/src/Axes.jl b/PlotsBase/src/Axes.jl
new file mode 100644
index 0000000000..9bdc2cc33d
--- /dev/null
+++ b/PlotsBase/src/Axes.jl
@@ -0,0 +1,525 @@
+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, get_guide
+
+import ..PlotsBase: PlotsBase, Subplot, DefaultsDict
+using Latexify: latexify
+
+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
+ return PlotsBase.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)
+ return 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 || @maxlog_warn "Invalid min $(letter)limit" lmin
+ end
+ if lmax isa Number && isfinite(lmax)
+ amax = lmax
+ elseif lmax isa Symbol
+ lmax ≡ :auto || @maxlog_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
+
+ return 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 || @maxlog_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
+ return 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
+ return 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) = @maxlog_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
+ return 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)
+ return _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)
+ return 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)
+ return 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),
+ )
+ @maxlog_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) 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, Dates.TimeType})
+ plotattributes[k] = (Dates.value(v[1]), Dates.value(v[2]))
+ elseif k ≡ :guide && v isa AbstractString && isempty(v)
+ plotattributes[:unitformat] = :nounit
+ plotattributes[k] = v
+ elseif k ≡ :unit
+ if !isnothing(plotattributes[k]) && plotattributes[k] != v
+ @maxlog_warn "Overriding unit for $(axis[:letter]) axis: $(plotattributes[k]) -> $v. This will produce a plot, but series plotted before the override cannot update and will therefore be incorrectly treated as if they had the new units."
+ end
+ plotattributes[k] = v
+ else
+ plotattributes[k] = v
+ end
+ end
+
+ # replace scale aliases‚
+ if haskey(_scale_aliases, plotattributes[:scale])
+ plotattributes[:scale] = _scale_aliases[plotattributes[:scale]]
+ end
+
+ return axis
+end
+
+get_guide(axis::Axis) = if isnothing(axis[:guide])
+ ""
+elseif isnothing(axis[:unit]) || axis[:unitformat] ≡ :nounit
+ axis[:guide]
+else
+ unit = if axis[:unitformat] isa Function
+ axis[:unit]
+ elseif PlotsBase.backend_name() ≡ :pgfplotsx
+ latexify(axis[:unit])
+ else
+ string(axis[:unit])
+ end
+ isempty(string(axis[:guide])) && return unit
+ format_unit_label(axis[:guide], unit, axis[:unitformat])
+end
+
+# Keyword options for unit formats
+const UNIT_FORMATS = Dict(
+ :round => ('(', ')'),
+ :square => ('[', ']'),
+ :curly => ('{', '}'),
+ :angle => ('<', '>'),
+ :slash => '/',
+ :space => " ",
+ :slashround => (" / (", ")"),
+ :slashsquare => (" / [", "]"),
+ :slashcurly => (" / {", "}"),
+ :slashangle => (" / <", ">"),
+ :verbose => " in units of ",
+ :none => nothing,
+ :nounit => (l, u) -> l, # no unit, just label
+)
+
+# All options for unit formats
+format_unit_label(l, u, f::Nothing) = l
+format_unit_label(l, u, f::Function) = f(l, u)
+format_unit_label(l, u, f::AbstractString) = string(l, f, u)
+format_unit_label(l, u, f::NTuple{2, <:AbstractString}) = string(l, f[1], u, f[2])
+format_unit_label(l, u, f::NTuple{3, <:AbstractString}) = string(f[1], l, f[2], u, f[3])
+format_unit_label(l, u, f::Char) = string(l, ' ', f, ' ', u)
+format_unit_label(l, u, f::NTuple{2, Char}) = string(l, ' ', f[1], u, f[2])
+format_unit_label(l, u, f::NTuple{3, Char}) = string(f[1], l, ' ', f[2], u, f[3])
+format_unit_label(l, u, f::Bool) = f ? format_unit_label(l, u, :round) : format_unit_label(l, u, nothing)
+format_unit_label(l, u, f::Symbol) = format_unit_label(l, u, UNIT_FORMATS[f])
+
+# -----------------------------------------------------------------------------
+
+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 in Commons._all_axis_attrs
+ # first get the args without the letter: `tickfont = font(10)`
+ # note: we don't pop because we want this to apply to all axes! (delete after all have finished)
+ if haskey(plotattributes_in, k)
+ 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
+ PlotsBase.attr!(axis; kw...)
+ return 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)
+ return 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
+ return axis.plotattributes[:optimized_ticks]
+end
+
+function reset_extrema!(sp::Subplot)
+ for asym in (:x, :y, :z)
+ sp[get_attr_symbol(asym, :axis)][:extrema] = Extrema()
+ end
+ for series in sp.series_list
+ expand_extrema!(sp, series.plotattributes)
+ end
+ return
+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
+ return 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
+ return 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)
+ return ex
+end
+
+end
diff --git a/PlotsBase/src/BezierCurves.jl b/PlotsBase/src/BezierCurves.jl
new file mode 100644
index 0000000000..2dca0e6468
--- /dev/null
+++ b/PlotsBase/src/BezierCurves.jl
@@ -0,0 +1,22 @@
+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 in 0:n
+ p = p .+ bc.control_points[i + 1] .* binomial(n, i) .* (1 - t)^(n - i) .* t^i
+ end
+ return p
+end
+
+PlotsBase.coords(curve::BezierCurve, n::Integer = 30; range = [0, 1]) =
+ map(curve, Base.range(first(range), stop = last(range), length = n))
+
+end
diff --git a/src/colorbars.jl b/PlotsBase/src/Colorbars.jl
similarity index 50%
rename from src/colorbars.jl
rename to PlotsBase/src/Colorbars.jl
index b6c4be499c..ae85bdcd05 100644
--- a/src/colorbars.jl
+++ b/PlotsBase/src/Colorbars.jl
@@ -1,44 +1,59 @@
-# 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
+module Colorbars
-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} =
+export colorbar_style, update_clims, hascolorbar
+export get_colorbar_ticks, _update_subplot_colorbars
+
+import ..Surfaces
+import ..NaNMath
+import ..Ticks
+
+using ..Subplots: Subplot, series_list
+using ..DataSeries
+using ..Commons
+
+Commons.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, series::Series)::Tuple{Float64,Float64} =
- series[:colorbar_entry] ? get_clims(sp) : get_clims(series)
+ series[:clims_calculated]::Tuple{Float64, Float64} : update_clims(series)
+
+Commons.get_clims(sp::Subplot)::Tuple{Float64, Float64} =
+ haskey(sp.attr, :clims_calculated) ? sp[:clims_calculated] : update_clims(sp)
+
+Commons.get_clims(sp::Subplot, series::Series)::Tuple{Float64, Float64} =
+ series[:colorbar_entry] ? Commons.get_clims(sp) : Commons.get_clims(series)
-function update_clims(sp::Subplot, op = process_clims(sp[:clims]))::Tuple{Float64,Float64}
+# these functions return an operator for use in `update_clims`
+process_clims(lims::Tuple{<:Number, <:Number}) =
+ (zlims -> ifelse.(isfinite.(lims), lims, zlims)) ∘ Commons.ignorenan_extrema
+process_clims(s::Union{Symbol, Nothing, Missing}) = Commons.ignorenan_extrema
+# don't specialize on ::Function otherwise python functions won't work
+process_clims(f) = f
+
+function update_clims(sp::Subplot, op = process_clims(sp[:clims]))::Tuple{Float64, Float64}
zmin, zmax = Inf, -Inf
for series in series_list(sp)
if series[:colorbar_entry]::Bool
# Avoid calling the inner `update_clims` if at all possible; dynamic dispatch hell
- if (series[:seriestype] ∈ _z_colored_series && series[:z] !== nothing) ||
- series[:line_z] !== nothing ||
- series[:marker_z] !== nothing ||
- series[:fill_z] !== nothing
- zmin, zmax = _update_clims(zmin, zmax, update_clims(series, op)...)
+ zmin, zmax = if (series[:seriestype] ∈ Commons._z_colored_series && series[:z] ≢ nothing) ||
+ series[:line_z] ≢ nothing ||
+ series[:marker_z] ≢ nothing ||
+ series[:fill_z] ≢ nothing
+ _update_clims(zmin, zmax, update_clims(series, op)...)
else
- zmin, zmax = _update_clims(zmin, zmax, NaN, NaN)
+ _update_clims(zmin, zmax, NaN, NaN)
end
else
update_clims(series, op)
end
end
- return sp[:clims_calculated] = zmin <= zmax ? (zmin, zmax) : (NaN, NaN)
+ return sp[:clims_calculated] = zmin ≤ zmax ? (zmin, zmax) : (NaN, NaN)
end
function update_clims(
- sp::Subplot,
- series::Series,
- op = process_clims(sp[:clims]),
-)::Tuple{Float64,Float64}
- zmin, zmax = get_clims(sp)
+ sp::Subplot,
+ series::Series,
+ op = process_clims(sp[:clims]),
+ )::Tuple{Float64, Float64}
+ zmin, zmax = Commons.get_clims(sp)
old_zmin, old_zmax = zmin, zmax
if series[:colorbar_entry]::Bool
zmin, zmax = _update_clims(zmin, zmax, update_clims(series, op)...)
@@ -48,39 +63,39 @@ function update_clims(
isnan(zmin) && isnan(old_zmin) && isnan(zmax) && isnan(old_zmax) ||
zmin == old_zmin && zmax == old_zmax ||
update_clims(sp)
- return zmin <= zmax ? (zmin, zmax) : (NaN, NaN)
+ return zmin ≤ zmax ? (zmin, zmax) : (NaN, NaN)
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`.
"""
-function update_clims(series::Series, op = ignorenan_extrema)::Tuple{Float64,Float64}
+function update_clims(series::Series, op = Commons.ignorenan_extrema)::Tuple{Float64, Float64}
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)
+ return series[:clims_calculated] = zmin ≤ zmax ? (zmin, zmax) : (NaN, NaN)
end
-update_clims(zmin, zmax, vals::AbstractSurface, op)::Tuple{Float64,Float64} =
+update_clims(zmin, zmax, vals::Surfaces.AbstractSurface, op)::Tuple{Float64, Float64} =
update_clims(zmin, zmax, vals.surf, op)
-update_clims(zmin, zmax, vals::Any, op)::Tuple{Float64,Float64} =
+update_clims(zmin, zmax, vals::Any, op)::Tuple{Float64, Float64} =
_update_clims(zmin, zmax, op(vals)...)
-update_clims(zmin, zmax, ::Nothing, ::Any)::Tuple{Float64,Float64} = zmin, zmax
+update_clims(zmin, zmax, ::Nothing, ::Any)::Tuple{Float64, Float64} = zmin, zmax
_update_clims(zmin, zmax, emin, emax) = NaNMath.min(zmin, emin), NaNMath.max(zmax, emax)
@@ -89,41 +104,43 @@ _update_clims(zmin, zmax, emin, emax) = NaNMath.min(zmin, emin), NaNMath.max(zma
function colorbar_style(series::Series)
colorbar_entry = series[:colorbar_entry]
if !(colorbar_entry isa Bool)
- @warn "Non-boolean colorbar_entry ignored."
+ @maxlog_warn "Non-boolean colorbar_entry ignored."
colorbar_entry = true
end
- if !colorbar_entry
+ return if !colorbar_entry
nothing
elseif isfilledcontour(series)
cbar_fill
elseif iscontour(series)
cbar_lines
elseif series[:seriestype] ∈ (:heatmap, :surface) ||
- any(series[z] !== nothing for z in (:marker_z, :line_z, :fill_z))
+ any(series[z] ≢ nothing for z in (:marker_z, :line_z, :fill_z))
cbar_gradient
else
nothing
end
end
-hascolorbar(series::Series) = colorbar_style(series) !== nothing
+hascolorbar(series::Series) = colorbar_style(series) ≢ nothing
hascolorbar(sp::Subplot) =
- sp[:colorbar] !== :none && any(hascolorbar(s) for s in series_list(sp))
+ sp[:colorbar] ≢ :none && any(hascolorbar(s) for s in series_list(sp))
function get_colorbar_ticks(sp::Subplot; update = true, formatter = sp[:colorbar_formatter])
if update || !haskey(sp.attr, :colorbar_optimized_ticks)
- ticks = _transform_ticks(sp[:colorbar_ticks], sp[:colorbar_title])
+ ticks = Ticks._transform_ticks(sp[:colorbar_ticks], sp[:colorbar_title])
cvals = sp[:colorbar_continuous_values]
dvals = sp[:colorbar_discrete_values]
- clims = get_clims(sp)
+ clims = Commons.get_clims(sp)
scale = sp[:colorbar_scale]
sp.attr[:colorbar_optimized_ticks] =
- get_ticks(ticks, cvals, dvals, clims, scale, formatter)
+ Commons.get_ticks(ticks, cvals, dvals, clims, scale, formatter)
end
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
diff --git a/PlotsBase/src/Commons/Commons.jl b/PlotsBase/src/Commons/Commons.jl
new file mode 100644
index 0000000000..3f9d1f936c
--- /dev/null
+++ b/PlotsBase/src/Commons/Commons.jl
@@ -0,0 +1,332 @@
+"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!, reset_defaults
+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, @maxlog_warn
+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!
+
+export _subplot_defaults, _axis_defaults, _plot_defaults, _series_defaults, _match_map
+export _match_map2, @add_attributes, preprocess_attributes!, _override_seriestype_check
+
+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)
+const _max_log = Ref(1)
+
+macro maxlog_warn(exs...)
+ return :(@warn $(exs...) maxlog = $(_max_log[])) |> esc
+end
+
+# docs.julialang.org/en/v1/manual/methods/#Empty-generic-functions
+macro generic_functions(exs...)
+ blk = Expr(:block)
+ foreach(ex -> push!(blk.args, :(function $ex end)), exs)
+ return 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
+ return st
+end
+
+function fg_color(plotattributes::AKW)
+ fg = get(plotattributes, :foreground_color, :auto)
+ return 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)
+ return 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)
+ return NaNMath.min(ex[1], mn), NaNMath.max(ex[2], mx)
+end
+
+# helpers to figure out if there are NaN values in a list of array types
+anynan(i::Int, args::Tuple) = any(
+ a -> try
+ isnan(_cycle(a, i))
+ catch MethodError
+ false
+ end, args
+)
+anynan(args::Tuple) = i -> anynan(i, args)
+anynan(istart::Int, iend::Int, args::Tuple) = any(anynan(args), istart:iend)
+allnan(istart::Int, iend::Int, args::Tuple) = all(anynan(args), istart:iend)
+
+handle_surface(z) = z
+
+reverse_if(x, cond) = cond ? reverse(x) : x
+
+function get_aspect_ratio(sp)
+ ar = sp[:aspect_ratio]
+ check_aspect_ratio(ar)
+ if ar ≡ :auto
+ ar = :none
+ for series in series_list(sp)
+ if series[:seriestype] ≡ :image
+ ar = :equal
+ end
+ end
+ end
+ ar isa Bool && (ar = Int(ar)) # NOTE: Bool <: ... <: Number
+ ar isa Rational && (ar = float(ar))
+ return 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)
+ isempty(prefix) || println(io, prefix, ":")
+ for k in sort(collect(keys(plotattributes)))
+ Printf.@printf(io, "%14s: ", k)
+ debugshow(io, plotattributes[k])
+ println(io)
+ end
+ return 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 0000000000..d2328e8889
--- /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,
+ )
+ return 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 0000000000..c7dae3b801
--- /dev/null
+++ b/PlotsBase/src/Commons/attrs.jl
@@ -0,0 +1,1282 @@
+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
+ return 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
+ return
+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
+ return
+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)
+ return 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)
+ return if arg in _all_grid_syms
+ arg in (:all, :both, :on) || occursin(string(letter), string(arg))
+ else
+ @maxlog_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)
+ return if arg in _all_grid_syms
+ arg in (:all, :both, :on, :yes) || occursin(string(letter), string(arg))
+ else
+ @maxlog_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 => :none,
+ :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,
+ :unit => nothing,
+)
+
+# add defaults for the letter versions
+const _axis_defaults_byletter = KW()
+
+reset_axis_defaults_byletter!() =
+ for letter in (:x, :y, :z)
+ _axis_defaults_byletter[letter] = KW()
+ for (k, v) in _axis_defaults
+ _axis_defaults_byletter[letter][k] = v
+ end
+end
+reset_axis_defaults_byletter!()
+
+const _suppress_warnings = Set{Symbol}(
+ [
+ :x_discrete_indices,
+ :y_discrete_indices,
+ :z_discrete_indices,
+ :subplot,
+ :subplot_index,
+ :series_plotindex,
+ :series_index,
+ :link,
+ :plot_object,
+ :primary,
+ :smooth,
+ :relative_bbox,
+ :force_minpad,
+ :x_extrema,
+ :y_extrema,
+ :z_extrema,
+ ]
+)
+
+const _internal_attrs = [
+ :plot_object,
+ :series_plotindex,
+ :series_index,
+ :markershape_to_add,
+ :letter,
+ :idxfilter,
+]
+
+const _axis_attrs = Set(keys(_axis_defaults))
+const _series_attrs = Set(keys(_series_defaults))
+const _subplot_attrs = Set(keys(_subplot_defaults))
+const _plot_attrs = Set(keys(_plot_defaults))
+
+const _magic_axis_attrs = [:axis, :tickfont, :guidefont, :grid, :minorgrid]
+const _magic_subplot_attrs =
+ [:title_font, :legend_font, :legend_title_font, :plot_title_font, :colorbar_titlefont]
+const _magic_series_attrs = [:line, :marker, :fill]
+const _all_magic_attrs =
+ Set(union(_magic_axis_attrs, _magic_series_attrs, _magic_subplot_attrs))
+
+const _all_axis_attrs = union(_axis_attrs, _magic_axis_attrs)
+const _lettered_all_axis_attrs =
+ Set([Symbol(letter, kw) for letter in (:x, :y, :z) for kw in _all_axis_attrs])
+const _all_subplot_attrs = union(_subplot_attrs, _magic_subplot_attrs)
+const _all_series_attrs = union(_series_attrs, _magic_series_attrs)
+const _all_plot_attrs = _plot_attrs
+
+const _all_attrs =
+ union(_lettered_all_axis_attrs, _all_subplot_attrs, _all_series_attrs, _all_plot_attrs)
+
+const _deprecated_attributes = Dict{Symbol, Symbol}()
+const _all_defaults = KW[_series_defaults, _plot_defaults, _subplot_defaults]
+
+const _initial_defaults = deepcopy(_all_defaults)
+const _initial_axis_defaults = deepcopy(_axis_defaults)
+
+# to be able to reset font sizes to initial values
+const _initial_plt_fontsizes =
+ Dict(:plot_titlefontsize => _plot_defaults[:plot_titlefontsize])
+
+const _initial_sp_fontsizes = Dict(
+ :titlefontsize => _subplot_defaults[:titlefontsize],
+ :annotationfontsize => _subplot_defaults[:annotationfontsize],
+ :colorbar_tickfontsize => _subplot_defaults[:colorbar_tickfontsize],
+ :colorbar_titlefontsize => _subplot_defaults[:colorbar_titlefontsize],
+)
+
+const _initial_ax_fontsizes = Dict(
+ :tickfontsize => _axis_defaults[:tickfontsize],
+ :guidefontsize => _axis_defaults[:guidefontsize],
+)
+
+const _initial_fontsizes =
+ merge(_initial_plt_fontsizes, _initial_sp_fontsizes, _initial_ax_fontsizes)
+
+const _base_supported_attrs = [
+ :color_palette,
+ :background_color,
+ :background_color_subplot,
+ :foreground_color,
+ :foreground_color_subplot,
+ :group,
+ :seriestype,
+ :seriescolor,
+ :seriesalpha,
+ :smooth,
+ :xerror,
+ :yerror,
+ :zerror,
+ :subplot,
+ :x,
+ :y,
+ :z,
+ :show,
+ :size,
+ :margin,
+ :left_margin,
+ :right_margin,
+ :top_margin,
+ :bottom_margin,
+ :html_output_format,
+ :layout,
+ :link,
+ :primary,
+ :series_annotations,
+ :subplot_index,
+ :discrete_values,
+ :projection,
+ :show_empty_bins,
+ :z_order,
+ :permute,
+ :unitformat,
+]
+
+function merge_with_base_supported(v::AVec)
+ v = vcat(v, _base_supported_attrs)
+ for vi in v
+ if haskey(_axis_defaults, vi)
+ for letter in (:x, :y, :z)
+ push!(v, get_attr_symbol(letter, vi))
+ end
+ end
+ end
+ return 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 in ('x', 'y', 'z')
+ startswith(s, letter) &&
+ return (Symbol(letter), Symbol(chop(s, head = 1, tail = 0)))
+ end
+ return 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
+ return 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
+ return k in _suppress_warnings || error("Unknown key: ", k)
+end
+
+function default(; reset = true, kw...)
+ (reset && isempty(kw)) && reset_defaults()
+ kw = KW(kw)
+ preprocess_attributes!(kw)
+ for (k, v) in kw
+ default(k, v)
+ end
+ return
+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()
+ return 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
+ return false
+end
+
+function process_line_attr(plotattributes::AKW, arg)
+ # seriestype
+ return 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)
+ @maxlog_warn "Skipped line arg $arg."
+ end
+end
+
+function process_marker_attr(plotattributes::AKW, arg)
+ # markershape
+ return 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)
+ @maxlog_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
+ return nothing
+end
+
+function process_grid_attr!(plotattributes::AKW, arg, letter)
+ return 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),
+ )
+ @maxlog_warn "Skipped grid arg $arg."
+ end
+end
+
+function process_minor_grid_attr!(plotattributes::AKW, arg, letter)
+ return 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
+ @maxlog_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
+ @maxlog_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)
+ return if !haskey(plotattributes, :markershape) && ms ≢ :none
+ plotattributes[:markershape] = ms
+ end
+end
+
+function convert_legend_value(val::Symbol)
+ return 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)
+ return if !(levels isa Union{Integer, AVec})
+ "the levels keyword argument must be an integer or AbstractVector" |>
+ ArgumentError |>
+ throw
+ elseif levels isa Integer && levels ≤ 0
+ "must pass a positive number of contours to the levels keyword argument" |>
+ ArgumentError |>
+ throw
+ end
+end
+
+# -----------------------------------------------------------------------------
+
+# when a value can be `:match`, this is the key that should be used instead for value retrieval
+const _match_map = Dict(
+ :background_color_outside => :background_color,
+ :legend_background_color => :background_color_subplot,
+ :background_color_inside => :background_color_subplot,
+ :legend_foreground_color => :foreground_color_subplot,
+ :foreground_color_title => :foreground_color_subplot,
+ :left_margin => :margin,
+ :top_margin => :margin,
+ :right_margin => :margin,
+ :bottom_margin => :margin,
+ :titlefontfamily => :fontfamily_subplot,
+ :titlefontcolor => :foreground_color_subplot,
+ :legend_font_family => :fontfamily_subplot,
+ :legend_font_color => :foreground_color_subplot,
+ :legend_title_font_family => :fontfamily_subplot,
+ :legend_title_font_color => :foreground_color_subplot,
+ :colorbar_fontfamily => :fontfamily_subplot,
+ :colorbar_titlefontfamily => :fontfamily_subplot,
+ :colorbar_titlefontcolor => :foreground_color_subplot,
+ :colorbar_tickfontfamily => :fontfamily_subplot,
+ :colorbar_tickfontcolor => :foreground_color_subplot,
+ :plot_titlefontfamily => :fontfamily,
+ :plot_titlefontcolor => :foreground_color,
+ :tickfontcolor => :foreground_color_text,
+ :guidefontcolor => :foreground_color_guide,
+ :annotationfontfamily => :fontfamily_subplot,
+ :annotationcolor => :foreground_color_subplot,
+)
+
+# these can match values from the parent container (axis --> subplot --> plot)
+const _match_map2 = Dict(
+ :background_color_subplot => :background_color,
+ :foreground_color_subplot => :foreground_color,
+ :foreground_color_axis => :foreground_color_subplot,
+ :foreground_color_border => :foreground_color_subplot,
+ :foreground_color_grid => :foreground_color_subplot,
+ :foreground_color_minor_grid => :foreground_color_subplot,
+ :foreground_color_guide => :foreground_color_subplot,
+ :foreground_color_text => :foreground_color_subplot,
+ :fontfamily_subplot => :fontfamily,
+ :tickfontfamily => :fontfamily_subplot,
+ :guidefontfamily => :fontfamily_subplot,
+)
+
+# -----------------------------------------------------------------------------
+
+# -----------------------------------------------------------------------------
+
+# -----------------------------------------------------------------------------
+
+has_black_border_for_default(st) = error(
+ "The seriestype attribute only accepts Symbols, you passed the $(typeof(st)) $st.",
+)
+has_black_border_for_default(st::Function) =
+ error("The seriestype attribute only accepts Symbols, you passed the function $st.")
+has_black_border_for_default(st::Symbol) =
+ like_histogram(st) || st in (:hexbin, :bar, :shape)
+
+ensure_gradient!(plotattributes::AKW, csym::Symbol, asym::Symbol) =
+if plotattributes[csym] isa ColorPalette
+ α = nothing
+ plotattributes[asym] isa AbstractVector || (α = plotattributes[asym])
+ plotattributes[csym] = cgrad(plotattributes[csym], categorical = true, alpha = α)
+elseif !(plotattributes[csym] isa ColorGradient)
+ plotattributes[csym] =
+ typeof(plotattributes[asym]) <: AbstractVector ? cgrad() :
+ cgrad(alpha = plotattributes[asym])
+end
+
+# get a good default linewidth... 0 for surface and heatmaps
+_replace_linewidth(plotattributes::AKW) =
+if plotattributes[:linewidth] ≡ :auto
+ plotattributes[:linewidth] =
+ (get(plotattributes, :seriestype, :path) ∉ (:surface, :heatmap, :image)) *
+ DEFAULT_LINEWIDTH[]
+end
+
+label_to_string(label::Bool, series_plotindex) =
+ label ? label_to_string(:auto, series_plotindex) : ""
+label_to_string(label::Nothing, series_plotindex) = ""
+label_to_string(label::Missing, series_plotindex) = ""
+label_to_string(label::Symbol, series_plotindex) =
+if label ≡ :auto
+ string("y", series_plotindex)
+elseif label ≡ :none
+ ""
+else
+ throw(ArgumentError("unsupported symbol $(label) passed to `label`"))
+end
+label_to_string(label, series_plotindex) = string(label) # Fallback to string promotion
+
+_series_index(plotattributes, sp) =
+if haskey(plotattributes, :series_index)
+ plotattributes[:series_index]::Int
+elseif get(plotattributes, :primary, true)
+ plotattributes[:series_index] = sp.primary_series_count += 1
+else
+ plotattributes[:series_index] = sp.primary_series_count
+end
+
+#--------------------------------------------------
+## inspired by Base.@kwdef
+"""
+ add_attributes(level, expr, match_table)
+
+Takes a `struct` definition and recurses into its fields to create keywords by chaining the field names with the structs' name with underscore.
+Also creates pluralized and non-underscore aliases for these keywords.
+- `level` indicates which group of `plot`, `subplot`, `series`, etc. the keywords belong to.
+- `expr` is the struct definition with default values like `Base.@kwdef`
+- `match_table` is an expression of the form `:match = (symbols)`, with symbols whose default value should be `:match`
+"""
+macro add_attributes(level, expr, match_table)
+ expr = macroexpand(__module__, expr) # to expand @static
+ expr isa Expr && expr.head ≡ :struct || error("Invalid usage of @add_attributes")
+ if (T = expr.args[2]) isa Expr && T.head ≡ :<:
+ T = T.args[1]
+ end
+
+ key_dict = KW()
+ _splitdef!(expr.args[3], key_dict)
+
+ insert_block = Expr(:block)
+ for (key, value) in key_dict
+ # e.g. _series_defaults[key] = value
+ exp_key = Symbol(lowercase(string(T)), "_", key)
+ pl_key = makeplural(exp_key)
+ if QuoteNode(exp_key) in match_table.args[2].args
+ value = QuoteNode(:match)
+ end
+ field = QuoteNode(Symbol("_", level, "_defaults"))
+ push!(
+ insert_block.args,
+ Expr(
+ :(=),
+ Expr(:ref, Expr(:call, getfield, 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
+ return 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(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
+ return blk
+end
diff --git a/PlotsBase/src/Commons/layouts.jl b/PlotsBase/src/Commons/layouts.jl
new file mode 100644
index 0000000000..2a782ab289
--- /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 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
+ @maxlog_warn "Unused origin arg in bbox construction: $oarg"
+ end
+ end
+ return 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
+ return 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 in widths],
+ Measure[h * pct for h in heights],
+ KW(kw),
+ )
+ for i in eachindex(grid)
+ grid[i] = EmptyLayout(layout)
+ end
+ return 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 0000000000..36da37ba74
--- /dev/null
+++ b/PlotsBase/src/Commons/measures.jl
@@ -0,0 +1,74 @@
+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
+ return 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
+ return 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))
+ return 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 0000000000..5e592ab550
--- /dev/null
+++ b/PlotsBase/src/Commons/postprocess_attrs.jl
@@ -0,0 +1,19 @@
+# add all pluralized forms to the _keyAliases dict
+foreach(arg -> add_aliases(arg, makeplural(arg)), _all_attrs)
+
+# fill symbol cache
+for letter in (:x, :y, :z)
+ new_attr_dict!(letter)
+ for keyword in _axis_attrs
+ # populate attribute cache
+ letter_keyword = set_attr_symbol!(letter, string(keyword))
+ # allow the underscore version too: `xguide` or `x_guide`
+ add_aliases(letter_keyword, Symbol(letter, "_", keyword))
+ end
+ for keyword in (_magic_axis_attrs..., :(_discrete_indices))
+ _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/DF.jl b/PlotsBase/src/DF.jl
new file mode 100644
index 0000000000..3def7e9327
--- /dev/null
+++ b/PlotsBase/src/DF.jl
@@ -0,0 +1,233 @@
+module DF
+
+export @df, extract_columns_and_names
+
+import ..TableOperations
+
+const Tables = TableOperations.Tables
+"""
+ `@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)
+ return 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)
+ return esc(df_helper(x))
+end
+
+function df_helper(x)
+ i = gensym()
+ return 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 in 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) in 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 in x.args)...)
+end
+
+function column_names(t)
+ s = Tables.schema(t)
+ return 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
+ return x.args = vcat(x.args[1:(index - 1)], Expr(:kw, s, v), x.args[index:end])
+end
+
+_argnames(names, x::Expr) =
+ Expr(:vect, [_arg2string(names, s) for s in x.args[2:end] if not_kw(s)]...)
+
+_arg2string(names, x) = stringify(x)
+_arg2string(names, x::Expr) =
+if x.head ≡ :call && x.args[1] ≡ :cols
+ :($(@__MODULE__).compute_name($names, $(x.args[2])))
+elseif x.head ≡ :call && x.args[1] ≡ :hcat
+ hcat(stringify.(x.args[2:end])...)
+elseif x.head ≡ :hcat
+ hcat(stringify.(x.args)...)
+else
+ stringify(x)
+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 in 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)
+ return 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 in syms)...)
+
+# get the appropriate name when passed an Integer
+add_sym!(cols, i::Integer, names) = push!(cols, names[i])
+# check for errors in Symbols
+add_sym!(cols, s::Symbol, names) = s in names ? push!(cols, s) : cols
+# recursively extract column names
+function add_sym!(cols, s, names)
+ for si in s
+ add_sym!(cols, si, names)
+ end
+ return 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 in syms), names
+end
+
+end
diff --git a/PlotsBase/src/DataSeries.jl b/PlotsBase/src/DataSeries.jl
new file mode 100644
index 0000000000..5e1cefc40c
--- /dev/null
+++ b/PlotsBase/src/DataSeries.jl
@@ -0,0 +1,344 @@
+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 ..PlotsBase
+import ..PlotUtils
+
+using ..PlotsBase: DefaultsDict, RecipesPipeline, KW
+using ..RecipesBase: @recipe
+using ..Commons
+
+mutable struct Series
+ plotattributes::DefaultsDict
+end
+
+@recipe function f(s::Series)
+ for (k, v) in s.plotattributes
+ k in (:subplot, :yerror, :xerror, :zerror) && continue
+ plotattributes[k] = v
+ end
+ ()
+end
+
+Base.getindex(series::Series, k::Symbol) = series.plotattributes[k]
+Base.setindex!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v)
+Base.get(series::Series, k::Symbol, v) = get(series.plotattributes, k, v)
+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] |> !isempty &&
+ 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)
+ return 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][Commons.get_attr_symbol(letter, :axis)], d)
+ return 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
+ return
+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)
+ return extend_by_data!(v, vmax .+ (1:(n - length(v))))
+end
+extend_by_data!(v::AbstractVector, x) = isimmutable(v) ? vcat(v, x) : push!(v, x)
+extend_by_data!(v::AbstractVector, x::AbstractVector) =
+ isimmutable(v) ? vcat(v, x) : append!(v, x)
+
+for comp in (:line, :fill, :marker)
+ compcolor = string(comp, :color)
+ get_compcolor = Symbol(:get_, compcolor)
+ comp_z = string(comp, :_z)
+
+ compalpha = string(comp, :alpha)
+ get_compalpha = Symbol(:get_, compalpha)
+
+ @eval begin
+ # defines `get_linecolor`, `get_fillcolor` and `get_markercolor` <- for grep
+ function $get_compcolor(
+ series,
+ cmin::Real,
+ cmax::Real,
+ i::Integer = 1,
+ s::Symbol = :identity,
+ )
+ c = series[$Symbol($compcolor)] # series[:linecolor], series[:fillcolor], series[:markercolor]
+ z = series[$Symbol($comp_z)] # series[:line_z], series[:fill_z], series[:marker_z]
+ return if z ≡ nothing
+ isa(c, PlotUtils.ColorGradient) ? c : PlotUtils.plot_color(_cycle(c, i))
+ else
+ grad = Commons.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)
+ return 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 PlotUtils.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)
+ return 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
+ return 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
+
+ return 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))
+ return NaNSegmentsIterator(tup, n1, n2)
+end
+
+# we want to check if a series needs to be split into segments just because
+# of its attributes
+# check relevant attributes if they have multiple inputs
+has_attribute_segments(series::Series) =
+ any(
+ series[attr] isa AbstractVector && length(series[attr]) > 1 for
+ attr in PlotsBase.Commons._segmenting_vector_attributes
+) || any(
+ series[attr] isa AbstractArray for
+ attr in 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) in enumerate(args)
+ (scale = get(series, scales[n], :identity)) ∈ PlotsBase.Commons._log_scales ||
+ continue
+ for (i, v) in enumerate(s)
+ if v ≤ 0
+ @maxlog_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)
+ (
+ if seriestype ≡ :shape
+ (SeriesSegment(segment, j),)
+ elseif seriestype in (:scatter, :scatter3d)
+ (SeriesSegment(i:i, i) for i in segment)
+ else
+ (SeriesSegment(i:(i + 1), i) for i in first(segment):(last(segment) - 1))
+ end for (j, segment) in enumerate(nan_segments)
+ ) |> Iterators.flatten
+ else
+ (SeriesSegment(r, 1) for r in nan_segments)
+ end
+
+ return 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 PlotsBase.Commons._segmenting_vector_attributes
+ if (v = get(series, attr, nothing)) isa PlotsBase.Commons.AVec &&
+ eachindex(v) != seg_range
+ @maxlog_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
+ return
+end
+
+function warn_on_inconsistent_shape_attrs(series, x, y, z, r)
+ for attr in PlotsBase.Commons._segmenting_vector_attributes
+ v = get(series, attr, nothing)
+ if v isa PlotsBase.Commons.AVec && length(unique(v[r])) > 1
+ @maxlog_warn "Different values of `$attr` specified for different shape vertices. Only first one will be used."
+ break
+ end
+ end
+ return
+end
+
+PlotsBase.attr(series::Series, k::Symbol) = series.plotattributes[k]
+PlotsBase.attr!(series::Series, v, k::Symbol) = (series.plotattributes[k] = v)
+function PlotsBase.attr!(series::Series; kw...)
+ plotattributes = KW(kw)
+ Commons.preprocess_attributes!(plotattributes)
+ for (k, v) in plotattributes
+ if haskey(Commons._series_defaults, k)
+ series[k] = v
+ else
+ @maxlog_warn "unused key $k in series attr"
+ end
+ end
+ PlotsBase._series_updated(series[:subplot].plt, series)
+ return series
+end
+
+end
diff --git a/PlotsBase/src/Fonts.jl b/PlotsBase/src/Fonts.jl
new file mode 100644
index 0000000000..4ef4498239
--- /dev/null
+++ b/PlotsBase/src/Fonts.jl
@@ -0,0 +1,180 @@
+module Fonts
+
+using ..Commons
+using ..Colors
+
+# 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 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 ∈ Commons._haligns
+ halign = arg
+ elseif arg ∈ Commons._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
+ @maxlog_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
+ @maxlog_warn "Unused font kwarg: $sym"
+ end
+ end
+
+ return Font(family, pointsize, halign, valign, rotation, color)
+end
+
+function scalefontsize(k::Symbol, factor::Number)
+ f = default(k)
+ f = round(Int, factor * f)
+ return 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 merge(Commons._initial_plt_fontsizes, Commons._initial_sp_fontsizes) |> keys
+ scalefontsize(k, factor)
+ end
+
+ for letter in (:x, :y, :z)
+ for k in keys(Commons._initial_ax_fontsizes)
+ scalefontsize(get_attr_symbol(letter, k), factor)
+ end
+ end
+ return
+end
+
+"""
+ scalefontsizes()
+
+Resets font sizes to initial default values.
+"""
+function scalefontsizes()
+ for k in merge(Commons._initial_plt_fontsizes, Commons._initial_sp_fontsizes) |> keys
+ f = default(k)
+ if k in keys(Commons._initial_fontsizes)
+ factor = f / Commons._initial_fontsizes[k]
+ scalefontsize(k, 1.0 / factor)
+ end
+ end
+
+ for letter in (:x, :y, :z)
+ keys_initial_fontsizes = keys(Commons._initial_fontsizes)
+ for k in keys(Commons._initial_ax_fontsizes)
+ if k in keys_initial_fontsizes
+ f = default(get_attr_symbol(letter, k))
+ factor = f / Commons._initial_fontsizes[k]
+ scalefontsize(get_attr_symbol(letter, k), 1.0 / factor)
+ end
+ end
+ end
+ return
+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
diff --git a/PlotsBase/src/Plots.jl b/PlotsBase/src/Plots.jl
new file mode 100644
index 0000000000..0ef4248899
--- /dev/null
+++ b/PlotsBase/src/Plots.jl
@@ -0,0 +1,289 @@
+module Plots
+
+export Plot,
+ PlotOrSubplot,
+ plottitlefont,
+ _update_plot_attrs,
+ InputWrapper,
+ protect
+
+import ..RecipesBase: AbstractLayout, AbstractBackend, AbstractPlot
+import ..RecipesPipeline: RecipesPipeline, DefaultsDict
+import ..Colorbars
+
+using ..DataSeries
+using ..PlotUtils
+using ..Subplots
+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()
+ return 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)
+ return 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
+Commons._cycle(wrapper::InputWrapper, ::Int) = wrapper.obj
+Commons._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 in 1:(plt.n)
+ push!(plt, i, y[mod1(i, ny)])
+ end
+ return 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
+ return 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
+ return plt
+end
+
+# ---------------------------------------------------------------
+
+"smallest x in plot"
+xmin(plt::Plot) =
+ ignorenan_minimum([ignorenan_minimum(series.plotattributes[:x]) for series in plt.series_list])
+
+"largest x in plot"
+xmax(plt::Plot) =
+ ignorenan_maximum([ignorenan_maximum(series.plotattributes[:x]) for series in plt.series_list])
+
+
+"extrema of x-values in plot"
+Commons.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) in 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)
+ return color_or_nothing!(plt.attr, :background_color_outside)
+end
+
+function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol)
+ # handle linking here. if we're passed a list of
+ # other subplots to link to, link them together
+ (link = axis[:link]) |> isempty && return
+ for other_sp in link
+ PlotsBase.link_axes!(axis, get_axis(get_subplot(plt, other_sp), letter))
+ end
+ axis.plotattributes[:link] = []
+ return 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)
+ return nothing
+end
+
+# update a subplots args and axes
+function _update_subplot_attrs(
+ plt::Plot,
+ sp::Subplot,
+ plotattributes_in,
+ subplot_index::Int,
+ remove_pair::Bool,
+ )
+ anns = RecipesPipeline.pop_kw!(sp.attr, :annotations)
+
+ # grab those args which apply to this subplot
+ for k in keys(Commons._subplot_defaults)
+ # We don't apply annotations to the plot-title sub-plot
+ if k ≡ :annotations && get(plt.attr, :plot_titleindex, 0) == subplot_index
+ continue
+ end
+ PlotsBase.slice_arg!(plotattributes_in, sp.attr, k, subplot_index, remove_pair)
+ end
+
+ Subplots._update_subplot_colors(sp)
+ Subplots._update_margins(sp)
+ cbar_update_keys = :clims, :colorbar, :seriestype, :marker_z, :line_z, :fill_z, :colorbar_entry
+ if any(haskey.(Ref(plotattributes_in), cbar_update_keys))
+ Colorbars._update_subplot_colorbars(sp)
+ end
+
+ lims_warned = false
+ for letter in (: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
+ @maxlog_warn "lims should be a Tuple, not $(typeof(plotattributes_in[lk]))."
+ lims_warned = true
+ end
+ end
+
+ return Subplots._update_subplot_periphery(sp, anns)
+end
+
+function Commons.scale_lims!(plt::Plot, letter, factor)
+ foreach(sp -> scale_lims!(sp, letter, factor), plt.subplots)
+ return plt
+end
+function Commons.scale_lims!(plt::Union{Plot, Subplot}, factor)
+ foreach(letter -> scale_lims!(plt, letter, factor), (:x, :y, :z))
+ return plt
+end
+Commons.get_size(plt::Plot) = get_size(plt.attr)
+Commons.get_thickness_scaling(plt::Plot) = get_thickness_scaling(plt.attr)
+
+end
diff --git a/PlotsBase/src/PlotsBase.jl b/PlotsBase/src/PlotsBase.jl
new file mode 100644
index 0000000000..5023f694d1
--- /dev/null
+++ b/PlotsBase/src/PlotsBase.jl
@@ -0,0 +1,187 @@
+module PlotsBase
+
+if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
+ @eval Base.Experimental.@optlevel 1
+end
+if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_methods"))
+ @eval Base.Experimental.@max_methods 1
+end
+
+using Base.Meta
+
+import PrecompileTools
+import TableOperations
+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
+
+export
+ grid,
+ bbox,
+ plotarea,
+ KW,
+
+ theme,
+ protect,
+ plot,
+ plot!,
+ attr!,
+ @df,
+
+ 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,
+ Animation,
+
+ test_examples,
+ coords,
+
+ plotattr,
+ scalefontsizes,
+ resetfontsizes
+
+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! annotate!
+include("DF.jl")
+using .DF
+include("Fonts.jl")
+Reexport.@reexport using .Fonts
+include("Ticks.jl")
+using .Ticks
+include("DataSeries.jl")
+using .DataSeries
+include("Subplots.jl")
+using .Subplots
+include("Axes.jl")
+using .Axes
+include("Surfaces.jl")
+using .Surfaces
+include("Colorbars.jl")
+using .Colorbars
+include("Plots.jl")
+using .Plots
+include("layouts.jl")
+include("utils.jl")
+include("axes_utils.jl")
+include("legend.jl")
+include("Shapes.jl")
+using .Shapes
+include("Annotations.jl")
+using .Annotations
+include("Arrows.jl")
+using .Arrows
+include("Strokes.jl")
+using .Strokes
+include("BezierCurves.jl")
+using .BezierCurves
+include("themes.jl")
+include("plot.jl")
+include("pipeline.jl")
+include("arg_desc.jl")
+include("recipes.jl")
+include("animation.jl")
+include("examples.jl")
+include("plotattr.jl")
+include("alignment.jl")
+include("output.jl")
+include("shorthands.jl")
+include("backends.jl")
+include("web.jl")
+include("plotly.jl")
+using .Plotly
+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 0000000000..5693b4dc44
--- /dev/null
+++ b/PlotsBase/src/Shapes.jl
@@ -0,0 +1,232 @@
+module Shapes
+
+import ..RecipesPipeline
+import ..PlotsBase
+
+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)
+Shape(x::AVec{X}, y::AVec{Y}) where {X, Y} =
+ Shape(convert(Vector{X}, x), convert(Vector{Y}, y))
+
+# 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 in range(start_angle, end_angle, n)]
+
+"interleave 2 vectors into each other (like a zipper's teeth)"
+function weave(x, y; ordering = Vector[x, y])
+ 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
+ return 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)
+ return 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)
+ return 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 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
+ return 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
+ return 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
+ return 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
+ return 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...)
+ return Shape(x_new, y_new)
+end
+
+end
diff --git a/PlotsBase/src/Strokes.jl b/PlotsBase/src/Strokes.jl
new file mode 100644
index 0000000000..888719c84a
--- /dev/null
+++ b/PlotsBase/src/Strokes.jl
@@ -0,0 +1,82 @@
+module Strokes
+
+export Stroke, Brush, stroke, brush
+
+import ..Colors: Colorant
+using ..Commons
+
+struct Stroke
+ width
+ color
+ alpha
+ style
+end
+
+"""
+ stroke(args...; alpha = nothing)
+
+Define the properties of the stroke used in plotting lines
+"""
+function stroke(args...; alpha = nothing)
+ width = 1
+ color = :black
+ style = :solid
+
+ for arg in args
+ T = typeof(arg)
+
+ # if arg in _all_styles
+ if Commons.all_styles(arg)
+ style = arg
+ elseif T <: Colorant
+ color = arg
+ elseif T <: Symbol || T <: AbstractString
+ try
+ color = parse(Colorant, string(arg))
+ catch
+ end
+ elseif Commons.all_alphas(arg)
+ alpha = arg
+ elseif Commons.all_reals(arg)
+ width = arg
+ else
+ @maxlog_warn "Unused stroke arg: $arg ($(typeof(arg)))"
+ end
+ end
+
+ return 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 Commons.all_alphas(arg)
+ alpha = arg
+ elseif Commons.all_reals(arg)
+ size = arg
+ else
+ @maxlog_warn "Unused brush arg: $arg ($(typeof(arg)))"
+ end
+ end
+
+ return Brush(size, color, alpha)
+end
+
+end
diff --git a/PlotsBase/src/Subplots.jl b/PlotsBase/src/Subplots.jl
new file mode 100644
index 0000000000..8e275c3fef
--- /dev/null
+++ b/PlotsBase/src/Subplots.jl
@@ -0,0 +1,284 @@
+module Subplots
+
+export Subplot,
+ colorbartitlefont,
+ legendfont,
+ legendtitlefont,
+ titlefont,
+ get_series_color,
+ needs_any_3d_axes
+import PlotsBase
+
+import ..RecipesPipeline: RecipesPipeline, DefaultsDict
+import ..RecipesBase: AbstractLayout, AbstractBackend
+import ..Commons: BoundingBox
+import ..DataSeries: Series
+import ..PlotUtils
+
+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(), Commons._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
+ Commons.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 in vcat(anns, sp[:annotations])
+ append!(newanns, PlotsBase.process_annotation(sp, ann))
+ end
+ sp.attr[:annotations] = newanns
+
+ # handle legend/colorbar
+ sp.attr[:legend_position] = Commons.convert_legend_value(sp.attr[:legend_position])
+ sp.attr[:colorbar] = Commons.convert_legend_value(sp.attr[:colorbar])
+ if sp.attr[:colorbar] ≡ :legend
+ sp.attr[:colorbar] = sp.attr[:legend_position]
+ end
+ return 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)
+ return 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 * PlotsBase.mm
+ sp.attr[sym] = margin[1] * getfield(@__MODULE__, margin[2])
+ end
+end
+
+needs_any_3d_axes(sp::Subplot) = any(
+ RecipesPipeline.needs_3d_axes(
+ Commons._override_seriestype_check(s.plotattributes, s.plotattributes[:seriestype]),
+ ) for s in series_list(sp)
+)
+
+function PlotsBase.expand_extrema!(sp::Subplot, plotattributes::AKW)
+
+ # first expand for the data
+ for letter in (:x, :y, :z)
+ data = plotattributes[letter]
+ if (
+ letter ≢ :z &&
+ plotattributes[:seriestype] ≡ :straightline &&
+ any(series[:seriestype] ≢ :straightline for series in series_list(sp)) &&
+ length(data) > 1 &&
+ data[1] != data[2]
+ )
+ data = [NaN]
+ end
+ axis = sp[get_attr_symbol(letter, :axis)]
+
+ if isa(data, RecipesPipeline.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, RecipesPipeline.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] = RecipesPipeline.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
+ return 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, PlotsBase.heatmap_edges(data, scale))
+ end
+ end
+end
+
+function PlotsBase.expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax)
+ expand_extrema!(sp[:xaxis], (xmin, xmax))
+ return 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)
+
+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 PlotsBase.attr!(sp::Subplot; kw...)
+ plotattributes = KW(kw)
+ PlotsBase.Commons.preprocess_attributes!(plotattributes)
+ for (k, v) in plotattributes
+ if haskey(Commons._subplot_defaults, k)
+ sp[k] = v
+ else
+ @maxlog_warn "unused key $k in subplot attr"
+ end
+ end
+ return sp
+end
+
+end
diff --git a/PlotsBase/src/Surfaces.jl b/PlotsBase/src/Surfaces.jl
new file mode 100644
index 0000000000..2868caeaa3
--- /dev/null
+++ b/PlotsBase/src/Surfaces.jl
@@ -0,0 +1,24 @@
+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)
+ return ex
+end
+
+"For the case of representing a surface as a function of x/y... can possibly avoid allocations."
+struct SurfaceFunction <: AbstractSurface
+ f::Function
+end
+
+Commons.handle_surface(z::Surface) = permutedims(z.surf)
+
+end
diff --git a/PlotsBase/src/Ticks.jl b/PlotsBase/src/Ticks.jl
new file mode 100644
index 0000000000..6a9281e38a
--- /dev/null
+++ b/PlotsBase/src/Ticks.jl
@@ -0,0 +1,107 @@
+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]
+ return if !(n_intervals isa Bool) && n_intervals isa Integer && n_intervals ≥ 0
+ max(1, n_intervals) # 0 intervals makes no sense
+ else # `:auto` or `true`
+ if (base = get(_log_scale_bases, axis[:scale], nothing)) == 10
+ Int(base - 1)
+ else
+ DEFAULT_MINOR_INTERVALS[]
+ end
+ end::Int
+end
+
+no_minor_intervals(axis) =
+if (n_intervals = axis[:minorticks]) ≡ false
+ true # must be tested with `===` since Bool <: Integer
+elseif n_intervals ∈ (:none, nothing)
+ true
+elseif (n_intervals ≡ :auto && !axis[:minorgrid])
+ true
+else
+ false
+end
+
+function get_minor_ticks(sp, axis, ticks_and_labels)
+ no_minor_intervals(axis) && return
+ ticks = first(ticks_and_labels)
+ length(ticks) < 2 && return
+
+ amin, amax = axis_limits(sp, axis[:letter])
+ scale = axis[:scale]
+ base = get(_log_scale_bases, scale, nothing)
+
+ # add one phantom tick either side of the ticks to ensure minor ticks extend to the axis limits
+ if (log_scaled = scale ∈ _log_scales)
+ sub = round(Int, log(base, ticks[2] / ticks[1]))
+ ticks = [ticks[1] / base; ticks; ticks[end] * base]
+ else
+ sub = 1 # unused
+ ratio = length(ticks) > 2 ? (ticks[3] - ticks[2]) / (ticks[2] - ticks[1]) : 1
+ first_step = ticks[2] - ticks[1]
+ last_step = ticks[end] - ticks[end - 1]
+ ticks = [ticks[1] - first_step / ratio; ticks; ticks[end] + last_step * ratio]
+ end
+
+ n_minor_intervals = num_minor_intervals(axis)
+ minorticks = sizehint!(eltype(ticks)[], n_minor_intervals * sub * length(ticks))
+ for i in 2:length(ticks)
+ lo = ticks[i - 1]
+ hi = ticks[i]
+ (isfinite(lo) && isfinite(hi) && hi > lo) || continue
+ if log_scaled
+ for e in 1:sub
+ lo_ = lo * base^(e - 1)
+ hi_ = lo_ * base
+ step = (hi_ - lo_) / n_minor_intervals
+ rng = (lo_ + (e > 1 ? 0 : step)):step:(hi_ - (e < sub ? 0 : step / 2))
+ append!(minorticks, collect(rng))
+ end
+ else
+ step = (hi - lo) / n_minor_intervals
+ append!(minorticks, collect((lo + step):step:(hi - step / 2)))
+ end
+ end
+ return minorticks[amin .≤ minorticks .≤ amax]
+end
+
+end
diff --git a/PlotsBase/src/alignment.jl b/PlotsBase/src/alignment.jl
new file mode 100644
index 0000000000..df916bdb41
--- /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
+ return 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)
+ return 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)
+ return 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 63%
rename from src/animation.jl
rename to PlotsBase/src/animation.jl
index 2b08479638..996adada62 100644
--- a/src/animation.jl
+++ b/PlotsBase/src/animation.jl
@@ -13,25 +13,32 @@ struct Animation
frames::Vector{String}
end
-Animation(dir = convert(String, mktempdir())) = Animation(dir, String[])
+const ANIM_PATTERN = "plots-anim-%06d.png"
+
+Animation(dir = convert(String, mktempdir(tmpdir_name()))) = Animation(dir, String[])
"""
frame(animation[, plot])
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
+function frame(anim::Animation, plt::P = current()) where {P <: AbstractPlot}
+ filename = Printf.format(Printf.Format(ANIM_PATTERN), length(anim.frames) + 1)
png(plt, joinpath(anim.dir, filename))
- push!(anim.frames, filename)
+ return 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 = tmpdir_name()) = if isijulia()
+ "tmp"
+else
+ tempname(parent)
+end * ext
+
+giffn(parent = tmpdir_name()) = anim_filename(".gif", parent)
+movfn(parent = tmpdir_name()) = anim_filename(".mov", parent)
+mp4fn(parent = tmpdir_name()) = anim_filename(".mp4", parent)
+webmfn(parent = tmpdir_name()) = anim_filename(".webm", parent)
+apngfn(parent = tmpdir_name()) = anim_filename(".png", parent)
mutable struct FrameIterator
itr
@@ -51,7 +58,7 @@ function animate(fitr::FrameIterator, fn = giffn(); kw...)
frame(anim)
end
end
- gif(anim, fn; kw...)
+ return gif(anim, fn; kw...)
end
# most things will implement this
@@ -88,98 +95,99 @@ 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(
- anim::Animation,
- fn::AbstractString,
- is_animated_gif::Bool = true;
- fps::Real = 20,
- loop::Integer = 0,
- variable_palette::Bool = false,
- verbose = false,
- show_msg::Bool = true,
-)
+function build_animation(
+ anim::Animation,
+ fn::AbstractString,
+ is_animated_gif::Bool = true;
+ fps::Real = 20,
+ loop::Integer = 0,
+ variable_palette::Bool = false,
+ verbose = false,
+ show_msg::Bool = true,
+ )
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` |>
- ffmpeg_exe
+ `-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"` |>
- ffmpeg_exe
+ `-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` |>
- ffmpeg_exe
+ `-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` |>
- ffmpeg_exe
+ `-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` |>
- ffmpeg_exe
+ `-v $verbose_level -framerate $framerate -i $pattern -vf format=yuv420p -loop $loop -y $fn` |>
+ ffmpeg_exe
end
show_msg && @info "Saved animation to $fn"
- AnimatedGif(fn)
+ return AnimatedGif(fn)
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
+ return nothing
end
# Only gifs can be shown via image/gif
Base.showable(::MIME"image/gif", agif::AnimatedGif) = file_extension(agif.filename) == "gif"
Base.showable(::MIME"image/png", agif::AnimatedGif) =
- let ext = file_extension(agif.filename)
- ext == "apng" || ext == "png"
- end
+let ext = file_extension(agif.filename)
+ ext == "apng" || ext == "png"
+end
Base.show(io::IO, ::MIME"image/gif", agif::AnimatedGif) =
open(fio -> write(io, fio), agif.filename)
@@ -199,13 +207,13 @@ function _animate(forloop::Expr, args...; type::Symbol = :none)
freqassert = :()
block = forloop.args[2]
- animationsKwargs = Any[]
+ kw = Any[]
filterexpr = true
n = length(args)
i = 1
# create filter and read parameters
- while i <= n
+ while i ≤ n
arg = args[i]
if arg in (:when, :every)
# specification of frame filter
@@ -213,44 +221,46 @@ function _animate(forloop::Expr, args...; type::Symbol = :none)
filterexpr == true ||
error("Can only specify one filterexpression (one of 'when' or 'every')")
- filterexpr = # when every
- arg == :when ? args[i + 1] : :(mod1($countersym, $(args[i + 1])) == 1)
+ 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
i += 1
end
- push!(block.args, :(
- if $filterexpr
- Plots.frame($animsym)
- end
- ))
+ push!(
+ block.args, :(
+ if $filterexpr
+ 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
+ return quote
+ $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
@@ -268,11 +278,11 @@ Example:
This macro supports additional parameters, that may be added after the main loop body.
- Add `fps=n` with positive Integer n, to specify the desired frames per second.
- Add `every n` with positive Integer n, to take only one frame every nth iteration.
-- Add `when ` where `` is an Expression resulting in a Boolean, to take a
+- Add `when ` where `` is an Expression resulting in a Boolean, to take a
frame only when `` returns `true`. Is incompatible with `every`.
"""
macro gif(forloop::Expr, args...)
- _animate(forloop, args...; type = :gif)
+ return _animate(forloop, args...; type = :gif)
end
"""
@@ -289,11 +299,11 @@ Example:
This macro supports additional parameters, that may be added after the main loop body.
- Add `fps=n` with positive Integer n, to specify the desired frames per second.
- Add `every n` with positive Integer n, to take only one frame every nth iteration.
-- Add `when ` where `` is an Expression resulting in a Boolean, to take a
+- Add `when ` where `` is an Expression resulting in a Boolean, to take a
frame only when `` returns `true`. Is incompatible with `every`.
"""
macro apng(forloop::Expr, args...)
- _animate(forloop, args...; type = :apng)
+ return _animate(forloop, args...; type = :apng)
end
"""
@@ -310,9 +320,9 @@ gif(anim)
```
This macro supports additional parameters, that may be added after the main loop body.
- Add `every n` with positive Integer n, to take only one frame every nth iteration.
-- Add `when ` where `` is an Expression resulting in a Boolean, to take a
+- Add `when ` where `` is an Expression resulting in a Boolean, to take a
frame only when `` returns `true`. Is incompatible with `every`.
"""
macro animate(forloop::Expr, args...)
- _animate(forloop, args...)
+ return _animate(forloop, args...)
end
diff --git a/PlotsBase/src/arg_desc.jl b/PlotsBase/src/arg_desc.jl
new file mode 100644
index 0000000000..b739f69790
--- /dev/null
+++ b/PlotsBase/src/arg_desc.jl
@@ -0,0 +1,213 @@
+const AStr = AbstractString
+const ColorType = Union{Symbol, Colorant, PlotUtils.ColorSchemes.ColorScheme, Integer}
+const TicksType = Union{AVec{Real}, Tuple{AVec{Real}, AVec{AStr}}, Symbol, Bool, Nothing}
+
+# NOTE: when updating `arg_desc`, don't forget to modify `PlotDocs.make_attr_df` accordingly.
+const _arg_desc = KW(
+ # series args
+ :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 $(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 $(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 $(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."),
+ :bins => (
+ Union{Integer, NTuple{2, Integer}, AVec, Symbol}, """
+ Default is :auto (the Freedman-Diaconis rule). For histogram-types, defines the approximate number of bins to aim for, or the auto-binning algorithm to use (:sturges, :sqrt, :rice, :scott or :fd).
+ For fine-grained control pass a Vector of break values, e.g. `range(minimum(x), stop = maximum(x), length = 25)`.""",
+ ),
+ :smooth => (Bool, "Add a regression line ?"),
+ :group => (AVec, "Data is split into a separate series, one for each unique value in `group`."),
+ :x => (Any, "Input data (first dimension)."),
+ :y => (Any, "Input data (second dimension)."),
+ :z => (Any, "Input data (third dimension). May be wrapped by a `Surface` for surface and heatmap types."),
+ :marker_z => (Union{AVec, Function}, "z-values for each series data point, which correspond to the color to be used from a markercolor gradient (`f(x,y,z) -> z_value` or `f(x,y) -> z_value`)."),
+ :line_z => (Union{AVec, Function}, "z-values for each series line segment, which correspond to the color to be used from a linecolor gradient. Note that for N points, only the first N-1 values are used (one per line-segment)."),
+ :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)."),
+ :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`."),
+ :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)."),
+ :ribbon => (Union{Real, AVec}, "Creates a fillrange around the data points."),
+ :quiver => (Union{AVec, NTuple{2, AVec}}, "The directional vectors U,V which specify velocity/gradient vectors for a quiver plot."),
+ :arrow => (Union{Bool, Arrow}, "Defines arrowheads that should be displayed at the end of path line segments (just before a NaN and the last non-NaN point). Used in quiverplot, streamplot, or similar."),
+ :normalize => (Union{Bool, Symbol}, "Histogram normalization mode. Possible values are: false/:none (no normalization, default), true/:pdf (normalize to a discrete PDF, where the total area of the bins is 1), :probability (bin heights sum to 1) and :density (the area of each bin, rather than the height, is equal to the counts - useful for uneven bin sizes)."),
+ :weights => (AVec, "Used in histogram types for weighted counts."),
+ :show_empty_bins => (Bool, "Whether empty bins in a 2D histogram are colored as 0 (true), or transparent (the default)."),
+ :contours => (Bool, "Add contours to the side-grids of 3D plots? Used in surface/wireframe."),
+ :contour_labels => (Bool, "Show labels at the contour lines ?"),
+ :match_dimensions => (Bool, "For heatmap types: should the first dimension of a matrix (rows) correspond to the first dimension of the plot (`x`-axis) ? Defaults to `false`, which matches the behavior of Matplotlib, Plotly, and others. Note: when passing a function for `z`, the function should still map `(x,y) -> z`."),
+ :subplot => (Union{Integer, Subplot}, "The subplot that this series belongs to."),
+ :series_annotations => (Union{AVec, AStr, PlotText}, "These are annotations which are mapped to data points/positions."),
+ :primary => (Bool, "Does this count as a 'real series'? For example, you could have a path (primary), and a scatter (secondary) as two separate series, maybe with different data (see `sticks` recipe for an example). The secondary series will get the same color, etc as the primary."),
+ :hover => (AVec{AStr}, "Text to display when hovering over each data point."),
+ :colorbar_entry => (Bool, "Include this series in the color bar? Set to `false` to exclude."),
+ :z_order => (Union{Symbol, Integer}, ":front (default), :back or index of position where 1 is furthest in the background."),
+
+ # plot args
+ :plot_title => (AStr, "Whole plot title (not to be confused with the title for individual subplots)."),
+ :plot_titlevspan => (Real, "Vertical span of the whole plot title (fraction of the plot height)."),
+ :background_color => (ColorType, " Base color for all backgrounds."),
+ :background_color_outside => (ColorType, "Color outside the plot area(s) (`:match` matches `:background_color`)."),
+ :foreground_color => (ColorType, "Base color for all foregrounds."),
+ :size => (NTuple{2, Integer}, "(width_px, height_px) of the whole Plot."),
+ :pos => (NTuple{2, Integer}, "(left_px, top_px) position of the GUI window (note: currently unimplemented)."),
+ :window_title => (AStr, "Title of the standalone gui-window."),
+ :show => (Bool, "Should this command open/refresh a GUI/display ? Allows to display plots in scripts or functions without explicitly calling `display`."),
+ :layout => (Union{Integer, NTuple{2, Integer}, AbstractLayout}, "Number of subplot, grid dimensions, layout (for example `grid(2,2)`), or the return from the `@layout` macro. This builds the layout of subplots."),
+ :link => (Symbol, "How/whether to link axis limits between subplots. Values: `:none`, `:x` (x axes are linked by columns), `:y` (y axes are linked by rows), `:both` (x and y are linked), `:all` (every subplot is linked together regardless of layout position)."),
+ :overwrite_figure => (Bool, "Should we reuse the same GUI window/figure when plotting (true) or open a new one (false)."),
+ :html_output_format => (Symbol, "When writing html output, what is the format? `:png` and `:svg` are currently supported."),
+ :tex_output_standalone => (Bool, "When writing tex output, should the source include a preamble for a standalone document class."),
+ :inset_subplots => (AVec{NTuple{2, Any}}, "Optionally pass a vector of (parent,bbox) tuples which are the parent layout and the relative bounding box of inset subplots."),
+ :dpi => (Real, "Dots Per Inch of output figures."),
+ :thickness_scaling => (Real, "Scale for the thickness of all line elements like lines, borders, axes, grid lines, ... defaults to 1."),
+ :display_type => (Symbol, "When supported, `display` will either open a GUI window or plot inline. Choose from (`:auto`, `:gui`, or `:inline`)."),
+ :extra_kwargs => (
+ Symbol, """
+ Specify for which element extra keyword args are collected or a KW (Dict{Symbol,Any}) to pass a map of extra keyword args which may be specific to a backend. Choose from (`:plot`, `:subplot`, `:series`), defaults to `:series`.
+ Example: `pgfplotsx(); scatter(1:5, extra_kwargs=Dict(:subplot=>Dict("axis line shift" => "10pt"))`.""",
+ ),
+ :fontfamily => (Union{AStr, Symbol}, "Default font family for title, legend entries, tick labels and guides."),
+ :warn_on_unsupported => (Bool, "Warn on unsupported attributes, series types and marker shapes."),
+
+ # subplot args
+ :title => (AStr, "Subplot title."),
+ :titlelocation => (Symbol, "Position of subplot title. Choose from (`:left`, `:center`, `:right`)."),
+ :titlefontfamily => (Union{AStr, Symbol}, "Font family of subplot title."),
+ :titlefontsize => (Integer, "Font pointsize of subplot title."),
+ :titlefonthalign => (Symbol, "Font horizontal alignment of subplot title. Choose from (:hcenter, :left, :right, :center)."),
+ :titlefontvalign => (Symbol, "Font vertical alignment of subplot title. Choose from (:vcenter, :top, :bottom, :center)."),
+ :titlefontrotation => (Real, "Font rotation of subplot title."),
+ :titlefontcolor => (ColorType, "Color Type. Font color of subplot title."),
+ :background_color_subplot => (ColorType, "Base background color of the subplot (`:match` matches `:background_color`)."),
+ :legend_background_color => (ColorType, "Background color of the legend (`:match` matches :background_color_subplot`)."),
+ :background_color_inside => (ColorType, "Background color inside the plot area (under the grid) (`:match` matches :background_color_subplot`)."),
+ :foreground_color_subplot => (ColorType, "Base foreground color of the subplot (`:match` matches :foreground_color`)."),
+ :legend_foreground_color => (ColorType, "Foreground color of the legend (`:match` matches :foreground_color_subplot`)."),
+ :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,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 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."),
+ :legend_font_halign => (Symbol, "Font horizontal alignment of legend entries. Choose from (:hcenter, :left, :right, :center)."),
+ :legend_font_valign => (Symbol, "Font vertical alignment of legend entries. Choose from (:vcenter, :top, :bottom, :center)."),
+ :legend_font_rotation => (Real, "Font rotation of legend entries."),
+ :legend_title_font_color => (ColorType, "Font color of legend entries."),
+ :legend_title => (AStr, "Legend title."),
+ :legend_title_font_family => (Union{AStr, Symbol}, "Font family of the legend title."),
+ :legend_title_font_pointsize => (Integer, "Font pointsize the legend title."),
+ :legend_title_font_halign => (Symbol, "Font horizontal alignment of the legend title. Choose from (:hcenter, :left, :right, :center)."),
+ :legend_title_font_valign => (Symbol, "Font vertical alignment of the legend title. Choose from (:vcenter, :top, :bottom, :center)."),
+ :legend_title_font_rotation => (Real, "Font rotation of the legend title."),
+ :legend_title_font_color => (ColorType, "Font color of the legend title."),
+ :colorbar => (Union{Bool, Symbol}, "Show the colorbar ? A symbol specifies a colorbar position. Choose from (`:none`, `:best`, `:right`, `:left`, `:top`, `:bottom`, `:legend`): `legend` matches legend value (note: only some may be supported in each backend)."),
+ :clims => (Union{NTuple{2, Real}, Symbol, Function}, "Fixes the limits of the colorbar: values, `:auto`, or a function taking series data in and returning a NTuple{2,Real}."),
+ :colorbar_fontfamily => (Union{AStr, Symbol}, "Font family of colorbar entries."),
+ :colorbar_ticks => (TicksType, "Tick values, (tickvalues, ticklabels), `:auto`/`true`, or `:none`/`false`/`nothing` (ticks disabled)."),
+ :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 $(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."),
+ :annotations => (Union{AVec{Tuple}, Tuple{Real, Real, Union{AStr, PlotText, Tuple}}}, "(x,y,text) tuple(s), where text can be String, PlotText (created with `text(args...)`), or a tuple of arguments to `text` (e.g., `(\"Label\", 8, :red, :top)`). Add one-off text annotations at the (x,y) coordinates."),
+ :annotationfontfamily => (Union{AStr, Symbol}, "Font family of annotations."),
+ :annotationfontsize => (Integer, "Font pointsize of annotations."),
+ :annotationhalign => (Symbol, "horizontal alignment of annotations. Choose from (:hcenter, :left, :right, :center)."),
+ :annotationvalign => (Symbol, "Vertical alignment of annotations. Choose from (:vcenter, :top, :bottom, :center)."),
+ :annotationrotation => (Real, "Rotation of annotations in degrees."),
+ :annotationcolor => (ColorType, "Annotations color."),
+ :projection => (Union{AStr, Symbol}, "`3d` or `polar`."),
+ :projection_type => (Symbol, "3d plots projection type: :auto (backend dependent), :persp(ective), :ortho(graphic)."),
+ :aspect_ratio => (Union{Symbol, Real}, "Plot area is resized so that 1 y-unit is the same size as `aspect_ratio` x-units. With `:none`, images inherit aspect ratio of the plot area. Use `:equal` for unit aspect ratio."),
+ :margin => (Union{Tuple, Real}, "Number multiplied by `mm`, `px`, etc... or Tuple `(0, :mm)`. Base for individual margins... not directly used. Specifies the extra padding around subplots."),
+ :left_margin => (Union{Tuple, Real, Symbol}, "Specifies the extra padding to the left of the subplot (`:match` matches `:margin`)."),
+ :top_margin => (Union{Tuple, Real, Symbol}, "Specifies the extra padding on the top of the subplot (`:match` matches `:margin`)."),
+ :right_margin => (Union{Tuple, Real, Symbol}, "Specifies the extra padding to the right of the subplot (`:match` matches `:margin`)."),
+ :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 $(Commons._all_framestyles)."),
+ :camera => (NTuple{2, Real}, "Sets the view angle (azimuthal, elevation) for 3D plots."),
+
+ # axis args
+ :guide => (AStr, "Axis guide (label)."),
+ :guide_position => (Symbol, "Position of axis guides. Choose from (:top, :bottom, :left, :right)."),
+ :lims => (
+ Union{NTuple{2, Real}, Symbol}, """
+ Force axis limits. Only finite values are used (you can set only the right limit with `xlims = (-Inf, 2)` for example).
+ `:round` widens the limit to the nearest round number, i.e. [0.1,3.6]=>[0.0,4.0].
+ `: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 $(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."),
+ :tickfontfamily => (Union{AStr, Symbol}, "Font family of tick labels."),
+ :tickfontsize => (Integer, "Font pointsize of tick labels."),
+ :tickfonthalign => (Symbol, "Font horizontal alignment of tick labels. Choose from (:hcenter, :left, :right, :center)."),
+ :tickfontvalign => (Symbol, "Font vertical alignment of tick labels. Choose from (:vcenter, :top, :bottom, :center)."),
+ :tickfontrotation => (Real, "Font rotation of tick labels."),
+ :tickfontcolor => (ColorType, "Font color of tick labels."),
+ :guidefontfamily => (Union{AStr, Symbol}, "Font family of axes guides."),
+ :guidefontsize => (Integer, "Font pointsize of axes guides."),
+ :guidefonthalign => (Symbol, "Font horizontal alignment of axes guides. Choose from (:hcenter, :left, :right, :center)."),
+ :guidefontvalign => (Symbol, "Font vertical alignment of axes guides. Choose from (:vcenter, :top, :bottom, :center)."),
+ :guidefontrotation => (Real, "Font rotation of axes guides."),
+ :guidefontcolor => (ColorType, "Font color of axes guides."),
+ :foreground_color_axis => (ColorType, "Color of axis ticks (`:match` matches `:foreground_color_subplot`)."),
+ :foreground_color_border => (ColorType, "Color of plot area border/spines (`:match` matches `:foreground_color_subplot`)."),
+ :foreground_color_text => (ColorType, "Color of tick labels (`:match` matches `:foreground_color_subplot`)."),
+ :foreground_color_guide => (ColorType, "Color of axis guides/labels (`:match` matches `:foreground_color_subplot`)."),
+ :mirror => (Bool, "Switch the side of the tick labels (right or top)."),
+ :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 $(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 $(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 $(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.""",
+ ),
+ :draw_arrow => (Bool, "Draw arrow at the end of the axis."),
+ :unitformat => (Union{Bool, Nothing, Symbol, Char, String, NTuple{<:Union{Char, String}}, Function}, """Check examples in https://docs.juliaplots.org/stable/generated/unitfulext_examples/#Unit-formatting"""),
+)
diff --git a/PlotsBase/src/axes_utils.jl b/PlotsBase/src/axes_utils.jl
new file mode 100644
index 0000000000..4821f2f881
--- /dev/null
+++ b/PlotsBase/src/axes_utils.jl
@@ -0,0 +1,555 @@
+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(864.0e5 * amin, 864.0e5 * amax; k_min = 2, k_max = 4)
+ # Now the ticks are converted back to floats corresponding to Dates.
+ return ticks / 864.0e5, 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
+
+ return 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)
+ return fticks
+end
+
+# Ticks getter functions
+for l in (:x, :y, :z)
+ axis = string(l, "-axis") # "x-axis"
+ ticks = string(l, "ticks") # "xticks"
+ f = Symbol(ticks) # :xticks
+ @eval begin
+ """
+ $($f)(p::Plot)
+
+ returns a vector of the $($axis) ticks of the subplots of `p`.
+
+ Example use:
+
+ ```jldoctest
+ julia> p = plot(1:5, $($ticks)=[1,2])
+
+ julia> $($f)(p)
+ 1-element Vector{Tuple{Vector{Float64}, Vector{String}}}:
+ ([1.0, 2.0], ["1", "2"])
+ ```
+
+ If `p` consists of a single subplot, you might want to grab
+ only the first element, via
+
+ ```jldoctest
+ julia> $($f)(p)[1]
+ ([1.0, 2.0], ["1", "2"])
+ ```
+
+ or you can call $($f) on the first (only) subplot of `p` via
+
+ ```jldoctest
+ julia> $($f)(p[1])
+ ([1.0, 2.0], ["1", "2"])
+ ```
+ """
+ $f(p::Plot) = get_ticks(p, $(Meta.quot(l)))
+ """
+ $($f)(sp::Subplot)
+
+ returns the $($axis) ticks of the subplot `sp`.
+
+ Note that the ticks are returned as tuples of values and labels:
+
+ ```jldoctest
+ julia> sp = plot(1:5, $($ticks)=[1,2]).subplots[1]
+ Subplot{1}
+
+ julia> $($f)(sp)
+ ([1.0, 2.0], ["1", "2"])
+ ```
+ """
+ $f(sp::Subplot) = get_ticks(sp, $(Meta.quot(l)))
+ export $f
+ end
+end
+
+# -------------------------------------------------------------------------
+
+# using the axis extrema and limit overrides, return the min/max value for this axis
+
+# -------------------------------------------------------------------------
+
+# these methods track the discrete (categorical) values which correspond to axis continuous values (cv)
+# whenever we have discrete values, we automatically set the ticks to match.
+# we return (continuous_value, discrete_index)
+discrete_value!(plotattributes, letter::Symbol, dv) =
+let l = if plotattributes[:permute] ≢ :none
+ filter(!=(letter), plotattributes[:permute]) |> only
+ else
+ letter
+ end
+ discrete_value!(plotattributes[:subplot][get_attr_symbol(l, :axis)], dv)
+end
+
+discrete_value!(axis::Axis, dv) =
+if (cv_idx = get(axis[:discrete_map], dv, -1)) == -1
+ ex = axis[:extrema]
+ cv = NaNMath.max(0.5, ex.emax + 1)
+ expand_extrema!(axis, cv)
+ push!(axis[:discrete_values], dv)
+ push!(axis[:continuous_values], cv)
+ cv_idx = length(axis[:discrete_values])
+ axis[:discrete_map][dv] = cv_idx
+ cv, cv_idx
+else
+ cv = axis[:continuous_values][cv_idx]
+ cv, cv_idx
+end
+
+# continuous value... just pass back with axis negative index
+discrete_value!(axis::Axis, cv::Number) = (cv, -1)
+
+# add the discrete value for each item. return the continuous values and the indices
+function discrete_value!(axis::Axis, v::AVec)
+ cvec = zeros(axes(v))
+ discrete_indices = similar(Array{Int}, axes(v))
+ for i in eachindex(v)
+ cvec[i], discrete_indices[i] = discrete_value!(axis, v[i])
+ end
+ return 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
+ return 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
+ return
+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
+
+ return (
+ 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
+ return
+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
+
+ return (
+ 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 0000000000..f6eceee77b
--- /dev/null
+++ b/PlotsBase/src/backends.jl
@@ -0,0 +1,260 @@
+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 in _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}) =
+ @maxlog_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)
+ @maxlog_warn "backend `$name` is not compatible with `PlotsBase`."
+ return
+ end
+ pkg_str = string(get(_backend_packages, name, pkg)) # lowercase -> CamelCase
+ # check supported
+ if warn && !haskey(_compat, pkg_str)
+ @maxlog_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 && @maxlog_warn "`package $pkg_str` is not installed."
+ return 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) = isempty(sp[:title]) ? 0mm : sp[:titlefontsize] * pt
+guide_padding(axis::Axis) =
+ isempty(PlotsBase.get_guide(axis)) ? 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
+ return instance
+end
+
+backend(name::Symbol) =
+if name ∈ _supported_backends
+ if name ∈ _initialized_backends
+ backend(backend_type(name))
+ else
+ pkg_name = backend_package_name(name)
+ @maxlog_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
+ return ext, concrete_backend
+end
+
+# create backend init functions by hand as the corresponding structs do not exist yet
+for be in _supported_backends
+ @eval begin
+ function $be(; kw...)
+ default(; reset = false, kw...)
+ return backend(Symbol($be))
+ end
+ export $be
+ end
+end
+
+# create the various `is_xxx_supported` and `supported_xxxs` methods
+# these methods should be overloaded (dispatched) by each backend in its init_code
+for sym in _default_supported_syms
+ f1 = _f1_sym(sym)
+ f2 = _f2_sym(sym)
+ @eval begin
+ $f1(::AbstractBackend, $sym) = false
+ $f1(be::AbstractBackend, $sym::AbstractVector) = all(v -> $f1(be, v), $sym)
+ $f1($sym) = $f1(backend(), $sym)
+ $f2() = $f2(backend())
+ end
+end
+
+function backend_defines(be_type::Symbol, be::Symbol)
+ be_sym = QuoteNode(be)
+ blk = Expr(
+ :block,
+ :(get_concrete_backend() = $be_type),
+ :(PlotsBase.backend_name(::$be_type)::Symbol = $be_sym),
+ :(
+ PlotsBase.backend_package_name(::$be_type)::Symbol =
+ PlotsBase.backend_package_name($be_sym)
+ ),
+ )
+ #=
+ Overload (dispatch) abstract `is_xxx_supported` and `supported_xxxs` methods,
+ results in:
+ PlotsBase.is_attr_supported(::GRbackend, attrname) -> Bool
+ ...
+ PlotsBase.supported_attrs(::GRbackend) -> ::Vector{Symbol}
+ ...
+ PlotsBase.supported_scales(::GRbackend) -> ::Vector{Symbol}
+ =#
+ for sym in _default_supported_syms
+ be_syms = Symbol("_$(be)_$(sym)s")
+ push!(
+ blk.args,
+ :(PlotsBase.$(_f1_sym(sym))(::$be_type, $sym::Symbol)::Bool = $sym in $be_syms),
+ :(PlotsBase.$(_f2_sym(sym))(::$be_type)::Vector = sort!(collect($be_syms))),
+ )
+ end
+ return 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)
+ return 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
+ return @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 in PlotsBase.explicitkeys(plotattributes)
+ (is_attr_supported(pkg, k) && k ∉ keys(Commons._deprecated_attributes)) && continue
+ k in Commons._suppress_warnings && continue
+ if ismissing(default(k))
+ extra_kwargs[k] = pop_kw!(plotattributes, k)
+ elseif plotattributes[k] != default(k)
+ k in already_warned || push!(_to_warn, k)
+ end
+ end
+
+ if !isempty(_to_warn) &&
+ get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg))
+ for k in sort!(collect(_to_warn))
+ push!(already_warned, k)
+ if k in keys(Commons._deprecated_attributes)
+ @maxlog_warn """
+ Keyword argument `$k` is deprecated.
+ Please use `$(Commons._deprecated_attributes[k])` instead.
+ """
+ else
+ @maxlog_warn "Keyword argument $k not supported with $pkg. Choose from: $(join(supported_attrs(pkg), ", "))"
+ end
+ end
+ end
+ return 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]) ||
+ @maxlog_warn "seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))"
+ is_style_supported(pkg, plotattributes[:linestyle]) ||
+ @maxlog_warn "linestyle $(plotattributes[:linestyle]) is unsupported with $pkg. Choose from: $(supported_styles(pkg))"
+ is_marker_supported(pkg, plotattributes[:markershape]) ||
+ @maxlog_warn "markershape $(plotattributes[:markershape]) is unsupported with $pkg. Choose from: $(supported_markers(pkg))"
+ return nothing
+end
+
+function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW)
+ get(plotattributes, :warn_on_unsupported, should_warn_on_unsupported(pkg)) || return
+ for k in (:xscale, :yscale, :zscale, :scale)
+ haskey(plotattributes, k) || continue
+ v = plotattributes[k]
+ if !all(is_scale_supported.(Ref(pkg), v))
+ @maxlog_warn """
+ scale $v is unsupported with $pkg.
+ Choose from: $(supported_scales(pkg))
+ """
+ end
+ end
+ return
+end
diff --git a/src/examples.jl b/PlotsBase/src/examples.jl
similarity index 88%
rename from src/examples.jl
rename to PlotsBase/src/examples.jl
index 2fe65b1d61..aceca58758 100644
--- a/src/examples.jl
+++ b/PlotsBase/src/examples.jl
@@ -5,10 +5,12 @@ mutable struct PlotExample
header::AbstractString
desc::AbstractString
external::Bool # requires external optional dependencies not listed in [deps]
- imports::Union{Nothing,Expr}
+ imports::Union{Nothing, Expr}
exprs::Expr
end
+ref_name(i) = "ref" * lpad(i, 3, '0')
+
# COV_EXCL_START
PlotExample(header::AbstractString, expr::Expr) = PlotExample(header, "", expr)
PlotExample(header::AbstractString, imports::Expr, expr::Expr) =
@@ -24,7 +26,7 @@ const _examples = PlotExample[
PlotExample( # 1
"Lines",
"A simple line plot of the columns.",
- :(plot(Plots.fakedata(50, 5), w = 3)),
+ :(plot(PlotsBase.fakedata(50, 5), w = 3)),
),
PlotExample( # 2
"Functions, adding data, and animations",
@@ -71,7 +73,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 +134,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 +166,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 +181,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 +236,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 +248,7 @@ const _examples = PlotExample[
:(using Random),
quote
Random.seed!(111)
- plot!(Plots.fakedata(100, 10))
+ plot!(PlotsBase.fakedata(100, 10))
end,
),
PlotExample( # 19
@@ -261,11 +266,11 @@ const _examples = PlotExample[
closepct = rand(n)
y = OHLC[
(
- openpct[i] * hgt[i] + bot[i],
- bot[i] + hgt[i],
- bot[i],
- closepct[i] * hgt[i] + bot[i],
- ) for i in 1:n
+ openpct[i] * hgt[i] + bot[i],
+ bot[i] + hgt[i],
+ bot[i],
+ closepct[i] * hgt[i] + bot[i],
+ ) for i in 1:n
]
ohlc(y)
end,
@@ -283,7 +288,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,12 +299,18 @@ 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)),
- (10, y[10], ("this is #10", :right, 20, "courier")),
- ])
+ annotate!(
+ [
+ (5, y[5], ("this is #5", 16, :red, :center)),
+ (10, y[10], ("this is #10", :right, 20, "courier")),
+ ]
+ )
# `x, y, text` vectors
annotate!([2, 8], y[[2, 8]], ["#2", "#8"])
scatter!(
@@ -312,16 +323,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 +405,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,
@@ -453,7 +464,7 @@ const _examples = PlotExample[
),
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),
@@ -589,7 +600,7 @@ const _examples = PlotExample[
"Ribbons",
"""
Ribbons can be added to lines via the `ribbon` keyword;
- you can pass a tuple of arrays (upper and lower bounds),
+ you can pass a tuple of arrays (lower and upper bounds, in this order),
a single Array (for symmetric ribbons), a Function, or a number.
""",
quote
@@ -707,18 +718,18 @@ const _examples = PlotExample[
value(m::Measurement) = m.val
uncertainty(m::Measurement) = m.err
- @recipe function f(::Type{T}, m::T) where {T<:AbstractArray{<:Measurement}}
+ @recipe function f(::Type{T}, m::T) where {T <: AbstractArray{<:Measurement}}
if !(
- get(plotattributes, :seriestype, :path) in (
- :contour,
- :contourf,
- :contour3d,
- :heatmap,
- :surface,
- :wireframe,
- :image,
+ get(plotattributes, :seriestype, :path) in (
+ :contour,
+ :contourf,
+ :contour3d,
+ :heatmap,
+ :surface,
+ :wireframe,
+ :image,
+ )
)
- )
error_sym = Symbol(plotattributes[:letter], :error)
plotattributes[error_sym] = uncertainty.(m)
end
@@ -786,7 +797,7 @@ const _examples = PlotExample[
ylabel = "y",
zlabel = "z",
legend = :none,
- margin = 2Plots.mm,
+ margin = 2PlotsBase.mm,
)
end,
),
@@ -830,7 +841,7 @@ const _examples = PlotExample[
z;
projection = :polar,
color = :cividis,
- right_margin = 2Plots.mm,
+ right_margin = 2PlotsBase.mm,
)
end,
),
@@ -937,7 +948,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]) / π)
@@ -957,9 +968,9 @@ const _examples = PlotExample[
wireframe(
args...,
title = "wire-flip-$ax",
- xflip = ax === :x,
- yflip = ax === :y,
- zflip = ax === :z;
+ xflip = ax ≡ :x,
+ yflip = ax ≡ :y,
+ zflip = ax ≡ :z;
kw...,
),
)
@@ -971,9 +982,9 @@ const _examples = PlotExample[
wireframe(
args...,
title = "wire-mirror-$ax",
- xmirror = ax === :x,
- ymirror = ax === :y,
- zmirror = ax === :z;
+ xmirror = ax ≡ :x,
+ ymirror = ax ≡ :y,
+ zmirror = ax ≡ :z;
kw...,
),
)
@@ -982,7 +993,7 @@ const _examples = PlotExample[
plot(
plots...,
layout = (@layout [_ ° _; ° ° °; ° ° °]),
- margin = 0Plots.px,
+ margin = 0PlotsBase.px,
)
end
end,
@@ -1148,22 +1159,22 @@ const _examples = PlotExample[
plot(
x,
- sin.(x),
+ sin.(x);
xaxis = "common X label",
yaxis = "Y label 1",
color = :red,
- title = "twinx";
+ title = "twinx",
kw...,
)
pl = plot!(twinx(), x, 2cos.(x), yaxis = "Y label 2"; kw...)
plot(
x,
- cos.(x),
+ cos.(x);
xaxis = "X label 1",
yaxis = "common Y label",
color = :red,
- title = "twiny";
+ title = "twiny",
kw...,
)
pr = plot!(twiny(), 2x, cos.(2x), xaxis = "X label 2"; kw...)
@@ -1192,13 +1203,14 @@ const _examples = PlotExample[
marker = :circle,
ticks = :none,
leg_title = leg,
+ label = :auto,
leg,
kw...,
),
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,
@@ -1224,13 +1236,14 @@ const _examples = PlotExample[
marker = :circle,
ticks = :none,
leg_title = leg,
+ label = :auto,
leg = leg isa Symbol ? Symbol(:outer, leg) : :none,
kw...,
),
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,
@@ -1238,24 +1251,26 @@ const _examples = PlotExample[
PlotExample( # 66
"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
- legend = false,
- )),
+ :(
+ 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
+ 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,41 +1283,17 @@ _backend_skips = Dict(
66, # bar: vector-valued `color` unsupported
],
:pgfplotsx => [
- 6, # images
- 16, # pgfplots thinks the upper panel is too small
+ 06, # images
+ 16, # pgfplotsx thinks the upper panel is too small
32, # spy
49, # polar heatmap
51, # image with custom axes
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 +1317,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 in keys(_backend_skips)
+ append!(_backend_skips[backend], [25, 30])
+end
# ---------------------------------------------------------------------------------
# replace `f(args...)` with `f(rng, args...)` for `f ∈ (rand, randn)`
@@ -1339,46 +1333,68 @@ 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
+ return expr
+end
+
+replace_module(ex) = ex
+
+function replace_module(ex::Expr)
+ if Meta.isexpr(ex, :import) || Meta.isexpr(ex, :using)
+ expr = Expr(ex.head)
+ for arg in ex.args
+ mod = last(arg.args)
+ new_arg = if Meta.isexpr(arg, :.)
+ mod ≡ :PlotsBase ? arg : Expr(:., :PlotsBase, mod)
+ else
+ arg
+ end
+ push!(expr.args, new_arg)
+ end
+ else
+ expr = ex
+ end
+ return expr
end
# make and display one plot
test_examples(i::Integer; kw...) = test_examples(backend_name(), i; kw...)
function test_examples(
- pkgname::Symbol,
- i::Integer;
- debug = false,
- disp = false,
- rng = nothing,
- callback = nothing,
-)
+ pkgname::Symbol,
+ i::Integer;
+ debug = false,
+ disp = false,
+ rng = nothing,
+ callback = nothing,
+ )
@info "Testing plot: $pkgname:$i:$(_examples[i].header)"
m = Module(Symbol(:PlotsExamples, pkgname))
# prevent leaking variables (esp. functions) directly into Plots namespace
- Base.eval(m, quote
- using Random
- using Plots
- Plots.debug!($debug)
- backend($(QuoteNode(pkgname)))
- rng = $rng
- rng === nothing || Random.seed!(rng, Plots.PLOTS_SEED)
- theme(:default)
- end)
- (imp = _examples[i].imports) === nothing || Base.eval(m, imp)
+ Base.eval(
+ m, quote
+ using Random
+ using PlotsBase
+ PlotsBase.Commons.debug!($debug)
+ backend($(QuoteNode(pkgname)))
+ rng = $rng
+ rng ≡ nothing || Random.seed!(rng, PlotsBase.SEED)
+ theme(:default)
+ end
+ )
+ (imp = _examples[i].imports) ≡ nothing || Base.eval(m, imp)
exprs = _examples[i].exprs
- rng === nothing || (exprs = Plots.replace_rand(exprs))
+ rng ≡ nothing || (exprs = PlotsBase.replace_rand(exprs))
Base.eval(m, exprs)
disp && Base.eval(m, :(gui(current())))
- callback === nothing || callback(m, pkgname, i)
- m.Plots.current()
+ callback ≡ nothing || Base.invokelatest(callback, m, pkgname, i)
+ return Base.eval(m, :(current()))
end
# generate all plots and create a dict mapping idx --> plt
@@ -1388,15 +1404,15 @@ test_examples(pkgname[, idx]; debug=false, disp=false, sleep=nothing, skip=[], o
Run the `idx` test example for a given backend, or all examples if `idx` is not specified.
"""
function test_examples(
- pkgname::Symbol;
- debug = false,
- disp = false,
- sleep = nothing,
- skip = [],
- only = nothing,
- callback = nothing,
- strict = false,
-)
+ pkgname::Symbol;
+ debug = false,
+ disp = false,
+ sleep = nothing,
+ skip = [],
+ only = nothing,
+ callback = nothing,
+ strict = false,
+ )
plts = Dict()
for i in eachindex(_examples)
i ∈ something(only, (i,)) || continue
@@ -1408,11 +1424,11 @@ function test_examples(
if strict
rethrow(ex)
else
- @warn "Example $pkgname:$i:$(_examples[i].header) failed with: $ex"
+ @maxlog_warn "Example $pkgname:$i:$(_examples[i].header) failed with: $ex"
end
# COV_EXCL_STOP
end
- sleep === nothing || Base.sleep(sleep)
+ sleep ≡ nothing || Base.sleep(sleep)
end
- plts
+ return plts
end
diff --git a/PlotsBase/src/init.jl b/PlotsBase/src/init.jl
new file mode 100644
index 0000000000..04f12116bf
--- /dev/null
+++ b/PlotsBase/src/init.jl
@@ -0,0 +1,189 @@
+using Scratch: @get_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 _requirejs_version = v"2.3.7"
+
+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()
+ return 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
+ return _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
+
+ return 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
+ return 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
+ return 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
+ return nothing
+end
+
+macro precompile_backend(backend_package)
+ abstract_backend = Symbol(backend_package, :Backend)
+ return 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(PlotsBase.tmpdir_name())
+ for i in setdiff(
+ 1:n,
+ PlotsBase._backend_skips[backend_name()],
+ PlotsBase._animation_examples,
+ )
+ PlotsBase._examples[i].external && continue
+ (imp = PlotsBase._examples[i].imports) ≡ nothing ||
+ push!(imports, PlotsBase.replace_module(imp))
+ func = gensym(string(i))
+ push!(
+ examples,
+ quote
+ $func() = begin # evaluate each example in a local scope
+ 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
+ PrecompileTools.@compile_workload begin
+ withenv("GKSwstype" => "nul", "MPLBACKEND" => "agg") do
+ eval.(imports)
+ eval.(examples)
+ PlotsBase.CURRENT_PLOT.nullableplot = nothing
+ end
+ end
+ end
+ end |> esc
+end
diff --git a/PlotsBase/src/layouts.jl b/PlotsBase/src/layouts.jl
new file mode 100644
index 0000000000..fa7cb5f0e8
--- /dev/null
+++ b/PlotsBase/src/layouts.jl
@@ -0,0 +1,451 @@
+"""
+ 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...)
+
+# padding_w(layout::AbstractLayout) = left_padding(layout) + right_padding(layout)
+# padding_h(layout::AbstractLayout) = bottom_padding(layout) + top_padding(layout)
+# padding(layout::AbstractLayout) = (padding_w(layout), padding_h(layout))
+
+update_position!(layout::AbstractLayout) = nothing
+update_child_bboxes!(
+ layout::AbstractLayout,
+ minimum_perimeter = [0mm, 0mm, 0mm, 0mm];
+ kw...,
+) = nothing
+
+# pass these through to the bbox methods if there's no plotarea
+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
+
+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)
+# for the whole layout tree. then we can compute the "padding borders" of this
+# layout as the biggest padding of the children on the perimeter. then we need to
+# recursively pass those borders back down the tree, one side at a time, but ONLY
+# to those perimeter children.
+
+function paddings(args...)
+ funcs = (leftpad, toppad, rightpad, bottompad)
+ args = length(args) == 1 ? ntuple(i -> first(args), Val(4)) : args
+ return map(i -> map(funcs[i], args[i]), Tuple(1:4))
+end
+
+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
+ parent(isp) == sp || continue
+ _update_min_padding!(isp)
+ sp.minpad = max.(sp.minpad, isp.minpad)
+end
+
+# leftpad, toppad, rightpad, bottompad
+function _update_min_padding!(layout::GridLayout)
+ map(_update_min_padding!, layout.grid)
+ map(_update_inset_padding!, layout.grid)
+ layout.minpad = compute_minpad(
+ layout.grid[:, 1],
+ layout.grid[1, :],
+ layout.grid[:, end],
+ layout.grid[end, :],
+ )
+ return layout.minpad
+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"
+function recompute_lengths(v)
+ # dump(v)
+ tot = 0pct
+ cnt = 0
+ for vi in v
+ if vi == 0pct
+ cnt += 1
+ else
+ tot += vi
+ end
+ end
+ leftover = 1.0pct - tot
+ if cnt > 1 && leftover.value ≤ 0
+ error(
+ "Not enough length left over in layout! v = $v, cnt = $cnt, leftover = $leftover",
+ )
+ end
+
+ return 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!)
+function update_child_bboxes!(layout::GridLayout, minimum_perimeter = [0mm, 0mm, 0mm, 0mm])
+ nr, nc = size(layout)
+
+ # create a matrix for each minimum padding direction
+ minpad_left, minpad_top, minpad_right, minpad_bottom = paddings(layout.grid)
+
+ # get the max horizontal (left and right) padding over columns,
+ # and max vertical (bottom and top) padding over rows
+ # TODO: add extra padding here
+ pad_left = maximum(minpad_left, dims = 1)
+ pad_top = maximum(minpad_top, dims = 2)
+ pad_right = maximum(minpad_right, dims = 1)
+ pad_bottom = maximum(minpad_bottom, dims = 2)
+
+ # make sure the perimeter match the parent
+ pad_left[1] = max(pad_left[1], leftpad(minimum_perimeter))
+ pad_top[1] = max(pad_top[1], toppad(minimum_perimeter))
+ pad_right[end] = max(pad_right[end], rightpad(minimum_perimeter))
+ pad_bottom[end] = max(pad_bottom[end], bottompad(minimum_perimeter))
+
+ # scale this up to the total padding in each direction, and limit padding to 95%
+ total_pad_horizontal = min(0.95width(layout), sum(pad_left + pad_right))
+ total_pad_vertical = min(0.95height(layout), sum(pad_top + pad_bottom))
+
+ # now we can compute the total plot area in each direction
+ total_plotarea_horizontal = width(layout) - total_pad_horizontal
+ total_plotarea_vertical = height(layout) - total_pad_vertical
+
+ @assert total_plotarea_horizontal > 0mm
+ @assert total_plotarea_vertical > 0mm
+
+ # recompute widths/heights
+ layout.widths = recompute_lengths(layout.widths)
+ 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
+ child = layout[r, c]
+
+ # get the top-left corner of this child... the first one is top-left of the parent (i.e. layout)
+ child_left = c == 1 ? left(layout.bbox) : right(layout[r, c - 1].bbox)
+ child_top = r == 1 ? top(layout.bbox) : bottom(layout[r - 1, c].bbox)
+
+ # compute plot area
+ plotarea_left = child_left + pad_left[c]
+ plotarea_top = child_top + pad_top[r]
+ plotarea_width = total_plotarea_horizontal * layout.widths[c]
+ plotarea_height = total_plotarea_vertical * layout.heights[r]
+
+ bb = BoundingBox(plotarea_left, plotarea_top, plotarea_width, plotarea_height)
+ plotarea!(child, bb)
+
+ # compute child bbox
+ child_width = pad_left[c] + plotarea_width + pad_right[c]
+ child_height = pad_top[r] + plotarea_height + pad_bottom[r]
+ bbox!(child, BoundingBox(child_left, child_top, child_width, child_height))
+
+ # this is the minimum perimeter as decided by this child's parent, so that
+ # all children on this border have the same value
+ min_child_perim = [
+ c == 1 ? leftpad(layout) : pad_left[c],
+ r == 1 ? toppad(layout) : pad_top[r],
+ c == nc ? rightpad(layout) : pad_right[c],
+ r == nr ? bottompad(layout) : pad_bottom[r],
+ ]
+ # recursively update the child's children
+ update_child_bboxes!(child, min_child_perim)
+ end
+ return
+end
+
+"""
+For each inset (floating) subplot, resolve the relative position
+to absolute canvas coordinates, relative to the parent's plotarea.
+"""
+update_inset_bboxes!(plt::Plot) =
+ for sp in plt.inset_subplots
+ p_area = Measures.resolve(plotarea(sp.parent), sp[:relative_bbox])
+ plotarea!(sp, p_area)
+ # NOTE: `lens` example, `pgfplotsx` for non-regression
+ bbox!(
+ sp,
+ bbox(
+ left(p_area) - leftpad(sp),
+ top(p_area) - toppad(sp),
+ width(p_area) + leftpad(sp) + rightpad(sp),
+ height(p_area) + toppad(sp) + bottompad(sp),
+ ),
+ )
+end
+# ----------------------------------------------------------------------
+
+calc_num_subplots(layout::AbstractLayout) = get(layout.attr, :blank, false) ? 0 : 1
+calc_num_subplots(layout::GridLayout) = sum(map(l -> calc_num_subplots(l), layout.grid))
+
+function compute_gridsize(numplts::Int, nr::Int, nc::Int)
+ # figure out how many rows/columns we need
+ if nr < 1
+ if nc < 1
+ nr = round(Int, sqrt(numplts))
+ nc = ceil(Int, numplts / nr)
+ else
+ nr = ceil(Int, numplts / nc)
+ end
+ else
+ nc = ceil(Int, numplts / nr)
+ end
+ return nr, nc
+end
+
+# ----------------------------------------------------------------------
+# constructors
+
+# pass the layout arg through
+layout_attrs(plotattributes::AKW) = layout_attrs(plotattributes[:layout])
+
+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.",
+ )
+ end
+ return layout, n
+end
+
+function layout_attrs(n::Integer)
+ nr, nc = compute_gridsize(n, -1, -1)
+ return GridLayout(nr, nc), n
+end
+
+function layout_attrs(sztup::NTuple{2, Integer})
+ nr, nc = sztup
+ return GridLayout(nr, nc), nr * nc
+end
+
+layout_attrs(n_override::Integer, n::Integer) = layout_attrs(n)
+layout_attrs(n, sztup::NTuple{2, Integer}) = layout_attrs(sztup)
+
+function layout_attrs(n, sztup::Tuple{Colon, Integer})
+ nc = sztup[2]
+ nr = ceil(Int, n / nc)
+ return GridLayout(nr, nc), n
+end
+
+function layout_attrs(n, sztup::Tuple{Integer, Colon})
+ nr = sztup[1]
+ nc = ceil(Int, n / nr)
+ return GridLayout(nr, nc), n
+end
+
+function layout_attrs(sztup::NTuple{3, Integer})
+ n, nr, nc = sztup
+ nr, nc = compute_gridsize(n, nr, nc)
+ return GridLayout(nr, nc), n
+end
+
+layout_attrs(nt::NamedTuple) = EmptyLayout(; nt...), 1
+
+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_attrs(m[ci])[1]
+ end
+ return layout_attrs(gl)
+end
+
+# recursively get the size of the grid
+layout_attrs(layout::GridLayout) = layout, calc_num_subplots(layout)
+
+layout_attrs(n_override::Integer, layout::Union{AbstractVecOrMat, GridLayout}) =
+ layout_attrs(layout)
+
+# ----------------------------------------------------------------------
+
+function build_layout(args...)
+ layout, n = layout_attrs(args...)
+ return build_layout(layout, n, Array{Plot}(undef, 0))
+end
+
+# n is the number of subplots...
+function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot})
+ nr, nc = size(layout)
+ subplots = Subplot[]
+ spmap = Plots.SubplotMap()
+ empty = isempty(plts)
+ i = 0
+ for r in 1:nr, c in 1:nc
+ l = layout[r, c]
+ if isa(l, EmptyLayout) && !get(l.attr, :blank, false)
+ if empty
+ # initialize the inner subplots recursively
+ sp = Subplot(backend(), parent = layout)
+ layout[r, c] = sp
+ push!(subplots, sp)
+ spmap[attr(l, :label, gensym())] = sp
+ inc = 1
+ else
+ # build a layout from a list of existing Plot objects
+ plt = popfirst!(plts) # grab the first plot out of the list
+ layout[r, c] = plt.layout
+ append!(subplots, plt.subplots)
+ merge!(spmap, plt.spmap)
+ inc = length(plt.subplots)
+ end
+ if get(l.attr, :width, :auto) ≢ :auto
+ layout.widths[c] = attr(l, :width)
+ end
+ 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
+ layout.widths[c] = attr(l, :width)
+ end
+ if get(l.attr, :height, :auto) ≢ :auto
+ layout.heights[r] = attr(l, :height)
+ end
+ l, sps, m = build_layout(l, n - i, plts)
+ append!(subplots, sps)
+ merge!(spmap, m)
+ i += length(sps)
+ elseif isa(l, Subplot) && empty
+ error("Subplot exists. Cannot reuse existing layout. Please make a new one.")
+ end
+ i ≥ n && break # only add n subplots
+ end
+
+ return layout, subplots, spmap
+end
+
+# -------------------------------------------------------------------------
+
+# make all reference the same axis extrema/values.
+# merge subplot lists.
+function link_axes!(axes::Axis...)
+ a1 = axes[1]
+ for i in 2:length(axes)
+ a2 = axes[i]
+ a1[:unit] ≡ a2[:unit] ||
+ error("Cannot link axes with different units: $(a1[:unit]) and $(a2[:unit])")
+ expand_extrema!(a1, Axes.ignorenan_extrema(a2))
+ for k in (: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
+ sp in a1.sps || push!(a1.sps, sp)
+ end
+ a2.sps = a1.sps
+ end
+ return
+end
+
+# figure out which subplots to link
+function link_subplots(a::AbstractArray{AbstractLayout}, axissym::Symbol)
+ subplots = []
+ for l in a
+ if isa(l, Subplot)
+ push!(subplots, l)
+ elseif isa(l, GridLayout) && size(l) == (1, 1)
+ push!(subplots, l[1, 1])
+ end
+ end
+ return subplots
+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]
+ return length(axes) > 0 && link_axes!(axes...)
+end
+
+# don't do anything for most layout types
+function link_axes!(l::AbstractLayout, link::Symbol) end
+
+# process a GridLayout, recursively linking axes according to the link symbol
+function link_axes!(layout::GridLayout, link::Symbol)
+ nr, nc = size(layout)
+ link in (:x, :both) && for c in 1:nc
+ link_axes!(layout.grid[:, c], :xaxis)
+ end
+ link in (:y, :both) && for r in 1:nr
+ link_axes!(layout.grid[r, :], :yaxis)
+ end
+ link ≡ :square && if (sps = filter(l -> isa(l, Subplot), layout.grid)) |> !isempty
+ base_axis = sps[1][:xaxis]
+ for sp in sps
+ link_axes!(base_axis, sp[:xaxis])
+ link_axes!(base_axis, sp[:yaxis])
+ end
+ end
+ if link ≡ :all
+ link_axes!(layout.grid, :xaxis)
+ link_axes!(layout.grid, :yaxis)
+ end
+ return foreach(l -> link_axes!(l, link), layout.grid)
+end
+
+# -------------------------------------------------------------------------
+
+function twin(sp, letter)
+ plt = sp.plt
+ orig_sp = first(plt.subplots)
+ for letter in 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
+ # incompatible with shared axes (see github.com/JuliaPlots/Plots.jl/issues/2894)
+ orig_sp[:framestyle] = :axes
+ end
+ plot!(
+ plt;
+ inset = (sp[:subplot_index], bbox(0, 0, 1, 1)),
+ left_margin = orig_sp[:left_margin],
+ top_margin = orig_sp[:top_margin],
+ right_margin = orig_sp[:right_margin],
+ bottom_margin = orig_sp[:bottom_margin],
+ )
+ twin_sp = last(plt.subplots)
+ letters = axes_letters(twin_sp, letter)
+ tax, oax = map(l -> twin_sp[get_attr_symbol(l, :axis)], letters)
+ tax[:grid] = false
+ tax[:showaxis] = false
+ tax[:ticks] = :none
+ tax[:unitformat] = :nounit
+ tax[:unit] = orig_sp[get_attr_symbol(letter, :axis)][:unit]
+ oax[:grid] = false
+ oax[:mirror] = true
+ twin_sp[:background_color_inside] = RGBA{Float64}(0, 0, 0, 0)
+ link_axes!(sp[get_attr_symbol(letter, :axis)], tax)
+ return twin_sp
+end
+
+"""
+ twinx(sp)
+
+Adds a new, empty subplot overlaid on top of `sp`, with a mirrored y-axis and linked x-axis.
+"""
+twinx(sp::Subplot) = twin(sp, :x)
+twinx(plt::Plot = current()) = twinx(first(plt))
+
+"""
+ twiny(sp)
+
+Adds a new, empty subplot overlaid on top of `sp`, with a mirrored x-axis and linked y-axis.
+"""
+twiny(sp::Subplot) = twin(sp, :y)
+twiny(plt::Plot = current()) = twiny(first(plt))
diff --git a/PlotsBase/src/legend.jl b/PlotsBase/src/legend.jl
new file mode 100644
index 0000000000..f8d9f040c8
--- /dev/null
+++ b/PlotsBase/src/legend.jl
@@ -0,0 +1,79 @@
+@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)
+```
+
+Return `(x,y)` at an angle `theta` degrees from
+`(xcenter,ycenter)` on a rectangle defined by (`xmin`, `xmax`, `ymin`, `ymax`).
+"""
+function legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax)
+ (s, c) = sincosd(theta)
+ x = c < 0 ? (xmin - xcenter) / c : (xmax - xcenter) / c
+ y = s < 0 ? (ymin - ycenter) / s : (ymax - ycenter) / s
+ A = min(x, y)
+ return (xcenter + A * c, ycenter + A * s)
+end
+
+"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
+end
+
+"""
+Turn legend argument into a (theta, :inner) or (theta, :outer) tuple.
+For backends where legend position is given in normal coordinates (0,0) -- (1,1),
+so :topleft exactly corresponds to (45, :inner) etc.
+
+If `leg` is a (::Real,::Real) tuple, keep it as is.
+"""
+legend_angle(leg::Real) = (leg, :inner)
+legend_angle(leg::Tuple{S, T}) where {S <: Real, T <: Real} = leg
+legend_angle(leg::Tuple{S, Symbol}) where {S <: Real} = leg
+legend_angle(leg::Symbol) = get(
+ (
+ topleft = (135, :inner),
+ top = (90, :inner),
+ topright = (45, :inner),
+ left = (180, :inner),
+ right = (0, :inner),
+ bottomleft = (225, :inner),
+ bottom = (270, :inner),
+ bottomright = (315, :inner),
+ outertopleft = (135, :outer),
+ outertop = (90, :outer),
+ outertopright = (45, :outer),
+ outerleft = (180, :outer),
+ outerright = (0, :outer),
+ outerbottomleft = (225, :outer),
+ outerbottom = (270, :outer),
+ outerbottomright = (315, :outer),
+ ),
+ 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 71%
rename from src/output.jl
rename to PlotsBase/src/output.jl
index ce7a3f2448..30bb20e963 100644
--- a/src/output.jl
+++ b/PlotsBase/src/output.jl
@@ -1,10 +1,11 @@
+struct PlotsDisplay <: AbstractDisplay end
-defaultOutputFormat(plt::Plot) = "png"
+default_output_format(plt::Plot) = "png"
function png(plt::Plot, fn)
fn = addExtension(fn, "png")
open(io -> show(io, MIME("image/png"), plt), fn, "w")
- fn
+ return fn
end
png(fn) = png(current(), fn)
@@ -14,7 +15,7 @@ png(io::IO) = png(current(), io)
function svg(plt::Plot, fn)
fn = addExtension(fn, "svg")
open(io -> show(io, MIME("image/svg+xml"), plt), fn, "w")
- fn
+ return fn
end
svg(fn) = svg(current(), fn)
@@ -25,7 +26,7 @@ svg(io::IO) = svg(current(), io)
function pdf(plt::Plot, fn)
fn = addExtension(fn, "pdf")
open(io -> show(io, MIME("application/pdf"), plt), fn, "w")
- fn
+ return fn
end
pdf(fn) = pdf(current(), fn)
@@ -35,7 +36,7 @@ pdf(io::IO) = pdf(current(), io)
function ps(plt::Plot, fn)
fn = addExtension(fn, "ps")
open(io -> show(io, MIME("application/postscript"), plt), fn, "w")
- fn
+ return fn
end
ps(fn) = ps(current(), fn)
@@ -45,7 +46,7 @@ ps(io::IO) = ps(current(), io)
function eps(plt::Plot, fn)
fn = addExtension(fn, "eps")
open(io -> show(io, MIME("image/eps"), plt), fn, "w")
- fn
+ return fn
end
eps(fn) = eps(current(), fn)
@@ -55,7 +56,7 @@ eps(io::IO) = eps(current(), io)
function tex(plt::Plot, fn)
fn = addExtension(fn, "tex")
open(io -> show(io, MIME("application/x-tex"), plt), fn, "w")
- fn
+ return fn
end
tex(fn) = tex(current(), fn)
@@ -65,7 +66,7 @@ tex(io::IO) = tex(current(), io)
function json(plt::Plot, fn)
fn = addExtension(fn, "json")
open(io -> show(io, MIME("application/vnd.plotly.v1+json"), plt), fn, "w")
- fn
+ return fn
end
json(fn) = json(current(), fn)
@@ -75,7 +76,7 @@ json(io::IO) = json(current(), io)
function html(plt::Plot, fn)
fn = addExtension(fn, "html")
open(io -> show(io, MIME("text/html"), plt), fn, "w")
- fn
+ return fn
end
html(fn) = html(current(), fn)
@@ -85,7 +86,7 @@ html(io::IO) = html(current(), io)
function txt(plt::Plot, fn; color::Bool = true)
fn = addExtension(fn, "txt")
open(io -> show(IOContext(io, :color => color), MIME("text/plain"), plt), fn, "w")
- fn
+ return fn
end
txt(fn) = txt(current(), fn)
@@ -125,7 +126,7 @@ function addExtension(fp, ext::AbstractString)
dn, fn = splitdir(fp)
_, oldext = splitext(fn)
oldext = chop(oldext, head = 1, tail = 0)
- get(_extension_map, oldext, oldext) == ext ? fp : joinpath(dn, string(fn, ".", ext))
+ return get(_extension_map, oldext, oldext) == ext ? fp : joinpath(dn, string(fn, ".", ext))
end
"""
@@ -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,46 +159,51 @@ 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)
+ return _display(plt)
end
_do_plot_show(plt, showval::Bool) = showval && gui(plt)
function _do_plot_show(plt, showval::Symbol)
- showval === :gui && gui(plt)
- showval in (:inline, :ijulia) && inline(plt)
+ showval ≡ :gui && gui(plt)
+ return showval in (:inline, :ijulia) && inline(plt)
end
# ---------------------------------------------------------
-const _best_html_output_type =
- KW(:pyplot => :png, :unicodeplots => :txt, :plotlyjs => :html, :plotly => :html)
+const _best_html_output_type = KW(
+ :unicodeplots => :png, # FIXME: https://github.com/fredrikekre/Literate.jl/issues/276
+ :pythonplot => :png,
+ :pgfplotsx => :png,
+ :plotlyjs => :html,
+ :plotly => :html,
+ :gaston => :png,
+ :gr => :png,
+)
# 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
+ return 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))")
@@ -205,25 +211,26 @@ function _show(io::IO, ::MIME"text/html", plt::Plot)
end
# delegate showable to _show instead
-Base.showable(m::M, ::P) where {M<:MIME,P<:Plot} = showable(m, P)
-Base.showable(::M, ::Type{P}) where {M<:MIME,P<:Plot} = hasmethod(_show, Tuple{IO,M,P})
+Base.showable(m::M, ::P) where {M <: MIME, P <: Plot} = showable(m, P)
+Base.showable(::M, ::Type{P}) where {M <: MIME, P <: Plot} = hasmethod(_show, Tuple{IO, M, P})
+
+_display(plt::Plot) = @maxlog_warn "_display is not defined for this backend."
-_display(plt::Plot) = @warn "_display is not defined for this backend."
+Base.show(io::IO, ::MIME"text/plain", plt::Plot) = show(io, plt)
-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 (
- "text/html",
- "text/latex",
- "image/png",
- "image/eps",
- "image/svg+xml",
- "application/eps",
- "application/pdf",
- "application/postscript",
- "application/x-tex",
- "application/vnd.plotly.v1+json",
-)
+ "application/vnd.plotly.v1+json",
+ "application/postscript",
+ "application/x-tex",
+ "application/pdf",
+ "application/eps",
+ "image/svg+xml",
+ "text/latex",
+ "image/png",
+ "image/eps",
+ "text/html",
+ )
@eval function Base.show(io::IO, m::MIME{Symbol($mime)}, plt::Plot)
if haskey(io, :juno_plotsize)
showjuno(io, m, plt)
@@ -240,19 +247,33 @@ 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
+ return try
_showjuno(io, m, plt)
finally
plt[:dpi] = dpi
@@ -260,13 +281,14 @@ function showjuno(io::IO, m, plt)
end
_showjuno(io::IO, m::MIME"image/svg+xml", plt) =
- if Symbol(plt.attr[:html_output_format]) ≠ :svg
- throw(MethodError(show, (typeof(m), typeof(plt))))
- else
- _show(io, m, plt)
- end
+if Symbol(plt.attr[:html_output_format]) ≠ :svg
+ throw(MethodError(show, (typeof(m), typeof(plt))))
+else
+ _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 76%
rename from src/pipeline.jl
rename to PlotsBase/src/pipeline.jl
index 3babfb5ab9..04aa75110f 100644
--- a/src/pipeline.jl
+++ b/PlotsBase/src/pipeline.jl
@@ -3,18 +3,19 @@
## Warnings
function RecipesPipeline.warn_on_recipe_aliases!(
- plt::Plot,
- plotattributes::AKW,
- recipe_type::Symbol,
- @nospecialize(args)
-)
+ plt::Plot,
+ plotattributes::AKW,
+ recipe_type::Symbol,
+ @nospecialize(args)
+ )
pkeys = keys(plotattributes)
for k in pkeys
- if (dk = get(_keyAliases, k, nothing)) !== nothing
+ if (dk = get(Commons._keyAliases, k, nothing)) ≢ nothing
kv = RecipesPipeline.pop_kw!(plotattributes, k)
dk ∈ pkeys || (plotattributes[dk] = kv)
end
end
+ return
end
## Grouping
@@ -24,27 +25,24 @@ RecipesPipeline.splittable_attribute(plt::Plot, key, val::SeriesAnnotations, len
RecipesPipeline.split_attribute(plt::Plot, key, val::SeriesAnnotations, indices) =
SeriesAnnotations(
- RecipesPipeline.split_attribute(plt, key, val.strs, indices),
- val.font,
- val.baseshape,
- val.scalefactor,
- )
+ RecipesPipeline.split_attribute(plt, key, val.strs, indices),
+ val.font,
+ val.baseshape,
+ val.scalefactor,
+)
## 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)
+ return 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
@@ -58,17 +56,17 @@ function RecipesPipeline.process_userrecipe!(plt::Plot, kw_list, kw)
push!(kw_list, kw)
_add_errorbar_kw(kw_list, kw)
_add_smooth_kw(kw_list, kw)
- nothing
+ return nothing
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 in Commons._axis_attrs
+ k1 = Commons._attrsymbolcache[l1][k]
+ k2 = Commons._attrsymbolcache[l2][k]
kwk = keys(kw)
if k1 in kwk || k2 in kwk
kw[k1], kw[k2] = get(kw, k2, default(k2)), get(kw, k1, default(k1))
@@ -90,7 +88,7 @@ function _preprocess_userrecipe(kw::AKW)
map(kw[:line_z], kw[:x], kw[:y], kw[:z])
end
- nothing
+ return nothing
end
function _add_errorbar_kw(kw_list::Vector{KW}, kw::AKW)
@@ -98,9 +96,9 @@ function _add_errorbar_kw(kw_list::Vector{KW}, kw::AKW)
# the same recipedata index as the recipedata they are copied from
st = get(kw, :seriestype, :none)
errors = (:xerror, :yerror, :zerror)
- if st ∉ errors
+ return if st ∉ errors
for esym in errors
- if get(kw, esym, nothing) !== nothing
+ if get(kw, esym, nothing) ≢ nothing
# we make a copy of the KW and apply an errorbar recipe
errkw = copy(kw)
errkw[:seriestype] = esym
@@ -114,7 +112,7 @@ end
function _add_smooth_kw(kw_list::Vector{KW}, kw::AKW)
# handle smoothing by adding a new series
- if get(kw, :smooth, false)
+ return if get(kw, :smooth, false)
x, y = kw[:x], kw[:y]
β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y)
sx = [ignorenan_minimum(x), ignorenan_maximum(x)]
@@ -140,17 +138,17 @@ 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
function RecipesPipeline.plot_setup!(plt::Plot, plotattributes, kw_list)
_plot_setup(plt, plotattributes, kw_list)
_subplot_setup(plt, plotattributes, kw_list)
- nothing
+ return 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
@@ -163,7 +161,7 @@ function RecipesPipeline.process_sliced_series_attributes!(plt::Plots.Plot, kw_l
err_inds =
findall(kw -> get(kw, :seriestype, :path) in (:xerror, :yerror, :zerror), kw_list)
for ind in err_inds
- if ind > 1 && get(kw_list[ind - 1], :seriestype, :path) === :scatter
+ if ind > 1 && get(kw_list[ind - 1], :seriestype, :path) ≡ :scatter
tmp = copy(kw_list[ind])
kw_list[ind] = copy(kw_list[ind - 1])
kw_list[ind - 1] = tmp
@@ -178,21 +176,20 @@ 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
+ return 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
@@ -200,7 +197,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
end
# TODO: init subplots here
- _update_plot_args(plt, plotattributes)
+ _update_plot_attrs(plt, plotattributes)
if !plt.init
plt.o = Base.invokelatest(_create_backend_figure, plt)
@@ -215,7 +212,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
end
# handle inset subplots
- if (insets = plt[:inset_subplots]) !== nothing
+ if (insets = plt[:inset_subplots]) ≢ nothing
typeof(insets) <: AVec || (insets = [insets])
for inset in insets
parent, bb = is_2tuple(inset) ? inset : (nothing, inset)
@@ -234,7 +231,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
sp.attr[:subplot_index] = length(plt.subplots)
end
end
- plt[:inset_subplots] = nothing
+ return plt[:inset_subplots] = nothing
end
function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
@@ -242,30 +239,27 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
# Subplot/Axis attributes set by a user/series recipe apply only to the
# Subplot object which they belong to.
# TODO: allow matrices to still apply to all subplots
- sp_attrs = Dict{Subplot,Any}()
+ sp_attrs = Dict{Subplot, Any}()
for kw in 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)
+ if Commons.is_subplot_attrs(k) || Commons.is_axis_attrs(k)
v = pop!(kw, k)
if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps)
v = v[series_idx(kw_list, kw)]
end
attr[k] = v
end
- if is_axis_attr_noletter(k)
+ if Commons.is_axis_attr_noletter(k)
v = pop!(kw, k)
if sps isa AbstractArray && v isa AbstractArray && length(v) == length(sps)
v = v[series_idx(kw_list, kw)]
@@ -291,12 +285,12 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
else
get(sp_attrs, sp, KW())
end
- _update_subplot_args(plt, sp, attr, idx, false)
+ Plots._update_subplot_attrs(plt, sp, attr, idx, false)
end
# do we need to link any axes together?
link_axes!(plt.layout, plt[:link])
- nothing
+ return nothing
end
series_idx(kw_list::AVec{KW}, kw::AKW) =
@@ -306,7 +300,7 @@ function _add_plot_title!(plt)
plot_title = plt[:plot_title]
plot_titleindex = nothing
- if plot_title != ""
+ if plot_title |> !isempty
# make new subplot for plot title
if plt[:plot_titleindex] == 0
the_layout = plt.layout
@@ -316,8 +310,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
@@ -336,7 +330,7 @@ function _add_plot_title!(plt)
end
end
- plot_titleindex
+ return plot_titleindex
end
## Series recipes
@@ -345,28 +339,28 @@ 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))
- nothing
+ _slice_series_attrs!(kw, plt, sp, series_idx(kw_list, kw))
+ return 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] =
+ sp[get_attr_symbol(letter2, :axis)][:lims] =
sp[get_attr_symbol(letter2, :axis)][:lims],
sp[get_attr_symbol(letter1, :axis)][:lims]
end
@@ -375,7 +369,7 @@ function RecipesPipeline.add_series!(plt::Plot, plotattributes)
end
_expand_subplot_extrema(sp, plotattributes, plotattributes[:seriestype])
_update_series_attributes!(plotattributes, plt, sp)
- _add_the_series(plt, sp, plotattributes)
+ return _add_the_series(plt, sp, plotattributes)
end
# getting ready to add the series... last update to subplot from anything
@@ -383,16 +377,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
@@ -401,30 +392,12 @@ function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where {T}
_initialize_subplot(plt, sp)
sp.attr[:init] = true
end
- 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
+ return sp
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))
@@ -437,20 +410,20 @@ function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol)
expand_extrema!(sp[:xaxis], 0.0)
expand_extrema!(sp[:yaxis], 0.0)
end
- nothing
+ return nothing
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 +431,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)
@@ -468,5 +441,5 @@ function _add_the_series(plt, sp, plotattributes)
@error "Wrong type $(typeof(z_order)) for attribute z_order"
end
_series_added(plt, series)
- _update_subplot_colorbars(sp, series)
+ return _update_subplot_colorbars(sp, series)
end
diff --git a/src/plot.jl b/PlotsBase/src/plot.jl
similarity index 74%
rename from src/plot.jl
rename to PlotsBase/src/plot.jl
index f165874894..7892a97fbf 100644
--- a/src/plot.jl
+++ b/PlotsBase/src/plot.jl
@@ -1,10 +1,11 @@
+struct PlaceHolder end
mutable struct CurrentPlot
- nullableplot::Union{AbstractPlot,Nothing}
+ nullableplot::Union{AbstractPlot, Nothing}
end
const CURRENT_PLOT = CurrentPlot(nothing)
-isplotnull() = CURRENT_PLOT.nullableplot === nothing
+isplotnull() = CURRENT_PLOT.nullableplot ≡ nothing
"""
current()
@@ -12,22 +13,24 @@ Returns the Plot object for the current plot
"""
function current()
isplotnull() && error("No current plot/subplot")
- CURRENT_PLOT.nullableplot
+ return CURRENT_PLOT.nullableplot
end
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)
s_ekwargs = getindex.(plt.series_list, :extra_kwargs)
(
isempty(plt[:extra_plot_kwargs]) &&
- all(isempty, sp_ekwargs) &&
- all(isempty, s_ekwargs)
+ all(isempty, sp_ekwargs) &&
+ all(isempty, s_ekwargs)
) && return
print(io, "\nCaptured extra kwargs:\n")
do_show = true
@@ -38,7 +41,7 @@ function Base.show(io::IO, plt::Plot)
end
do_show = true
for (i, ekwargs) in enumerate(sp_ekwargs)
- for (key, value) in ekwargs
+ for (key, value) in pairs(ekwargs)
do_show && println(io, " SubplotPlot{$i}:")
println(io, " "^4, key, ": ", value)
do_show = false
@@ -46,27 +49,28 @@ function Base.show(io::IO, plt::Plot)
do_show = true
end
for (i, ekwargs) in enumerate(s_ekwargs)
- for (key, value) in ekwargs
+ for (key, value) in pairs(ekwargs)
do_show && println(io, " Series{$i}:")
println(io, " "^4, key, ": ", value)
do_show = false
end
do_show = true
end
+ return
end
getplot(plt::Plot) = plt
-getattr(plt::Plot, idx::Int = 1) = plt.attr
+getattr(plt::Plot, ::Int = 1) = plt.attr
# ---------------------------------------------------------
"""
The main plot command. Use `plot` to create a new plot object, and `plot!` to add to an existing one:
-```
- plot(args...; kw...) # creates a new plot window, and sets it to be the current
- plot!(args...; kw...) # adds to the `current`
- plot!(plotobj, args...; kw...) # adds to the plot `plotobj`
+```julia
+plot(args...; kw...) # creates a new plot window, and sets it to be the current
+plot!(args...; kw...) # adds to the `current`
+plot!(plotobj, args...; kw...) # adds to the plot `plotobj`
```
There are lots of ways to pass in data, and lots of keyword arguments... just try it and it will likely work as expected.
@@ -78,47 +82,48 @@ 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
- _plot!(plt, plotattributes, args)
+ # plt.user_attrs = plotattributes
+ return _plot!(plt, plotattributes, args)
end
# build a new plot from existing plots
# note: we split into plt1, plt2 and plts_tail so we can dispatch correctly
plot(
plt1::Plot,
- plt2::Union{PlaceHolder,Plot},
- plts_tail::Union{PlaceHolder,Plot}...;
+ plt2::Union{PlaceHolder, Plot},
+ plts_tail::Union{PlaceHolder, Plot}...;
kw...,
) = plot!(deepcopy(plt1), deepcopy(plt2), deepcopy.(plts_tail)...; kw...)
+
function plot!(
- plt1::Plot,
- plt2::Union{PlaceHolder,Plot},
- plts_tail::Union{PlaceHolder,Plot}...;
- kw...,
-)
+ plt1::Plot,
+ plt2::Union{PlaceHolder, Plot},
+ plts_tail::Union{PlaceHolder, Plot}...;
+ kw...,
+ )
@nospecialize
plotattributes = KW(kw)
- Plots.preprocess_attributes!(plotattributes)
+ PlotsBase.Commons.preprocess_attributes!(plotattributes)
# build our plot vector from the args
plts = Plot[plt1]
@@ -127,7 +132,7 @@ function plot!(
n = length(plts)
# compute the layout
- layout = layout_args(plotattributes, n)[1]
+ layout = layout_attrs(plotattributes, n)[1]
num_sp = sum(length(p.subplots) for p in plts)
# create a new plot object, with subplot list/map made of existing subplots.
@@ -135,26 +140,25 @@ function plot!(
# note: all subplots and series "belong" to this new plot...
plt = Plot()
- # TODO: build the user_attr dict by creating "Any matrices" for the args of each subplot
+ # TODO: build the user_attrs dict by creating "Any matrices" for the args of each subplot
- # TODO: replace this with proper processing from a merged user_attr KW
+ # TODO: replace this with proper processing from a merged user_attrs KW
# update plot args
for p in plts
plt.attr = merge(p.attr, plt.attr) # plt.attr preempts p.attr (for `twinx`)
plt.n += p.n
end
plt[:size] = last(sort(getindex.(plts, :size), by = x -> x[1] * x[2]))
- _update_plot_args(plt, plotattributes)
+ _update_plot_attrs(plt, plotattributes)
# pass new plot to the backend
plt.o = _create_backend_figure(plt)
plt.init = true
- series_attr = KW()
+ series_attrs = KW()
for (k, v) in plotattributes
- is_series_attr(k) && (series_attr[k] = pop!(plotattributes, k))
+ Commons.is_series_attrs(k) && (series_attrs[k] = pop!(plotattributes, k))
end
-
# create the layout
plt.layout, plt.subplots, plt.spmap = build_layout(layout, num_sp, copy(plts))
@@ -170,8 +174,8 @@ function plot!(
sp.plt = plt
sp.attr[:subplot_index] = idx
for series in serieslist
- merge!(series.plotattributes, series_attr)
- _slice_series_args!(series.plotattributes, plt, sp, cmdidx)
+ merge!(series.plotattributes, series_attrs)
+ _slice_series_attrs!(series.plotattributes, plt, sp, cmdidx)
push!(plt.series_list, series)
_series_added(plt, series)
cmdidx += 1
@@ -181,13 +185,19 @@ function plot!(
# first apply any args for the subplots
for (idx, sp) in enumerate(plt.subplots)
- _update_subplot_args(plt, sp, idx == ttl_idx ? KW() : plotattributes, idx, false)
+ Plots._update_subplot_attrs(
+ plt,
+ sp,
+ idx == ttl_idx ? KW() : plotattributes,
+ idx,
+ false,
+ )
end
# finish up
current(plt)
_do_plot_show(plt, get(plotattributes, :show, default(:show)))
- plt
+ return plt
end
# this adds to the current plot, or creates a new plot if none are current
@@ -199,18 +209,18 @@ function plot!(args...; kw...)
catch
return plot(args...; kw...)
end
- plot!(current(), args...; kw...)
+ return plot!(current(), args...; kw...)
end
# this adds to a specific plot... most plot commands will flow through here
-plot(plt::Plot, args...; kw...) = plot!(deepcopy(plt), args...; kw...)
+plot(plt::Plot, args...; kw...) = plot!(deepcopy(plt), PlaceHolder(), args...; kw...)
function plot!(plt::Plot, args...; kw...)
@nospecialize
plotattributes = KW(kw)
- Plots.preprocess_attributes!(plotattributes)
- # merge!(plt.user_attr, plotattributes)
- _plot!(plt, plotattributes, args)
+ PlotsBase.Commons.preprocess_attributes!(plotattributes)
+ # merge!(plt.user_attrs, plotattributes)
+ return _plot!(plt, plotattributes, args)
end
# -------------------------------------------------------------------------------
@@ -247,8 +257,7 @@ function prepare_output(plt::Plot)
force_minpad = get(plt, :force_minpad, ())
isempty(force_minpad) || for i in eachindex(plt.layout.grid)
plt.layout.grid[i].minpad = Tuple(
- i === nothing ? j : i for
- (i, j) in zip(force_minpad, plt.layout.grid[i].minpad)
+ i ≡ nothing ? j : i for (i, j) in zip(force_minpad, plt.layout.grid[i].minpad)
)
end
@@ -259,7 +268,7 @@ function prepare_output(plt::Plot)
update_inset_bboxes!(plt)
# the backend callback, to reposition subplots, etc
- _update_plot_object(plt)
+ return _update_plot_object(plt)
end
"""
@@ -270,7 +279,7 @@ Returns `nothing` if the backend does not support this.
"""
function backend_object(plt::Plot)
prepare_output(plt)
- plt.o
+ return plt.o
end
# --------------------------------------------------------------------
@@ -289,13 +298,13 @@ julia> plot(pl.subplots[2]) # extract 2nd subplot as a standalone plot
"""
function plot(sp::Subplot, args...; kw...)
@nospecialize
- plt = Plots.Plot(sp)
- plot(plt, PlaceHolder(), PlaceHolder(), args...; kw...)
+ plt = PlotsBase.Plot(sp)
+ return plot(plt, PlaceHolder(), PlaceHolder(), args...; kw...)
end
# plot to a Subplot
function plot!(sp::Subplot, args...; kw...)
@nospecialize
plt = sp.plt
- plot!(plt, args...; kw..., subplot = findfirst(isequal(sp), plt.subplots))
+ return plot!(plt, args...; kw..., subplot = findfirst(isequal(sp), plt.subplots))
end
diff --git a/src/plotattr.jl b/PlotsBase/src/plotattr.jl
similarity index 70%
rename from src/plotattr.jl
rename to PlotsBase/src/plotattr.jl
index 69018dc64b..9f995cf105 100644
--- a/src/plotattr.jl
+++ b/PlotsBase/src/plotattr.jl
@@ -1,4 +1,3 @@
-
const _attribute_defaults = Dict(
:Series => _series_defaults,
:Subplot => _subplot_defaults,
@@ -10,7 +9,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
@@ -23,53 +22,53 @@ end
Look up the properties of a Plots attribute, or specify an attribute type.
Options are $(attrtypes()).
Call `plotattr()` to search for an attribute via fuzzy finding.
-The information is the same as that given on https://docs.juliaplots.org/latest/attributes/.
+The information is the same as that given on https://docs.juliaplots.org/stable/attributes/.
"""
function plotattr()
if isijulia()
- @warn "Fuzzy finding of attributes is disabled in notebooks."
+ @maxlog_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("""
+ return """
# $letter$attr
- $attrtype attribute
- Default: `$(d isa Symbol ? string(':', d) : d)`.
- $(_argument_description(attr))
- """)
+ """ |> print
end
# COV_EXCL_STOP
function plotattr(attrtype::Symbol)
attrtype ∈ keys(_attribute_defaults) || error("Viable options are $(attrtypes())")
- println("Defined $attrtype attributes are:\n$(join(attributes(attrtype), ", "))")
+ return println("Defined $attrtype attributes are:\n$(join(attributes(attrtype), ", "))")
end
function plotattr(attribute::AbstractString)
attribute = Symbol(attribute)
- attribute = get(_keyAliases, attribute, attribute)
+ attribute = get(Commons._keyAliases, attribute, attribute)
for (k, v) in _attribute_defaults
attribute ∈ keys(v) && return plotattr(k, attribute)
end
@@ -82,20 +81,20 @@ 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
+ def = string(_attribute_defaults[attrtype][attribute])
+ aliases = if (al = PlotsBase.Commons.aliases(attribute)) |> length > 0
"Aliases: " * string(Tuple(al)) * ".\n\n"
else
""
end
# Looks up the different elements and plots them
- println(
+ return println(
":$attribute\n\n",
"$desc\n\n",
aliases,
"Type: $type.\n\n",
"`$attrtype` attribute",
- def == "" ? "" : ", defaults to `$def`.",
+ isempty(def) ? "" : ", defaults to `$def`.",
)
end
diff --git a/src/backends/plotly.jl b/PlotsBase/src/plotly.jl
similarity index 72%
rename from src/backends/plotly.jl
rename to PlotsBase/src/plotly.jl
index 12f81ad60a..6b4a232f3e 100644
--- a/src/backends/plotly.jl
+++ b/PlotsBase/src/plotly.jl
@@ -1,23 +1,192 @@
# https://plot.ly/javascript/getting-started
+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,
+ ]
+)
-_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
+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)
- x -> begin
+ texfunc = PlotsBase.labelfunc_tex(scale)
+ return x -> begin
tex_x = texfunc(x)
sup_x = replace(tex_x, r"\^{(.*)}" => s"\1")
# replace dash with \minus (U+2212)
@@ -25,10 +194,19 @@ 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)
+ @maxlog_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),
- :color => rgba_string(color),
+ :size => round(Int, 1.4font.pointsize),
+ :color => rgba_string(color),
)
plotly_annotation_dict(x, y, val; xref = "paper", yref = "paper") =
@@ -49,8 +227,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,22 +245,22 @@ 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
- lo + amt, sz - 2amt
+ return lo + amt, sz - 2amt
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
@@ -98,13 +276,13 @@ function plotly_apply_aspect_ratio(sp::Subplot, plotarea, pcts)
end
pcts
end
- pcts
+ return pcts
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]]
@@ -114,37 +292,37 @@ function plotly_domain(sp::Subplot)
colorbar_width = subplot_width * colorbar_fractional_size
x_domain[2] = x_domain[2] - colorbar_width
end
- x_domain, y_domain
+ return x_domain, y_domain
end
function plotly_axis(axis, sp, anchor = nothing, domain = nothing)
letter = axis[:letter]
framestyle = sp[:framestyle]
ax = KW(
- :visible => framestyle !== :none,
- :title => axis[:guide],
+ :visible => framestyle ≢ :none,
+ :title => PlotsBase.get_guide(axis),
: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 +335,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
@@ -176,19 +354,19 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing)
# flip
axis[:flip] && (ax[:range] = reverse(ax[:range]))
- ax
+ return ax
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)
ax[:orientation] = -90
end
- ax
+ return ax
end
function plotly_layout(plt::Plot)
@@ -205,19 +383,19 @@ function plotly_layout(plt::Plot)
for sp in 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] != ""
+ if sp[:title] |> !isempty
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
@@ -289,11 +467,13 @@ function plotly_layout(plt::Plot)
for ann in sp[:annotations]
append!(
plotattributes_out[:annotations],
- KW[plotly_annotation_dict(
- locate_annotation(sp, ann...)...;
- xref = "x$(x_idx)",
- yref = "y$(y_idx)",
- )],
+ KW[
+ plotly_annotation_dict(
+ locate_annotation(sp, ann...)...;
+ xref = "x$(x_idx)",
+ yref = "y$(y_idx)",
+ ),
+ ],
)
end
# series_annotations
@@ -333,14 +513,15 @@ function plotly_layout(plt::Plot)
plotattributes_out[:hovermode] = "none"
end
- plotattributes_out = recursive_merge(plotattributes_out, plt.attr[:extra_plot_kwargs])
+ return 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
- plotattributes_out[:legend] = KW(
+ sp[:legend_position] ≡ :none && return
+ return plotattributes_out[:legend] = KW(
:bgcolor => rgba_string(sp[:legend_background_color]),
:bordercolor => rgba_string(sp[:legend_foreground_color]),
:borderwidth => 1,
@@ -352,19 +533,18 @@ 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)),
),
)
end
-#! format: off
plotly_legend_position_mapping = let center = 0.5,
- xcenter = 0.55, ycenter = 0.52,
- xleft = 0.07, xright = 1.0,
- ybot = 0.07, ytop = 1.0,
- youtertop = 1.1, youterbot = -0.15,
- xouterright = 1.05, xouterleft = -0.15
+ xcenter = 0.55, ycenter = 0.52,
+ xleft = 0.07, xright = 1.0,
+ ybot = 0.07, ytop = 1.0,
+ youtertop = 1.1, youterbot = -0.15,
+ xouterright = 1.05, xouterleft = -0.15
(
right = (coords = [xright, ycenter], xanchor = "right", yanchor = "middle"),
left = (coords = [xleft, ycenter], xanchor = "left", yanchor = "middle"),
@@ -397,22 +577,21 @@ plotly_legend_position_mapping = let center = 0.5,
default = (coords = [xright, ytop], xanchor = "auto", yanchor = "auto"),
)
end
-#! format: on
plotly_legend_pos(pos::Symbol) =
get(plotly_legend_position_mapping, pos, plotly_legend_position_mapping.default)
-plotly_legend_pos(v::Tuple{S,T}) where {S<:Real,T<:Real} =
+plotly_legend_pos(v::Tuple{S, T}) where {S <: Real, T <: Real} =
(coords = v, xanchor = "left", yanchor = "top")
plotly_legend_pos(theta::Real) = plotly_legend_pos((theta, :inner))
-function plotly_legend_pos(v::Tuple{S,Symbol}) where {S<:Real}
+function plotly_legend_pos(v::Tuple{S, Symbol}) where {S <: Real}
(s, c) = sincosd(v[1])
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)]
@@ -433,19 +612,22 @@ plotly_layout_json(plt::Plot) = JSON.json(plotly_layout(plt), 4)
plotly_colorscale(cg::ColorGradient, α = nothing) =
map(v -> [v, rgba_string(plot_color(cg.colors[v], α))], cg.values)
plotly_colorscale(c::AbstractVector{<:Colorant}, α = nothing) =
- if length(c) == 1
- [[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))
- end
+if length(c) == 1
+ [[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))
+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)...)
- map(
- i -> [cg.values[vinds[i]], rgba_string(plot_color(color_list(cg)[cinds[i]], α))],
+ return map(
+ i -> [
+ cg.values[vinds[i]],
+ rgba_string(plot_color(PlotsBase.color_list(cg)[cinds[i]], α)),
+ ],
eachindex(cinds),
)
end
@@ -466,18 +648,18 @@ 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]
else
x_idx = y_idx = sp[:subplot_index]
end
- x_idx, y_idx
+ return 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)
@@ -485,54 +667,54 @@ function plotly_close_shapes(x, y)
shape = Shape(xs[i], ys[i])
xs[i], ys[i] = coords(shape)
end
- nanvcat(xs), nanvcat(ys)
+ return nanvcat(xs), nanvcat(ys)
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
end
- if series[:seriestype] in (:heatmap, :contour, :surface, :wireframe, :mesh3d)
+ return if series[:seriestype] in (:heatmap, :contour, :surface, :wireframe, :mesh3d)
handle_surface(data)
else
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)
+plotly_data(v::AbstractArray{R}) where {R <: Rational} = float(v)
plotly_native_data(axis::Axis, a::Surface) = Surface(plotly_native_data(axis, a.surf))
plotly_native_data(axis::Axis, data::AbstractArray) =
- if !isempty(axis[:discrete_values])
- map(
- xi -> axis[:discrete_values][searchsortedfirst(axis[:continuous_values], xi)],
- data,
- )
- elseif axis[:formatter] in (datetimeformatter, dateformatter, timeformatter)
- plotly_convert_to_datetime(data, axis[:formatter])
- else
- data
- end
+if !isempty(axis[:discrete_values])
+ map(
+ xi -> axis[:discrete_values][searchsortedfirst(axis[:continuous_values], xi)],
+ data,
+ )
+elseif axis[:formatter] in (datetimeformatter, dateformatter, timeformatter)
+ plotly_convert_to_datetime(data, axis[:formatter])
+else
+ data
+end
plotly_convert_to_datetime(x::AbstractArray, formatter::Function) =
- if formatter == datetimeformatter
- map(xi -> isfinite(xi) ? replace(formatter(xi), "T" => " ") : missing, x)
- elseif formatter == dateformatter
- map(xi -> isfinite(xi) ? replace(formatter(xi), "T" => " ") : missing, x)
- elseif formatter == timeformatter
- 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",
- )
- end
+if formatter == datetimeformatter
+ map(xi -> isfinite(xi) ? replace(formatter(xi), "T" => " ") : missing, x)
+elseif formatter == dateformatter
+ map(xi -> isfinite(xi) ? replace(formatter(xi), "T" => " ") : missing, x)
+elseif formatter == timeformatter
+ map(xi -> isfinite(xi) ? string(Dates.today(), " ", formatter(xi)) : missing, x)
+else
+ error(
+ "Invalid DateTime formatter. Expected PlotsBase.datetime/date/time formatter but got $formatter",
+ )
+end
#ensures that a gradient is called if a single color is supplied where a gradient is needed (e.g. if a series recipe defines marker_z)
as_gradient(grad::ColorGradient, α) = grad
@@ -543,7 +725,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 +737,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) in 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 +765,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 +773,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
@@ -623,7 +804,7 @@ function plotly_series(plt::Plot, series::Series)
plotattributes_out[:contours][:start] = first(levels_range)
plotattributes_out[:contours][:end] = last(levels_range)
plotattributes_out[:contours][:size] = step(levels_range)
- @warn """
+ @maxlog_warn """
setting arbitrary contour levels with Plotly backend is not supported;
use a range to set equally-spaced contours or an integer to set the
approximate number of contours with the keyword `levels`.
@@ -640,7 +821,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,17 +836,17 @@ 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 typeof(series[:connections]) <: Tuple{Array,Array,Array}
+ if series[:connections] ≢ nothing
+ if typeof(series[:connections]) <: Tuple{Array, Array, Array}
# 0-based indexing
i, j, k = series[:connections]
if !(length(i) == length(j) == length(k))
@@ -678,12 +859,10 @@ function plotly_series(plt::Plot, series::Series)
plotattributes_out[:i] = i
plotattributes_out[:j] = j
plotattributes_out[:k] = k
- elseif typeof(series[:connections]) <: AbstractVector{NTuple{3,Int}}
+ 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 in series[:connections]], (1, 2, 3))
plotattributes_out[:i] = i
plotattributes_out[:j] = j
plotattributes_out[:k] = k
@@ -701,12 +880,12 @@ 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)
else
- @warn "Plotly: seriestype $st isn't supported."
+ @maxlog_warn "Plotly: seriestype $st isn't supported."
return KW()
end
@@ -718,21 +897,16 @@ function plotly_series(plt::Plot, series::Series)
get_plotly_marker(series[:markershape], string(series[:markershape])),
# :opacity => series[:markeralpha],
:size => 2_cycle(series[:markersize], inds),
- :color =>
- rgba_string.(
+ :color => rgba_string.(
+ plot_color.(get_markercolor.(series, inds), get_markeralpha.(series, inds)),
+ ),
+ :line => KW(
+ :color => rgba_string.(
plot_color.(
- get_markercolor.(series, inds),
- get_markeralpha.(series, inds),
+ get_markerstrokecolor.(series, inds),
+ get_markerstrokealpha.(series, inds),
),
),
- :line => KW(
- :color =>
- rgba_string.(
- plot_color.(
- get_markerstrokecolor.(series, inds),
- get_markerstrokealpha.(series, inds),
- ),
- ),
:width => _cycle(series[:markerstrokewidth], inds),
),
)
@@ -741,14 +915,14 @@ function plotly_series(plt::Plot, series::Series)
plotly_polar!(plotattributes_out, series)
plotly_adjust_hover_label!(plotattributes_out, series[:hover])
- [merge(plotattributes_out, series[:extra_kwargs])]
+ return [merge(plotattributes_out, series[:extra_kwargs])]
end
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],
)
@@ -756,14 +930,11 @@ function plotly_colorbar(sp::Subplot)
end
function plotly_series_shapes(plt::Plot, series::Series, clims)
- segments = series_segments(series; check = true)
+ segments = collect(series_segments(series, series[:seriestype]; check = true))
plotattributes_outs = map(i -> KW(), 1:length(segments))
- # TODO: create a plotattributes_out for each polygon
- # 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,7 +944,7 @@ function plotly_series_shapes(plt::Plot, series::Series, clims)
x, y = (
plotly_data(series, letter, data) for
- (letter, data) in zip((:x, :y), shape_data(series, 100))
+ (letter, data) in zip((:x, :y), PlotsBase.shape_data(series, 100))
)
for (k, segment) in enumerate(segments)
@@ -808,24 +979,24 @@ 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),
)
end
- plotattributes_outs
+ return plotattributes_outs
end
function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z, clims)
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) &&
@@ -845,26 +1016,26 @@ 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
hasline ? "lines" : "none"
end
if series[:fillrange] == true ||
- series[:fillrange] == 0 ||
- isa(series[:fillrange], Tuple)
+ series[:fillrange] == 0 ||
+ isa(series[:fillrange], Tuple)
plotattributes_out[:fill] = "tozeroy"
plotattributes_out[:fillcolor] = rgba_string(
plot_color(get_fillcolor(series, clims, i), get_fillalpha(series, i)),
)
- elseif typeof(series[:fillrange]) <: Union{AbstractVector{<:Real},Real}
+ elseif typeof(series[:fillrange]) <: Union{AbstractVector{<:Real}, Real}
plotattributes_out[:fill] = "tonexty"
plotattributes_out[:fillcolor] = rgba_string(
plot_color(get_fillcolor(series, clims, i), get_fillalpha(series, i)),
)
elseif !(series[:fillrange] in (false, nothing))
- @warn "fillrange ignored... plotly only supports filling to zero and to a vector of values. fillrange: $(series[:fillrange])"
+ @maxlog_warn "fillrange ignored... plotly only supports filling to zero and to a vector of values. fillrange: $(series[:fillrange])"
end
plotattributes_out[:x], plotattributes_out[:y] = x[rng], y[rng]
@@ -884,7 +1055,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),
@@ -900,9 +1071,9 @@ function plotly_series_segments(series::Series, plotattributes_base::KW, x, y, z
)
lcolor_next =
plot_color(
- get_markerstrokecolor(series, i + 1),
- get_markerstrokealpha(series, i + 1),
- ) |> rgba_string
+ get_markerstrokecolor(series, i + 1),
+ get_markerstrokealpha(series, i + 1),
+ ) |> rgba_string
plotattributes_out[:marker] = KW(
:symbol => get_plotly_marker(
@@ -926,11 +1097,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 +1138,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,18 +1152,18 @@ 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),
)
end
- plotattributes_outs
+ return plotattributes_outs
end
function plotly_colorbar_hack(series::Series, plotattributes_base::KW, sym::Symbol)
@@ -1008,8 +1179,8 @@ function plotly_colorbar_hack(series::Series, plotattributes_base::KW, sym::Symb
end
# zrange = zmax == zmin ? 1 : zmax - zmin # if all marker_z values are the same, plot all markers same color (avoids division by zero in next line)
plotattributes_out[:marker] = KW(
- :size => 1e-10,
- :opacity => 1e-10,
+ :size => 1.0e-10,
+ :opacity => 1.0e-10,
:color => [0.5],
:cmin => cmin,
:cmax => cmax,
@@ -1020,15 +1191,15 @@ function plotly_colorbar_hack(series::Series, plotattributes_base::KW, sym::Symb
end
plotly_polar!(plotattributes_out::KW, series::Series) =
- if ispolar(series[:subplot]) # convert polar plots x/y to theta/radius
- theta, r = pop!(plotattributes_out, :x), pop!(plotattributes_out, :y)
- plotattributes_out[:theta] = rad2deg.(theta)
- plotattributes_out[:r] = r
- plotattributes_out[:type] = :scatterpolar
- end
+if ispolar(series[:subplot]) # convert polar plots x/y to theta/radius
+ theta, r = pop!(plotattributes_out, :x), pop!(plotattributes_out, :y)
+ plotattributes_out[:theta] = rad2deg.(theta)
+ plotattributes_out[:r] = r
+ plotattributes_out[:type] = :scatterpolar
+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"
@@ -1036,13 +1207,13 @@ function plotly_adjust_hover_label!(plotattributes_out::KW, hover)
plotattributes_out[:hoverinfo] = "text"
plotattributes_out[:text] = hover
end
- nothing
+ return nothing
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)
+ return reduce(vcat, plotly_series(plt, series) for series in plt.series_list)
end
# get json string for a list of dictionaries, each representing the series params
@@ -1054,11 +1225,11 @@ 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()
- else
- "https://cdn.plot.ly/$_plotly_min_js_filename"
- end
+if PlotsBase._use_local_dependencies[]
+ "file:///$(PlotsBase._plotly_local_file_path[])"
+else
+ "https://cdn.plot.ly/$(PlotsBase._plotly_min_js_filename)"
+end
function plotly_html_head(plt::Plot)
plotly = plotly_url()
@@ -1077,7 +1248,7 @@ function plotly_html_head(plt::Plot)
"\n\t\t"
end
- if isijulia()
+ return if PlotsBase.isijulia()
mathjax_head
else
"$mathjax_head"
@@ -1085,13 +1256,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
@@ -1106,20 +1277,31 @@ function plotly_html_body(plt, style = nothing)
requirejs_suffix = "});"
end
- uuid = UUIDs.uuid4()
- html = """
-
+ unique_tag = "id_$(replace(string(UUIDs.uuid4()), '-' => '_'))"
+
+ return """
+
"""
- html
end
-js_body(plt::Plot, uuid) =
- "Plotly.newPlot('$(uuid)', $(plotly_series_json(plt)), $(plotly_layout_json(plt)));"
+js_body(
+ plt::Plot,
+ unique_tag,
+) = "Plotly.newPlot('$unique_tag', $(plotly_series_json(plt)), $(plotly_layout_json(plt)));"
plotly_show_js(io::IO, plot::Plot) =
JSON.print(io, Dict(:data => plotly_series(plot), :layout => plotly_layout(plot)))
@@ -1128,9 +1310,18 @@ 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}) = PlotsBase.standalone_html_window(plt)
-_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))
+ return out
+end
+
+end
diff --git a/src/recipes.jl b/PlotsBase/src/recipes.jl
similarity index 81%
rename from src/recipes.jl
rename to PlotsBase/src/recipes.jl
index c82a0ff013..094c9835c9 100644
--- a/src/recipes.jl
+++ b/PlotsBase/src/recipes.jl
@@ -13,26 +13,26 @@ function seriestype_supported(pkg::AbstractBackend, st::Symbol)
supported = true
for dep in _series_recipe_deps[st]
- if seriestype_supported(pkg, dep) === :no
+ if seriestype_supported(pkg, dep) ≡ :no
supported = false
break
end
end
- supported ? :recipe : :no
+ return supported ? :recipe : :no
end
macro deps(st, args...)
- :(Plots.series_recipe_dependencies($(quot(st)), $(map(quot, args)...)))
+ return :(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 in _initialized_backends
+ be = backend_instance(bsym)
+ sts = union(sts, Set{Symbol}(supported_seriestypes(be)))
end
- sts |> collect |> sort
+ return sts |> collect |> sort
end
# ----------------------------------------------------------------------------------
@@ -199,11 +199,11 @@ function make_steps(x::AbstractArray, st, even)
for i in 2:n
xindex = xstartindex - 1 + i
idx = 2i - 1
- if st === :mid
+ if st ≡ :mid
newx[idx] = newx[idx - 1] = (x[xindex] + x[xindex - 1]) / 2
else
newx[idx] = x[xindex]
- newx[idx - 1] = x[st === :pre ? xindex : xindex - 1]
+ newx[idx - 1] = x[st ≡ :pre ? xindex : xindex - 1]
end
end
even && (newx[end] = x[end])
@@ -223,7 +223,7 @@ make_steps(t::Tuple, st, even) = Tuple(make_steps(ti, st, even) for ti in t)
plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :pre, false)
# create a secondary series for the markers
- if plotattributes[:markershape] !== :none
+ if plotattributes[:markershape] ≢ :none
@series begin
seriestype := :scatter
x := x
@@ -248,7 +248,7 @@ end
plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :post, true)
# create a secondary series for the markers
- if plotattributes[:markershape] !== :none
+ if plotattributes[:markershape] ≢ :none
@series begin
seriestype := :scatter
x := x
@@ -273,7 +273,7 @@ end
plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :post, false)
# create a secondary series for the markers
- if plotattributes[:markershape] !== :none
+ if plotattributes[:markershape] ≢ :none
@series begin
seriestype := :scatter
x := x
@@ -294,19 +294,19 @@ end
# create vertical line segments from fill
@recipe function f(::Type{Val{:sticks}}, x, y, z) # COV_EXCL_LINE
n = length(x)
- if (fr = plotattributes[:fillrange]) === nothing
+ if (fr = plotattributes[:fillrange]) ≡ nothing
sp = plotattributes[:subplot]
- fr = if sp[:yaxis][:scale] === :identity
+ fr = if sp[:yaxis][:scale] ≡ :identity
0.0
else
NaNMath.min(axis_limits(sp, :y)[1], ignorenan_minimum(y))
end
end
- newx, newy, newz = zeros(3n), zeros(3n), z !== nothing ? zeros(3n) : nothing
- for (i, (xi, yi, zi)) in enumerate(zip(x, y, z !== nothing ? z : 1:n))
+ newx, newy, newz = zeros(3n), zeros(3n), z ≢ nothing ? zeros(3n) : nothing
+ for (i, (xi, yi, zi)) in enumerate(zip(x, y, z ≢ nothing ? z : 1:n))
rng = (3i - 2):(3i)
newx[rng] = [xi, xi, NaN]
- if z !== nothing
+ if z ≢ nothing
newy[rng] = [yi, yi, NaN]
newz[rng] = [_cycle(fr, i), zi, NaN]
else
@@ -315,27 +315,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 in y]
+ else
+ plotattributes[:markershape]
+ end
seriestype := :scatter
x := x
y := y
- if z !== nothing
+ if z ≢ nothing
z := z
end
primary := true
@@ -359,42 +364,42 @@ function bezier_value(pts::AVec, t::Real)
for (i, p) in enumerate(pts)
val += p * binomial(n, i - 1) * (1 - t)^(n - i + 1) * t^(i - 1)
end
- val
+ return val
end
@nospecialize
# 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 in DataSeries.iter_segments(args...)
length(rng) < 2 && continue
ts = range(0, stop = 1, length = npoints)
nanappend!(newx, map(t -> bezier_value(_cycle(x, rng), t), ts))
nanappend!(newy, map(t -> bezier_value(_cycle(y, rng), t), ts))
- if z !== nothing
+ if z ≢ nothing
nanappend!(newz, map(t -> bezier_value(_cycle(z, rng), t), ts))
end
- if fr !== nothing
+ if fr ≢ nothing
nanappend!(newfr, map(t -> bezier_value(_cycle(fr, rng), t), ts))
end
end
x := newx
y := newy
- if z === nothing
+ if z ≡ nothing
seriestype := :path
else
seriestype := :path3d
z := newz
end
- if fr !== nothing
+ if fr ≢ nothing
fillrange := newfr
end
()
@@ -405,10 +410,18 @@ end
# create a bar plot as a filled step function
@recipe function f(::Type{Val{:bar}}, x, y, z) # COV_EXCL_LINE
+ if typeof(y) <: NamedTuple
+ names = collect(keys(y))
+ values_y = [values(y)...]
+ plotnames = string.(names)
+
+ x = plotnames
+ y = values_y
+ 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 +435,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 +446,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
@@ -462,16 +475,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
@@ -480,18 +487,6 @@ end
primary := true
x := xseg.pts
y := yseg.pts
- # expand attributes to match indices in new series data
- for k in _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))."
- end
- # Each segment is 6 elements long, including the NaN separator.
- # One segment is created for each non-NaN element of `procy`.
- # There is no trailing NaN, so the last repetition is dropped.
- plotattributes[k] = @views repeat(v[valid_i]; inner = 6)[1:(end - 1)]
- end
- end
()
end
@@ -508,7 +503,7 @@ end
@deps bar shape
# ---------------------------------------------------------------------------
-# Plots Heatmap
+# PlotsBase Heatmap
@recipe function f(::Type{Val{:plots_heatmap}}, x, y, z) # COV_EXCL_LINE
xe, ye = heatmap_edges(x), heatmap_edges(y)
m, n = size(z.surf)
@@ -552,32 +547,32 @@ _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])
- else
- zero(T)
- end
+_binbarlike_baseline(min_value::T, scale::Symbol) where {T <: Real} =
+if scale in _log_scales
+ isnan(min_value) ? T(1.0e-3) : floor_base(min_value, _log_scale_bases[scale])
+else
+ zero(T)
+end
function _preprocess_binbarlike_weights(
- ::Type{T},
- w,
- wscale::Symbol,
-) where {T<:AbstractFloat}
+ ::Type{T},
+ w,
+ wscale::Symbol,
+ ) where {T <: AbstractFloat}
w_adj = _scale_adjusted_values(T, w, wscale)
w_min = ignorenan_minimum(w_adj)
w_max = ignorenan_maximum(w_adj)
baseline = _binbarlike_baseline(w_min, wscale)
- w_adj, baseline
+ return w_adj, baseline
end
function _preprocess_barlike(plotattributes, x, y)
xscale = get(plotattributes, :xscale, :identity)
yscale = get(plotattributes, :yscale, :identity)
weights, baseline = _preprocess_binbarlike_weights(float(eltype(y)), y, yscale)
- x, weights, xscale, yscale, baseline
+ return x, weights, xscale, yscale, baseline
end
function _preprocess_binlike(plotattributes, x, y)
@@ -586,14 +581,14 @@ function _preprocess_binlike(plotattributes, x, y)
T = float(promote_type(eltype(x), eltype(y)))
edge = T.(x)
weights, baseline = _preprocess_binbarlike_weights(T, y, yscale)
- edge, weights, xscale, yscale, baseline
+ return edge, weights, xscale, yscale, baseline
end
@nospecialize
@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 +617,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 +635,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)
@@ -673,22 +668,18 @@ function _stepbins_path(edge, weights, baseline::Real, xscale::Symbol, yscale::S
push!(y, baseline)
end
- (x, y)
+ return (x, y)
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)
@@ -711,21 +702,24 @@ end
@deps stepbins path
function wand_edges(x...)
- @warn """"
+ @maxlog_warn """"
Load the StatsPlots package in order to use :wand bins.
Defaulting to :auto
""" once = true
- :auto
+ return :auto
end
function _auto_binning_nbins(
- vs::NTuple{N,AbstractVector},
- dim::Integer;
- mode::Symbol = :auto,
-) where {N}
+ vs::NTuple{N, AbstractVector},
+ dim::Integer;
+ mode::Symbol = :auto,
+ ) 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,76 +733,76 @@ function _auto_binning_nbins(
end
v = vs[dim]
- mode === :auto && (mode = :fd)
+ mode ≡ :auto && (mode = :fd)
- if mode === :sqrt # Square-root choice
+ return 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")
end
end
-_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::Integer) where {N} =
+_hist_edge(vs::NTuple{N, AbstractVector}, dim::Integer, binning::Integer) where {N} =
StatsBase.histrange(vs[dim], binning, :left)
-_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::Symbol) where {N} =
+_hist_edge(vs::NTuple{N, AbstractVector}, dim::Integer, binning::Symbol) where {N} =
_hist_edge(vs, dim, _auto_binning_nbins(vs, dim, mode = binning))
-_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::AbstractVector) where {N} =
+_hist_edge(vs::NTuple{N, AbstractVector}, dim::Integer, binning::AbstractVector) where {N} =
binning
-_hist_edges(vs::NTuple{N,AbstractVector}, binning::NTuple{N,Any}) where {N} =
+_hist_edges(vs::NTuple{N, AbstractVector}, binning::NTuple{N, Any}) where {N} =
map(dim -> _hist_edge(vs, dim, binning[dim]), Tuple(1:N))
_hist_edges(
- vs::NTuple{N,AbstractVector},
- binning::Union{Integer,Symbol,AbstractVector},
+ vs::NTuple{N, AbstractVector},
+ binning::Union{Integer, Symbol, AbstractVector},
) where {N} = map(dim -> _hist_edge(vs, dim, binning), Tuple(1:N))
_hist_norm_mode(mode::Symbol) = mode
_hist_norm_mode(mode::Bool) = mode ? :pdf : :none
-_filternans(vs::NTuple{1,AbstractVector}) = filter!.(isfinite, vs)
-function _filternans(vs::NTuple{N,AbstractVector}) where {N}
+_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)]
nots = union(Set.(findall.(!isfinite, vs))...)
- _invertedindex.(vs, Ref(nots))
+ return _invertedindex.(vs, Ref(nots))
end
function _make_hist(
- vs::NTuple{N,AbstractVector},
- binning;
- normed = false,
- weights = nothing,
-) where {N}
+ vs::NTuple{N, AbstractVector},
+ binning;
+ normed = false,
+ weights = nothing,
+ ) where {N}
localvs = _filternans(vs)
edges = _hist_edges(localvs, binning)
h = float(
- weights === nothing ?
- StatsBase.fit(StatsBase.Histogram, localvs, edges, closed = :left) :
- StatsBase.fit(
- StatsBase.Histogram,
- localvs,
- StatsBase.Weights(weights),
- edges,
- closed = :left,
- ),
+ weights ≡ nothing ?
+ StatsBase.fit(StatsBase.Histogram, localvs, edges, closed = :left) :
+ StatsBase.fit(
+ StatsBase.Histogram,
+ localvs,
+ StatsBase.Weights(weights),
+ edges,
+ closed = :left,
+ ),
)
- normalize!(h, mode = _hist_norm_mode(normed))
+ return LinearAlgebra.normalize!(h, mode = _hist_norm_mode(normed))
end
@nospecialize
@recipe function f(::Type{Val{:histogram}}, x, y, z) # COV_EXCL_LINE
- seriestype := length(y) > 1e6 ? :stephist : :barhist
+ seriestype := length(y) > 1.0e6 ? :stephist : :barhist
()
end
@deps histogram barhist
@@ -855,7 +849,7 @@ end
end
@deps scatterhist scatterbins
-@recipe function f(h::StatsBase.Histogram{T,1,E}) where {T,E} # COV_EXCL_LINE
+@recipe function f(h::StatsBase.Histogram{T, 1, E}) where {T, E} # COV_EXCL_LINE
seriestype --> :barbins
st_map = Dict(
@@ -866,24 +860,24 @@ 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
+@recipe f(hv::AbstractVector{H}) where {H <: StatsBase.Histogram} = # COV_EXCL_LINE
for h in hv
- @series begin
- h
- end
+ @series begin
+ h
end
+end
# ---------------------------------------------------------------------------
# Histogram 2D
@@ -893,7 +887,7 @@ end
float_weights = float(weights)
if !plotattributes[:show_empty_bins]
- if float_weights === weights
+ if float_weights ≡ weights
float_weights = deepcopy(float_weights)
end
for (i, c) in enumerate(float_weights)
@@ -901,13 +895,13 @@ end
end
end
- x := Plots._bin_centers(edge_x)
- y := Plots._bin_centers(edge_y)
+ x := PlotsBase._bin_centers(edge_x)
+ y := PlotsBase._bin_centers(edge_y)
z := Surface(permutedims(float_weights))
seriestype := :heatmap
()
end
-Plots.@deps bins2d heatmap
+PlotsBase.@deps bins2d heatmap
@recipe function f(::Type{Val{:histogram2d}}, x, y, z) # COV_EXCL_LINE
h = _make_hist(
@@ -924,7 +918,7 @@ Plots.@deps bins2d heatmap
end
@deps histogram2d bins2d
-@recipe function f(h::StatsBase.Histogram{T,2,E}) where {T,E} # COV_EXCL_LINE
+@recipe function f(h::StatsBase.Histogram{T, 2, E}) where {T, E} # COV_EXCL_LINE
seriestype --> :bins2d
(h.edges[1], h.edges[2], Surface(h.weights))
end
@@ -958,10 +952,10 @@ 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
+ ArgumentError |>
+ throw
end
()
end
@@ -971,7 +965,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,22 +981,27 @@ 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]
yscale = sp[:yaxis][:scale]
xl1, xl2 = xlims(sp)
- bbx1 = xl1 + left(inset_bbox).value * (xl2 - xl1)
- bbx2 = bbx1 + width(inset_bbox).value * (xl2 - xl1)
+ xls1, xls2 = RecipesPipeline.scale_func(xscale).((xl1, xl2))
+ bbx1 = xls1 + left(inset_bbox).value * (xls2 - xls1)
+ bbx2 = bbx1 + width(inset_bbox).value * (xls2 - xls1)
yl1, yl2 = ylims(sp)
- bby1 = yl1 + (1 - bottom(inset_bbox).value) * (yl2 - yl1)
- bby2 = bby1 + height(inset_bbox).value * (yl2 - yl1)
- bbx = bbx1 + width(inset_bbox).value * (xl2 - xl1) / 2 * (sp[:xaxis][:flip] ? -1 : 1)
- bby = bby1 + height(inset_bbox).value * (yl2 - yl1) / 2 * (sp[:yaxis][:flip] ? -1 : 1)
+ yls1, yls2 = RecipesPipeline.scale_func(yscale).((yl1, yl2))
+ bby1 = yls1 + (1 - bottom(inset_bbox).value) * (yls2 - yls1)
+ bby2 = bby1 + height(inset_bbox).value * (yls2 - yls1)
+ bbx = bbx1 + width(inset_bbox).value * (xls2 - xls1) / 2 * (sp[:xaxis][:flip] ? -1 : 1)
+ bby = bby1 + height(inset_bbox).value * (yls2 - yls1) / 2 * (sp[:yaxis][:flip] ? -1 : 1)
lens_index = last(plt.subplots)[:subplot_index] + 1
- x1, x2 = RecipesPipeline.inverse_scale_func(xscale).(plotattributes[:x])
- y1, y2 = RecipesPipeline.inverse_scale_func(yscale).(plotattributes[:y])
+ x1, x2 = plotattributes[:x]
+ y1, y2 = plotattributes[:y]
+ xs1, xs2 = RecipesPipeline.scale_func(xscale).((x1, x2))
+ ys1, ys2 = RecipesPipeline.scale_func(yscale).((y1, y2))
+
backup = copy(plotattributes)
empty!(plotattributes)
@@ -1016,19 +1015,21 @@ export lens!
if haskey(backup, :linewidth)
linewidth := backup[:linewidth]
end
- bbx_mag = (x1 + x2) / 2
- bby_mag = (y1 + y2) / 2
+ bbx_mag = (xs1 + xs2) / 2
+ bby_mag = (ys1 + ys2) / 2
xi_lens, yi_lens =
intersection_point(bbx_mag, bby_mag, bbx, bby, abs(bby2 - bby1), abs(bbx2 - bbx1))
xi_mag, yi_mag =
intersection_point(bbx, bby, bbx_mag, bby_mag, abs(y2 - y1), abs(x2 - x1))
+ xi_mag, xi_lens = RecipesPipeline.inverse_scale_func(xscale).((xi_mag, xi_lens))
+ yi_mag, yi_lens = RecipesPipeline.inverse_scale_func(yscale).((yi_mag, yi_lens))
# add lines
if xl1 < xi_lens < xl2 && yl1 < yi_lens < yl2
@series begin
primary := false
subplot := sp_index
- x := RecipesPipeline.scale_func(xscale).([xi_mag, xi_lens])
- y := RecipesPipeline.scale_func(yscale).([yi_mag, yi_lens])
+ x := ([xi_mag, xi_lens])
+ y := ([yi_mag, yi_lens])
()
end
end
@@ -1036,8 +1037,8 @@ export lens!
@series begin
primary := false
subplot := sp_index
- x := RecipesPipeline.scale_func(xscale).([x1, x1, x2, x2, x1])
- y := RecipesPipeline.scale_func(yscale).([y1, y2, y2, y1, y1])
+ x := ([x1, x1, x2, x2, x1])
+ y := ([y1, y2, y2, y1, y1])
()
end
# add subplot
@@ -1046,8 +1047,8 @@ export lens!
plotattributes = merge(backup, copy(series.plotattributes))
subplot := lens_index
primary := false
- xlims := RecipesPipeline.scale_func(xscale).((x1, x2))
- ylims := RecipesPipeline.scale_func(yscale).((y1, y2))
+ xlims := (x1, x2)
+ ylims := (y1, y2)
()
end
end
@@ -1060,14 +1061,14 @@ function intersection_point(xA, yA, xB, yB, h, w)
s = (yA - yB) / (xA - xB)
hh, hw = h / 2, w / 2
# left or right?
- if -hh <= s * hw <= hh
+ return if -hh ≤ s * hw ≤ hh
if xA > xB # right
xB + hw, yB + s * hw
else # left
xB - hw, yB - s * hw
end
# top or bot?
- elseif -hw <= hh / s <= hw
+ elseif -hw ≤ hh / s ≤ hw
if yA > yB # top
xB + hh / s, yB + hh
else # bottom
@@ -1088,15 +1089,16 @@ 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
- get_series_color(
+ elseif msc ≡ :auto
+ PlotsBase.get_series_color(
plotattributes[:linecolor],
plotattributes[:subplot],
plotattributes[:series_plotindex],
@@ -1135,7 +1137,7 @@ function error_coords(errorbar, errordata, otherdata...)
end
# clamp non-NaN values in an array to Base.eps(Float64) for log-scale plots
-clamp_to_eps!(ary) = (replace!(x -> x <= 0.0 ? Base.eps(Float64) : x, ary); nothing)
+clamp_to_eps!(ary) = (replace!(x -> x ≤ 0.0 ? Base.eps(Float64) : x, ary); nothing)
# we will create a series of path segments, where each point represents one
# side of an errorbar
@@ -1143,16 +1145,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 +1162,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 +1179,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
()
@@ -1245,7 +1247,7 @@ function quiver_using_arrows(plotattributes::AKW)
is_3d && nanappend!(z, [zi, zi + vz, NaN])
end
plotattributes[:x], plotattributes[:y] = x, y
- is_3d && (plotattributes[:z] = z)
+ return is_3d && (plotattributes[:z] = z)
# KW[plotattributes]
end
@@ -1291,7 +1293,7 @@ function quiver_using_hack(plotattributes::AKW)
nanappend!(pts, P2[p, ppv .- U1, ppv .- U1 .+ U2, ppv, ppv .- U1 .- U2, ppv .- U1])
end
- plotattributes[:x], plotattributes[:y] = RecipesPipeline.unzip(pts[2:end])
+ return plotattributes[:x], plotattributes[:y] = RecipesPipeline.unzip(pts[2:end])
end
# function apply_series_recipe(plotattributes::AKW, ::Type{Val{:quiver}})
@@ -1316,7 +1318,7 @@ function clamp_greys!(mat::AMat{<:Gray})
mat[i].val < 0 && (mat[i] = Gray(0))
mat[i].val > 1 && (mat[i] = Gray(1))
end
- mat
+ return mat
end
@recipe function f(mat::AMat{<:Gray}) # COV_EXCL_LINE
@@ -1338,7 +1340,7 @@ end
@nospecialize
# images - colors
-@recipe function f(mat::AMat{T}) where {T<:Colorant} # COV_EXCL_LINE
+@recipe function f(mat::AMat{T}) where {T <: Colorant} # COV_EXCL_LINE
n, m = map(a -> range(first(a) - 0.5, stop = last(a) + 0.5), axes(mat))
if is_seriestype_supported(:image)
@@ -1369,7 +1371,7 @@ end
for attr in union(_segmenting_array_attributes, _segmenting_vector_attributes)
v = get(plotattributes, attr, nothing)
if v isa AVec || v isa AMat && size(v, 2) == 1
- @warn """
+ @maxlog_warn """
Column vector attribute `$attr` reinterpreted as row vector (one value per shape).
Pass a row vector instead (e.g. using `permutedims`) to suppress this warning.
"""
@@ -1391,7 +1393,7 @@ end
# --------------------------------------------------------------------
# images - grays
-@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T<:Gray} # COV_EXCL_LINE
+@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T <: Gray} # COV_EXCL_LINE
if is_seriestype_supported(:image)
seriestype := :image
yflip --> true
@@ -1406,7 +1408,7 @@ end
end
# images - colors
-@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T<:Colorant} # COV_EXCL_LINE
+@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T <: Colorant} # COV_EXCL_LINE
if is_seriestype_supported(:image)
seriestype := :image
yflip --> true
@@ -1427,7 +1429,7 @@ end
# TODO: move OHLC to PlotRecipes finance.jl
"Represent Open High Low Close data (used in finance)"
-mutable struct OHLC{T<:Real}
+mutable struct OHLC{T <: Real}
open::T
high::T
low::T
@@ -1441,7 +1443,7 @@ function get_xy(o::OHLC, x, xdiff)
xl, xm, xr = x - xdiff, x, x + xdiff
ox = [xl, xm, NaN, xm, xm, NaN, xm, xr]
oy = [o.open, o.open, NaN, o.low, o.high, NaN, o.close, o.close]
- ox, oy
+ return ox, oy
end
# get the joined vector
@@ -1453,7 +1455,7 @@ function get_xy(v::AVec{OHLC}, x = eachindex(v))
nanappend!(x_out, ox)
nanappend!(y_out, oy)
end
- x_out, y_out
+ return x_out, y_out
end
# these are for passing in a vector of OHLC objects
@@ -1462,10 +1464,10 @@ end
@nospecialize
-@recipe f(x::AVec, ohlc::AVec{NTuple{N,<:Number}}) where {N} = x, map(t -> OHLC(t...), ohlc)
+@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 +1509,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]
+ return 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 +1547,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
@@ -1557,7 +1559,7 @@ abline!(args...; kw...) = abline!(current(), args...; kw...)
# -------------------------------------------------
# Complex Numbers
-@recipe function f(A::AbstractArray{Complex{T}}) where {T<:Number} # COV_EXCL_LINE
+@recipe function f(A::AbstractArray{Complex{T}}) where {T <: Number} # COV_EXCL_LINE
xguide --> "Re(x)"
yguide --> "Im(x)"
real.(A), imag.(A)
@@ -1589,7 +1591,7 @@ end
for c in axes(weights, 2)
sx = vcat(weights[:, c], c == 1 ? zeros(n) : reverse(weights[:, c - 1]))
sy = vcat(returns, reverse(returns))
- @series Plots.isvertical(plotattributes) ? (sx, sy) : (sy, sx)
+ @series (sx, sy)
end
end
diff --git a/src/shorthands.jl b/PlotsBase/src/shorthands.jl
similarity index 90%
rename from src/shorthands.jl
rename to PlotsBase/src/shorthands.jl
index 09fa9fd2b3..d1eaf0705e 100644
--- a/src/shorthands.jl
+++ b/PlotsBase/src/shorthands.jl
@@ -25,6 +25,7 @@ julia> scatter([(1,4),(2,5),(3,6)])
bar!(x,y)
Make a bar plot of `y` vs `x`.
+If `y` is a named tuple the keys are used as ticklabels.
# Keyword arguments
- $(_document_argument(:bar_position))
@@ -37,6 +38,7 @@ Make a bar plot of `y` vs `x`.
```julia-repl
julia> bar([1,2,3],[4,5,6],fillcolor=[:red,:green,:blue],fillalpha=[0.2,0.4,0.6])
julia> bar([(1,4),(2,5),(3,6)])
+julia> bar((A=5, B=8, C=3, D=5, E=8, F=10, G=3))
```
"""
@shorthands bar
@@ -469,7 +471,7 @@ for letter in ("x", "y", "z")
plot!(; $(Symbol(letter, :lims)) = lims, kw...)
$(Symbol(letter, :lims!))(xmin::Real, xmax::Real; kw...) =
plot!(; $(Symbol(letter, :lims)) = (xmin, xmax), kw...)
- $(Symbol(letter, :lims!))(plt::PlotOrSubplot, lims::Tuple{<:Real,<:Real}; kw...) =
+ $(Symbol(letter, :lims!))(plt::PlotOrSubplot, lims::Tuple{<:Real, <:Real}; kw...) =
plot!(plt; $(Symbol(letter, :lims)) = lims, kw...)
$(Symbol(letter, :lims!))(plt::PlotOrSubplot, xmin::Real, xmax::Real; kw...) =
plot!(plt; $(Symbol(letter, :lims)) = (xmin, xmax), kw...)
@@ -482,7 +484,7 @@ for letter in ("x", "y", "z")
ticks::AVec{T},
labels::AVec{S};
kw...,
- ) where {T<:Real,S<:AbstractString} =
+ ) where {T <: Real, S <: AbstractString} =
plot!(; $(Symbol(letter, :ticks)) = (ticks, labels), kw...)
$(Symbol(letter, :ticks!))(plt::PlotOrSubplot, v::TicksArgs; kw...) =
plot!(plt; $(Symbol(letter, :ticks)) = v, kw...)
@@ -491,7 +493,7 @@ for letter in ("x", "y", "z")
ticks::AVec{T},
labels::AVec{S};
kw...,
- ) where {T<:Real,S<:AbstractString} =
+ ) where {T <: Real, S <: AbstractString} =
plot!(plt; $(Symbol(letter, :ticks)) = (ticks, labels), kw...)
export $(Symbol(letter, :ticks!))
@@ -520,7 +522,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 +530,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 90%
rename from src/themes.jl
rename to PlotsBase/src/themes.jl
index 67d3569f6d..1708c53194 100644
--- a/src/themes.jl
+++ b/PlotsBase/src/themes.jl
@@ -7,10 +7,10 @@ function theme(s::Symbol; kw...)
defaults = if haskey(PlotThemes._themes, s)
copy(PlotThemes._themes[s].defaults)
else
- @warn ":$s is not a known theme, using :default"
- Dict{Symbol,Any}()
+ @maxlog_warn ":$s is not a known theme, using :default"
+ Dict{Symbol, Any}()
end
- _theme(s, defaults; kw...)
+ return _theme(s, defaults; kw...)
end
function _theme(s::Symbol, defaults::AKW; kw...)
@@ -41,11 +41,11 @@ end
_color_functions =
KW(:protanopic => protanopic, :deuteranopic => deuteranopic, :tritanopic => tritanopic)
-_get_showtheme_args(thm::Symbol) = thm, identity
-_get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func, identity)
+_get_showtheme_attrs(thm::Symbol) = thm, identity
+_get_showtheme_attrs(thm::Symbol, func::Symbol) = thm, get(_color_functions, func, identity)
@recipe function showtheme(st::ShowTheme)
- thm, cfunc = _get_showtheme_args(st.args...)
+ thm, cfunc = _get_showtheme_attrs(st.args...)
defaults = PlotThemes._themes[thm].defaults
# get the gradient
@@ -60,7 +60,7 @@ _get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func
for k in keys(defaults)
k in (:colorgradient, :palette) && continue
def = defaults[k]
- arg = get(_keyAliases, k, k)
+ arg = get(Commons._keyAliases, k, k)
plotattributes[arg] = if typeof(def) <: Colorant
cfunc(RGB(def))
elseif eltype(def) <: Colorant
diff --git a/PlotsBase/src/users.jl b/PlotsBase/src/users.jl
new file mode 100644
index 0000000000..9e7274cd67
--- /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 0000000000..32e0f93a22
--- /dev/null
+++ b/PlotsBase/src/utils.jl
@@ -0,0 +1,950 @@
+# browsers can have issue opening files in /tmp (chromium, firefox, ...), so let the user decide.
+tmpdir_name()::String = if haskey(ENV, "PLOTSBASE_TMPDIR")
+ ENV["PLOTSBASE_TMPDIR"]
+else
+ tempdir()
+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))
+ return 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)
+ return 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)
+ return 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 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 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)
+ return 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)
+ return 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)
+ return nothing
+end
+
+function _slice_series_attrs!(
+ plotattributes::AKW,
+ plt::Plot,
+ sp::Subplot,
+ commandIndex::Int,
+ )
+ for k in keys(_series_defaults)
+ haskey(plotattributes, k) &&
+ slice_arg!(plotattributes, plotattributes, k, commandIndex, false)
+ end
+ return 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])
+ return 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)
+ return 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
+ return (
+ _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 = 1.0e-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)
+ return x, y
+end
+
+fakedata(sz::Int...) = fakedata(Random.seed!(SEED), sz...)
+
+function fakedata(rng::Random.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
+ return 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
+ return vs
+end
+
+function nanvcat(vs::AVec)
+ v_out = zeros(0)
+ foreach(v -> nanappend!(v_out, v), vs)
+ return 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) in pairs(y)
+ frs[i] = yi + _cycle(rib, i)
+ end
+ return 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)
+ return (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
+ return 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 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
+ processGridArg! = Commons.process_grid_attr!
+ args = RecipesPipeline.pop_kw!(plotattributes, :grid, ())
+ for arg in wraptuple(args)
+ for letter in (:x, :y, :z)
+ processGridArg!(plotattributes, arg, letter)
+ end
+ end
+ # handle individual axes grid args
+ for letter in (:x, :y, :z)
+ gridsym = get_attr_symbol(letter, :grid)
+ args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ())
+ for arg in wraptuple(args)
+ processGridArg!(plotattributes, arg, letter)
+ end
+ end
+ # handle minor grid args common to all axes
+ args = RecipesPipeline.pop_kw!(plotattributes, :minorgrid, ())
+ for arg in wraptuple(args)
+ for letter in (:x, :y, :z)
+ Commons.process_minor_grid_attr!(plotattributes, arg, letter)
+ end
+ end
+ # handle individual axes grid args
+ for letter in (:x, :y, :z)
+ gridsym = get_attr_symbol(letter, :minorgrid)
+ args = RecipesPipeline.pop_kw!(plotattributes, gridsym, ())
+ for arg in wraptuple(args)
+ Commons.process_minor_grid_attr!(plotattributes, arg, letter)
+ end
+ end
+ # handle font args common to all axes
+ for fontname in (:tickfont, :guidefont)
+ args = RecipesPipeline.pop_kw!(plotattributes, fontname, ())
+ for arg in wraptuple(args)
+ for letter in (:x, :y, :z)
+ Commons.process_font_attr!(
+ plotattributes,
+ get_attr_symbol(letter, fontname),
+ arg,
+ )
+ end
+ end
+ end
+ # handle individual axes font args
+ for letter in (:x, :y, :z)
+ for fontname in (:tickfont, :guidefont)
+ args = RecipesPipeline.pop_kw!(
+ plotattributes,
+ get_attr_symbol(letter, fontname),
+ (),
+ )
+ for arg in wraptuple(args)
+ Commons.process_font_attr!(
+ plotattributes,
+ get_attr_symbol(letter, fontname),
+ arg,
+ )
+ end
+ end
+ end
+ # handle axes args
+ for k in Commons._axis_attrs
+ if haskey(plotattributes, k) && k ≢ :link
+ v = plotattributes[k]
+ for letter in (:x, :y, :z)
+ lk = get_attr_symbol(letter, k)
+ if !is_explicit(plotattributes, lk)
+ plotattributes[lk] = v
+ end
+ end
+ end
+ end
+
+ # fonts
+ for fontname in
+ (:titlefont, :legend_title_font, :plot_titlefont, :colorbar_titlefont, :legend_font)
+ args = RecipesPipeline.pop_kw!(plotattributes, fontname, ())
+ for arg in wraptuple(args)
+ Commons.process_font_attr!(plotattributes, fontname, arg)
+ end
+ end
+
+ # handle line args
+ for arg in 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 in wraptuple(get(plotattributes, :marker, ()))
+ Commons.process_marker_attr(plotattributes, arg)
+ anymarker = true
+ end
+ RecipesPipeline.reset_kw!(plotattributes, :marker)
+ if haskey(plotattributes, :markershape)
+ plotattributes[:markershape] =
+ Commons._replace_markershape(plotattributes[:markershape])
+ if plotattributes[:markershape] ≡ :none &&
+ get(plotattributes, :seriestype, :path) in
+ (:scatter, :scatterbins, :scatterhist, :scatter3d) #the default should be :auto, not :none, so that :none can be set explicitly and would be respected
+ plotattributes[:markershape] = :circle
+ end
+ elseif anymarker
+ plotattributes[:markershape_to_add] = :circle # add it after _apply_recipe
+ end
+
+ # handle fill
+ for arg in 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"),
+ )
+ @maxlog_warn "seriestype $st has been moved to StatsPlots. To use: \`Pkg.add(\"StatsPlots\"); using StatsPlots\`"
+ end
+ return 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 in keys(new_defs)
+ old_defs[k] = default(k)
+ end
+
+ # save the backend
+ old_backend = backend_name()
+
+ for arg in 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
+ return 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, pythonplot, and pgfplotsx).
+
+function convert_sci_unicode(label::AbstractString)
+ for key in keys(_convert_sci_unicode_dict)
+ label = replace(label, key => _convert_sci_unicode_dict[key])
+ end
+ occursin("×10^{", label) && (label = string(label, "}"))
+ return 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
+ return (
+ 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 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)
+ return 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])
+
+ return _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
+ return 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
+ return 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])
+
+ return (
+ _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]
+ return 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
+ return 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
+ return 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)
+
+ return 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
+ return 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.0, 0.25), (0.0, 0.25)), # bottomleft
+ ((0.75, 1.0), (0.0, 0.25)), # bottomright
+ ((0.0, 0.25), (0.75, 1.0)), # topleft
+ ((0.75, 1.0), (0.75, 1.0)), # 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
+ return _guess_best_legend_position(xlims(plt), ylims(plt), plt)
+end
+
+_generate_doclist(attributes) =
+ replace(join(sort(collect(attributes)), "\n- "), "_" => "\\_")
+
+# 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
+ return vmin - buffer, vmax + buffer
+end
diff --git a/src/backends/web.jl b/PlotsBase/src/web.jl
similarity index 67%
rename from src/backends/web.jl
rename to PlotsBase/src/web.jl
index cfa7e454c0..28d6e4114e 100644
--- a/src/backends/web.jl
+++ b/PlotsBase/src/web.jl
@@ -1,5 +1,4 @@
-
-# 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
@@ -7,38 +6,38 @@ standalone_html(
plt::AbstractPlot;
title::AbstractString = get(plt.attr, :window_title, "Plots.jl"),
) = """
-
-
-
- $title
-
- $(html_head(plt))
-
-
- $(html_body(plt))
-
-
- """
+
+
+
+ $title
+
+ $(html_head(plt))
+
+
+ $(html_body(plt))
+
+
+"""
embeddable_html(plt::AbstractPlot) = html_head(plt) * html_body(plt)
function open_browser_window(filename::AbstractString)
@static if Sys.isapple()
- return run(`open $(filename)`)
+ return run(`open $filename`)
elseif Sys.islinux() || Sys.isbsd() # Sys.isbsd() addition is as yet untested, but based on suggestion in https://github.com/JuliaPlots/Plots.jl/issues/681
- return run(`xdg-open $(filename)`)
+ return run(`xdg-open $filename`)
elseif Sys.iswindows()
- return run(`$(ENV["COMSPEC"]) /c start "" "$(filename)"`)
+ return run(`$(ENV["COMSPEC"]) /c start "" "$filename"`)
else
- @warn "Unknown OS... cannot open browser window."
+ @maxlog_warn "Unknown OS... cannot open browser window."
end
end
function write_temp_html(plt::AbstractPlot)
html = standalone_html(plt; title = plt.attr[:window_title])
- filename = string(tempname(), ".html")
+ filename = tempname(tmpdir_name()) * ".html"
write(filename, html)
- filename
+ return filename
end
function standalone_html_window(plt::AbstractPlot)
@@ -46,11 +45,11 @@ 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
- _use_local_dependencies[] = old
+ return _use_local_dependencies[] = old
end
# uses wkhtmltopdf/wkhtmltoimage: http://wkhtmltopdf.org/downloads.html
@@ -63,11 +62,13 @@ function show_png_from_html(io::IO, plt::AbstractPlot)
html_fn = write_temp_html(plt)
# convert that html file to a temporary png file using wkhtmltoimage
- png_fn = tempname() * ".png"
+ png_fn = tempname(tmpdir_name()) * ".png"
w, h = plt.attr[:size]
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)
+ return 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/Project.toml b/PlotsBase/test/Project.toml
new file mode 100644
index 0000000000..a634df87d6
--- /dev/null
+++ b/PlotsBase/test/Project.toml
@@ -0,0 +1,43 @@
+[deps]
+Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
+Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
+Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
+Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+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"
+JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
+Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
+LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
+LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
+PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925"
+Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
+PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c"
+PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737"
+Preferences = "21216c6a-2e73-6563-6e65-726566657250"
+PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
+RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
+RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c"
+SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
+SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
+Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+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"
diff --git a/PlotsBase/test/runtests.jl b/PlotsBase/test/runtests.jl
new file mode 100644
index 0000000000..cd36fb2210
--- /dev/null
+++ b/PlotsBase/test/runtests.jl
@@ -0,0 +1,131 @@
+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 in TEST_PACKAGES)
+
+get!(ENV, "MPLBACKEND", "agg")
+get!(ENV, "PLOTSBASE_PLOTLYJS_UNSAFE_ELECTRON", "true")
+
+using PlotsBase
+
+# initialize all backends
+for pkg in 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
+
+const broken_examples = Int[] # NOTE: unexpected pass is a failure
+Sys.isapple() && push!(broken_examples, 50) # FIXME: https://github.com/jheinen/GR.jl/issues/550
+
+const skipped_examples = Int[] # NOTE: won't error, regardless of the test output
+push!(skipped_examples, 62) # TODO: remove when new GR release is out and lands through CI (compat issues)
+
+function available_channels()
+ juliaup = "https://julialang-s3.julialang.org/juliaup"
+ for i in 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
+ return
+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
+ return !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
+
+# skip the majority of tests if we only want to update reference images or under `PkgEval` (timeout limit)
+names = if is_auto()
+ ["reference"]
+elseif is_pkgeval()
+ ["backends"]
+else
+ [
+ "misc",
+ "utils",
+ "args",
+ "defaults",
+ "dates",
+ "axes",
+ "layouts",
+ "contours",
+ "components",
+ "shorthands",
+ "recipes",
+ "unitful",
+ "hdf5plots",
+ "pgfplotsx",
+ "plotly",
+ "animations",
+ "output",
+ "reference",
+ "backends",
+ "preferences",
+ "quality",
+ ]
+end
+
+for name in names
+ @testset "$name" begin
+ haskey(TEST_BACKENDS, :GR) && gr() # reset to default backend
+ include("test_$name.jl")
+ end
+end
diff --git a/test/test_animations.jl b/PlotsBase/test/test_animations.jl
similarity index 92%
rename from test/test_animations.jl
rename to PlotsBase/test/test_animations.jl
index cc10250536..ec5a7a2399 100644
--- a/test/test_animations.jl
+++ b/PlotsBase/test/test_animations.jl
@@ -40,7 +40,7 @@ end
@test filesize(mov(anim, show_msg = false).filename) > 10_000
@test filesize(mp4(anim, show_msg = false).filename) > 10_000
@test filesize(webm(anim, show_msg = false).filename) > 10_000
- @test filesize(Plots.apng(anim, show_msg = false).filename) > 10_000
+ @test filesize(PlotsBase.apng(anim, show_msg = false).filename) > 10_000
@gif for i in 1:n
circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines)
@@ -54,7 +54,7 @@ end
circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines)
end when i % 5 == 0 fps = 10
- @test_throws LoadError macroexpand(
+ @test_throws ErrorException macroexpand(
@__MODULE__,
quote
@gif for i in 1:n
@@ -72,7 +72,7 @@ end
end,
)
- anim = Plots.@apng for i in 1:n
+ anim = PlotsBase.@apng for i in 1:n
circleplot(x, y, i, line_z = 1:n, cbar = false, framestyle = :zerolines)
end every 5
@test showable(MIME("image/png"), anim)
@@ -104,7 +104,7 @@ end
@testset "animate" begin
anim = animate([1:2, 2:3]; show_msg = false, fps = 1 // 10)
- @test anim isa Plots.AnimatedGif
+ @test anim isa PlotsBase.AnimatedGif
@test showable(MIME("image/gif"), anim)
fn = tempname() * ".apng"
@@ -122,6 +122,6 @@ end
@testset "coverage" begin
@test animate([1:2, 2:3]; variable_palette = true, show_msg = false) isa
- Plots.AnimatedGif
- @test Plots.FrameIterator([1:2, 2:3]).every == 1
+ PlotsBase.AnimatedGif
+ @test PlotsBase.FrameIterator([1:2, 2:3]).every == 1
end
diff --git a/test/test_args.jl b/PlotsBase/test/test_args.jl
similarity index 58%
rename from test/test_args.jl
rename to PlotsBase/test/test_args.jl
index 30e337a02e..b7b29c615c 100644
--- a/test/test_args.jl
+++ b/PlotsBase/test/test_args.jl
@@ -1,4 +1,7 @@
-using Plots, Dates, Test
+using PlotsBase, Dates, Test
+using PlotsBase.Surfaces: Surface
+import GR
+gr()
struct Foo{T}
x::Vector{T}
y::Vector{T}
@@ -16,14 +19,16 @@ 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 (@test_logs (:warn, "Skipped marker arg diamond.") plot(
- foo,
- marker = :diamond,
- markershape = :diamond,
- )[1][1][:markershape]) === :diamond
+ @test plot(foo)[1][1][:markershape] ≡ (backend_name() === :gr ? :+ : :circle)
+ @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
end
@testset "Subplot Attributes" begin
@@ -33,12 +38,14 @@ end
end
@testset "Series Attributes" begin
- pl = plot([[1, 2, 3], [2, 3, 4]], lw = 5)
- @test hline!(deepcopy(pl), [1.75])[1].series_list[3][:label] ==
- hline!(deepcopy(pl), [1.75], z_order = :front)[1].series_list[3][:label] ==
- "y3"
- @test hline!(deepcopy(pl), [1.75], z_order = :back)[1].series_list[1][:label] == "y3"
- @test hline!(deepcopy(pl), [1.75], z_order = 2)[1].series_list[2][:label] == "y3"
+ pl = plot([[1, 2, 3], [2, 3, 4]], lw = 5, label = :auto)
+ @test hline!(deepcopy(pl), [1.75], label = :auto)[1].series_list[3][:label] ==
+ hline!(deepcopy(pl), [1.75], z_order = :front, label = :auto)[1].series_list[3][:label] ==
+ "y3"
+ @test hline!(deepcopy(pl), [1.75], z_order = :back, label = :auto)[1].series_list[1][:label] ==
+ "y3"
+ @test hline!(deepcopy(pl), [1.75], z_order = 2, label = :auto)[1].series_list[2][:label] ==
+ "y3"
sp = pl[1]
@test isempty(sp[1][:extra_kwargs])
@test sp[2][:series_index] == 2
@@ -54,6 +61,16 @@ end
end
end
+@testset "Plotting Plots" begin
+ pl = @test_nowarn plot(rand(3, 3))
+ @test plot(pl, plot_title = "Test")[:plot_title] == "Test"
+ @test plot(pl, title = "Test")[1][:title] == "Test"
+ @test plot(pl, xtickfontsize = 1)[1][:xaxis][:tickfontsize] == 1
+ @test plot(pl, label = "Test")[1][1][:label] == "Test"
+ @test plot(pl, label = "Test")[1][2][:label] == "Test"
+ @test plot(pl, label = "Test")[1][3][:label] == "Test"
+end
+
@testset "Permute recipes" begin
pl1 = bar(["a", "b", "c"], [1, 2, 3])
pl2 = bar(["a", "b", "c"], [1, 2, 3], permute = (:x, :y))
@@ -64,8 +81,8 @@ end
end
@testset "@add_attributes" begin
- Font = Plots.Font
- Plots.@add_attributes subplot struct Legend
+ Font = PlotsBase.Font
+ PlotsBase.@add_attributes subplot struct Legend
background_color = :match
foreground_color = :match
position = :best
@@ -91,9 +108,9 @@ end
end
@testset "aliases" begin
- @test :legend in aliases(:legend_position)
- Plots.add_non_underscore_aliases!(Plots._typeAliases)
- Plots.add_axes_aliases(:ticks, :tick)
+ @test :legend in PlotsBase.Commons.aliases(:legend_position)
+ PlotsBase.Commons.add_non_underscore_aliases!(PlotsBase.Commons._typeAliases)
+ PlotsBase.Commons.add_axes_aliases(:ticks, :tick)
end
@userplot MatrixHeatmap
@@ -117,14 +134,14 @@ end
ts = range(DateTime(today()), step = Hour(1), length = 24)
p1 = plot(ts, 100randn(24))
vline!(p1, [now()])
- @test p1[1][:yaxis][:formatter] == :auto
- @test p1[1][:xaxis][:formatter] == Plots.datetimeformatter
+ @test p1[1][:yaxis][:formatter] ≡ :auto
+ @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
- @test p2[1][:xaxis][:formatter] == :plain
+ @test p2[1][:yaxis][:formatter] ≡ :plain
+ @test p2[1][:xaxis][:formatter] ≡ :plain
p3 = plot(rand(4) .* 10^6, rand(4) .* 10^6, yformatter = :plain)
vline!(p3, [10^6], xformatter = :plain)
- @test p3[1][:yaxis][:formatter] == :plain
- @test p3[1][:xaxis][:formatter] == :plain
+ @test p3[1][:yaxis][:formatter] ≡ :plain
+ @test p3[1][:xaxis][:formatter] ≡ :plain
end
diff --git a/test/test_axes.jl b/PlotsBase/test/test_axes.jl
similarity index 52%
rename from test/test_axes.jl
rename to PlotsBase/test/test_axes.jl
index 01df32ecd4..a21e10207c 100644
--- a/test/test_axes.jl
+++ b/PlotsBase/test/test_axes.jl
@@ -1,15 +1,15 @@
@testset "Axes" begin
pl = plot()
axis = pl.subplots[1][:xaxis]
- @test typeof(axis) == Plots.Axis
- @test Plots.discrete_value!(axis, "HI") == (0.5, 1)
- @test Plots.discrete_value!(axis, :yo) == (1.5, 2)
- @test Plots.ignorenan_extrema(axis) == (0.5, 1.5)
- @test axis[:discrete_map] == Dict{Any,Any}(:yo => 2, "HI" => 1)
+ @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)
@@ -17,40 +17,44 @@
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) ==
- ["1.0 MB", "2.0 MB", "3.0 MB"]
- @test Plots.get_labels(x -> string(x, " MB"), 1:3, :log10) ==
- ["10.0 MB", "100.0 MB", "1000.0 MB"]
+ @test PlotsBase.get_labels(x -> 1.0e3x, 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 -> 1.0e3x, 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 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 in PlotsBase.Commons._all_showaxis_attrs
@test plot(1:5, showaxis = value)[1][:yaxis][:showaxis] isa Bool
end
@test plot(1:5, showaxis = :y)[1][:yaxis][:showaxis]
@@ -66,9 +70,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 +86,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])
pl = plot(x; xlims)
- @test Plots.xlims(pl) == (1, 5)
+ @test PlotsBase.xlims(pl) == (1, 5)
pl = plot(x; xlims, widen = true)
- @test Plots.xlims(pl) == default_widen(1, 5)
+ @test PlotsBase.xlims(pl) == default_widen(1, 5)
end
pl = plot(1:5, lims = :symmetric, widen = false)
- @test Plots.xlims(pl) == Plots.ylims(pl) == (-5, 5)
+ @test PlotsBase.xlims(pl) == PlotsBase.ylims(pl) == (-5, 5)
for xlims in (0, 0.0, false, true, plot())
pl = plot(1:5; xlims)
plims =
- @test_logs (:warn, r"Invalid limits for x axis") match_mode = :any Plots.xlims(
- pl,
- )
+ @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])
pl = plot([-2, 3], ylims = ylims, widen = false)
- @test Plots.ylims(pl) == (-5.0, 3.0)
+ @test PlotsBase.ylims(pl) == (-5.0, 3.0)
end
for ylims in ((:auto, 4), [:auto, 4])
pl = plot([-2, 3], ylims = ylims, widen = false)
- @test Plots.ylims(pl) == (-2.0, 4.0)
+ @test PlotsBase.ylims(pl) == (-2.0, 4.0)
end
for xlims in ((-3, :auto), [-3, :auto])
pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false)
- @test Plots.xlims(pl) == (-3.0, 3.0)
+ @test PlotsBase.xlims(pl) == (-3.0, 3.0)
end
for xlims in ((:auto, 4), [:auto, 4])
pl = plot([-2, 3], [-1, 1], xlims = xlims, widen = false)
- @test Plots.xlims(pl) == (-2.0, 4.0)
+ @test PlotsBase.xlims(pl) == (-2.0, 4.0)
end
end
end
@@ -139,23 +144,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 PlotsBase.get_guide(pl[1][:xaxis]) ≡ "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 +168,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 +189,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 +213,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 +221,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,26 +231,26 @@ 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)
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)
@@ -253,9 +258,9 @@ end
pl = plot(1:4; minorgrid = true, minorticks = minor_intervals)
sp = first(pl)
for axis in (:xaxis, :yaxis)
- ticks = Plots.get_ticks(sp, sp[axis], update = false)
+ ticks = PlotsBase.get_ticks(sp, sp[axis], update = false)
n_expected_minor_ticks = (length(first(ticks)) - 1) * n_minor_ticks_per_major
- minor_ticks = Plots.get_minor_ticks(sp, sp[axis], ticks)
+ minor_ticks = PlotsBase.get_minor_ticks(sp, sp[axis], ticks)
n_minor_ticks = if minor_intervals isa Bool
if minor_intervals
length(minor_ticks)
@@ -263,9 +268,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
@@ -276,3 +281,20 @@ end
end
end
end
+
+@testset "axis guides (labels)" begin
+ yguide(pl, idx = length(pl.subplots)) = PlotsBase.get_guide(pl.subplots[idx].attr[:yaxis])
+
+ @test yguide(plot(1:3, ylabel = "hello")) == "hello"
+ @test yguide(plot(1:3, ylabel = L"hello")) == L"hello"
+ @test yguide(plot(1:3, ylabel = :hello)) == :hello
+
+ @test yguide(plot(1:3, yunit = 1)) == "1"
+ @test yguide(plot(1:3, yunit = 1, ylabel = :hello)) == "hello (1)"
+ @test yguide(plot(1:3, yunit = 1, ylabel = :hello, unitformat = :square)) == "hello [1]"
+ @test yguide(plot(1:3, yunit = 1, ylabel = nothing, unitformat = :square)) == ""
+ @test yguide(plot(1:3, yunit = 1, ylabel = L"hello")) == L"hello" * " (1)"
+
+ uf = (l, u) -> Symbol(l, "_symb_", u)
+ @test yguide(plot(1:3, yunit = 1, ylabel = :hello, unitformat = uf)) == :hello_symb_1
+end
diff --git a/PlotsBase/test/test_backends.jl b/PlotsBase/test/test_backends.jl
new file mode 100644
index 0000000000..6ef1081aa5
--- /dev/null
+++ b/PlotsBase/test/test_backends.jl
@@ -0,0 +1,72 @@
+@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() || @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 $be" for be in TEST_BACKENDS
+ 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() * PlotsBase.ref_name(i),
+ )
+ @test filesize(fn) > 1_000
+ end
+ !(Sys.islinux() && is_latest("release")) && continue
+ skip = vcat(PlotsBase._backend_skips[be], skipped_examples, broken_examples)
+ PlotsBase.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage
+ closeall()
+end
diff --git a/test/test_components.jl b/PlotsBase/test/test_components.jl
similarity index 58%
rename from test/test_components.jl
rename to PlotsBase/test/test_components.jl
index 971f4e5191..f2f4c74866 100644
--- a/test/test_components.jl
+++ b/PlotsBase/test/test_components.jl
@@ -1,18 +1,23 @@
+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 isa(square, Shape{Int64,Float64})
- @test coords(square) isa Tuple{Vector{S},Vector{T}} where {T,S}
+ @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
xs = view([0.0, 1.0, 2.0, 1.25], 1:3)
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 isa(tri, Shape{Float64, Int64})
+ @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
@@ -61,29 +66,29 @@
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]
- @test coords(myshapes) isa Tuple{Vector{Vector{S}},Vector{Vector{T}}} where {T,S}
+ @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,11 +115,11 @@ end
end
@testset "Text" begin
- t = Plots.PlotText("foo")
+ t = PlotsBase.PlotText("foo")
@test length(t) == 3
- f = Plots.font()
- @test Plots.PlotText(nothing).str == "nothing"
+ f = PlotsBase.font()
+ @test PlotsBase.PlotText(nothing).str == "nothing"
@test text(t).str == "foo"
@test text(t, f).str == "foo"
@test text("bar", f).str == "bar"
@@ -123,32 +128,32 @@ end
for rotation in -180:5:180
t = text("foo"; rotation)
if abs(rotation) ≤ 45 || abs(rotation) ≥ 135
- @test Plots.is_horizontal(t)
+ @test PlotsBase.is_horizontal(t)
else
- @test !Plots.is_horizontal(t)
+ @test !PlotsBase.is_horizontal(t)
end
end
end
@testset "Annotations" begin
- ann = Plots.series_annotations(missing)
+ ann = PlotsBase.series_annotations(missing)
- @test Plots.series_annotations(["1" "2"; "3" "4"]) isa AbstractMatrix
- @test Plots.series_annotations(10).strs[1].str == "10"
- @test Plots.series_annotations(nothing) === nothing
- @test Plots.series_annotations(ann) == ann
+ @test PlotsBase.series_annotations(["1" "2"; "3" "4"]) isa AbstractMatrix
+ @test PlotsBase.series_annotations(10).strs[1].str == "10"
+ @test PlotsBase.series_annotations(nothing) ≡ nothing
+ @test PlotsBase.series_annotations(ann) == ann
- @test Plots.annotations(["1" "2"; "3" "4"]) isa AbstractMatrix
- @test Plots.annotations(ann) == ann
- @test Plots.annotations([ann]) == [ann]
- @test Plots.annotations(nothing) == []
+ @test PlotsBase.annotations(["1" "2"; "3" "4"]) isa AbstractMatrix
+ @test PlotsBase.annotations(ann) == ann
+ @test PlotsBase.annotations([ann]) == [ann]
+ @test PlotsBase.annotations(nothing) == []
- t = Plots.text("foo")
+ t = PlotsBase.text("foo")
sp = plot(1)[1]
- @test Plots.locate_annotation(sp, 1, 2, t) == (1, 2, t)
- @test Plots.locate_annotation(sp, 1, 2, 3, t) == (1, 2, 3, t)
- @test Plots.locate_annotation(sp, (0.1, 0.2), t) isa Tuple
- @test Plots.locate_annotation(sp, (0.1, 0.2, 0.3), t) isa Tuple
+ @test PlotsBase.locate_annotation(sp, 1, 2, t) == (1, 2, t)
+ @test PlotsBase.locate_annotation(sp, 1, 2, 3, t) == (1, 2, 3, t)
+ @test PlotsBase.locate_annotation(sp, (0.1, 0.2), t) isa Tuple
+ @test PlotsBase.locate_annotation(sp, (0.1, 0.2, 0.3), t) isa Tuple
# see github.com/JuliaPlots/Plots.jl/issues/4073
anns = [(["x", "y"], [10, 20], :hexagon) (["a", "b"], [3, 4], :circle)]
@@ -166,7 +171,7 @@ end
annotate!(sp = 2, (0.03, 0.95), text("Cats&Dogs", :left))
end
- for scale in Plots._logScales
+ for scale in PlotsBase._log_scales
pl = plot(xlim = (1, 10), xscale = scale)
annotate!(pl, (0.5, 0.5), "hello")
end
@@ -182,6 +187,66 @@ end
annotate!(pl, loc, string(loc))
end
end
+
+ let p = scatter([4], [4], plot_title = "x", xlims = (0, 10), ylims = (0, 10))
+ for sp in p.subplots
+ @test sp[:annotations] == []
+ end
+ annotate!(4, 4, "4")
+
+ for (i, sp) in enumerate(p.subplots)
+ if i == p.attr[:plot_titleindex]
+ @test sp[:annotations] == []
+ else
+ @test length(sp[:annotations]) == 1
+ @test sp[:annotations][1][1] == 4
+ @test sp[:annotations][1][2] == 4
+ @test sp[:annotations][1][3].str == "4"
+ end
+ end
+ end
+
+ let p = scatter(
+ [4],
+ [4],
+ plot_title = "x",
+ xlims = (0, 10),
+ ylims = (0, 10),
+ annotations = (4, 4, "4"),
+ )
+ for (i, sp) in enumerate(p.subplots)
+ if i == p.attr[:plot_titleindex]
+ @test sp[:annotations] == []
+ else
+ @test length(sp[:annotations]) == 1
+ @test sp[:annotations][1][1] == 4
+ @test sp[:annotations][1][2] == 4
+ @test sp[:annotations][1][3].str == "4"
+ end
+ end
+ end
+
+ let p = plot(
+ scatter([4], [4], xlims = (0, 10), ylims = (0, 10)),
+ scatter([4], [4], xlims = (0, 10), ylims = (0, 10)),
+ plot_title = "x",
+ )
+ for sp in p.subplots
+ @test sp[:annotations] == []
+ end
+ annotate!(4, 4, "4")
+
+ for (i, sp) in enumerate(p.subplots)
+ if i == p.attr[:plot_titleindex]
+ @test sp[:annotations] == []
+ else
+ @test length(sp[:annotations]) == 1
+ @test sp[:annotations][1][1] == 4
+ @test sp[:annotations][1][2] == 4
+ @test sp[:annotations][1][3].str == "4"
+ end
+ end
+ end
end
@testset "Fonts" begin
@@ -198,26 +263,29 @@ end
:zguidefontsize,
]
# get initial font sizes
- initialSizes = [Plots.default(s) for s in sizesToCheck]
+ initialSizes = [PlotsBase.default(s) for s in sizesToCheck]
#scale up font sizes
scalefontsizes(2)
# get initial font sizes
- doubledSizes = [Plots.default(s) for s in sizesToCheck]
+ doubledSizes = [PlotsBase.default(s) for s in sizesToCheck]
@test doubledSizes == initialSizes * 2
# reset font sizes
resetfontsizes()
- finalSizes = [Plots.default(s) for s in sizesToCheck]
+ finalSizes = [PlotsBase.default(s) for s in sizesToCheck]
@test finalSizes == initialSizes
end
end
@testset "Series Annotations" begin
+ get_xs = 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(
@@ -247,7 +315,7 @@ end
)
for i in 1:5
@test only(spl.series_list[i].plotattributes[:series_annotations].strs).str ==
- "1/$i"
+ "1/$i"
end
series_anns(pl, series) = pl.series_list[series].plotattributes[:series_annotations]
@@ -275,10 +343,12 @@ end
let pl = plot(
ones(3, 2),
series_annotations = (
- permutedims([
- (["x", "y", "z"], [10, 20, 30], (14, 15), square),
- [("a", 42), "b", "c"],
- ]),
+ permutedims(
+ [
+ (["x", "y", "z"], [10, 20, 30], (14, 15), square),
+ [("a", 42), "b", "c"],
+ ]
+ ),
(12, 13),
),
)
@@ -308,7 +378,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 58%
rename from test/test_contours.jl
rename to PlotsBase/test/test_contours.jl
index 3c76727333..fdb10a6356 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)
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 87%
rename from test/test_dates.jl
rename to PlotsBase/test/test_dates.jl
index 68db3cfe47..031c3931a9 100644
--- a/test/test_dates.jl
+++ b/PlotsBase/test/test_dates.jl
@@ -9,8 +9,8 @@
ref_ylims = (y[1], y[end])
ref_xlims = (x[1].instant.periods.value, x[end].instant.periods.value)
- @test Plots.ylims(pl) == ref_ylims
- @test Plots.xlims(pl) == ref_xlims
+ @test PlotsBase.ylims(pl) == ref_ylims
+ @test PlotsBase.xlims(pl) == ref_xlims
end
@testset "Date xlims" begin
@@ -31,5 +31,5 @@ end
ref_xlims = map(date -> date.instant.periods.value, span)
pl = plot(x, y, xlims = span, widen = false)
- @test Plots.xlims(pl) == ref_xlims
+ @test PlotsBase.xlims(pl) == ref_xlims
end
diff --git a/test/test_defaults.jl b/PlotsBase/test/test_defaults.jl
similarity index 67%
rename from test/test_defaults.jl
rename to PlotsBase/test/test_defaults.jl
index d96f821526..6730b87e62 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,26 +62,26 @@ 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)
+ RGBA{Float64}(0.0, 0.5019607843137255, 0.0, 1.0)
#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 8430acc9f2..acab1535a3 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/test/test_layouts.jl b/PlotsBase/test/test_layouts.jl
similarity index 50%
rename from test/test_layouts.jl
rename to PlotsBase/test/test_layouts.jl
index 182720e927..696d0cd1cb 100644
--- a/test/test_layouts.jl
+++ b/PlotsBase/test/test_layouts.jl
@@ -1,4 +1,3 @@
-using Plots, Test
@testset "Plotting plots" begin
pl = @test_nowarn plot(plot(1:2), plot(1:2, size = (1_200, 400)))
@test pl[:size] == (1_200, 400)
@@ -12,10 +11,10 @@ end
layout = 4,
yscale = [:identity :identity :log10 :log10],
)
- @test pl[1][:yaxis][:scale] === :identity
- @test pl[2][:yaxis][:scale] === :identity
- @test pl[3][:yaxis][:scale] === :log10
- @test pl[4][:yaxis][:scale] === :log10
+ @test pl[1][:yaxis][:scale] ≡ :identity
+ @test pl[2][:yaxis][:scale] ≡ :identity
+ @test pl[3][:yaxis][:scale] ≡ :log10
+ @test pl[4][:yaxis][:scale] ≡ :log10
end
@testset "Plot title" begin
@@ -26,7 +25,7 @@ end
background_color = :darkgray,
background_color_inside = :lightgray,
)
- @test pl.layout.heights == [0.05Plots.pct, 0.95Plots.pct]
+ @test pl.layout.heights == [0.05PlotsBase.pct, 0.95PlotsBase.pct]
@test pl[:plot_title] == "My title"
@test pl[:plot_titleindex] == 5
@@ -43,9 +42,9 @@ end
@testset "Plots.jl/issues/4083" begin
pl = plot(plot(1:2), plot(1:2); border = :grid, plot_title = "abc")
- @test pl[1][:framestyle] === :grid
- @test pl[2][:framestyle] === :grid
- @test pl[3][:framestyle] === :none
+ @test pl[1][:framestyle] ≡ :grid
+ @test pl[2][:framestyle] ≡ :grid
+ @test pl[3][:framestyle] ≡ :none
end
@testset "Allowed subplot counts" begin
@@ -65,6 +64,17 @@ end
@test_throws ErrorException plot(map(_ -> plot(1:2), 1:5)...; layout = grid(2, 2))
end
+@testset "Allowed grid widths/heights" begin
+ @test_nowarn grid(2, 1, heights = [0.5, 0.5])
+ @test_nowarn grid(4, 1, heights = [0.3, 0.3, 0.3, 0.1])
+ @test_nowarn grid(1, 2, widths = [0.5, 0.5])
+ @test_nowarn grid(1, 4, widths = [0.3, 0.3, 0.3, 0.1])
+ @test_throws ErrorException grid(2, 1, heights = [0.5, 0.4])
+ @test_throws ErrorException grid(4, 1, heights = [1.5, -0.5])
+ @test_throws ErrorException grid(1, 2, widths = [0.5, 0.4])
+ @test_throws ErrorException grid(1, 4, widths = [1.5, -0.5])
+end
+
@testset "Invalid viewport" begin
# github.com/JuliaPlots/Plots.jl/issues/2804
pl = plot(1, layout = (10, 2))
@@ -75,67 +85,67 @@ end
pl = plot(map(plot, 1:4)..., layout = (2, 2))
sp = pl[end]
- @test sp isa Plots.Subplot
+ @test sp isa PlotsBase.Subplot
@test size(sp) == (1, 1)
@test length(sp) == 1
@test sp[1, 1] == sp
- @test Plots.get_subplot(pl, UInt32(4)) == sp
- @test Plots.series_list(sp) |> first |> Plots.get_subplot isa Plots.Subplot
- @test Plots.get_subplot(pl, keys(pl.spmap) |> first) isa Plots.Subplot
+ @test PlotsBase.get_subplot(pl, UInt32(4)) == sp
+ @test PlotsBase.series_list(sp) |> first |> PlotsBase.get_subplot isa PlotsBase.Subplot
+ @test PlotsBase.get_subplot(pl, keys(pl.spmap) |> first) isa PlotsBase.Subplot
gl = pl[2, 2]
- @test gl isa Plots.GridLayout
+ @test gl isa PlotsBase.GridLayout
@test length(gl) == 1
@test size(gl) == (1, 1)
- @test Plots.layout_args(gl) == (gl, 1)
+ @test PlotsBase.layout_attrs(gl) == (gl, 1)
@test size(pl, 1) == 2
@test size(pl, 2) == 2
@test size(pl) == (2, 2)
@test ndims(pl) == 2
- @test pl[1][end] isa Plots.Series
+ @test pl[1][end] isa PlotsBase.Series
io = devnull
show(io, pl[1])
- @test Plots.getplot(pl) == pl
- @test Plots.getattr(pl) == pl.attr
- @test Plots.backend_object(pl) == pl.o
+ @test PlotsBase.getplot(pl) == pl
+ @test PlotsBase.getattr(pl) == pl.attr
+ @test PlotsBase.backend_object(pl) == pl.o
@test occursin("Plot", string(pl))
print(io, pl)
- @test Plots.to_pixels(1Plots.mm) isa AbstractFloat
- @test Plots.ispositive(1Plots.mm)
- @test size(Plots.DEFAULT_BBOX[]) == (0Plots.mm, 0Plots.mm)
- show(io, Plots.DEFAULT_BBOX[])
+ @test PlotsBase.to_pixels(1PlotsBase.mm) isa AbstractFloat
+ @test PlotsBase.ispositive(1PlotsBase.mm)
+ @test size(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm)
+ show(io, PlotsBase.DEFAULT_BBOX[])
show(io, pl.layout)
- @test Plots.make_measure_hor(1Plots.mm) == 1Plots.mm
- @test Plots.make_measure_vert(1Plots.mm) == 1Plots.mm
+ @test PlotsBase.Commons.make_measure_hor(1PlotsBase.mm) == 1PlotsBase.mm
+ @test PlotsBase.Commons.make_measure_vert(1PlotsBase.mm) == 1PlotsBase.mm
- @test Plots.parent(pl.layout) isa Plots.RootLayout
- show(io, Plots.parent_bbox(pl.layout))
+ @test PlotsBase.parent(pl.layout) isa PlotsBase.RootLayout
+ show(io, PlotsBase.Commons.parent_bbox(pl.layout))
- rl = Plots.RootLayout()
+ rl = PlotsBase.RootLayout()
show(io, rl)
- @test parent(rl) === nothing
- @test Plots.parent_bbox(rl) == Plots.DEFAULT_BBOX[]
- @test Plots.bbox(rl) == Plots.DEFAULT_BBOX[]
- @test Plots.origin(Plots.DEFAULT_BBOX[]) == (0Plots.mm, 0Plots.mm)
+ @test parent(rl) ≡ nothing
+ @test PlotsBase.Commons.parent_bbox(rl) == PlotsBase.DEFAULT_BBOX[]
+ @test PlotsBase.bbox(rl) == PlotsBase.DEFAULT_BBOX[]
+ @test PlotsBase.origin(PlotsBase.DEFAULT_BBOX[]) == (0PlotsBase.mm, 0PlotsBase.mm)
for h_anchor in (:left, :right, :hcenter), v_anchor in (:top, :bottom, :vcenter)
- @test Plots.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa Plots.BoundingBox
+ @test PlotsBase.bbox(0, 0, 1, 1, h_anchor, v_anchor) isa PlotsBase.BoundingBox
end
- el = Plots.EmptyLayout()
- @test Plots.update_position!(el) === nothing
+ el = PlotsBase.EmptyLayout()
+ @test PlotsBase.update_position!(el) ≡ nothing
@test size(el) == (0, 0)
@test length(el) == 0
- @test el[1, 1] === nothing
+ @test el[1, 1] ≡ nothing
- @test Plots.left(el) == 0Plots.mm
- @test Plots.top(el) == 0Plots.mm
- @test Plots.right(el) == 0Plots.mm
- @test Plots.bottom(el) == 0Plots.mm
+ @test PlotsBase.left(el) == 0PlotsBase.mm
+ @test PlotsBase.top(el) == 0PlotsBase.mm
+ @test PlotsBase.right(el) == 0PlotsBase.mm
+ @test PlotsBase.bottom(el) == 0PlotsBase.mm
plot(map(plot, 1:4)..., layout = (2, :))
plot(map(plot, 1:4)..., layout = (:, 2))
diff --git a/test/test_misc.jl b/PlotsBase/test/test_misc.jl
similarity index 63%
rename from test/test_misc.jl
rename to PlotsBase/test/test_misc.jl
index 172aee38cd..e06c9bfea5 100644
--- a/test/test_misc.jl
+++ b/PlotsBase/test/test_misc.jl
@@ -1,41 +1,41 @@
# miscellaneous tests (not fitting into other test files)
@testset "Infrastructure" begin
- @test_nowarn JSON.Parser.parse(
- String(read(joinpath(dirname(pathof(Plots)), "..", ".zenodo.json"))),
+ @test_nowarn JSON.parse(
+ 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 [
- histogram([1, 0, 0, 0, 0, 0]),
- plot([missing]),
- plot([missing, missing]),
- plot(fill(missing, 10)),
- plot([missing; 1:4]),
- plot([fill(missing, 10); 1:4]),
- plot([1 1; 1 missing]),
- plot(["a" "b"; missing "d"], [1 2; 3 4]),
- ]
+ histogram([1, 0, 0, 0, 0, 0]),
+ plot([missing]),
+ plot([missing, missing]),
+ plot(fill(missing, 10)),
+ plot([missing; 1:4]),
+ plot([fill(missing, 10); 1:4]),
+ plot([1 1; 1 missing]),
+ plot(["a" "b"; missing "d"], [1 2; 3 4]),
+ ]
display(dsp, pl)
end
@test_nowarn plot(x -> x^2, 0, 2)
@@ -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),
- NTuple{2,<:AbstractFloat},
+ 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,19 +117,12 @@ end
value(m::MyType) = m.val
data = MyType.(sort(randn(20)))
- # A recipe that puts the axis letter in the title
- @recipe function f(::Type{T}, m::T) where {T<:AbstractArray{<:MyType}}
+ # 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)
@test f(data).subplots[1].attr[:title] == "y"
end
@@ -169,11 +157,11 @@ end
for i in axes(data4, 1)
for attribute in (:fillrange, :ribbon)
nt = NamedTuple{tuple(attribute)}
- get_attr(pl) = pl[1][i][attribute]
- @test plot(data4; nt(0)...) |> get_attr == 0
- @test plot(data4; nt(Ref([1, 2]))...) |> get_attr == [1.0, 2.0]
- @test plot(data4; nt(Ref([1 2]))...) |> get_attr == (iseven(i) ? 2 : 1)
- @test plot(data4; nt(Ref(mat))...) |> get_attr == [2(i - 1) + 1, 2i]
+ get_attrs(pl) = pl[1][i][attribute]
+ @test plot(data4; nt(0)...) |> get_attrs == 0
+ @test plot(data4; nt(Ref([1, 2]))...) |> get_attrs == [1.0, 2.0]
+ @test plot(data4; nt(Ref([1 2]))...) |> get_attrs == (iseven(i) ? 2 : 1)
+ @test plot(data4; nt(Ref(mat))...) |> get_attrs == [2(i - 1) + 1, 2i]
end
@test sp[i][:ribbon] == ([2(i - 1) + 1, 2i], [2(i - 1) + 1, 2i])
end
@@ -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,
@@ -315,11 +306,27 @@ Plots.with(:gr) do
fillcolor = :blue,
fillalpha = 0.2,
)
+ # Validate still works when we iuse different int types
+ mesh3d(
+ x,
+ y,
+ z;
+ connections = [
+ Int32[1, 2, 4, 3], # Quadrangle
+ Int32[1, 2, 5], # Triangle
+ Int32[2, 4, 5], # Triangle
+ Int32[4, 3, 5], # Triangle
+ Int32[3, 1, 5], # Triangle
+ ],
+ linecolor = :black,
+ fillcolor = :blue,
+ fillalpha = 0.2,
+ )
@test true
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,13 +338,14 @@ 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),
+ plot(1:2, legend_columns = 10, label = :auto),
tempname(),
)
end
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 86bfc75df0..e221c18198 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 80%
rename from test/test_pgfplotsx.jl
rename to PlotsBase/test/test_pgfplotsx.jl
index 12069b740c..05f952df78 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,23 +12,23 @@ 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)
+ return 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)
- scatter!(pl, rand(5))
+ scatter!(pl, rand(5), label = :auto)
axis_contents = first(get_pgf_axes(pl)).contents
leg_entries = filter(x -> x isa PGFPlotsX.LegendEntry, axis_contents)
series = filter(x -> x isa PGFPlotsX.Plot, axis_contents)
@@ -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"],
)
@@ -92,8 +93,8 @@ Plots.with(:pgfplotsx) do
end
@testset "Plot in pieces" begin
- pl = plot(rand(100) / 3, reg = true, fill = (0, :green))
- scatter!(pl, rand(100), markersize = 6, c = :orange)
+ pl = plot(rand(100) / 3, reg = true, fill = (0, :green), label = :auto)
+ scatter!(pl, rand(100), markersize = 6, c = :orange, label = :auto)
axis_contents = first(get_pgf_axes(pl)).contents
leg_entries = filter(x -> x isa PGFPlotsX.LegendEntry, axis_contents)
series = filter(x -> x isa PGFPlotsX.Plot, axis_contents)
@@ -106,9 +107,14 @@ 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 +127,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,11 +178,11 @@ 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
@@ -186,11 +192,11 @@ Plots.with(:pgfplotsx) do
pl = heatmap(xs, ys, z, aspect_ratio = 1)
axis = first(get_pgf_axes(pl))
if @test_nowarn(haskey(axis.options.dict, "colorbar"))
- @test axis["colorbar"] === nothing
+ @test axis["colorbar"] ≡ nothing
@test axis["colormap name"] == "plots1"
end
- @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 +210,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 +222,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
@@ -252,7 +258,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 +267,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)
@@ -274,10 +280,12 @@ Plots.with(:pgfplotsx) do
@test count(s -> occursin("node", s), lines) == 1
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")),
- ])
+ annotate!(
+ [
+ (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)
@test length(nodes) == 3
@@ -299,7 +307,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 +340,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
@@ -398,26 +406,26 @@ Plots.with(:pgfplotsx) do
@test pl[1][:extra_kwargs] == Dict(:add => raw"\node at (0,0.5) {\huge hi};")
axis_contents = first(get_pgf_axes(pl)).contents
@test filter(x -> x isa String, axis_contents)[1] ==
- raw"\node at (0,0.5) {\huge hi};"
+ raw"\node at (0,0.5) {\huge hi};"
plot!(pl)
@test pl[1][:extra_kwargs] == Dict(:add => raw"\node at (0,0.5) {\huge hi};")
axis_contents = first(get_pgf_axes(pl)).contents
@test filter(x -> x isa String, axis_contents)[1] ==
- raw"\node at (0,0.5) {\huge hi};"
+ raw"\node at (0,0.5) {\huge hi};"
end
@testset "Titlefonts" begin
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 +433,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.") ==
- "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("A string, with 2 punctuation chars.") ==
+ "A string, with 2 punctuation chars."
+ @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
@@ -472,6 +480,6 @@ Plots.with(:pgfplotsx) do
pl2_tex = String(repr("application/x-tex", pl2))
@test pl1_tex[findfirst(yreg, pl1_tex)] == "ylabel={diameter (\$\\mathrm{m}\$)}"
@test pl2_tex[findfirst(yreg, pl2_tex)] ==
- "ylabel={\$\\mathrm{m}\\,\\mathrm{s}^{-2}\$}"
+ "ylabel={\$\\mathrm{m}\\,\\mathrm{s}^{-2}\$}"
end
end
diff --git a/PlotsBase/test/test_plotly.jl b/PlotsBase/test/test_plotly.jl
new file mode 100644
index 0000000000..41eeece36b
--- /dev/null
+++ b/PlotsBase/test/test_plotly.jl
@@ -0,0 +1,74 @@
+using PlotsBase, Test
+Sys.isunix() && with(:plotly) do
+ @testset "Basic" begin
+ @test backend() == PlotsBase.PlotlyBackend()
+
+ pl = plot(rand(10))
+ @test pl isa PlotsBase.Plot
+ @test_nowarn PlotsBase.plotly_series(plot())
+ @test !haskey(PlotsBase.plotly_series(pl)[1], :zmax)
+ end
+
+ @testset "Contours" begin
+ x = (-2π):0.1:(2π)
+ y = (-π):0.1:π
+ z = cos.(y) .* sin.(x')
+
+ @testset "Contour numbers" begin
+ @testset "Default" begin
+ @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 PlotsBase.plotly_series(cont)[1][:ncontours] == 12
+ end
+ end
+
+ @testset "Contour values" begin
+ @testset "Range" begin
+ levels = -1:0.5:1
+ pl = contour(x, y, z, levels = levels)
+ @test pl[1][1].plotattributes[:levels] == 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
+ levels = [-1, -0.25, 0, 0.25, 1]
+ levels_range =
+ range(first(levels), stop = last(levels), length = length(levels))
+ pl = contour(x, y, z, levels = levels)
+ @test pl[1][1].plotattributes[:levels] == levels
+ series_dict = @test_logs (
+ :warn,
+ """
+ setting arbitrary contour levels with Plotly backend is not supported;
+ use a range to set equally-spaced contours or an integer to set the
+ approximate number of contours with the keyword `levels`.
+ Setting levels to -1.0:0.5:1.0
+ """,
+ ) 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)
+ end
+ end
+ end
+
+ @testset "Extra kwargs" begin
+ pl = plot(1:5, test = "me")
+ @test PlotsBase.plotly_series(pl)[1][:test] == "me"
+ pl = plot(1:5, test = "me", extra_kwargs = :plot)
+ @test PlotsBase.plotly_layout(pl)[:test] == "me"
+ end
+
+ @testset "Requirejs" begin
+ pl = plot(sin, 0, 2pi)
+ io = PipeBuffer()
+ show(io, MIME("text/html"), pl)
+ html = read(io, String)
+ # FIXME: how can we write a test checking that the html correctly draw a plotly plot ?
+ end
+end
diff --git a/PlotsBase/test/test_preferences.jl b/PlotsBase/test/test_preferences.jl
new file mode 100644
index 0000000000..12acf1dc69
--- /dev/null
+++ b/PlotsBase/test/test_preferences.jl
@@ -0,0 +1,97 @@
+# 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 in TEST_PACKAGES
+ @testset "persistent backend $pkg" begin
+ be = TEST_BACKENDS[pkg]
+ if is_ci()
+ (Sys.isapple() && be ≡ :gaston) && continue # FIXME: hangs
+ (Sys.iswindows() && be ≡ :plotlyjs) && continue # FIXME: OutOfMemory
+ end
+ @test_logs PlotsBase.set_default_backend!(be) # test the absence of warnings
+ rm.(Base.find_all_in_cache_path(Base.PkgId(PlotsBase, string(nameof(PlotsBase))))) # make sure the compiled cache is removed
+ script = tempname()
+ write(
+ script,
+ """
+ import $pkg
+ using Test, PlotsBase
+
+ $be()
+ res = @testset "[subtest] persistent backend $pkg" begin
+ @test PlotsBase.backend_name() ≡ :$be
+ end
+ exit(res.n_passed == 1 ? 0 : 123)
+ """,
+ )
+ DEBUG && print(read(script, String))
+ @test run(```$(Base.julia_cmd()) $script```) |> success # test default precompilation
+ end
+end
+
+PlotsBase.set_default_backend!() # clear `Preferences` key
+
+# -----------------------------------------------------------------------------
+if PREVIOUS_DEFAULT_BACKEND ≡ nothing
+ delete_preferences!(PlotsBase, "default_backend") # restore the absence of a preference
+else
+ set_default_backend!(PREVIOUS_DEFAULT_BACKEND) # reset to previous state
+end
diff --git a/PlotsBase/test/test_quality.jl b/PlotsBase/test/test_quality.jl
new file mode 100644
index 0000000000..e0cff77c7d
--- /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 = [:Colors, :Contour, :LaTeXStrings, :Latexify, :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 a7fe5007ff..dade6ff4e4 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)
+ defaultret = PlotsBase.axis_drawing_info_3d(sp, :x)
for letter in (:x, :y, :z)
for framestyle in [:box :semi :origin :zerolines :grid :none]
prevha = UInt64(0)
push!(sp.attr, :framestyle => framestyle)
- ret = Plots.axis_drawing_info_3d(sp, letter)
+ ret = PlotsBase.axis_drawing_info_3d(sp, letter)
ha = hash(string(ret))
@test ha != prevha
prevha = ha
@@ -93,29 +99,32 @@ 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
- @test p[1][1][:linestyle] == :solid
- @test p[1][2][:linestyle] == :dash
- @test p[1][3][:linestyle] == :dot
+ @test p[1][1][:linestyle] ≡ :solid
+ @test p[1][2][:linestyle] ≡ :dash
+ @test p[1][3][:linestyle] ≡ :dot
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 0000000000..83a60e34f0
--- /dev/null
+++ b/PlotsBase/test/test_reference.jl
@@ -0,0 +1,159 @@
+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
+ joinpath(ref_dir, args...)
+else
+ joinpath(first(Base.DEPOT_PATH), "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 in 1:6
+ try
+ repo = LibGit2.clone(
+ "https://github.com/JuliaPlots/PlotReferenceImages.jl.git",
+ dn,
+ )
+ break
+ catch err
+ @warn err
+ sleep(20i)
+ end
+ end
+ if (ver = PlotsBase._version).prerelease |> isempty
+ try
+ tag = LibGit2.GitObject(repo, "v$ver")
+ hash = string(LibGit2.target(tag))
+ LibGit2.checkout!(repo, hash)
+ catch err
+ @warn err
+ end
+ end
+ LibGit2.peel(LibGit2.head(repo)) |> println # print some information
+ return 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 = PlotsBase.ref_name(i) * ".png"
+ reffn = joinpath(refdir, string(version), fn)
+ for ver in sort(VersionNumber.(readdir(refdir)), rev = true)
+ if (tmpfn = joinpath(refdir, string(ver), fn)) |> isfile
+ reffn = tmpfn
+ break
+ end
+ end
+ return reffn
+end
+
+function image_comparison_tests(
+ pkg::Symbol,
+ idx::Int;
+ debug = false,
+ popup = !is_ci(),
+ sigma = [1, 1],
+ tol = 1.0e-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), PlotsBase.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))))
+ return 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 = 1.0e-2, # acceptable error (percent)
+ )
+ for i in setdiff(1:length(PlotsBase._examples), skip)
+ if only ≡ nothing || i in only
+ test = image_comparison_tests(pkg, i; debug, sigma, tol)
+ if i ∈ broken
+ @test_broken success(test)
+ elseif is_auto()
+ nothing
+ else
+ @test success(test)
+ end
+ end
+ end
+ return
+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], skipped_examples),
+ broken = broken_examples,
+ )
+ end
+end
diff --git a/test/test_shorthands.jl b/PlotsBase/test/test_shorthands.jl
similarity index 93%
rename from test/test_shorthands.jl
rename to PlotsBase/test/test_shorthands.jl
index 8b38465838..4598df0344 100644
--- a/test/test_shorthands.jl
+++ b/PlotsBase/test/test_shorthands.jl
@@ -33,11 +33,11 @@ end
sp = pl[1]
@test sp[:title] == "Foo"
xlabel!(pl, "xlabel")
- @test sp[:xaxis][:guide] == "xlabel"
+ @test PlotsBase.get_guide(sp[:xaxis]) == "xlabel"
ylabel!(pl, "ylabel")
- @test sp[:yaxis][:guide] == "ylabel"
+ @test PlotsBase.get_guide(sp[:yaxis]) == "ylabel"
zlabel!(pl, "zlabel")
- @test sp[:zaxis][:guide] == "zlabel"
+ @test PlotsBase.get_guide(sp[:zaxis]) == "zlabel"
end
@testset "Misc" begin
@@ -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 61%
rename from test/test_unitful.jl
rename to PlotsBase/test/test_unitful.jl
index b0d79fdeea..d3350b9122 100644
--- a/test/test_unitful.jl
+++ b/PlotsBase/test/test_unitful.jl
@@ -1,10 +1,12 @@
-using Plots, Test
+using PlotsBase, Test
using Unitful
+using Latexify
using Unitful: m, cm, s, DimensionError
# Some helper functions to access the subplot labels and the series inside each test plot
-xguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:xaxis].plotattributes[:guide]
-yguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:yaxis].plotattributes[:guide]
-zguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:zaxis].plotattributes[:guide]
+xguide(pl, idx = length(pl.subplots)) = PlotsBase.get_guide(pl.subplots[idx].attr[:xaxis])
+yguide(pl, idx = length(pl.subplots)) = PlotsBase.get_guide(pl.subplots[idx].attr[:yaxis])
+zguide(pl, idx = length(pl.subplots)) = PlotsBase.get_guide(pl.subplots[idx].attr[:zaxis])
+ctitle(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:colorbar_title]
xseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:x]
yseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:y]
zseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:z]
@@ -12,15 +14,15 @@ 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)
+ return :(@test $(esc(ex)) isa PlotsBase.Plot)
end
@testset "heatmap" begin
x = (1:3)m
@isplot heatmap(x * x', clims = (1, 7)) # unitless
@isplot heatmap(x * x', clims = (2m^2, 8m^2)) # units
- @isplot heatmap(x * x', clims = (2e6u"mm^2", 7e-6u"km^2")) # conversion
- @isplot heatmap(1:3, (1:3)m, x * x', clims = (1m^2, 7e-6u"km^2")) # mixed
+ @isplot heatmap(x * x', clims = (2.0e6u"mm^2", 7.0e-6u"km^2")) # conversion
+ @isplot heatmap(1:3, (1:3)m, x * x', clims = (1m^2, 7.0e-6u"km^2")) # mixed
end
@testset "plot(y)" begin
@@ -33,13 +35,24 @@ end
@testset "ylabel" begin
@test yguide(plot(y, ylabel = "hello")) == "hello (m)"
- @test yguide(plot(y, ylabel = P"hello")) == "hello"
+ @test yguide(plot(y, ylabel = "hello", unitformat = :nounit)) == "hello"
pl = plot(y, ylabel = "")
- @test yguide(pl) == ""
- @test yguide(plot!(pl, -y)) == ""
+ @test isempty(yguide(pl))
+ @test isempty(yguide(plot!(pl, -y)))
+ @test yguide(plot(y, ylabel = "", unitformat = :round)) == "m"
pl = plot(y; ylabel = "hello")
plot!(pl, -y)
@test yguide(pl) == "hello (m)"
+ plot!(pl, -y; ylabel = "goodbye")
+ @test yguide(pl) == "goodbye (m)"
+ pl = plot(y)
+ plot!(pl, -y; ylabel = "hello")
+ @test yguide(pl) == "hello (m)"
+ pl = plot(y)
+ @test_logs (:warn, r"Overriding unit") plot!(pl; yunit = cm)
+ @test yguide(pl) == "cm"
+ plot!(pl; ylabel = "hello")
+ @test yguide(pl) == "hello (cm)"
end
@testset "yunit" begin
@@ -102,19 +115,22 @@ end
@testset "labels" begin
@test xguide(plot(x, y, xlabel = "hello")) == "hello (m)"
- @test xguide(plot(x, y, xlabel = P"hello")) == "hello"
@test yguide(plot(x, y, ylabel = "hello")) == "hello (s)"
- @test yguide(plot(x, y, ylabel = P"hello")) == "hello"
@test xguide(plot(x, y, xlabel = "hello", ylabel = "hello")) == "hello (m)"
- @test xguide(plot(x, y, xlabel = P"hello", ylabel = P"hello")) == "hello"
@test yguide(plot(x, y, xlabel = "hello", ylabel = "hello")) == "hello (s)"
- @test yguide(plot(x, y, xlabel = P"hello", ylabel = P"hello")) == "hello"
+ @test xguide(plot(x, y, xlabel = "hello", unitformat = :nounit)) == "hello"
+ @test yguide(plot(x, y, ylabel = "hello", unitformat = :nounit)) == "hello"
+ @test xguide(
+ plot(x, y, xlabel = "hello", ylabel = "hello", unitformat = :nounit),
+ ) == "hello"
+ @test yguide(
+ plot(x, y, xlabel = "hello", ylabel = "hello", unitformat = :nounit),
+ ) == "hello"
end
@testset "unitformat" begin
args = (x, y)
kwargs = (:xlabel => "hello", :ylabel => "hello")
- @test yguide(plot(args...; kwargs..., unitformat = nothing)) == "hello s"
@test yguide(
plot(
args...;
@@ -124,14 +140,17 @@ end
) == "s is the unit of hello"
@test yguide(plot(args...; kwargs..., unitformat = ", dear ")) == "hello, dear s"
@test yguide(plot(args...; kwargs..., unitformat = (", dear ", " esq."))) ==
- "hello, dear s esq."
+ "hello, dear s esq."
@test yguide(
plot(args...; kwargs..., unitformat = ("well ", ", dear ", " esq.")),
) == "well hello, dear s esq."
@test yguide(plot(args...; kwargs..., unitformat = '?')) == "hello ? s"
@test yguide(plot(args...; kwargs..., unitformat = ('<', '>'))) == "hello "
@test yguide(plot(args...; kwargs..., unitformat = ('A', 'B', 'C'))) == "Ahello BsC"
- @test yguide(plot(args...; kwargs..., unitformat = false)) == "hello s"
+ @test yguide(plot(args...; kwargs..., unitformat = false)) == "hello"
+ @test yguide(plot(args...; kwargs..., unitformat = :none)) == "hello"
+ @test yguide(plot(args...; kwargs..., unitformat = nothing)) == "hello"
+ @test yguide(plot(args...; kwargs..., unitformat = :nounit)) == "hello"
@test yguide(plot(args...; kwargs..., unitformat = true)) == "hello (s)"
@test yguide(plot(args...; kwargs..., unitformat = :round)) == "hello (s)"
@test yguide(plot(args...; kwargs..., unitformat = :square)) == "hello [s]"
@@ -142,8 +161,9 @@ end
@test yguide(plot(args...; kwargs..., unitformat = :slashsquare)) == "hello / [s]"
@test yguide(plot(args...; kwargs..., unitformat = :slashcurly)) == "hello / {s}"
@test yguide(plot(args...; kwargs..., unitformat = :slashangle)) == "hello / "
+ @test yguide(plot(args...; kwargs..., unitformat = :space)) == "hello s"
@test yguide(plot(args...; kwargs..., unitformat = :verbose)) ==
- "hello in units of s"
+ "hello in units of s"
end
end
@@ -151,126 +171,171 @@ 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
- [:Vectors, :Matrices, Symbol("Vectors of vectors")]
- if dtype == :Vectors
+ [:Vectors, :Matrices, Symbol("Vectors of vectors")]
+ if dtype ≡ :Vectors
x, y, z = randn(10), randn(10), randn(10)
- elseif dtype == :Matrices
+ elseif dtype ≡ :Matrices
x, y, z = randn(10, 2), randn(10, 2), randn(10, 2)
else
x, y, z = [rand(10), rand(20)], [rand(10), rand(20)], [rand(10), rand(20)]
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 in [x, x * m], ys in [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)]
+ ys in [y, y * s],
+ zs in [z, z * (m / s)]
- @test plot(xs, ys, zs) isa Plots.Plot
+ @test plot(xs, ys, zs) isa PlotsBase.Plot
end
end
end
@testset "scatter(x::$(us[1]), y::$(us[2]))" for us in collect(
- Iterators.product(fill([1, u"m", u"s"], 2)...),
- )
+ 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, marker_z = x) isa PlotsBase.Plot
+ if us[1] != us[2] && us[1] != 1 && us[2] != 1 # Non-matching dimensions
+ @test_throws DimensionError scatter!(x, y, marker_z = y)
+ else # One is dimensionless, or have same dimensions
+ @test scatter!(x, y, marker_z = y) isa PlotsBase.Plot #
+ end
end
@testset "contour(x::$(us[1]), y::$(us[2]))" for us in collect(
- Iterators.product(fill([1, u"m", u"s"], 2)...),
- )
+ 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 "latexify as unitformat" begin
+ y = rand(10) * u"m^-1"
+ Latexify.set_default(labelformat = :slash)
+ @test yguide(plot(y, ylabel = "hello", unitformat = latexify)) == "\$hello\\;\\left/\\;\\mathrm{m}^{-1}\\right.\$"
+
+ uf = (l, u) -> l * " (" * latexify(u) * ")"
+ @test yguide(plot(y, ylabel = "hello", unitformat = uf)) == "hello (\$\\mathrm{m}^{-1}\$)"
+ Latexify.set_default(labelformat = :square)
+ @test yguide(plot(y, ylabel = "hello", unitformat = latexify)) == "\$hello\\;\\left[\\mathrm{m}^{-1}\\right]\$"
+ end
+
+ @testset "colorbar title" begin
+ x, y = (1:0.01:2) * m, (1:0.02:2) * s
+ z = x' ./ y
+ pl = contour(x, y, z)
+ @test ctitle(pl) ∈ ["m s^-1", "m s⁻¹"]
+ pl = contourf(x, y, z, zunit = u"km/hr")
+ @test ctitle(pl) ∈ ["km hr^-1", "km hr⁻¹"]
+ pl = heatmap(x, y, z, zunit = u"cm/s", zunitformat = :square, colorbar_title = "v")
+ @test ctitle(pl) ∈ ["v [cm s^-1]", "v [cm s⁻¹]"]
+ pl = heatmap(x, y, z, zunit = u"cm/s", zunitformat = :nounit, colorbar_title = nothing)
+ @test ctitle(pl) == ""
end
- @testset "ProtectedString" begin
+ @testset "twinx (#4750)" begin
y = rand(10) * u"m"
- @test plot(y, label = P"meters") isa Plots.Plot
+ pl = plot(y; xlabel = "check", ylabel = "hello")
+ pl2 = twinx(pl)
+ plot!(pl2, 1 ./ y; ylabel = "goodbye", yunit = u"cm^-1")
+ @test pl isa PlotsBase.Plot
+ @test pl2 isa PlotsBase.Subplot
+ @test yguide(pl, 1) == "hello (m)"
+ # on MacOS the superscript gets rendered with Unicode, on Ubuntu and Windows no
+ @test yguide(pl, 2) ∈ ["goodbye (cm^-1)", "goodbye (cm⁻¹)"]
+ @test xguide(pl, 1) == "check"
+ @test isempty(xguide(pl, 2))
+ end
+
+ @testset "bad link" begin
+ pl1 = plot(rand(10) * u"m")
+ pl2 = plot(rand(10) * u"s")
+ # TODO: On Julia 1.8 and above, can replace ErrorException with part of error message.
+ @test_throws ErrorException plot(pl1, pl2; link = :y)
end
end
@@ -316,27 +381,35 @@ 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"
+ pl = plot(x, y, xerr = ex, yerr = (ey, ey ./ 2))
+ @test pl isa PlotsBase.Plot
@test xguide(pl) == "mm"
@test yguide(pl) == "s"
end
@testset "Ribbon" begin
- x = rand(10) * u"mm"
+ x = (1:10) * u"mm"
y = rand(10) * u"s"
- ribbon = rand(10) * u"ms"
+ ribbon = 100 * 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"
+ pl = plot(x, y, ribbon = (ribbon, ribbon .* 2))
+ @test pl isa PlotsBase.Plot
@test xguide(pl) == "mm"
@test yguide(pl) == "s"
end
@testset "Fillrange" begin
- x = rand(10) * u"mm"
+ x = (1:10) * u"mm"
y = rand(10) * u"s"
- fillrange = rand(10) * u"ms"
+ fillrange = 100 * 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
@@ -374,8 +447,8 @@ end
@test length(pl.subplots[1].attr[:annotations]) == 1
end
-@testset "AbstractProtectedString" begin
- str = P"mass"
+@testset "UnitfulString" begin
+ str = Base.get_extension(PlotsBase, :UnitfulExt).UnitfulString("mass", u"kg")
@test pointer(str) isa Ptr
@test pointer(str, 1) isa Ptr
@test isvalid(str, 1)
@@ -389,18 +462,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 51%
rename from test/test_utils.jl
rename to PlotsBase/test/test_utils.jl
index adf07c2a3a..2366279c26 100644
--- a/test/test_utils.jl
+++ b/PlotsBase/test/test_utils.jl
@@ -12,79 +12,77 @@
[(missing, missing, missing), ("a", "b", "c")],
)
for z in zipped
- @test isequal(collect(zip(Plots.unzip(z)...)), z)
- @test isequal(collect(zip(Plots.unzip(GeometryBasics.Point.(z))...)), z)
+ @test isequal(collect(zip(PlotsBase.unzip(z)...)), z)
+ @test isequal(collect(zip(PlotsBase.unzip(GeometryBasics.Point.(z))...)), z)
end
- op1 = Plots.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])
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 6c1a6ef3ef..a116c0c70e 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,133 +1,20 @@
name = "Plots"
uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
+license = "MIT"
+desc = "Metapackage for PlotsBase + GR"
author = ["Tom Breloff (@tbreloff)"]
-version = "1.40.4"
+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"
-
-[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"
+[sources]
+PlotsBase = {path = "PlotsBase"}
-[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"
+[compat]
+GR = "0.73"
+PlotsBase = "0.1"
+Reexport = "1"
+julia = "1.10"
diff --git a/README.md b/README.md
index fec56f006d..59fa5e88f7 100644
--- a/README.md
+++ b/README.md
@@ -3,43 +3,43 @@
# Plots
-
-[gh-ci-img]: https://github.com/JuliaPlots/Plots.jl/workflows/ci/badge.svg?branch=master
-[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
-[pkgeval-url]: https://juliaci.github.io/NanosoldierReports/pkgeval_badges/report.html
-
-[gitter-img]: https://badges.gitter.im/tbreloff/Plots.jl.svg
-[gitter-url]: https://gitter.im/tbreloff/Plots.jl?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
-
-[docs-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg
-[docs-stable-url]: https://docs.juliaplots.org/stable
-
-[docs-dev-img]: https://img.shields.io/badge/docs-dev-blue.svg
-[docs-dev-url]: https://docs.juliaplots.org/dev
-
-[![][gh-ci-img]][gh-ci-url]
-[![][pkgeval-img]][pkgeval-url]
-[](https://julialang.zulipchat.com/#narrow/stream/236493-plots)
-[![][docs-stable-img]][docs-stable-url]
-[![][docs-dev-img]][docs-dev-url]
-[](https://codecov.io/gh/JuliaPlots/Plots.jl)
-[](https://juliapkgstats.com/pkg/Plots)
-
+[](
+ https://docs.juliaplots.org/stable/
+)
+[](
+ https://docs.juliaplots.org/dev
+)
+
+[](
+ https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci
+)
+[](
+ https://juliaci.github.io/NanosoldierReports/pkgeval_badges/report.html
+)
+[](
+ https://julialang.zulipchat.com/#narrow/stream/236493-plots
+)
+
+[](
+ https://codecov.io/gh/JuliaPlots/Plots.jl/tree/v2
+)
+[](
+ https://juliapkgstats.com/pkg/Plots
+)
+
+This is the DOI for all Versions, please follow the link to get the DOI for a specific version:
[](https://doi.org/10.5281/zenodo.4725317)
-This is the DOI for all Versions, please follow the link to get the DOI for a specific version.
#### Created by Tom Breloff (@tbreloff)
#### Maintained by the [JuliaPlots members](https://github.com/orgs/JuliaPlots/people)
-Plots is a plotting API and toolset. My goals with the package are:
+Plots is a plotting API and toolset.
-- **Powerful**. Do more with less. Complex visualizations become easy.
-- **Intuitive**. Stop reading so much documentation. Commands should "just work".
-- **Concise**. Less code means fewer mistakes and more efficient development/analysis.
-- **Flexible**. Produce your favorite plots from your favorite package, but quicker and simpler.
-- **Consistent**. Don't commit to one graphics package, use the same code everywhere.
-- **Lightweight**. Very few dependencies.
-- **Smart**. Attempts to figure out what you **want** it to do... not just what you **tell** it.
+The goals of the package are:
+- **Powerful**: do more with less, omplex visualizations become easy.
+- **Intuitive**: stop reading so much documentation, commands should "just work".
+- **Concise**: less code means fewer mistakes and more efficient development/analysis.
+- **Flexible**: produce your favorite plots from your favorite package, but quicker and simpler.
+- **Consistent**: don't commit to one graphics package, use the same code everywhere.
+- **Smart**: attempts to figure out what you **want** it to do ... not just what you **tell** it.
diff --git a/RecipesBase/Project.toml b/RecipesBase/Project.toml
index 2846477658..c3b0937e3b 100644
--- a/RecipesBase/Project.toml
+++ b/RecipesBase/Project.toml
@@ -8,11 +8,4 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
[compat]
PrecompileTools = "1"
-julia = "1.6"
-
-[extras]
-StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
-Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
-
-[targets]
-test = ["StableRNGs", "Test"]
+julia = "1.10"
diff --git a/RecipesBase/README.md b/RecipesBase/README.md
index c4674cb665..0730c647b6 100644
--- a/RecipesBase/README.md
+++ b/RecipesBase/README.md
@@ -1,10 +1,20 @@
# RecipesBase
-[](https://docs.juliaplots.org/stable/RecipesBase)
-[](https://docs.juliaplots.org/dev/RecipesBase)
-[](https://github.com/JuliaPlots/Plots.jl/actions/workflows/ci.yml)
-[](https://julialang.zulipchat.com/#narrow/stream/236493-plots)
-[](https://juliahub.com/ui/Packages/RecipesBase/8e2Mm?t=2)
+[](
+ https://docs.juliaplots.org/stable/RecipesBase/
+)
+[](
+ https://docs.juliaplots.org/dev/RecipesBase/
+)
+[](
+ https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci
+)
+[](
+ https://julialang.zulipchat.com/#narrow/stream/236493-plots
+)
+[](
+ https://juliahub.com/ui/Packages/RecipesBase/8e2Mm?t=2
+)
### Author: Thomas Breloff (@tbreloff)
@@ -91,7 +101,7 @@ gr()
# processing pipeline (see the Pipeline section of the Plots documentation).
# It will plot 5 line plots (a 5-column matrix is returned from the recipe).
# All will have black circles:
-# - user override for markershape: :c == :circle
+# - user override for markershape: :c ≡ :circle
# - customcolor overridden to :black, and markercolor is forced to be customcolor
# If markershape is an unsupported keyword, the call will error.
# By default, a warning will be shown for an unsupported keyword.
diff --git a/RecipesBase/src/RecipesBase.jl b/RecipesBase/src/RecipesBase.jl
index 5f3410affa..08988c8524 100644
--- a/RecipesBase/src/RecipesBase.jl
+++ b/RecipesBase/src/RecipesBase.jl
@@ -14,11 +14,11 @@ export @recipe,
# Common abstract types for the Plots ecosystem
abstract type AbstractBackend end
-abstract type AbstractPlot{T<:AbstractBackend} end
+abstract type AbstractPlot{T <: AbstractBackend} end
abstract type AbstractLayout end
-const KW = Dict{Symbol,Any}
-const AKW = AbstractDict{Symbol,Any}
+const KW = Dict{Symbol, Any}
+const AKW = AbstractDict{Symbol, Any}
# a placeholder to establish the name so that other packages (Plots.jl for example)
# can add their own definition of RecipesBase.plot since RecipesBase is the common
@@ -45,18 +45,18 @@ group_as_matrix(t) = false
# This holds the recipe definitions to be dispatched on
# the function takes in an attribute dict `d` and a list of args.
# This default definition specifies the "no-arg" case.
-apply_recipe(plotattributes::AbstractDict{Symbol,Any}) = ()
+apply_recipe(plotattributes::AbstractDict{Symbol, Any}) = ()
# Is a key explicitly provided by the user?
# Should be overridden for subtypes representing plot attributes.
-is_explicit(d::AbstractDict{Symbol,Any}, k) = haskey(d, k)
+is_explicit(d::AbstractDict{Symbol, Any}, k) = haskey(d, k)
function is_default end
# --------------------------------------------------------------------------
# this holds the data and attributes of one series, and is returned from apply_recipe
struct RecipeData
- plotattributes::AbstractDict{Symbol,Any}
+ plotattributes::AbstractDict{Symbol, Any}
args::Tuple
end
@@ -73,22 +73,22 @@ _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
function get_function_def(func_signature::Expr, args::Vector)
front = func_signature.args[1]
- if func_signature.head ≡ :where
+ return if func_signature.head ≡ :where
Expr(:where, get_function_def(front, args), esc.(func_signature.args[2:end])...)
elseif func_signature.head ≡ :call
func = Expr(
:call,
:($RecipesBase.apply_recipe),
- esc.([:(plotattributes::AbstractDict{Symbol,Any}); args])...,
+ esc.([:(plotattributes::AbstractDict{Symbol, Any}); args])...,
)
if isa(front, Expr) && front.head ≡ :curly
Expr(:where, func, esc.(front.args[2:end])...)
@@ -112,7 +112,7 @@ function create_kw_body(func_signature::Expr)
if isa(arg1, Expr) && arg1.head ≡ :parameters
for kwpair in arg1.args
k, v = kwpair.args
- if isa(k, Expr) && k.head === :(::)
+ if isa(k, Expr) && k.head ≡ :(::)
k = k.args[1]
@warn """
Type annotations on keyword arguments not currently supported in recipes.
@@ -124,13 +124,13 @@ function create_kw_body(func_signature::Expr)
cleanup_body.args,
:(
$RecipesBase.is_key_supported($(QuoteNode(k))) ||
- delete!(plotattributes, $(QuoteNode(k)))
+ delete!(plotattributes, $(QuoteNode(k)))
),
)
end
args = args[2:end]
end
- args, kw_body, cleanup_body
+ return args, kw_body, cleanup_body
end
# process the body of the recipe recursively.
@@ -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)
@@ -191,11 +191,11 @@ function process_recipe_body!(expr::Expr)
# error when not supported by the backend
:(
$RecipesBase.is_key_supported($k) ? $set_expr :
- error(
- "In recipe: required keyword ",
- $k,
- " is not supported by backend $(backend_name())",
- )
+ error(
+ "In recipe: required keyword ",
+ $k,
+ " is not supported by backend $(backend_name())",
+ )
)
else
set_expr
@@ -212,6 +212,7 @@ function process_recipe_body!(expr::Expr)
end
end
end
+ return
end
# --------------------------------------------------------------------------
@@ -298,7 +299,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,
@@ -335,7 +336,7 @@ end
```
"""
macro series(expr::Expr)
- quote
+ return quote
let plotattributes = copy(plotattributes)
args = $expr
push!(
@@ -362,7 +363,7 @@ grouphist(rand(1_000, 4))
```
"""
macro userplot(expr)
- _userplot(expr)
+ return _userplot(expr)
end
function _userplot(expr::Expr)
@@ -374,7 +375,7 @@ function _userplot(expr::Expr)
funcname2 = Symbol(funcname, "!")
# return a code block with the type definition and convenience plotting methods
- quote
+ return quote
$expr
export $funcname, $funcname2
Core.@__doc__ $funcname(args...; kw...) =
@@ -386,15 +387,19 @@ function _userplot(expr::Expr)
end |> esc
end
-_userplot(sym::Symbol) = _userplot(:(mutable struct $sym
- args
-end))
+_userplot(sym::Symbol) = _userplot(
+ :(
+ mutable struct $sym
+ args
+ end
+ )
+)
gettypename(sym::Symbol) = sym
function gettypename(expr::Expr)
expr.head ≡ :curly || @error "Unexpected struct name: $expr"
- expr.args[1]
+ return expr.args[1]
end
#----------------------------------------------------------------------------
@@ -422,7 +427,7 @@ Plot my series type!
"""
macro shorthands(funcname::Symbol)
funcname2 = Symbol(funcname, "!")
- quote
+ return quote
export $funcname, $funcname2
Core.@__doc__ $funcname(args...; kw...) =
$RecipesBase.plot(args...; kw..., seriestype = $(Meta.quot(funcname)))
@@ -481,26 +486,26 @@ iscol(v) = isa(v, Expr) && v.head ≡ :vcat
rowsize(v) = isrow(v) ? length(v.args) : 1
create_grid(expr::Expr) =
- if iscol(expr)
- create_grid_vcat(expr)
- elseif isrow(expr)
- sub(x) = :(cell[1, $(first(x))] = $(create_grid(last(x))))
- quote
- let cell = Matrix(undef, 1, $(length(expr.args)))
- $(map(sub, enumerate(expr.args))...)
- cell
- end
+if iscol(expr)
+ create_grid_vcat(expr)
+elseif isrow(expr)
+ sub(x) = :(cell[1, $(first(x))] = $(create_grid(last(x))))
+ quote
+ let cell = Matrix(undef, 1, $(length(expr.args)))
+ $(map(sub, enumerate(expr.args))...)
+ cell
end
- elseif expr.head ≡ :curly
- create_grid_curly(expr)
- else
- esc(expr) # if it's something else, just return that (might be an existing layout?)
end
+elseif expr.head ≡ :curly
+ create_grid_curly(expr)
+else
+ esc(expr) # if it's something else, just return that (might be an existing layout?)
+end
function create_grid_vcat(expr::Expr)
rowsizes = map(rowsize, expr.args)
rmin, rmax = extrema(rowsizes)
- if rmin > 0 && rmin == rmax
+ return if rmin > 0 && rmin == rmax
# we have a grid... build the whole thing
# note: rmin is the number of columns
nr = length(expr.args)
@@ -539,7 +544,7 @@ function create_grid_curly(expr::Expr)
add_layout_pct!(kw, arg, i, length(expr.args) - 1)
end
s = expr.args[1]
- if isa(s, Expr) && s.head ≡ :call && s.args[1] ≡ :grid
+ return if isa(s, Expr) && s.head ≡ :call && s.args[1] ≡ :grid
create_grid(
quote
grid(
@@ -593,7 +598,7 @@ julia> @layout [_ ° _; ° ° °; ° ° °]
```
"""
macro layout(mat::Expr)
- create_grid(mat)
+ return create_grid(mat)
end
# COV_EXCL_START
diff --git a/RecipesBase/test/Project.toml b/RecipesBase/test/Project.toml
new file mode 100644
index 0000000000..89533408c6
--- /dev/null
+++ b/RecipesBase/test/Project.toml
@@ -0,0 +1,4 @@
+[deps]
+RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/RecipesBase/test/runtests.jl b/RecipesBase/test/runtests.jl
index 2dae4af674..13f882d21e 100644
--- a/RecipesBase/test/runtests.jl
+++ b/RecipesBase/test/runtests.jl
@@ -5,7 +5,7 @@ import RecipesBase as RB
using StableRNGs
using Test
-const KW = Dict{Symbol,Any}
+const KW = Dict{Symbol, Any}
RB.is_key_supported(k::Symbol) = true
@@ -61,7 +61,7 @@ end
@testset "simple parametric type" begin
@test_throws MethodError RB.apply_recipe(KW(), T1())
- RB.@recipe function plot(t::T1, n::N = 1; customcolor = :green) where {N<:Integer}
+ RB.@recipe function plot(t::T1, n::N = 1; customcolor = :green) where {N <: Integer}
:markershape --> :auto, :require
:markercolor --> customcolor, :force
:xrotation --> 5
@@ -84,7 +84,7 @@ end
@testset "parametric type with where" begin
@test_throws MethodError RB.apply_recipe(KW(), T2())
- RB.@recipe function plot(t::T2, n::N = 1; customcolor = :green) where {N<:Integer}
+ RB.@recipe function plot(t::T2, n::N = 1; customcolor = :green) where {N <: Integer}
:markershape --> :auto, :require
:markercolor --> customcolor, :force
:xrotation --> 5
@@ -108,11 +108,11 @@ end
@test_throws MethodError RB.apply_recipe(KW(), T3())
RB.@recipe function plot(
- t::T3,
- n::N = 1,
- m::M = 0.0;
- customcolor = :green,
- ) where {N<:Integer} where {M<:Float64}
+ t::T3,
+ n::N = 1,
+ m::M = 0.0;
+ customcolor = :green,
+ ) where {N <: Integer} where {M <: Float64}
:markershape --> :auto, :require
:markercolor --> customcolor, :force
:xrotation --> 5
diff --git a/RecipesPipeline/Project.toml b/RecipesPipeline/Project.toml
index 8c30621746..4a28c04eb8 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"
@@ -10,17 +10,12 @@ PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
+[sources]
+RecipesBase = {path = "../RecipesBase"}
+
[compat]
-NaNMath = "0.3, 1"
-PlotUtils = "0.6.5, 1"
+NaNMath = "1"
+PlotUtils = "1"
RecipesBase = "1.3.1"
PrecompileTools = "1"
-julia = "1.6"
-
-[extras]
-BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
-StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
-Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
-
-[targets]
-test = ["BenchmarkTools", "StableRNGs", "Test"]
+julia = "1.10"
diff --git a/RecipesPipeline/README.md b/RecipesPipeline/README.md
index 88d0abde57..77402280a0 100644
--- a/RecipesPipeline/README.md
+++ b/RecipesPipeline/README.md
@@ -1,13 +1,20 @@
# RecipesPipeline
-[](https://docs.juliaplots.org/stable/RecipesPipeline)
-[](https://docs.juliaplots.org/dev/RecipesPipeline)
-[](https://github.com/JuliaPlots/Plots.jl/actions/workflows/ci.yml)
-[](https://codecov.io/gh/JuliaPlots/RecipesPipeline.jl)
-[](https://julialang.zulipchat.com/#narrow/stream/236493-plots)
+[](
+ https://docs.juliaplots.org/stable/RecipesPipeline/
+)
+[](
+ https://docs.juliaplots.org/dev/RecipesPipeline/
+)
+[](
+ https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci
+)
+[](
+ https://julialang.zulipchat.com/#narrow/stream/236493-plots
+)
#### 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.jl package can be depended on by any package to define "recipes": plot specifications of user-defined types, as well as custom plot types. RecipePipeline.jl contains the machinery to translate these recipes to full specifications for a plot.
+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.jl package can be depended on by any package to define "recipes": plot specifications of user-defined types, as well as custom plot types. RecipesPipeline.jl contains the machinery to translate these recipes to full specifications for a plot.
The package is intended to be used by consumer plotting packages, and is currently used by [Plots.jl](https://github.com/JuliaPlots/Plots.jl) (v.1.1.0 and above) and [MakieRecipes.jl](https://github.com/JuliaPlots/Makie.jl/tree/master/MakieRecipes), a package that bridges RecipesBase recipes to [Makie.jl](https://github.com/JuliaPlots/Makie.jl).
diff --git a/RecipesPipeline/src/RecipesPipeline.jl b/RecipesPipeline/src/RecipesPipeline.jl
index d2cf525887..199426e7cc 100644
--- a/RecipesPipeline/src/RecipesPipeline.jl
+++ b/RecipesPipeline/src/RecipesPipeline.jl
@@ -102,14 +102,14 @@ function recipe_pipeline!(plt, plotattributes, args)
# Return processed plot object
# --------------------------------
- plt
+ return plt
end
# COV_EXCL_START
using PrecompileTools
@setup_workload begin
- plotattributes = Dict{Symbol,Any}[
+ plotattributes = Dict{Symbol, Any}[
Dict(:x => 1, :y => "", :z => nothing, :seriestype => :path),
Dict(:x => 1, :y => "", :z => nothing, :seriestype => :surface),
]
@@ -127,22 +127,22 @@ using PrecompileTools
mats = (Int[1 2; 3 4], Float64[1 2; 3 4])
surfs = Surface.(mats)
vols = Volume(ones(Int, 1, 2, 3)), Volume(ones(Float64, 1, 2, 3))
- for pl_attr in plotattributes
- _series_data_vector(1, pl_attr)
- _series_data_vector([1], pl_attr)
- _series_data_vector(["a"], pl_attr)
- _series_data_vector([1 2], pl_attr)
- _series_data_vector(["a" "b"], pl_attr)
- _series_data_vector.(surfs, Ref(pl_attr))
- _apply_type_recipe.(Ref(pl_attr), surfs, Ref(:x))
- _apply_type_recipe.(Ref(pl_attr), mats, Ref(:x))
+ for pl_attrs in plotattributes
+ _series_data_vector(1, pl_attrs)
+ _series_data_vector([1], pl_attrs)
+ _series_data_vector(["a"], pl_attrs)
+ _series_data_vector([1 2], pl_attrs)
+ _series_data_vector(["a" "b"], pl_attrs)
+ _series_data_vector.(surfs, Ref(pl_attrs))
+ _apply_type_recipe.(Ref(pl_attrs), surfs, Ref(:x))
+ _apply_type_recipe.(Ref(pl_attrs), mats, Ref(:x))
_map_funcs(identity, [1, 2])
_map_funcs([identity, identity], [1, 2])
unzip([(1.0, 1.0)])
unzip([(1, 1)])
unzip([(1, 1.0)])
unzip([([1.0], [2.0])])
- # _process_seriesrecipe(nothing, pl_attr)
+ # _process_seriesrecipe(nothing, pl_attrs)
# recipe_pipeline!(plt, [1, 2], ["foo", "bar"])
end
end
diff --git a/RecipesPipeline/src/api.jl b/RecipesPipeline/src/api.jl
index ff0e1897b6..823f829d3b 100644
--- a/RecipesPipeline/src/api.jl
+++ b/RecipesPipeline/src/api.jl
@@ -9,28 +9,29 @@ Warn if an alias is detected in `plotattributes` after a recipe of type `recipe_
applied to 'args'. `recipe_type` is either `:user`, `:type`, `:plot` or `:series`.
"""
function warn_on_recipe_aliases!(
- plt,
- plotattributes::AKW,
- recipe_type::Symbol,
- @nospecialize(args)
-) end
+ plt,
+ plotattributes::AKW,
+ recipe_type::Symbol,
+ @nospecialize(args)
+ ) end
function warn_on_recipe_aliases!(
- plt,
- v::AbstractVector,
- recipe_type::Symbol,
- @nospecialize(args)
-)
+ plt,
+ v::AbstractVector,
+ recipe_type::Symbol,
+ @nospecialize(args)
+ )
for x in v
warn_on_recipe_aliases!(plt, x, recipe_type, args)
end
+ return
end
function warn_on_recipe_aliases!(
- plt,
- rd::RecipeData,
- recipe_type::Symbol,
- @nospecialize(args)
-)
- warn_on_recipe_aliases!(plt, rd.plotattributes, recipe_type, args)
+ plt,
+ rd::RecipeData,
+ recipe_type::Symbol,
+ @nospecialize(args)
+ )
+ return warn_on_recipe_aliases!(plt, rd.plotattributes, recipe_type, args)
end
# ## Grouping
@@ -87,12 +88,12 @@ is_axis_attribute(plt, attr) = false
# ### processing of axis args
# axis args before type recipes should still be mapped to all axes
"""
- preprocess_axis_args!(plt, plotattributes)
+ preprocess_axis_attrs!(plt, plotattributes)
Preprocessing of axis attributes.
Prepends the axis letter to axis attributes by default.
"""
-function preprocess_axis_args!(plt, plotattributes)
+function preprocess_axis_attrs!(plt, plotattributes)
for (k, v) in plotattributes
is_axis_attribute(plt, k) || continue
pop!(plotattributes, k)
@@ -100,25 +101,26 @@ function preprocess_axis_args!(plt, plotattributes)
get!(plotattributes, Symbol(letter, k), v)
end
end
+ return
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)
+ return 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
@@ -126,6 +128,7 @@ function postprocess_axis_args!(plt, plotattributes, letter)
pop!(plotattributes, k)
get!(plotattributes, Symbol(letter, k), v)
end
+ return
end
# ## User recipes
@@ -189,7 +192,7 @@ function process_sliced_series_attributes!(plt, kw_list) end
Returns a `Dict` storing the defaults for series attributes.
"""
-series_defaults(plt) = Dict{Symbol,Any}()
+series_defaults(plt) = Dict{Symbol, Any}()
# TODO: Add a more sensible fallback including e.g. path, scatter, ...
diff --git a/RecipesPipeline/src/group.jl b/RecipesPipeline/src/group.jl
index cfb5c5bf5a..5be8c9c5dd 100644
--- a/RecipesPipeline/src/group.jl
+++ b/RecipesPipeline/src/group.jl
@@ -8,7 +8,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}}()
+ res = Dict{eltype(v), Vector{Int}}()
for (i, label) in enumerate(v)
if haskey(res, label)
push!(res[label], i)
@@ -19,7 +19,7 @@ function _extract_group_attributes(v::AVec, args...; legend_entry = string)
group_labels = (sort ∘ collect ∘ keys)(res)
group_indices = getindex.(Ref(res), group_labels)
- GroupBy(map(legend_entry, group_labels), group_indices)
+ return GroupBy(map(legend_entry, group_labels), group_indices)
end
legend_entry_from_tuple(ns::Tuple) = join(ns, ' ')
@@ -27,7 +27,7 @@ legend_entry_from_tuple(ns::Tuple) = join(ns, ' ')
function _extract_group_attributes(vs::Tuple, args...)
isempty(vs) && return GroupBy([""], [axes(args[1], 1)])
v = map(tuple, vs...)
- _extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
+ return _extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
end
# allow passing NamedTuples for a named legend entry
@@ -36,28 +36,29 @@ legend_entry_from_tuple(ns::NamedTuple) = join(["$k = $v" for (k, v) in pairs(ns
function _extract_group_attributes(vs::NamedTuple, args...)
isempty(vs) && return GroupBy([""], [axes(args[1], 1)])
v = map(NamedTuple{keys(vs)} ∘ tuple, values(vs)...)
- _extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
+ return _extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
end
# expecting a mapping of "group label" to "group indices"
-function _extract_group_attributes(idxmap::Dict{T,V}, args...) where {T,V<:AVec{Int}}
+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]
- GroupBy(group_labels, group_indices)
+ return GroupBy(group_labels, group_indices)
end
filter_data(v::AVec, idxfilter::AVec{Int}) = v[idxfilter]
filter_data(v, idxfilter) = v
function filter_data!(plotattributes::AKW, idxfilter)
- for s in (:x, :y, :z)
+ for s in (:x, :y, :z, :xerror, :yerror, :zerror)
plotattributes[s] = filter_data(get(plotattributes, s, nothing), idxfilter)
end
+ return
end
function _filter_input_data!(plotattributes::AKW)
idxfilter = pop!(plotattributes, :idxfilter, nothing)
- idxfilter ≡ nothing || filter_data!(plotattributes, idxfilter)
+ return idxfilter ≡ nothing || filter_data!(plotattributes, idxfilter)
end
function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1])
@@ -72,7 +73,7 @@ function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1])
yi = y[groupby.group_indices[i]]
y_mat[getindex.(Ref(x_ind), xi), i] = yi
end
- y_mat
+ return y_mat
end
groupedvec2mat(x_ind, x, y::Tuple, groupby) =
@@ -104,9 +105,9 @@ group_as_matrix(t) = false # used in `StatsPlots`
for indexes in groupby.group_indices
x[indexes] = eachindex(indexes)
end
- last_args = g.args
+ last_attrs = g.args
else
- x, last_args... = g.args
+ x, last_attrs... = g.args
end
x_u = unique(sort(x))
x_ind = Dict(zip(x_u, eachindex(x_u)))
@@ -116,9 +117,11 @@ group_as_matrix(t) = false # used in `StatsPlots`
end
end
label --> reshape(groupby.group_labels, 1, :)
- typeof(g)((
- x_u,
- (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_args)...,
- ))
+ typeof(g)(
+ (
+ x_u,
+ (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_attrs)...,
+ )
+ )
end
end
diff --git a/RecipesPipeline/src/plot_recipe.jl b/RecipesPipeline/src/plot_recipe.jl
index 8cddfbd70d..a033f556dd 100644
--- a/RecipesPipeline/src/plot_recipe.jl
+++ b/RecipesPipeline/src/plot_recipe.jl
@@ -17,7 +17,7 @@ function _process_plotrecipes!(plt, kw_list)
next_kw = popfirst!(still_to_process)
_process_plotrecipe(plt, next_kw, kw_list, still_to_process)
end
- kw_list
+ return kw_list
end
function _process_plotrecipe(plt, kw, kw_list, still_to_process)
@@ -43,7 +43,7 @@ function _process_plotrecipe(plt, kw, kw_list, still_to_process)
else
push!(kw_list, kw)
end
- nothing
+ return nothing
end
@specialize
diff --git a/RecipesPipeline/src/recipes.jl b/RecipesPipeline/src/recipes.jl
index ce33844a6a..f8808dcdaf 100644
--- a/RecipesPipeline/src/recipes.jl
+++ b/RecipesPipeline/src/recipes.jl
@@ -7,7 +7,7 @@ function epochdays2datetime(fractionaldays::Real)::DateTime
days = floor(fractionaldays)
dayfraction = fractionaldays - days
missing_ms = Millisecond(round(Millisecond(Day(1)).value * dayfraction))
- DateTime(Dates.epochdays2date(days)) + missing_ms
+ return DateTime(Dates.epochdays2date(days)) + missing_ms
end
epochdays2epochms(x) = Dates.datetime2epochms(epochdays2datetime(x))
@@ -22,7 +22,7 @@ timeformatter(t) = string(Dates.Time(Dates.Nanosecond(round(t))))
@recipe f(::Type{Date}, dt::Date) = (dt -> Dates.value(dt), dateformatter)
@recipe f(::Type{DateTime}, dt::DateTime) = (dt -> Dates.value(dt), datetimeformatter)
@recipe f(::Type{Dates.Time}, t::Dates.Time) = (t -> Dates.value(t), timeformatter)
-@recipe f(::Type{P}, t::P) where {P<:Dates.Period} =
+@recipe f(::Type{P}, t::P) where {P <: Dates.Period} =
(t -> Dates.value(t), t -> string(P(round(t))))
# -------------------------------------------------
diff --git a/RecipesPipeline/src/series.jl b/RecipesPipeline/src/series.jl
index 08d7d40ec5..6cad52ffcb 100644
--- a/RecipesPipeline/src/series.jl
+++ b/RecipesPipeline/src/series.jl
@@ -1,16 +1,17 @@
# # Series handling
-const FuncOrFuncs{F} = Union{F,Vector{F},Matrix{F}}
-const MaybeNumber = Union{Number,Missing}
-const MaybeString = Union{AbstractString,Missing}
-const DataPoint = Union{MaybeNumber,MaybeString}
+const FuncOrFuncs{F} = Union{F, Vector{F}, Matrix{F}}
+const MaybeNumber = Union{Number, Missing}
+const MaybeString = Union{AbstractString, Missing}
+const DataPoint = Union{MaybeNumber, MaybeString}
_prepare_series_data(x) = error("Cannot convert $(typeof(x)) to series data for plotting")
_prepare_series_data(::Nothing) = nothing
-_prepare_series_data(t::Tuple{T,T}) where {T<:Number} = t
+_prepare_series_data(t::Tuple{T, T}) where {T <: Number} = t
_prepare_series_data(f::Function) = f
_prepare_series_data(ar::AbstractRange{<:Number}) = ar
-function _prepare_series_data(a::AbstractArray{T}) where {T<:MaybeNumber}
+_prepare_series_data(nt::NamedTuple) = nt
+function _prepare_series_data(a::AbstractArray{T}) where {T <: MaybeNumber}
# Get a non-missing AbstractFloat type for the array
# There may be a better way to do this?
F = typeof(float(zero(nonmissingtype(T))))
@@ -20,7 +21,7 @@ function _prepare_series_data(a::AbstractArray{T}) where {T<:MaybeNumber}
broadcast!(float_a, a) do x
ismissing(x) || isinf(x) ? NaN : x
end
- float_a
+ return float_a
end
_prepare_series_data(a::Base.SkipMissing) = collect(a)
_prepare_series_data(a::AbstractArray{<:Missing}) = fill(NaN, axes(a))
@@ -43,7 +44,7 @@ _series_data_vector(v::AVec{<:DataPoint}, plotattributes) = [_prepare_series_dat
# list of things (maybe other vectors, functions, or something else)
function _series_data_vector(v::AVec, plotattributes)
- if all(x -> x isa MaybeNumber, v)
+ return if all(x -> x isa MaybeNumber, v)
_series_data_vector(Vector{MaybeNumber}(v), plotattributes)
elseif all(x -> x isa MaybeString, v)
_series_data_vector(Vector{MaybeString}(v), plotattributes)
@@ -54,7 +55,7 @@ end
# Matrix is split into columns
function _series_data_vector(v::AMat{<:DataPoint}, plotattributes)
- if is3d(plotattributes)
+ return if is3d(plotattributes)
[_prepare_series_data(Surface(v))]
else
[_prepare_series_data(v[:, i]) for i in axes(v, 2)]
@@ -67,6 +68,7 @@ _compute_x(x::Nothing, y::Nothing, z) = axes(z, 1)
_compute_x(x::Nothing, y, z) = axes(y, 1)
_compute_x(x::Function, y, z) = map(x, y)
_compute_x(x, y, z) = x
+_compute_x(x::Nothing, y::NamedTuple, z) = length(y)
_compute_y(x::Nothing, y::Nothing, z) = axes(z, 2)
_compute_y(x, y::Function, z) = map(y, x)
@@ -89,9 +91,9 @@ _nobigs(v) = v
n = size(x, 1)
!isnothing(y) &&
size(y, 1) != n &&
- error("Expects $n elements in each col of y, found $(size(y,1)).")
+ error("Expects $n elements in each col of y, found $(size(y, 1)).")
end
- _nobigs(x), _nobigs(y), _nobigs(z)
+ return _nobigs(x), _nobigs(y), _nobigs(z)
end
# --------------------------------------------------------------------
diff --git a/RecipesPipeline/src/series_recipe.jl b/RecipesPipeline/src/series_recipe.jl
index bb4275803b..272c94429d 100644
--- a/RecipesPipeline/src/series_recipe.jl
+++ b/RecipesPipeline/src/series_recipe.jl
@@ -15,7 +15,7 @@ function _process_seriesrecipes!(plt, kw_list)
end
process_sliced_series_attributes!(plt, kw_list)
for kw in kw_list
- series_attr = DefaultsDict(kw, series_defaults(plt))
+ series_attrs = DefaultsDict(kw, series_defaults(plt))
# now we have a fully specified series, with colors chosen. we must recursively
# handle series recipes, which dispatch on seriestype. If a backend does not
# natively support a seriestype, we check for a recipe that will convert that
@@ -24,8 +24,9 @@ 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
+ return
end
# this method recursively applies series recipes when the seriestype is not supported
@@ -36,7 +37,7 @@ function _process_seriesrecipe(plt, plotattributes)
st = plotattributes[:seriestype] = type_alias(plt, st)
# shapes shouldn't have fillrange set
- if plotattributes[:seriestype] == :shape
+ if plotattributes[:seriestype] ≡ :shape
plotattributes[:fillrange] = nothing
end
@@ -66,7 +67,7 @@ function _process_seriesrecipe(plt, plotattributes)
end
end
end
- nothing
+ return nothing
end
@specialize
diff --git a/RecipesPipeline/src/type_recipe.jl b/RecipesPipeline/src/type_recipe.jl
index c1de00afc9..2dd2e0d8be 100644
--- a/RecipesPipeline/src/type_recipe.jl
+++ b/RecipesPipeline/src/type_recipe.jl
@@ -6,7 +6,7 @@
@recipe f(::Type{T}, v::T) where {T} = v
# this should catch unhandled "series recipes" and error with a nice message
-@recipe f(::Type{V}, x, y, z) where {V<:Val} = error(
+@recipe f(::Type{V}, x, y, z) where {V <: Val} = error(
"The backend must not support the series type $V, and there isn't a series recipe defined.",
)
@@ -17,11 +17,11 @@ 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)
- rdvec[1].args[1]
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ return rdvec[1].args[1]
end
# Handle type recipes when the recipe is defined on the elements.
@@ -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,19 +50,65 @@ function _apply_type_recipe(plotattributes, v::AbstractArray, letter)
v
end
end
- postprocess_axis_args!(plt, plotattributes, letter)
- w
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ return w
+end
+
+# Specialisation to apply type recipes on a vector of vectors. The type recipe can either
+# apply to the vector of elements or the elements themselves
+function _apply_type_recipe(plotattributes, v::AVec{<:AVec}, letter)
+ plt = plotattributes[:plot_object]
+ preprocess_axis_attrs!(plt, plotattributes, letter)
+ # First we attempt on the vector of vector type recipe across everything.
+ w = RecipesBase.apply_recipe(plotattributes, typeof(v), v)[1].args[1]
+ warn_on_recipe_aliases!(plt, plotattributes, :type, v)
+ if typeof(v) != typeof(w)
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ return w
+ end
+ # Next we attempt the array type recipe and if any of the vector elements applies,
+ # we will stop there. Note we use the same type equivalency test as for a general array
+ # to check if changes applied
+ did_replace = false
+ w = map(v) do u
+ newu = RecipesBase.apply_recipe(plotattributes, typeof(u), u)[1].args[1]
+ warn_on_recipe_aliases!(plt, plotattributes, :type, u)
+ did_replace |= typeof(u) !== typeof(newu)
+ newu
+ end
+
+ # if nothing changed, then we attempt it at a piecewise level
+ if !did_replace
+ if (smv = skipmissing(Base.Iterators.flatten(v))) |> isempty
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ # We'll just leave it untampered with if there are no elements
+ return v
+ end
+ x = first(smv)
+ args = RecipesBase.apply_recipe(plotattributes, typeof(x), x)[1].args
+ warn_on_recipe_aliases!(plt, plotattributes, :type, x)
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ return if length(args) == 2 && all(arg -> arg isa Function, args)
+ numfunc, formatter = args
+ Formatted(map(u -> map(numfunc, u), v), formatter)
+ else
+ v
+ end
+ end
+
+ postprocess_axis_attrs!(plt, plotattributes, letter)
+ return w
end
# special handling for Surface... need to properly unwrap and re-wrap
_apply_type_recipe(
plotattributes,
- v::Surface{<:AMat{<:Union{AbstractFloat,Integer,AbstractString,Missing}}},
+ v::Surface{<:AMat{<:Union{AbstractFloat, Integer, AbstractString, Missing}}},
letter,
) = v
function _apply_type_recipe(plotattributes, v::Surface, letter)
ret = _apply_type_recipe(plotattributes, v.surf, letter)
- if typeof(ret) <: Formatted
+ return if typeof(ret) <: Formatted
Formatted(Surface(ret.data), ret.formatter)
else
Surface(ret)
@@ -72,7 +118,7 @@ end
# don't do anything for datapoints or nothing
_apply_type_recipe(
plotattributes,
- v::AbstractArray{<:Union{AbstractFloat,Integer,AbstractString,Missing}},
+ v::AbstractArray{<:Union{AbstractFloat, Integer, AbstractString, Missing}},
letter,
) = v
_apply_type_recipe(plotattributes, v::Nothing, letter) = v
diff --git a/RecipesPipeline/src/user_recipe.jl b/RecipesPipeline/src/user_recipe.jl
index 4c0fcbaaaa..cece2fc888 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)
@@ -44,7 +44,7 @@ function _process_userrecipes!(plt, plotattributes, args)
# don't allow something else to handle it
plotattributes[:smooth] = false
- kw_list
+ return kw_list
end
# TODO Move this to api.jl?
@@ -72,13 +72,13 @@ function _recipedata_vector(plt, plotattributes, args)
end
end
- still_to_process
+ return still_to_process
end
function _expand_seriestype_array(plotattributes, args)
@nospecialize
sts = get(plotattributes, :seriestype, :path)
- if typeof(sts) <: AbstractArray
+ return if typeof(sts) <: AbstractArray
reset_kw!(plotattributes, :seriestype)
rd = Vector{RecipeData}(undef, size(sts, 1))
for r in axes(sts, 1)
@@ -99,7 +99,7 @@ function _finish_userrecipe!(plt, kw_list, recipedata)
preprocess_attributes!(plt, kw)
# if there was a grouping, filter the data here
_filter_input_data!(kw)
- process_userrecipe!(plt, kw_list, kw)
+ return process_userrecipe!(plt, kw_list, kw)
end
# --------------------------------
@@ -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,9 +167,9 @@ 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
- v isa Surface || (plotattributes[:fill_z] = Surface(v))
- end
+if (v = get(plotattributes, :fill_z, nothing)) ≢ nothing
+ v isa Surface || (plotattributes[:fill_z] = Surface(v))
+end
# --------------------------------
# Special Cases
@@ -179,33 +179,33 @@ wrap_surfaces!(plotattributes) =
# 1 argument
@recipe f(n::Integer) =
- if is3d(plotattributes)
- SliceIt, n, n, n
- else
- SliceIt, n, n, nothing
- end
+if is3d(plotattributes)
+ SliceIt, n, n, n
+else
+ SliceIt, n, n, nothing
+end
# return a surface if this is a 3d plot, otherwise let it be sliced up
@recipe f(mat::AMat) =
- if is3d(plotattributes)
- n, m = axes(mat)
- m, n, Surface(mat)
- else
- nothing, mat, nothing
- end
+if is3d(plotattributes)
+ n, m = axes(mat)
+ m, n, Surface(mat)
+else
+ nothing, mat, nothing
+end
# if a matrix is wrapped by Formatted, do similar logic, but wrap data with Surface
@recipe f(fmt::Formatted{<:AMat}) =
- if is3d(plotattributes)
- mat = fmt.data
- n, m = axes(mat)
- m, n, Formatted(Surface(mat), fmt.formatter)
- else
- nothing, fmt, nothing
- end
+if is3d(plotattributes)
+ mat = fmt.data
+ n, m = axes(mat)
+ m, n, Formatted(Surface(mat), fmt.formatter)
+else
+ nothing, fmt, nothing
+end
# assume this is a Volume, so construct one
-@recipe function f(vol::AbstractArray{<:MaybeNumber,3}, args...) # COV_EXCL_LINE
+@recipe function f(vol::AbstractArray{<:MaybeNumber, 3}, args...) # COV_EXCL_LINE
seriestype := :volume
SliceIt, nothing, Volume(vol, args...), nothing
end
@@ -216,7 +216,7 @@ end
collect(keys(d)), collect(values(d))
end
# function without range... use the current range of the x-axis
-@recipe function f(f::FuncOrFuncs{F}) where {F<:Function} # COV_EXCL_LINE
+@recipe function f(f::FuncOrFuncs{F}) where {F <: Function} # COV_EXCL_LINE
plt = plotattributes[:plot_object]
xmin, xmax = if haskey(plotattributes, :xlims)
plotattributes[:xlims]
@@ -237,7 +237,7 @@ end
# if functions come first, just swap the order (not to be confused with parametric
# functions... as there would be more than one function passed in)
-@recipe function f(f::FuncOrFuncs{F}, x) where {F<:Function} # COV_EXCL_LINE
+@recipe function f(f::FuncOrFuncs{F}, x) where {F <: Function} # COV_EXCL_LINE
F2 = typeof(x)
@assert !(F2 <: Function || (F2 <: AbstractArray && F2.parameters[1] <: Function))
# otherwise we'd hit infinite recursion here
@@ -264,11 +264,11 @@ end
xscale, yscale = map(sym -> get(plotattributes, sym, :identity), (:xscale, :yscale))
_scaled_adapted_grid(f, xscale, yscale, xmin, xmax)
end
-@recipe function f(fs::AbstractArray{F}, xmin::Number, xmax::Number) where {F<:Function} # COV_EXCL_LINE
+@recipe function f(fs::AbstractArray{F}, xmin::Number, xmax::Number) where {F <: Function} # COV_EXCL_LINE
xscale, yscale = map(sym -> get(plotattributes, sym, :identity), (:xscale, :yscale))
unzip(_scaled_adapted_grid.(vec(fs), xscale, yscale, xmin, xmax))
end
-@recipe f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, u::AVec) where {F<:Function,G<:Function} =
+@recipe f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, u::AVec) where {F <: Function, G <: Function} =
_map_funcs(fx, u), _map_funcs(fy, u)
@recipe f(
fx::FuncOrFuncs{F},
@@ -276,7 +276,7 @@ end
umin::Number,
umax::Number,
n = 200,
-) where {F<:Function,G<:Function} = fx, fy, range(umin, stop = umax, length = n)
+) where {F <: Function, G <: Function} = fx, fy, range(umin, stop = umax, length = n)
# special handling... 3D parametric function(s)
@recipe f(
@@ -284,7 +284,7 @@ end
fy::FuncOrFuncs{G},
fz::FuncOrFuncs{H},
u::AVec,
-) where {F<:Function,G<:Function,H<:Function} =
+) where {F <: Function, G <: Function, H <: Function} =
_map_funcs(fx, u), _map_funcs(fy, u), _map_funcs(fz, u)
@recipe f(
@@ -294,7 +294,7 @@ end
umin::Number,
umax::Number,
numPoints = 200,
-) where {F<:Function,G<:Function,H<:Function} =
+) where {F <: Function, G <: Function, H <: Function} =
fx, fy, fz, range(umin, stop = umax, length = numPoints)
# list of tuples
@@ -302,12 +302,12 @@ end
@recipe f(tup::Tuple) = [tup]
# list of NamedTuples
-@recipe function f(ntv::AVec{<:NamedTuple{K,Tuple{S,T}}}) where {K,S,T} # COV_EXCL_LINE
+@recipe function f(ntv::AVec{<:NamedTuple{K, Tuple{S, T}}}) where {K, S, T} # COV_EXCL_LINE
xguide --> string(K[1])
yguide --> string(K[2])
Tuple.(ntv)
end
-@recipe function f(ntv::AVec{<:NamedTuple{K,Tuple{R,S,T}}}) where {K,R,S,T} # COV_EXCL_LINE
+@recipe function f(ntv::AVec{<:NamedTuple{K, Tuple{R, S, T}}}) where {K, R, S, T} # COV_EXCL_LINE
xguide --> string(K[1])
yguide --> string(K[2])
zguide --> string(K[3])
@@ -320,5 +320,5 @@ function _scaled_adapted_grid(f, xscale, yscale, xmin, xmax)
(xf, xinv), (yf, yinv) =
map(s -> (scale_func(s), inverse_scale_func(s)), (xscale, yscale))
xs, ys = PlotUtils.adapted_grid(yf ∘ f ∘ xinv, xf.((xmin, xmax)))
- xinv.(xs), yinv.(ys)
+ return xinv.(xs), yinv.(ys)
end
diff --git a/RecipesPipeline/src/utils.jl b/RecipesPipeline/src/utils.jl
index 682b91d015..638a73d55a 100644
--- a/RecipesPipeline/src/utils.jl
+++ b/RecipesPipeline/src/utils.jl
@@ -2,14 +2,14 @@
const AVec = AbstractVector
const AMat = AbstractMatrix
-const KW = Dict{Symbol,Any}
-const AKW = AbstractDict{Symbol,Any}
+const KW = Dict{Symbol, Any}
+const AKW = AbstractDict{Symbol, Any}
# --------------------------------
# ## DefaultsDict
# --------------------------------
-struct DefaultsDict <: AbstractDict{Symbol,Any}
+struct DefaultsDict <: AbstractDict{Symbol, Any}
explicit::KW
defaults::KW
end
@@ -17,33 +17,33 @@ end
Base.merge(d1::DefaultsDict, d2::DefaultsDict) =
DefaultsDict(merge(d1.explicit, d2.explicit), merge(d1.defaults, d2.defaults))
Base.getindex(dd::DefaultsDict, k) =
- if haskey(dd.explicit, k)
- dd.explicit[k]
- else
- dd.defaults[k]
- end
+if haskey(dd.explicit, k)
+ dd.explicit[k]
+else
+ dd.defaults[k]
+end
Base.haskey(dd::DefaultsDict, k) = haskey(dd.explicit, k) || haskey(dd.defaults, k)
Base.get(dd::DefaultsDict, k, default) = haskey(dd, k) ? dd[k] : default
Base.get!(dd::DefaultsDict, k, default) =
- if haskey(dd, k)
- dd[k]
- else
- dd.defaults[k] = default
- end
+if haskey(dd, k)
+ dd[k]
+else
+ dd.defaults[k] = default
+end
function Base.delete!(dd::DefaultsDict, k)
haskey(dd.explicit, k) && delete!(dd.explicit, k)
haskey(dd.defaults, k) && delete!(dd.defaults, k)
- dd
+ return dd
end
Base.length(dd::DefaultsDict) = length(union(keys(dd.explicit), keys(dd.defaults)))
function Base.iterate(dd::DefaultsDict)
key_list = union!(collect(keys(dd.explicit)), keys(dd.defaults))
- iterate(dd, (key_list, 1))
+ return iterate(dd, (key_list, 1))
end
function Base.iterate(dd::DefaultsDict, (key_list, i))
i > length(key_list) && return nothing
k = key_list[i]
- (k => dd[k], (key_list, i + 1))
+ return (k => dd[k], (key_list, i + 1))
end
Base.copy(dd::DefaultsDict) = DefaultsDict(copy(dd.explicit), dd.defaults)
@@ -56,7 +56,7 @@ Base.setindex!(dd::DefaultsDict, v, k) = dd.explicit[k] = v
# Reset to default value and return dict
function reset_kw!(dd::DefaultsDict, k)
is_explicit(dd, k) && delete!(dd.explicit, k)
- dd
+ return dd
end
# Reset to default value and return old value
pop_kw!(dd::DefaultsDict, k) = is_explicit(dd, k) ? pop!(dd.explicit, k) : dd.defaults[k]
@@ -77,7 +77,7 @@ defaultkeys(dd::DefaultsDict) = keys(dd.defaults)
abstract type AbstractSurface end
"represents a contour or surface mesh"
-struct Surface{M<:AMat} <: AbstractSurface
+struct Surface{M <: AMat} <: AbstractSurface
surf::M
end
@@ -92,21 +92,21 @@ Base.copy(surf::Surface) = Surface(copy(surf.surf))
Base.eltype(surf::Surface{T}) where {T} = eltype(T)
struct Volume{T}
- v::Array{T,3}
- x_extents::Tuple{T,T}
- y_extents::Tuple{T,T}
- z_extents::Tuple{T,T}
+ v::Array{T, 3}
+ x_extents::Tuple{T, T}
+ y_extents::Tuple{T, T}
+ z_extents::Tuple{T, T}
end
default_extents(::Type{T}) where {T} = (zero(T), one(T))
function Volume(
- v::Array{T,3},
- x_extents = default_extents(T),
- y_extents = default_extents(T),
- z_extents = default_extents(T),
-) where {T}
- Volume(v, x_extents, y_extents, z_extents)
+ v::Array{T, 3},
+ x_extents = default_extents(T),
+ y_extents = default_extents(T),
+ z_extents = default_extents(T),
+ ) where {T}
+ return Volume(v, x_extents, y_extents, z_extents)
end
Base.Array(vol::Volume) = vol.v
@@ -139,18 +139,18 @@ Returns `true` if `myseriestype` represents a 3D series, `false` otherwise.
"""
is3d(st) = false
for st in (
- :contour,
- :contourf,
- :contour3d,
- :heatmap,
- :image,
- :path3d,
- :scatter3d,
- :surface,
- :volume,
- :wireframe,
- :mesh3d,
-)
+ :contour,
+ :contourf,
+ :contour3d,
+ :heatmap,
+ :image,
+ :path3d,
+ :scatter3d,
+ :surface,
+ :volume,
+ :wireframe,
+ :mesh3d,
+ )
@eval is3d(::Type{Val{Symbol($(string(st)))}}) = true
end
is3d(st::Symbol) = is3d(Val{st})
@@ -186,13 +186,13 @@ needs_3d_axes(plotattributes::AbstractDict) =
# ## Scales
# --------------------------------
-const SCALE_FUNCTIONS = Dict{Symbol,Function}(
+const SCALE_FUNCTIONS = Dict{Symbol, Function}(
:log10 => NaNMath.log10,
:log2 => NaNMath.log2,
:ln => NaNMath.log,
)
const INVERSE_SCALE_FUNCTIONS =
- Dict{Symbol,Function}(:log10 => exp10, :log2 => exp2, :ln => exp)
+ Dict{Symbol, Function}(:log10 => exp10, :log2 => exp2, :ln => exp)
scale_func(scale::Symbol) = x -> get(SCALE_FUNCTIONS, scale, identity)(Float64(x))
inverse_scale_func(scale::Symbol) =
@@ -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 in fs]
# --------------------------------
# ## Signature strings
diff --git a/RecipesPipeline/test/Project.toml b/RecipesPipeline/test/Project.toml
new file mode 100644
index 0000000000..6b64ea5fb9
--- /dev/null
+++ b/RecipesPipeline/test/Project.toml
@@ -0,0 +1,7 @@
+[deps]
+BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
+Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
+RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
+RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/RecipesPipeline/test/runtests.jl b/RecipesPipeline/test/runtests.jl
index fcdff96f7c..3124eb9202 100644
--- a/RecipesPipeline/test/runtests.jl
+++ b/RecipesPipeline/test/runtests.jl
@@ -3,8 +3,10 @@ using BenchmarkTools
using StableRNGs
using Test
-import RecipesPipeline: _prepare_series_data
+import Dates
+import RecipesPipeline: _prepare_series_data, _apply_type_recipe
import RecipesBase
+import RecipesBase: @recipe
@testset "DefaultsDict" begin
dd = DefaultsDict(Dict(:foo => 1, :bar => missing), Dict(:foo => nothing, :baz => 'x'))
@@ -28,12 +30,12 @@ end
@testset "coverage" begin
@test RecipesPipeline.userrecipe_signature_string((missing, 1)) ==
- "(::Missing, ::Int64)"
+ "(::Missing, ::Int64)"
@test RecipesPipeline.typerecipe_signature_string(1) == "(::Type{Int64}, ::Int64)"
@test RecipesPipeline.plotrecipe_signature_string(:wireframe) ==
- "(::Type{Val{:wireframe}}, ::AbstractPlot)"
+ "(::Type{Val{:wireframe}}, ::AbstractPlot)"
@test RecipesPipeline.seriesrecipe_signature_string(:wireframe) ==
- "(::Type{Val{:wireframe}}, x, y, z)"
+ "(::Type{Val{:wireframe}}, x, y, z)"
plt = nothing
plotattributes = Dict(:x => 1, :y => "", :z => nothing, :seriestype => :path)
@@ -50,7 +52,7 @@ end
@test slice_series_attributes!(plt, kw_list, kw) isa Nothing
@test process_sliced_series_attributes!(plt, kw_list) isa Nothing
- @test RecipesPipeline.series_defaults(plt) == Dict{Symbol,Any}()
+ @test RecipesPipeline.series_defaults(plt) == Dict{Symbol, Any}()
@test !RecipesPipeline.is_seriestype_supported(plt, :wireframe)
@test RecipesPipeline.add_series!(plt, kw) isa Nothing
@@ -82,13 +84,48 @@ end
@test RecipesBase.is_key_supported("key")
end
+@testset "_apply_type_recipe" begin
+ plt = nothing
+ plotattributes = Dict{Symbol, Any}(:plot_object => plt)
+ @test _apply_type_recipe(plotattributes, [1, 2, 3], :x) == [1, 2, 3]
+ @test _apply_type_recipe(plotattributes, [[1, 2], [3, 4]], :x) == [[1, 2], [3, 4]]
+ res = _apply_type_recipe(plotattributes, [Dates.Date(2001)], :x)
+ @test typeof(res) <: Formatted
+ @test res.data == [Dates.value(Dates.Date(2001))]
+ @test res.formatter(Dates.value(Dates.Date(2001))) == "2001-01-01"
+
+ res = _apply_type_recipe(plotattributes, [[Dates.Date(2001)]], :x)
+ @test typeof(res) <: Formatted
+ @test res.data == [[Dates.value(Dates.Date(2001))]]
+ @test res.formatter(Dates.value(Dates.Date(2001))) == "2001-01-01"
+
+ struct Test1 <: Number
+ val::Float64
+ end
+
+ @recipe f(::Type{T}, v::T) where {T <: AbstractVector{<:Test1}} = map(x -> x.val + 1, v)
+
+ @test _apply_type_recipe(plotattributes, Test1.([1, 2, 3]), :x) == [2.0, 3.0, 4.0]
+ @test _apply_type_recipe(plotattributes, [Test1.([1, 2, 3])], :x) == [[2.0, 3.0, 4.0]]
+
+ struct Test2 <: Number
+ val::Float64
+ end
+
+ @recipe f(::Type{T}, v::T) where {T <: AbstractVector{<:AbstractVector{<:Test2}}} =
+ map(x -> map(y -> y.val + 2, x), v)
+
+ @test _apply_type_recipe(plotattributes, Test2.([1, 2, 3]), :x) == Test2.([1, 2, 3])
+ @test _apply_type_recipe(plotattributes, [Test2.([1, 2, 3])], :x) == [[3.0, 4.0, 5.0]]
+end
+
@testset "_prepare_series_data" begin
@test_throws ErrorException _prepare_series_data(:test)
@test _prepare_series_data(nothing) ≡ nothing
@test _prepare_series_data((1.0, 2.0)) ≡ (1.0, 2.0)
@test _prepare_series_data(identity) ≡ identity
@test _prepare_series_data(1:5:10) ≡ 1:5:10
- a = ones(Union{Missing,Float64}, 100, 100)
+ a = ones(Union{Missing, Float64}, 100, 100)
sd = _prepare_series_data(a)
@test sd == a
@test eltype(sd) == Float64
@@ -108,7 +145,7 @@ end
@testset "unzip" begin
x, y, z = unzip([(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)])
@test all(x .== 1.0) && all(y .== 2.0) && all(z .== 3.0)
- x, y, z = unzip(Tuple{Float64,Float64,Float64}[])
+ x, y, z = unzip(Tuple{Float64, Float64, Float64}[])
@test isempty(x) && isempty(y) && isempty(z)
end
diff --git a/RecipesPipeline/test/test_group.jl b/RecipesPipeline/test/test_group.jl
index 41f4c862a9..0f75a80474 100644
--- a/RecipesPipeline/test/test_group.jl
+++ b/RecipesPipeline/test/test_group.jl
@@ -1,13 +1,13 @@
function _extract_group_attributes_old_slow_known_good_implementation(
- v,
- args...;
- legend_entry = string,
-)
+ v,
+ args...;
+ legend_entry = string,
+ )
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]
- RecipesPipeline.GroupBy(map(legend_entry, group_labels), group_indices)
+ return RecipesPipeline.GroupBy(map(legend_entry, group_labels), group_indices)
end
sc = ["C", "C", "C", "A", "A", "A", "B", "B", "D"]
@@ -43,7 +43,19 @@ lp = map(i -> "xx" * "$(i % 599)", 1:2_000)
@test RecipesPipeline._extract_group_attributes(Tuple(sc)) isa RecipesPipeline.GroupBy
@test RecipesPipeline._extract_group_attributes((; A = [1], B = [2])) isa
- RecipesPipeline.GroupBy
+ RecipesPipeline.GroupBy
@test RecipesPipeline._extract_group_attributes(Dict(:A => [1], :B => [2])) isa
- RecipesPipeline.GroupBy
+ RecipesPipeline.GroupBy
+
+ @testset "_filter_input_data!" begin
+ filtered_keys = [:x, :y, :z, :xerror, :yerror, :zerror]
+ orig_akw = Dict{Symbol, Any}(k => rand(10) for k in filtered_keys)
+ orig_akw[:idxfilter] = [1, 4, 10]
+
+ akw = deepcopy(orig_akw)
+ RecipesPipeline._filter_input_data!(akw)
+ for k in filtered_keys
+ @test akw[k] == orig_akw[k][orig_akw[:idxfilter]]
+ end
+ end
end
diff --git a/StatsPlots/LICENSE.md b/StatsPlots/LICENSE.md
new file mode 100644
index 0000000000..6fed4c2e61
--- /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 0000000000..56cf4de886
--- /dev/null
+++ b/StatsPlots/Project.toml
@@ -0,0 +1,45 @@
+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"
+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"
+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"
+
+[sources]
+PlotsBase = {path = "../PlotsBase"}
+RecipesBase = {path = "../RecipesBase"}
+RecipesPipeline = {path = "../RecipesPipeline"}
+
+[weakdeps]
+Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1"
+
+[extensions]
+InteractExt = "Interact"
+
+[compat]
+AbstractFFTs = "1"
+Clustering = "0.15"
+Distributions = "0.25"
+Interact = "0.10"
+Interpolations = "0.15 - 0.16"
+KernelDensity = "0.6"
+MultivariateStats = "0.10"
+NaNMath = "1"
+PlotsBase = "0.1"
+RecipesBase = "1"
+RecipesPipeline = "1"
+Reexport = "1"
+StatsBase = "0.34"
+julia = "1.10"
diff --git a/StatsPlots/README.md b/StatsPlots/README.md
new file mode 100644
index 0000000000..196d730a46
--- /dev/null
+++ b/StatsPlots/README.md
@@ -0,0 +1,520 @@
+# StatsPlots
+
+[](
+ https://github.com/JuliaPlots/Plots.jl/actions?query=workflow%3Aci
+)
+[](
+ https://docs.juliaplots.org/stable/generated/statsplots/)
+[](
+ 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 = 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)
+```
+
+
+
+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
+using RDatasets
+iris = dataset("datasets", "iris")
+using StatsPlots, Interact
+using Blink
+w = Window()
+body!(w, dataviewer(iris))
+```
+
+
+
+
+## marginalhist with DataFrames
+
+```julia
+using RDatasets
+iris = dataset("datasets","iris")
+@df iris marginalhist(:PetalLength, :PetalWidth)
+```
+
+
+
+---
+
+## marginalscatter with DataFrames
+
+```julia
+using RDatasets
+iris = dataset("datasets","iris")
+@df iris marginalscatter(:PetalLength, :PetalWidth)
+```
+
+
+
+---
+
+## marginalkde
+
+```julia
+x = randn(1024)
+y = randn(1024)
+marginalkde(x, x+y)
+```
+
+
+
+
+* `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)
+```
+
+
+
+
+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])
+```
+
+
+
+```julia
+cornerplot(M)
+```
+
+
+
+
+```julia
+cornerplot(M, compact=true)
+```
+
+
+
+---
+
+## boxplot, dotplot, and violin
+
+```julia
+using RDatasets
+singers = 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)))
+```
+
+
+
+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.
+
+
+
+
+---
+
+## 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)
+```
+
+
+
+---
+
+## 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)
+```
+
+
+
+---
+
+## 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(x, y[:, :, 1], errorstyle=:ribbon, label="Ribbon")
+errorline!(x, y[:, :, 2], errorstyle=:stick, label="Stick", secondarycolor=:matched)
+errorline!(x, y[:, :, 3], errorstyle=:plume, label="Plume")
+```
+
+
+
+---
+
+## Distributions
+
+```julia
+using Distributions
+plot(Normal(3,5), fill=(0, .5,:orange))
+```
+
+
+
+```julia
+dist = Gamma(2)
+scatter(dist, leg=false)
+bar!(dist, func=cdf, alpha=0.3)
+```
+
+
+
+### 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
+)
+```
+
+
+## Grouped Bar plots
+
+```julia
+groupedbar(rand(10, 3), bar_position = :stack, bar_width = 0.7)
+```
+
+
+
+This is the default:
+
+```julia
+groupedbar(rand(10, 3), bar_position = :dodge, bar_width = 0.7)
+```
+
+
+
+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)
+```
+
+
+
+## Grouped Histograms
+
+```
+using RDatasets
+iris = dataset("datasets", "iris")
+@df iris groupedhist(:SepalLength, group = :Species, bar_position = :dodge)
+```
+
+
+```
+@df iris groupedhist(:SepalLength, group = :Species, bar_position = :stack)
+```
+
+
+## Dendrograms
+
+```julia
+using Clustering
+D = rand(10, 10)
+D += D'
+hc = hclust(D, linkage=:single)
+plot(hc)
+```
+
+
+
+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])
+ )
+```
+
+
+
+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])
+ )
+```
+
+
+
+### 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))
+)
+
+```
+
+
+
+
+## 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)
+```
+
+
+
+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")
+```
+
diff --git a/StatsPlots/ext/InteractExt.jl b/StatsPlots/ext/InteractExt.jl
new file mode 100644
index 0000000000..55fc708a72
--- /dev/null
+++ b/StatsPlots/ext/InteractExt.jl
@@ -0,0 +1,125 @@
+module InteractExt
+
+import StatsPlots: StatsPlots, PlotsBase
+import Interact: Observables, Widgets, OrderedCollections
+import PlotsBase: TableOperations
+
+function StatsPlots.dataviewer(t; throttle = 0.1, nbins = 30, nbins_range = 1:100)
+ (t isa Observables.AbstractObservable) || (t = Observables.Observable{Any}(t))
+
+ coltable = map(TableOperations.Tables.columntable, t)
+
+ names = map(collect ∘ keys, coltable)
+ # @show names
+
+ dict = Observables.@map Dict((key, val) for (key, val) in 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(
+ OrderedCollections.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
+ bins_plots = (
+ StatsPlots.corrplot,
+ StatsPlots.cornerplot,
+ StatsPlots.histogram,
+ StatsPlots.marginalhist,
+ )
+ display_nbins = Observables.@map (&plot_type) in bins_plots ? "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)
+
+ 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 in ns)...) : dict[ns[1]]
+
+ by = Widgets.dropdown(names, multiple = true, placeholder = "Group by")
+ by_toggle = Widgets.togglecontent(by, value = false, label = "Split data")
+ plt = Widgets.button("plot")
+ output = Observables.@map begin
+ if (&plt == 0)
+ PlotsBase.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 in 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 = Widgets.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,
+ )
+ return Widgets.@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
+
+end
diff --git a/StatsPlots/src/StatsPlots.jl b/StatsPlots/src/StatsPlots.jl
new file mode 100644
index 0000000000..36300e2a00
--- /dev/null
+++ b/StatsPlots/src/StatsPlots.jl
@@ -0,0 +1,53 @@
+module StatsPlots
+
+using Reexport
+import RecipesBase: recipetype
+using RecipesPipeline
+@reexport using PlotsBase
+import PlotsBase.Commons: _cycle, mm
+
+using LinearAlgebra: eigen, diagm
+using Distributions
+using StatsBase
+
+using MultivariateStats: MultivariateStats
+using AbstractFFTs: fft, ifft
+using Interpolations
+using NaNMath
+
+import Clustering: Hclust, nnodes
+import KernelDensity
+
+@recipe f(k::KernelDensity.UnivariateKDE) = k.x, k.density
+@recipe f(k::KernelDensity.BivariateKDE) = k.x, k.y, permutedims(k.density)
+
+@shorthands cdensity
+
+export dataviewer
+
+isvertical(plotattributes) =
+let val = get(plotattributes, :orientation, missing)
+ val ≡ missing || val in (:vertical, :v)
+end
+
+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")
+
+function dataviewer end # InteractExt
+
+end
diff --git a/StatsPlots/src/andrews.jl b/StatsPlots/src/andrews.jl
new file mode 100644
index 0000000000..2d6a24ecea
--- /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 in 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 in 1:rows
+ @series begin
+ primary := false
+ ys = zeros(length(x))
+ terms =
+ [isodd(i) ? cos((i ÷ 2) .* ti) : sin((i ÷ 2) .* ti) for i in 2:cols, ti in x]
+ for ti in eachindex(x)
+ ys[ti] = y[j, 1] / sqrt(2) + sum(y[j, i] .* terms[i - 1, ti] for i in 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 0000000000..8d6b9f5fe1
--- /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 in 1:nr
+ bw = _cycle(bws, r)
+ farleft = xnums[r] - 0.5 * (bw * nc)
+ for c in 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 in 1:nr
+ y_neg = 0
+ # upper & lower bounds for positive bar
+ y_pos = sum([e for e in y[r, :] if e > 0])
+ # division subtract towards 0
+ for c in 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
+ return y, fr
+end
diff --git a/StatsPlots/src/boxplot.jl b/StatsPlots/src/boxplot.jl
new file mode 100644
index 0000000000..e59225d5bd
--- /dev/null
+++ b/StatsPlots/src/boxplot.jl
@@ -0,0 +1,258 @@
+# ---------------------------------------------------------------------------
+# 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) in 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 in 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 in 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 in 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 0000000000..03824bbf4d
--- /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 in 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 in 1:n, j in 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 in 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 in 1:N
+ compact && j == N && continue
+ @series begin
+ seriestype := :histogram
+ subplot := indices[1, j]
+ grid := false
+ view(mat, :, j)
+ end
+ end
+
+ # scatters
+ for i in 1:N
+ vi = view(mat, :, i)
+ for j in 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 0000000000..8f1fc72cf7
--- /dev/null
+++ b/StatsPlots/src/corrplot.jl
@@ -0,0 +1,119 @@
+"""
+ 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)
+ return 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 in 1:n
+ @series begin
+ if title |> !isempty && 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 in 1:n
+ ylink := setdiff(vec(indices[i, :]), indices[i, i])
+ vi = view(mat, :, i)
+ for j in 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 |> !isempty && 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 0000000000..9a5b44461d
--- /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(Σ)
+ return μ, 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 0000000000..eddc44ea74
--- /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 in hc.order)
+
+ xs = Array{Float64}(undef, 4, size(hc.merges, 1))
+ ys = Array{Float64}(undef, 4, size(hc.merges, 1))
+
+ for i in 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/distributions.jl b/StatsPlots/src/distributions.jl
new file mode 100644
index 0000000000..313fbaf648
--- /dev/null
+++ b/StatsPlots/src/distributions.jl
@@ -0,0 +1,104 @@
+# 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)
+ return minval, maxval
+end
+
+function default_range(m::Distributions.UnivariateMixture, alpha = 0.0001)
+ return 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 in 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 in 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 0000000000..a489f7039d
--- /dev/null
+++ b/StatsPlots/src/dotplot.jl
@@ -0,0 +1,115 @@
+# ---------------------------------------------------------------------------
+# 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) in 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 in y] .|> findmin .|> first
+ lowercenters = findmax.([violincenters[violincenters .≤ yval] for yval in y])
+ lowerbounds, lowerindexes = first.(lowercenters), last.(lowercenters)
+ δs = (y .- lowerbounds) ./ (upperbounds .- lowerbounds)
+
+ itp = interpolate(widths, BSpline(Quadratic(Reflect(OnCell()))))
+ return localwidths = itp.(lowerindexes .+ δs)
+ end
+
+ violinwidths, violincenters = violin_coords(y)
+ violinwidths = normalizewidths(maxwidth, violinwidths)
+ localwidths = getlocalwidths(violinwidths, violincenters, y)
+ return 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 in 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 in 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 0000000000..78999bbd59
--- /dev/null
+++ b/StatsPlots/src/ecdf.jl
@@ -0,0 +1,25 @@
+# ---------------------------------------------------------------------------
+# 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 0000000000..a996529a80
--- /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 in 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 in 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 in 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) in 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 in 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 0000000000..ab9a063dd2
--- /dev/null
+++ b/StatsPlots/src/hist.jl
@@ -0,0 +1,248 @@
+# ---------------------------------------------------------------------------
+# 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 in 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 in 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
+
+ return 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 in 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
+ return 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 in (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)
+
+ return sum(gcounts .* (real(ifft(kappam .* Gcounts)))[1:M]) / (n^2)
+end
diff --git a/StatsPlots/src/marginalhist.jl b/StatsPlots/src/marginalhist.jl
new file mode 100644
index 0000000000..2ec56da020
--- /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 --> 0mm
+ top_margin --> 0mm
+ 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 --> 0mm
+ bins := edges1
+ y := x
+ xlims --> xlims
+ end
+
+ # right histogram
+ @series begin
+ orientation := :h
+ subplot := 3
+ left_margin --> 0mm
+ 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 0000000000..ff3efbc099
--- /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 in 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 0000000000..09004a2348
--- /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 --> 0mm
+ top_margin --> 0mm
+ 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 --> 0mm
+ 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 --> 0mm
+ # 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 0000000000..5615b64241
--- /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 0000000000..b8319dc3af
--- /dev/null
+++ b/StatsPlots/src/violin.jl
@@ -0,0 +1,214 @@
+# ---------------------------------------------------------------------------
+# 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 in kd.x]
+ return (kd.density[inside], kd.x[inside])
+ end
+ return 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) in 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 in 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 in 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 in 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/Project.toml b/StatsPlots/test/Project.toml
new file mode 100644
index 0000000000..d8d0dcb3db
--- /dev/null
+++ b/StatsPlots/test/Project.toml
@@ -0,0 +1,18 @@
+[deps]
+Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
+Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1"
+MultivariateStats = "6f286f6a-111f-5878-ab1e-185364afe411"
+NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
+Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
+RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
+StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+
+[compat]
+Plots = "2"
+
+[sources]
+Plots = {path = "../.."}
diff --git a/StatsPlots/test/runtests.jl b/StatsPlots/test/runtests.jl
new file mode 100644
index 0000000000..ff550e522b
--- /dev/null
+++ b/StatsPlots/test/runtests.jl
@@ -0,0 +1,631 @@
+using MultivariateStats
+using Distributions
+using StableRNGs
+using Clustering
+using StatsPlots
+using StatsBase
+using RDatasets
+using Interact
+using NaNMath
+using Plots
+using Test
+
+const PlotsBase = Plots.PlotsBase
+
+const iris = dataset("datasets", "iris")
+const singers = dataset("lattice", "singer")
+
+const Widgets = Base.get_extension(StatsPlots, :InteractExt).Widgets
+@test Widgets isa Module
+
+@testset "grouped histogram" begin
+ gpl = groupedhist(
+ rand(StableRNG(1337), 1000),
+ yscale = :log10,
+ ylims = (1.0e-2, 1.0e4),
+ bar_position = :stack,
+ )
+ @test NaNMath.minimum(gpl[1][1][:y]) ≤ 1.0e-2
+ @test NaNMath.minimum(gpl[1][1][:y]) > 0
+ gpl = groupedhist(
+ rand(StableRNG(1337), 1000),
+ yscale = :log10,
+ ylims = (1.0e-2, 1.0e4),
+ bar_position = :dodge,
+ )
+ @test NaNMath.minimum(gpl[1][1][:y]) ≤ 1.0e-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
+ ]
+
+ D = rand(StableRNG(1337), 10, 10)
+ D += D'
+ pl = hclust(D, linkage = :single)
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "histogram" begin
+ data = randn(StableRNG(1337), 1_000)
+ @test 0.2 < StatsPlots.wand_bins(data) < 0.4
+end
+
+@testset "distributions" begin
+ @testset "univariate" 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
+
+ dist = Gamma(2)
+ scatter(dist, leg = false)
+ bar!(dist, func = cdf, alpha = 0.3)
+end
+
+@testset "ordinations" begin
+ @testset "MDS" begin
+ X = randn(StableRNG(1337), 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 PlotsBase.get_guide(mds_plt[1][:xaxis]) == "MDS1"
+ @test PlotsBase.get_guide(mds_plt[1][:yaxis]) == "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 PlotsBase.get_guide(mds_plt2[1][:xaxis]) == "MDS3"
+ @test PlotsBase.get_guide(mds_plt2[1][:yaxis]) == "MDS1"
+ @test PlotsBase.get_guide(mds_plt2[1][:zaxis]) == "MDS2"
+ end
+end
+
+@testset "errorline" begin
+ @testset "input types" begin
+ x = 1:10
+ # test for floats
+ y = rand(StableRNG(1337), 10, 100) .* collect(1:2:20)
+ @test errorline(x, y)[1][1][:x] == x # x-input
+ @test all(
+ round.(errorline(x, y)[1][1][:y], digits = 3) .==
+ round.(mean(y, dims = 2), digits = 3),
+ ) # mean of y
+ @test all(
+ round.(errorline(x, 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(x, y)[1][1][:y] .== mean(y, dims = 2))
+ @test all(
+ round.(errorline(x, y)[1][1][:ribbon], digits = 3) .==
+ round.(std(y, dims = 2), digits = 3),
+ )
+ # test colors
+ y = rand(StableRNG(1337), 10, 100, 3) .* collect(1:2:20)
+ c = palette(:default)
+ e = errorline(x, 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 "example" begin
+ rng = StableRNG(1337)
+ x = 1:10
+ y = fill(NaN, 10, 100, 6)
+ for i in axes(y, 3)
+ y[:, :, i] =
+ collect(1:2:20) .+ 5rand(rng, 10, 100) .* collect(1:2:20) .+ 100rand(rng)
+ end
+
+ pl = errorline(x, y[:, :, 1], errorstyle = :ribbon, label = "Ribbon")
+ errorline!(
+ x,
+ y[:, :, 2],
+ errorstyle = :stick,
+ label = "Stick",
+ secondarycolor = :matched,
+ )
+ errorline!(x, y[:, :, 3], errorstyle = :plume, label = "Plume")
+ errorline!(x, y[:, :, 4], errortype = :sem)
+ errorline!(x, y[:, :, 5], errortype = :percentile)
+ errorline!(x, y[:, :, 6], errortype = :percentile, errorstyle = :stick)
+ @test show(devnull, pl) isa Nothing
+ end
+end
+
+@testset "qqplot" begin
+ rng = StableRNG(1337)
+ x = rand(rng, Normal(), 100)
+ y = rand(rng, Cauchy(), 100)
+ pl = 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
+ )
+ @test show(devnull, pl) isa Nothing
+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 "marginalkde" begin
+ rng = StableRNG(1337)
+ x = randn(rng, 1024)
+ y = randn(rng, 1024)
+ pl = marginalkde(x, x + y)
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "violin" begin
+ y = [i * randn(StableRNG(1337), 100) for i in 1:4]
+ violin(y, median = true)
+ violin(y, quantiles = [0.1, 0.5, 0.9], linecolor = :white, linewidth = 3)
+ violin(y, quantiles = 3, mean = true)
+end
+
+@testset "violin df" begin
+ pl = violin(
+ repeat([0.1, 0.2, 0.3], outer = 100),
+ randn(StableRNG(1337), 300),
+ side = :right,
+ )
+ @test show(devnull, pl) isa Nothing
+
+ @df singers violin(
+ string.(:VoicePart),
+ :Height,
+ side = :right,
+ linewidth = 0,
+ label = "Scala",
+ )
+ @df singers dotplot!(
+ string.(:VoicePart),
+ :Height,
+ side = :right,
+ marker = (:black, stroke(0)),
+ label = "",
+ )
+end
+
+@testset "groupedviolin" begin
+ df = DataFrame(
+ x = repeat(["A", "B"], inner = 10),
+ y = (1:20) .+ randn(20),
+ g = repeat(["Group 1", "Group 2"], inner = 5, outer = 2),
+ )
+ @df df groupedviolin(:x, :y; group = :g)
+end
+
+@testset "density" begin
+ pl = density(rand(StableRNG(1337), 100_000), label = "density(rand())")
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "covellipse" begin
+ pl = covellipse([0, 2], [2 1; 1 4]; n_std = 2, showaxes = true, label = "cov1")
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "ecdf" begin
+ pl = plot(StatsBase.ecdf(randn(StableRNG(1337), 100)), label = "Normal")
+ ecdfplot!(rand(Cauchy(), 100); label = "Cauchy")
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "corrplot / cornerplot" begin
+ M = randn(StableRNG(1337), 1_000, 4)
+ @. M[:, 2] += 0.8sqrt(abs(M[:, 1])) - 0.5M[:, 3] + 5
+ @. M[:, 3] -= 0.7M[:, 1]^2 + 2
+ pl = corrplot(M; label = ["x$i" for i in 1:4])
+ @test show(devnull, pl) isa Nothing
+
+ pl = cornerplot(M)
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "boxplot / dotplot / violin" begin
+ @df singers violin(string.(:VoicePart), :Height, show_mean = true, show_median = true)
+ @df singers boxplot!(string.(:VoicePart), :Height, fillalpha = 0.75, linewidth = 2)
+ @df singers dotplot!(string.(:VoicePart), :Height, marker = (:black, stroke(0)))
+ @test true
+end
+
+@testset "ea_histogram" begin
+ rng = StableRNG(1337)
+ a = [randn(rng, 100); randn(rng, 100) .+ 3; randn(rng, 100) ./ 2 .+ 3]
+ pl = ea_histogram(a, bins = :scott, fillalpha = 0.4)
+ @test show(devnull, pl) isa Nothing
+end
+
+@testset "andrewsplot" begin
+ @df iris andrewsplot(:Species, cols(1:4), legend = :topleft)
+end
+
+@testset "interaction" begin
+ dv = dataviewer(iris)
+ @test true
+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 a57b19cdf1..89da81b969 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 0000000000..365e982a4a
--- /dev/null
+++ b/ci/build-docs.sh
@@ -0,0 +1,84 @@
+#!/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
+
+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 JULIA_CONDAPKG_BACKEND=MicroMamba
+
+banner() {
+ echo "running action $GITHUB_ACTION with workflow $GITHUB_WORKFLOW for $GITHUB_REPOSITORY@$GITHUB_REF"
+ echo "triggered by actor $GITHUB_ACTOR on event $GITHUB_EVENT_NAME"
+ echo "commit SHA is $GITHUB_SHA"
+}
+
+julia_project() {
+ xvfb-run -a julia --color=yes --project=docs "$@"
+}
+
+install_ubuntu_deps() {
+ echo '== install system dependencies =='
+ banner
+ 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'
+}
+
+install_and_precompile_julia_deps() {
+ echo "== install julia dependencies =="
+ banner
+ JULIA_PKG_PRECOMPILE_AUTO=0 julia_project ci/matplotlib.jl
+ echo '== precompile docs dependencies =='
+ julia_project docs/make.jl none
+}
+
+build_documenter_docs() {
+ echo "== build documentation =="
+ banner
+ export PLOTSBASE_UNICODEPLOTS_COLORED=true
+ export PLOTDOCS_PUSH_PREVIEW=false
+ export PLOTDOCS_ANSICOLOR=true
+ # export PLOTDOCS_PACKAGES='UnicodePlots'
+ # export PLOTDOCS_EXAMPLES=1
+ julia_project docs/make.jl all
+}
diff --git a/ci/downstream.jl b/ci/downstream.jl
new file mode 100644
index 0000000000..1c68ca43c0
--- /dev/null
+++ b/ci/downstream.jl
@@ -0,0 +1,85 @@
+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, "PlotThemes", pkg_version("PlotThemes"))
+ 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", "PlotThemes", "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 0000000000..458780f5fa
--- /dev/null
+++ b/ci/matplotlib.jl
@@ -0,0 +1,41 @@
+using Pkg
+Pkg.add("CondaPkg")
+
+using CondaPkg
+CondaPkg.resolve()
+
+# table adapted from `4. Symbol versioning on the libstdc++.so binary` in
+# gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
+# see also github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/config/abi/pre/gnu.ver
+_compatible_libstdcxx_ng_versions = [
+ (v"3.4.34", (">=15.1", "<16.1")), # NOTE: hypothetical upper bound
+ (v"3.4.33", (">=14.1", "<15.1")),
+ (v"3.4.32", (">=13.2", "<14.1")),
+ (v"3.4.31", (">=13.1", "<13.2")),
+ (v"3.4.30", (">=12.1", "<13.1")),
+ (v"3.4.29", (">=11.1", "<12.1")),
+ (v"3.4.28", (">=9.3", "<11.1")),
+ (v"3.4.27", (">=9.2", "<9.3")),
+ (v"3.4.26", (">=9.1", "<9.2")),
+ (v"3.4.25", (">=8.1", "<9.1")),
+ (v"3.4.24", (">=7.2", "<8.1")),
+ (v"3.4.23", (">=7.1", "<7.2")),
+ (v"3.4.22", (">=6.1", "<7.1")),
+ (v"3.4.21", (">=5.1", "<6.1")),
+ (v"3.4.20", (">=4.9", "<5.1")),
+ (v"3.4.19", (">=4.8.3", "<4.9")),
+]
+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)
+ versions = Dict(_compatible_libstdcxx_ng_versions)
+ lo, hi = extrema(keys(versions))
+ _, ub = versions[Base.BinaryPlatforms.detect_libstdcxx_version(Int(hi.patch))]
+ specs = ">=$(VersionNumber(lo.major, lo.minor)),$ub"
+ ("libgcc-ng$specs", "libstdcxx-ng$specs")
+else
+ ()
+end
+
+CondaPkg.PkgREPL.add([libgcc..., "matplotlib>=3.4"]) # "openssl>=3.4"
+CondaPkg.status()
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 74f283a282..0000000000
--- a/codecov.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-github_checks:
- annotations: false
-
-ignore:
- - "src/backends/inspectdr.jl"
- - "src/backends/orca.jl"
- - "src/backends/pgfplots.jl"
- - "src/backends/plotly.jl"
- - "src/backends/plotlyjs.jl"
- - "src/backends/pyplot.jl"
- - "src/backends/web.jl"
- - "src/fileio.jl"
- - "src/ijulia.jl"
diff --git a/docs/Project.toml b/docs/Project.toml
new file mode 100644
index 0000000000..4059596176
--- /dev/null
+++ b/docs/Project.toml
@@ -0,0 +1,84 @@
+[deps]
+AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
+Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
+CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
+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"
+PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
+PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c"
+Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
+PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737"
+PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a"
+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"
+Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
+
+[compat]
+DataFrames = "1"
+Dates = "1"
+Documenter = "1"
+FileIO = "1"
+FreeType = "4"
+Gaston = "1"
+Glob = "1"
+LaTeXStrings = "1"
+Literate = "2"
+LinearAlgebra = "1"
+OffsetArrays = "1"
+OrderedCollections = "1"
+Pkg = "1"
+PlotlyKaleido = "2"
+Plots = "2"
+PlotThemes = "3"
+PGFPlotsX = "1"
+PythonPlot = "1"
+SparseArrays = "1"
+StableRNGs = "1"
+StaticArrays = "1"
+TestImages = "1"
+UnicodePlots = "3"
+Unitful = "1"
+
+[sources]
+Plots = {path = ".."}
+PlotsBase = {path = "../PlotsBase"}
+PlotThemes = {path = "../PlotThemes"}
+RecipesBase = {path = "../RecipesBase"}
+RecipesPipeline = {path = "../RecipesPipeline"}
+StatsPlots = {path = "../StatsPlots"}
+GraphRecipes = {path = "../GraphRecipes"}
diff --git a/docs/gallery/gaston/config.json b/docs/gallery/gaston/config.json
new file mode 100644
index 0000000000..d909064818
--- /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 0000000000..53eb141d71
--- /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 0000000000..d909064818
--- /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 0000000000..e1a44c5b5b
--- /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/pgfplotsx/config.json b/docs/gallery/pgfplotsx/config.json
new file mode 100644
index 0000000000..d909064818
--- /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 0000000000..285c43e3fd
--- /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 0000000000..d909064818
--- /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 0000000000..b2f8085a7f
--- /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 0000000000..d909064818
--- /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 0000000000..c35ce7f9e7
--- /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 0000000000..d909064818
--- /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 0000000000..32737eceaf
--- /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 0000000000..5ce5590f58
--- /dev/null
+++ b/docs/make.jl
@@ -0,0 +1,957 @@
+# oneliner fast build PLOTDOCS_SUFFIX='' PLOTDOCS_PACKAGES='UnicodePlots' PLOTDOCS_EXAMPLES='1' julia --project make.jl
+import Pkg
+
+using RecipesBase, RecipesPipeline, PlotsBase, Plots
+using DemoCards, Literate, Documenter
+
+import OrderedCollections
+import UnicodePlots
+import GraphRecipes
+import StableRNGs
+import PythonPlot
+import StatsPlots
+import MacroTools
+import DataFrames
+import PlotThemes
+import PGFPlotsX
+import PlotlyJS
+import Unitful
+import Gaston
+import Dates
+import JSON
+import Glob
+
+PythonPlot.pygui(false) # prevent segfault on event loop in ci
+
+suffix = get(ENV, "PLOTDOCS_SUFFIX", "")
+const WRK_DIR = joinpath(@__DIR__, "work" * suffix)
+const BLD_DIR = joinpath(@__DIR__, "build" * suffix)
+const SRC_DIR = joinpath(@__DIR__, "src")
+const GEN_DIR = joinpath(WRK_DIR, "generated")
+const BRANCH = ("master", "v2")[2] # transition to v2
+const ASSETS = "assets"
+
+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" maxlog = 10
+ for (attr, alias) in $(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`" maxlog = 10
+ 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(basename(WRK_DIR), args...)
+end
+
+autogenerated() = "(Automatically generated: " * Dates.format(Dates.now(), Dates.RFC1123Format) * ')'
+
+author() = "[Plots.jl](https://github.com/JuliaPlots/Plots.jl)"
+
+recursive_rmlines(x) = x
+function recursive_rmlines(x::Expr)
+ x = MacroTools.rmlines(x)
+ x.args .= recursive_rmlines.(x.args)
+ return 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, ":")
+
+# ----------------------------------------------------------------------
+
+function generate_cards(
+ prefix::AbstractString, backend::Symbol, slice;
+ skip = get(PlotsBase._backend_skips, backend, Int[]),
+ debug = false
+ )
+ @show backend
+ # create folder: for each backend we generate a DemoSection "generated" under "gallery"
+ cards_path = let dn = joinpath(prefix, string(backend), "generated" * suffix)
+ isdir(dn) && rm(dn; recursive = true)
+ mkpath(dn)
+ end
+ sec_config = Dict{String, Any}("order" => [])
+ needs_rng_fix = Dict{Int, Bool}()
+
+ for (i, example) in enumerate(PlotsBase._examples)
+ i ∈ skip && continue
+ i ∈ slice || continue
+ # write out the header, description, code block, and image link
+ jl_name = "$backend-$(PlotsBase.ref_name(i)).jl"
+ jl = PipeBuffer()
+
+ # DemoCards YAML frontmatter
+ # https://johnnychen94.github.io/DemoCards.jl/stable/quickstart/usage_example/julia_demos/1.julia_demo/#juliademocard_example
+ svg_ready_backends = if false
+ (:gr, :pythonplot, :pgfplotsx, :plotlyjs, :gaston) # NOTE: this causes ↗ of docs size from ~150MB to ~350MB ...
+ else
+ ()
+ end
+ cover_name = "$(backend)_$(PlotsBase.ref_name(i))"
+ cover_path = let cover_file = cover_name * if i ∈ PlotsBase._animation_examples
+ ".gif"
+ elseif backend ∈ svg_ready_backends
+ ".svg"
+ else
+ ".png"
+ end
+ joinpath(ASSETS, cover_file)
+ end
+ if !isempty(example.header)
+ push!(sec_config["order"], jl_name)
+ # start a new demo file
+ debug && @info "generate demo $(example.header |> repr) - writing `$jl_name`"
+
+ extra = if backend ≡ :unicodeplots
+ "import FileIO, FreeType #hide" # weak deps for png export
+ else
+ ""
+ end
+ write(
+ jl, """
+ # ---
+ # title: $(example.header)
+ # id: $cover_name
+ # cover: $cover_path
+ # author: "$(author())"
+ # description: ""
+ # date: $(Dates.now())
+ # execute: true
+ # ---
+ using Plots
+ const PlotsBase = Plots.PlotsBase #hide
+ $backend()
+ $extra
+ PlotsBase.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.
+ # """
+ # this command creates the card cover file, NOT the output of the `@example` block in the `index.html` file !
+ cover_cmd = if i ∈ PlotsBase._animation_examples
+ "PlotsBase.gif(anim, $(cover_path |> repr))"
+ elseif backend ∈ svg_ready_backends
+ "PlotsBase.svg($(cover_path |> repr))"
+ else
+ "PlotsBase.png($(cover_path |> repr))"
+ end
+ # this command creates the output of the `@example` block in the `index.html` card file
+ show_cmd = if i ∈ PlotsBase._animation_examples
+ "PlotsBase.gif(anim)"
+ elseif backend ≡ :plotlyjs
+ # FIXME: failing to render the html script outputted by :plotlyjs so instead include the cover .svg file
+ """
+ nothing #hide
+ # 
+ """
+ else
+ "current()" # triggers MIME("text/html"), see PlotsBase._best_html_output_type (mostly `:svg` and `:html`)
+ end
+ write(
+ jl, """
+ mkpath($(ASSETS |> repr)) #src
+ $cover_cmd #src
+ $show_cmd #hide
+ """
+ )
+ card_jl = read(jl, String)
+ debug && @info card_jl
+
+ fn, mode = if isempty(example.header)
+ "$backend-$(PlotsBase.ref_name(i - 1)).jl", "a" # continued example
+ else
+ jl_name, "w"
+ end
+ card = joinpath(cards_path, fn)
+ # @info "writing" card
+ open(io -> write(io, card_jl), card, mode)
+ # 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(cards_path, 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: $(Dates.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(cards_path, "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.json(sec_config))
+ end
+ return 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 in bs # cols
+ be_supported_vals = fill("", length(vals))
+ for (i, val) in 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
+ return df
+end
+
+function generate_supported_markdown(; default_backends)
+ supported_args = OrderedCollections.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)
+ )
+ return 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) in 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 = DataFrames.DataFrame(
+ Attribute = fill("", n),
+ Aliases = fill("", n),
+ Default = fill("", n),
+ Type = fill("", n),
+ Description = fill("", n),
+ )
+ for (i, (k, def)) in 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])
+ return 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))
+
+ return 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 = DataFrames.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.",
+ ]
+ )
+ return 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
+
+generate_colorschemes_markdown() = open(joinpath(GEN_DIR, "colorschemes.md"), "w") do md
+ write(
+ md, """
+ ```@meta
+ EditURL = "$(edit_url())"
+ ```
+ """
+ )
+ foreach(line -> write(md, line * '\n'), readlines(joinpath(SRC_DIR, "colorschemes.md")))
+ 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 in ("cmocean", "scientific", "matplotlib", "colorbrewer", "gnuplot", "colorcet", "seaborn", "general")
+ ks = sort([k for (k, v) in 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
+
+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(
+ """
+
+
+ "
+end
+
+function make_colorschemes_df(ks)
+ n = length(ks)
+ df = DataFrames.DataFrame(
+ Name = fill("", n),
+ Palette = fill("", n),
+ Gradient = fill("", n),
+ )
+ len, w, h = 100, 60, 5
+ for (i, k) in 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
+ return df
+end
+
+# ----------------------------------------------------------------------
+
+function to_html(df::DataFrames.AbstractDataFrame; table = ["font-size" => "12px"], kw...)
+ io = PipeBuffer() # NOTE: `DataFrames` exports `PrettyTables`
+ style = DataFrames.PrettyTables.HtmlTableStyle(; table)
+ show(
+ IOContext(io, :limit => false, :compact => false), MIME"text/html"(), df;
+ show_row_number = false, summary = false, eltypes = false, style,
+ kw...
+ )
+ return read(io, String)
+end
+
+function main(args)
+ args = @. Symbol(args)
+ default_build_cmds = [:generate, :gallery, :make, :deploy]
+ build_cmds = length(args) > 0 ? args : default_build_cmds
+ :all ∈ build_cmds && (build_cmds = default_build_cmds)
+ if :none ∈ build_cmds
+ Pkg.precompile()
+ return
+ end
+ @show build_cmds
+
+ get!(ENV, "MPLBACKEND", "agg") # set matplotlib gui backend
+ get!(ENV, "GKSwstype", "nul") # disable default GR ws
+
+ # cleanup
+ isdir(WRK_DIR) && rm(WRK_DIR; recursive = true)
+ isdir(BLD_DIR) && rm(BLD_DIR; recursive = true)
+ mkpath(GEN_DIR)
+
+ # initialize all backends
+ gr()
+ pythonplot()
+ plotlyjs()
+ pgfplotsx()
+ unicodeplots()
+ gaston()
+
+ # NOTE: for a faster representative test build use `PLOTDOCS_PACKAGES='GR' PLOTDOCS_EXAMPLES='1'`
+ all_packages = "GR PythonPlot PlotlyJS PGFPlotsX UnicodePlots Gaston"
+ packages = get(ENV, "PLOTDOCS_PACKAGES", "ALL")
+ packages = let val = packages == "ALL" ? all_packages : packages
+ Symbol.(filter(!isempty, strip.(split(val))))
+ end
+ packages_backends = NamedTuple(p => Symbol(lowercase(string(p))) for p in packages)
+ backends = values(packages_backends) |> collect
+
+ @info "selected packages: $packages"
+ @info "selected backends: $backends"
+
+ slice = parse.(Int, split(get(ENV, "PLOTDOCS_EXAMPLES", "")))
+ slice = (len_sl = length(slice)) == 0 ? range(1; stop = length(PlotsBase._examples)) : slice
+ @info "selected examples: $slice"
+
+ debug = length(packages) ≤ 1 || 1 < len_sl ≤ 3
+
+ work_dir = basename(WRK_DIR)
+ bld_dir = basename(BLD_DIR)
+ src_dir = basename(SRC_DIR)
+ @show debug SRC_DIR WRK_DIR BLD_DIR
+
+ if :generate ∈ build_cmds
+ @info "generate markdown"
+ @time "generate markdown" begin
+ generate_attr_markdown()
+ generate_supported_markdown(; default_backends = backends)
+ generate_graph_attr_markdown()
+ generate_colorschemes_markdown()
+ end
+ end
+
+ for (pkg, dest) in (
+ (PlotThemes, "plotthemes.md"),
+ (StatsPlots, "statsplots.md"),
+ )
+ cp(pkgdir(pkg, "README.md"), joinpath(GEN_DIR, dest); force = true)
+ end
+
+ gallery = Pair{String, String}[]
+ gallery_assets, gallery_callbacks, user_gallery = map(_ -> [], 1:3)
+ needs_rng_fix = Dict{Symbol, Any}()
+ if :gallery ∈ build_cmds
+ @info "gallery"
+
+ @time "gallery" for pkg in packages
+ be = packages_backends[pkg]
+ needs_rng_fix[pkg] = generate_cards(joinpath(@__DIR__, "gallery"), be, slice; debug)
+ let (path, cb, asset) = makedemos(
+ joinpath(@__DIR__, "gallery", string(be));
+ root = @__DIR__, src = joinpath(work_dir, "gallery"), edit_branch = BRANCH
+ )
+ push!(gallery, string(pkg) => joinpath("gallery", path))
+ push!(gallery_callbacks, cb)
+ push!(gallery_assets, asset)
+ end
+ end
+ if !debug
+ user_gallery, cb, assets = makedemos(
+ joinpath("user_gallery");
+ root = @__DIR__, src = work_dir, edit_branch = BRANCH
+ )
+ push!(gallery_callbacks, cb)
+ push!(gallery_assets, assets)
+ unique!(gallery_assets)
+ @show user_gallery gallery_assets
+ end
+ end
+
+ pages = Any["Home" => "index.md"]
+ if debug
+ :gallery ∈ build_cmds && push!(pages, "Gallery" => gallery)
+ :generate ∈ build_cmds && push!(pages, "Series Attributes" => "generated/attributes_series.md")
+ else # release
+ push!(
+ pages,
+ "Getting Started" => [
+ "Installation" => "install.md",
+ "Basics" => "basics.md",
+ "Tutorial" => "tutorial.md",
+ "Series Types" => [
+ "Contour Plots" => "series_types/contour.md",
+ "Histograms" => "series_types/histogram.md",
+ ],
+ ]
+ )
+ :generate ∈ build_cmds && push!(
+ pages,
+ "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",
+ ],
+ )
+ push!(
+ pages,
+ "Learning" => "learning.md",
+ "Contributing" => "contributing.md"
+ )
+ :generate ∈ build_cmds && push!(
+ pages,
+ "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",
+ ]
+ )
+ push!(pages, "Advanced Topics" => ["Plot objects" => "plot_objects.md", "Plotting pipeline" => "pipeline.md"])
+ :generate ∈ build_cmds && push!(
+ pages,
+ "Gallery" => gallery,
+ "User Gallery" => user_gallery
+ )
+ push!(pages, "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 length(gallery) selected_pages pages
+
+ n = 0
+ @time "copy to src" for (root, dirs, files) in walkdir(SRC_DIR)
+ prefix = replace(root, SRC_DIR => WRK_DIR)
+ foreach(dir -> mkpath(joinpath(WRK_DIR, dir)), dirs)
+ for file in files
+ _, ext = splitext(file)
+ (ext == ".md" && file ∉ selected_pages) && continue
+ src = joinpath(root, file)
+ dst = joinpath(prefix, file)
+ if debug
+ endswith(root, r"RecipesBase|RecipesPipeline|UnitfulExt|GraphRecipes|StatsPlots") && continue
+ println('\t', src, " -> ", dst)
+ end
+ cp(src, dst; force = true)
+ n += 1
+ end
+ end
+ @info "copied $n source file(s) to scratch directory `$work_dir`"
+
+ if !debug
+ @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
+ @time "UnitfulExt" for (root, _, files) in walkdir(unitfulext), file in files
+ last(splitext(file)) == ".jl" || continue
+ ipath = joinpath(root, file)
+ opath = replace(ipath, src_unitfulext => joinpath(work_dir, "generated")) |> splitdir |> first
+ Literate.markdown(ipath, opath; documenter = execute)
+ nb && Literate.notebook(ipath, notebooks; execute)
+ end
+ end
+
+ failed = false
+ if :make ∈ build_cmds
+ ansicolor = Base.get_bool_env("PLOTDOCS_ANSICOLOR", true)
+ @info "makedocs ansicolor=$ansicolor"
+ try
+ @time "makedocs" makedocs(;
+ format = Documenter.HTML(;
+ size_threshold = nothing,
+ prettyurls = Base.get_bool_env("CI", false),
+ assets = [joinpath(ASSETS, "favicon.ico"), gallery_assets...],
+ collapselevel = 2,
+ edit_link = BRANCH,
+ ansicolor,
+ ),
+ root = @__DIR__,
+ source = work_dir,
+ build = bld_dir,
+ # 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
+ end
+ failed && return # don't deploy and post-process on failure
+
+ if :deploy ∈ build_cmds
+ @info "gallery callbacks" # URL redirection for DemoCards-generated gallery
+ @time "gallery callbacks" foreach(cb -> cb(), gallery_callbacks)
+
+ @info "post-process gallery html files to remove `rng` in user displayed code in gallery"
+ # 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]
+ @time "post-process `rng`" for pkg in packages
+ be = packages_backends[pkg]
+ prefix = joinpath(BLD_DIR, "gallery", string(be), "generated" * suffix)
+ must_fix = needs_rng_fix[pkg]
+ for file in Glob.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 in 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" maxlog = 10
+ @assert count > 0 "idx=$idx - count=$count - file=$file"
+ end
+ end
+ end
+
+ # post-process files for edit link
+ @info "post-process work dir to fix edit link in `html` files"
+ @time "post-process work dir" for file in vcat(
+ Glob.glob("**/*.html", BLD_DIR), # NOTE: this does not match BLD_DIR/index.html :/
+ Glob.glob("*.html", BLD_DIR), # I don't understand how Glob works :/
+ ) |> unique!
+ # @show file
+ lines = readlines(file; keep = true)
+ any(line -> occursin(joinpath("blob", BRANCH, "docs"), line), lines) || continue
+ old = joinpath("blob", BRANCH, "docs", work_dir)
+ new = joinpath("blob", BRANCH, "docs", src_dir)
+ @info "fixing $file $old -> $new" maxlog = 10
+ open(file, "w") do io
+ foreach(line -> write(io, replace(line, old => new, joinpath(WRK_DIR) => new)), lines)
+ end
+ end
+
+ debug && for (rt, dirs, fns) in walkdir(BLD_DIR)
+ if length(dirs) > 0
+ println("dirs in $rt:")
+ foreach(d -> println('\t', joinpath(rt, d)), dirs)
+ end
+ if length(fns) > 0
+ println("files in $rt:")
+ foreach(f -> println('\t', joinpath(rt, f)), fns)
+ end
+ end
+
+ @info "deploydocs"
+ repo = "JuliaPlots/Plots.jl"
+ @time "deploydocs" withenv("GITHUB_REPOSITORY" => repo) do
+ deploydocs(;
+ root = @__DIR__,
+ target = bld_dir,
+ deploy_repo = "github.com/JuliaPlots/PlotDocs.jl", # see https://documenter.juliadocs.org/stable/man/hosting/#Out-of-repo-deployment
+ repo_previews = "github.com/JuliaPlots/PlotDocs.jl",
+ push_preview = Base.get_bool_env("PLOTDOCS_PUSH_PREVIEW", false),
+ devbranch = BRANCH,
+ forcepush = true,
+ repo,
+ )
+ end
+ end
+ @info "done !"
+ return nothing
+end
+
+@main
diff --git a/docs/src/GraphRecipes/examples.md b/docs/src/GraphRecipes/examples.md
new file mode 100644
index 0000000000..76a6bc3f02
--- /dev/null
+++ b/docs/src/GraphRecipes/examples.md
@@ -0,0 +1,183 @@
+```@setup graphexamples
+using Plots, GraphRecipes, Graphs, LinearAlgebra, SparseArrays, AbstractTrees; gr()
+PlotsBase.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)
+```
+
+
+#### 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)
+```
+
+
+#### 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 0000000000..7b05490173
--- /dev/null
+++ b/docs/src/GraphRecipes/introduction.md
@@ -0,0 +1,25 @@
+```@setup graphintro
+using Plots, GraphRecipes; gr()
+PlotsBase.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 0000000000..8d25af3066
--- /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 0000000000..ed872257c0
--- /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 0000000000..057d366073
--- /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 0000000000..29741c3350
--- /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 0000000000..80e6c78515
--- /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 0000000000..6ec819cfc4
--- /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 0000000000..638bbcc6ef
--- /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 0000000000..38052dfad2
--- /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 0000000000..5bee733797
--- /dev/null
+++ b/docs/src/UnitfulExt/unitfulext_examples.jl
@@ -0,0 +1,223 @@
+#---------------------------------------------------------
+# # [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")
+
+# If you want it untouched, set the `yunitformat` to `:nounit`.
+
+plot(y, ylabel = "mass in kilograms", yunitformat = :nounit)
+
+# 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 = PlotsBase.Axes.UNIT_FORMATS |> keys
+
+# which correspond to these unit formats:
+
+plot([plot(y, ylab = "mass", title = repr(s), unitformat = s) for s in URsymbols]..., size = (800, 600))
+
+# `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]..., size = (600, 600))
+
+# 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 choose axis units. However, doing this
+# after the first series is plotted will produce incorrect plots--units get stripped
+# according to the current units for each axis. So, this works:
+
+plot(y, yunit = u"g")
+
+
+# This will be wrong:
+
+plot(y)
+plot!(2y, 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"))
+
+# To specify colorbar units and unit formatting, use `zunit`, `zunitformat`,
+# and `cbar_title`:
+
+heatmap((1:5)u"μs", 1:4, rand(5, 4)u"m", zunit = u"cm", zunitformat = :square, cbar_title = "dist")
+
+# ## 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 0000000000..171e945f9d
--- /dev/null
+++ b/docs/src/UnitfulExt/unitfulext_plots.jl
@@ -0,0 +1,199 @@
+#---------------------------------------------------------
+# # [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 in 1:10]
+ys = [string("y", i) for i in 1:4]
+z = float((1:4) * reshape(1:10, 1, :)) * u"km"
+heatmap(xs, ys, z, aspect_ratio = 1, colorbar_title = "dist", zunitformat = :square)
+
+# ## 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)
+
+# ## Shared axes
+
+x = range(0.0u"s", 10.0u"s", length = 21)
+y = x * 5u"m/s" .+ 1u"m"
+pl = plot(x, y)
+pl2 = twinx()
+plot!(pl2, x, 1 ./ y, ylabel = "inverse distance")
diff --git a/docs/src/animations.md b/docs/src/animations.md
new file mode 100644
index 0000000000..084f29d455
--- /dev/null
+++ b/docs/src/animations.md
@@ -0,0 +1,74 @@
+```@setup animations
+using Plots; gr()
+PlotsBase.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 0000000000..2137dfcbc7
--- /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 0000000000..7a5e516eb5
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 0000000000..37b4c6e57c
--- /dev/null
+++ b/docs/src/assets/axis_logo.svg
@@ -0,0 +1,146 @@
+
+
+
+
diff --git a/docs/src/assets/axis_logo_600x400.png b/docs/src/assets/axis_logo_600x400.png
new file mode 100644
index 0000000000..2d212c3d25
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 0000000000..73f818ed2d
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 0000000000..00316cdaf5
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 0000000000..7a5e516eb5
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 0000000000..1eb3b10502
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 0000000000..767e3472ae
--- /dev/null
+++ b/docs/src/attributes.md
@@ -0,0 +1,123 @@
+# [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
+plotattr("size") # specific attribute example
+```
+
+!!! 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, it aliases to `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, it aliases to `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, it aliases to `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 your 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 0000000000..6088f06e15
--- /dev/null
+++ b/docs/src/backends.md
@@ -0,0 +1,501 @@
+```@setup backends
+using StatsPlots
+using Plots, RecipesBase, Statistics; gr()
+PlotsBase.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 `API`s: 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`, `Gaston` |
+| interactivity | `PythonPlot`, `Plotly(JS)` |
+| beauty | `GR, Plotly(JS)`, `PGFPlotsX` |
+| REPL plotting | `UnicodePlots` |
+| 3D plots | `GR, PythonPlot`, `Plotly(JS)`, `UnicodePlots`, `Gaston` |
+| a GUI window | `GR, PythonPlot`, `PlotlyJS`, `Gaston` |
+| 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, etc ...
+
+---
+
+## [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.
+
+In order to save a `Plotly` figure in a format different than `html` (e.g. `pdf` or `png`), one needs to install and import `PlotlyKaleido` explicitly in order to trigger the `Plots` [extension](https://pkgdocs.julialang.org/v1/creating-packages/#Conditional-loading-of-code-in-packages-(Extensions)).
+
+```@example backends
+plotlyjs(); backendplot(n = 2) #hide
+png("backends_plotlyjs.png") #hide
+```
+
+
+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`](https://matplotlib.org).
+It uses [`PythonCall.jl`](https://juliapy.github.io/PythonCall.jl/stable/) 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
+```
+
+## [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.
+
+
+
+**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
+
+## [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)
+
+### [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 0000000000..2d6bfe533a
--- /dev/null
+++ b/docs/src/basics.md
@@ -0,0 +1,87 @@
+### 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()`.
+
+---
+
+### Environment variables
+
+A few environment variables control `PlotsBase` internals:
+- `PLOTSBASE_HOST_DEPENDENCY_LOCAL`: use a local `plotly` resource instead of a cloud hosted one.
+- `PLOTSBASE_DEFAULT_BACKEND`: default backend, preempting the one set through the `Preferences` based mechanism.
+- `PLOTSBASE_TMPDIR`: temporary files prefix for (html) files (some browser are denied access to temporary directories such as `/tmp`).
+
+#### For testing purposes
+- `PLOTSBASE_PLOTLYJS_UNSAFE_ELECTRON`: use a workaround for the `ECONNREFUSED` error when using the `plotlyjs` backend
+- `PLOTSBASE_TEST_PACKAGES`: list of backends to run the tests for.
+- `PLOTSBASE_REFERENCE_DIR`: where to find the reference images.
+- `PLOTSBASE_IMG_TOL`: tolerance when comparing images during tests.
+
+#### For building documentation
+- `PLOTDOCS_PUSH_PREVIEW`: allow pushing the docs previews for pull requests.
+- `PLOTDOCS_ANSICOLOR`: use ansicolor in julia code output, mostly for `unicodeplots`.
+- `PLOTDOCS_PACKAGES`: restrict the docs build to these backends, e.g. `PLOTDOCS_PACKAGES='GR UnicodePlots`.
+- `PLOTDOCS_EXAMPLES`: restrict the docs build to these examples, e.g. `PLOTDOCS_EXAMPLES='1 2 5`.
+- `PLOTDOCS_SUFFIX`: docs build suffix in order to build multiple docs versions at a time.
+
+### Special julia variables
+- `PLOTSBASE_DEFAULTS`: a way to set default values for plotattributes (e.g. in your `startup.jl` file) : it should be a dictionary.
diff --git a/docs/src/colors.md b/docs/src/colors.md
new file mode 100644
index 0000000000..b4217f6506
--- /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 0000000000..01475639e7
--- /dev/null
+++ b/docs/src/colorschemes.md
@@ -0,0 +1,81 @@
+```@setup colors
+using Plots; gr()
+PlotsBase.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 0000000000..8690fe3369
--- /dev/null
+++ b/docs/src/contributing.md
@@ -0,0 +1,304 @@
+```@setup contributing
+using Plots; gr()
+PlotsBase.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)...
+
+
+
+---
+
+## 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 the development branch (can be `master` or `v2`), where `v2` is the "bleeding edge".
+- **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.
+
+```bash
+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:
+
+```bash
+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:
+
+```bash
+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 Runic; exit(Runic.main(ARGS))' -- --inplace .
+```
+
+#### 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:
+
+```bash
+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).
+
+`Plots` has some [pre-commit](https://pre-commit.com) hooks configuration set in order to enhance code quality.
+Run `pre-commit install` in the `Plots.jl` repository to set up the git hook scripts. After this, checks should automatically run when using `git commit`.
+One can also run the `pre-commit` checks manually using `pre-commit run --all-files`.
+
+#### 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:
+
+```bash
+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
+Testing individual `Plots` components suche as `PlotsBase` is as simple as :
+```bash
+$ cd Plots.jl/PlotsBase
+$ julia --project=. -e 'using Pkg; Pkg.test()'
+```
+
+, or for `RecipesBase` :
+```bash
+$ cd Plots.jl
+$ julia --project=RecipesBase -e 'using Pkg; Pkg.test()'
+```
+
+#### 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:
+
+```bash
+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 0000000000..e22e780571
--- /dev/null
+++ b/docs/src/ecosystem.md
@@ -0,0 +1,120 @@
+```@setup ecosystem
+using StatsPlots, Plots, RDatasets, Distributions; gr()
+PlotsBase.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.
+
+
+
+## [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.
+
+
+
+
+## [JuliaML](https://github.com/JuliaML)
+Tools, models, and math related to machine learning in Julia.
+
+
+
+## [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.
+
+
+
+## [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.
+
+
+
+## [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.
+
+
+
+## [EEG](https://github.com/codles/EEG.jl)
+Process EEG files and visualize brain activity.
+
+
+
+
+
+## [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.
+
+
+
+## [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.
+
+
+
+## [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
+
+
+
+## [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.
+
+
+
+
+## [AverageShiftedHistograms](https://github.com/joshday/AverageShiftedHistograms.jl)
+Density estimation using Average Shifted Histograms.
+
+
+
+## [MLPlots](https://github.com/JuliaML/MLPlots.jl)
+Common plotting recipes for statistics and machine learning.
+
+
+
+
+
+
+## [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):
+
+
+
+---
+
+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 0000000000..5203c68f0c
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,137 @@
+```@setup index
+using Plots; gr()
+PlotsBase.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 0000000000..d84eea3fe8
--- /dev/null
+++ b/docs/src/input_data.md
@@ -0,0 +1,255 @@
+```@setup input_data
+using Plots; gr()
+PlotsBase.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")
+PlotsBase.svg("input_data_1") # hide
+nothing # hide
+```
+
+
+## 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 = PlotsBase.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 = PlotsBase.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)
+windowrange(dim, denom) =
+ range(0, 1; length = max(3, round(Int, dim/denom)))[2:end - 1]
+
+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 0000000000..c67ab340ec
--- /dev/null
+++ b/docs/src/install.md
@@ -0,0 +1,76 @@
+### Install
+First, add the package:
+
+```julia
+import Pkg
+Pkg.add("Plots") # ≡ `PlotsBase` + `GR` backend
+
+Pkg.add("PlotBase", "PythonPlot") # `PlotsBase` + `PythonPlot` backend, which avoids installing the `GR` backend
+
+# if you want the latest features:
+Pkg.pkg"add Plots#v2"
+```
+
+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("PythonPlot") # depends only on PythonPlot package
+
+Pkg.add("PGFPlotsX") # you need to have LaTeX installed on your system
+
+Pkg.add("PlotlyJS")
+# 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("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
+pythonplot() # activate the chosen backend
+```
+
+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 0000000000..7f15e1a55b
--- /dev/null
+++ b/docs/src/layouts.md
@@ -0,0 +1,112 @@
+```@setup layouts
+using Plots; gr()
+PlotsBase.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: it 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)
+)
+```
+
+---
+### Inset subplots
+
+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 PlotsBase # hide
+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, 20PlotsBase.px, 100PlotsBase.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 (they will be rendered as 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 0000000000..1647681f1c
--- /dev/null
+++ b/docs/src/learning.md
@@ -0,0 +1,28 @@
+# 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 0000000000..877ff12b55
--- /dev/null
+++ b/docs/src/output.md
@@ -0,0 +1,62 @@
+# [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
+`Plot`s 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)
+```
+
+### 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 `PlotsBase.pdf` or `PlotsBase.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` | `plotlyjs`, `pythonplot` |
+| `html` | `plotly`, `plotlyjs ` |
+| `json` | `plotly`, `plotlyjs` |
+| `pdf` | `gr`, `pythonplot`, `plotlyjs`, `pgfplotsx`, `gaston` |
+| `png` | `gr`, `pythonplot`, `unicodeplots`, `plotlyjs`, `pgfplotsx`, `gaston` |
+| `ps ` | `gr`, `pythonplot` |
+| `svg` | `gr`, `pythonplot`, `pgfplotsx`, `plotlyjs`, `gaston` |
+| `tex` | `pythonplot`, `pgfplotsx` |
+| `text` | `unicodeplots`, `hdf5` |
+
+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 0000000000..639e7bef76
--- /dev/null
+++ b/docs/src/pipeline.md
@@ -0,0 +1,144 @@
+```@setup pipeline
+using Plots; gr()
+PlotsBase.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.gcf() #hide
+```
+
+---
+
+### [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
+PlotsBase.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 0000000000..1fe49a4b88
--- /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
+PlotsBase.Plot
+PlotsBase.Subplot
+PlotsBase.Series
+```
diff --git a/docs/src/recipes.md b/docs/src/recipes.md
new file mode 100644
index 0000000000..a869342658
--- /dev/null
+++ b/docs/src/recipes.md
@@ -0,0 +1,478 @@
+```@setup recipes
+using Plots; gr()
+PlotsBase.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
+
+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 0000000000..52ef7b58f4
--- /dev/null
+++ b/docs/src/series_types/contour.md
@@ -0,0 +1,146 @@
+```@setup contour
+using Plots
+PlotsBase.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 0000000000..c7489cb057
--- /dev/null
+++ b/docs/src/series_types/histogram.md
@@ -0,0 +1,125 @@
+```@setup histogram
+using Plots; gr()
+PlotsBase.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 0000000000..7aa86c5131
--- /dev/null
+++ b/docs/src/tutorial.md
@@ -0,0 +1,525 @@
+```@setup tutorial
+using Plots; gr()
+PlotsBase.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
+```
+
+
+```@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 0000000000..5a97b60487
--- /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 0000000000..ee84430465
--- /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 0000000000..1df675c316
--- /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 0000000000..7120e2d27e
--- /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
+ )
+ return 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 0000000000..7a786fefa4
--- /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
+ return 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 in 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/FileIOExt.jl b/ext/FileIOExt.jl
deleted file mode 100644
index 2aba42921b..0000000000
--- a/ext/FileIOExt.jl
+++ /dev/null
@@ -1,36 +0,0 @@
-module FileIOExt
-
-import Plots: Plots, Plot, @ext_imp_use
-@ext_imp_use :import FileIO
-
-_fileio_load(@nospecialize(filename::AbstractString)) =
- FileIO.load(filename::AbstractString)
-_fileio_save(@nospecialize(filename::AbstractString), @nospecialize(x)) =
- FileIO.save(filename::AbstractString, x)
-
-function _show_pdfbackends(io::IO, ::MIME"image/png", plt::Plot)
- fn = tempname()
-
- # first save a pdf file
- Plots.pdf(plt, fn)
-
- # load that pdf into a FileIO Stream
- s = _fileio_load("$fn.pdf")
-
- # save a png
- pngfn = "$fn.png"
- _fileio_save(pngfn, s)
-
- # 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)
-end
-
-end # module
diff --git a/ext/IJuliaExt.jl b/ext/IJuliaExt.jl
deleted file mode 100644
index 5322c155ba..0000000000
--- 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 d03dc4aea2..0000000000
--- 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/ext/UnitfulExt.jl b/ext/UnitfulExt.jl
deleted file mode 100644
index 34b2fc9501..0000000000
--- a/ext/UnitfulExt.jl
+++ /dev/null
@@ -1,345 +0,0 @@
-# previously https://github.com/jw3126/UnitfulRecipes.jl
-# authors: Benoit Pasquier (@briochemc) - David Gustavsson (@gustaphe) - Jan Weidner (@jw3126)
-
-module UnitfulExt
-
-import Plots: Plots, @ext_imp_use, @recipe, PlotText, Subplot, AVec, AMat, Axis
-import RecipesBase
-@ext_imp_use :import Unitful Quantity unit ustrip Unitful dimension Units NoUnits LogScaled logunit MixedUnits Level Gain uconvert
-import LaTeXStrings: LaTeXString
-import Latexify: latexify
-using UnitfulLatexify
-
-const MissingOrQuantity = Union{Missing,<:Quantity,<:LogScaled}
-
-#==========
-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
- u = get(plotattributes, :zunit, _unit(eltype(x)))
- ustripattribute!(plotattributes, :clims, u)
- append_unit_if_needed!(plotattributes, :colorbar_title, u)
- end
- fixaxis!(plotattributes, x, axisletter)
-end
-
-function fixaxis!(attr, x, axisletter)
- # Attribute keys
- axislabel = Symbol(axisletter, :guide) # xguide, yguide, zguide
- axislims = Symbol(axisletter, :lims) # xlims, ylims, zlims
- axisticks = Symbol(axisletter, :ticks) # xticks, yticks, zticks
- err = Symbol(axisletter, :error) # xerror, yerror, zerror
- axisunit = Symbol(axisletter, :unit) # xunit, yunit, zunit
- axis = Symbol(axisletter, :axis) # xaxis, yaxis, zaxis
- u = pop!(attr, axisunit, _unit(eltype(x))) # get the unit
- # if the subplot already exists with data, get its unit
- sp = get(attr, :subplot, 1)
- if sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0
- label = attr[:plot_object][sp][axis][:guide]
- u = getaxisunit(label)
- get!(attr, axislabel, label) # if label was not given as an argument, reuse
- end
- # fix the attributes: labels, lims, ticks, marker/line stuff, etc.
- append_unit_if_needed!(attr, axislabel, u)
- ustripattribute!(attr, err, u)
- if axisletter === :y
- ustripattribute!(attr, :ribbon, u)
- ustripattribute!(attr, :fillrange, u)
- end
- fixaspectratio!(attr, u, axisletter)
- fixmarkercolor!(attr)
- fixmarkersize!(attr)
- fixlinecolor!(attr)
- _ustrip.(u, x) # strip the unit
-end
-
-# Recipe for (x::AVec, y::AVec, z::Surface) types
-@recipe function f(x::AVec, y::AVec, z::AMat{T}) where {T<:Quantity} # COV_EXCL_LINE
- u = get(plotattributes, :zunit, _unit(eltype(z)))
- ustripattribute!(plotattributes, :clims, u)
- z = fixaxis!(plotattributes, z, :z)
- append_unit_if_needed!(plotattributes, :colorbar_title, u)
- x, y, z
-end
-
-# Recipe for vectors of vectors
-@recipe function f(::Type{T}, x::T) where {T<:AVec{<:AVec{<:MissingOrQuantity}}} # COV_EXCL_LINE
- axisletter = plotattributes[:letter] # x, y, or z
- unitsymbol = Symbol(axisletter, :unit)
- axisunit = pop!(plotattributes, unitsymbol, _unit(eltype(first(x))))
- map(
- x -> (
- plotattributes[unitsymbol] = axisunit; fixaxis!(plotattributes, x, axisletter)
- ),
- x,
- )
-end
-
-# Recipe for bare units
-@recipe function f(::Type{T}, x::T) where {T<:Units} # COV_EXCL_LINE
- primary := false
- Float64[] * x
-end
-
-# Recipes for functions
-@recipe f(f::Function, x::T) where {T<:AVec{<:MissingOrQuantity}} = x, f.(x)
-@recipe f(x::T, f::Function) where {T<:AVec{<:MissingOrQuantity}} = x, f.(x)
-@recipe f(x::T, y::AVec, f::Function) where {T<:AVec{<:MissingOrQuantity}} = x, y, f.(x', y)
-@recipe f(x::AVec, y::T, f::Function) where {T<:AVec{<:MissingOrQuantity}} = x, y, f.(x', y)
-@recipe function f( # COV_EXCL_LINE
- x::T1,
- y::T2,
- f::Function,
-) where {T1<:AVec{<:MissingOrQuantity},T2<:AVec{<:MissingOrQuantity}}
- x, y, f.(x', y)
-end
-@recipe function f(f::Function, u::Units) # COV_EXCL_LINE
- uf = UnitFunction(f, [u])
- recipedata = RecipesBase.apply_recipe(plotattributes, uf)
- _, xmin, xmax = recipedata[1].args
- f, xmin * u, xmax * u
-end
-
-"""
-```julia
-UnitFunction
-```
-A function, bundled with the assumed units of each of its inputs.
-
-```julia
-f(x, y) = x^2 + y
-uf = UnitFunction(f, u"m", u"m^2")
-uf(3, 2) == f(3u"m", 2u"m"^2) == 7u"m^2"
-```
-"""
-struct UnitFunction <: Function
- f::Function
- u::Vector{Units}
-end
-(f::UnitFunction)(args...) = f.f((args .* f.u)...)
-
-#===============
-Attribute fixing
-===============#
-# Aspect ratio
-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)
- return
- end
- if aspect_ratio === :equal
- aspect_ratio = 1
- end
- #=======================================================================================
- Implementation example:
-
- Consider an x axis in `u"m"` and a y axis in `u"s"`, and an `aspect_ratio` in `u"m/s"`.
- 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
- to compare `0 < 1u"m"`.
- =======================================================================================#
- if axisletter === :y
- attr[:aspect_ratio] = aspect_ratio * u
- elseif axisletter === :x
- attr[:aspect_ratio] = aspect_ratio / u
- end
- nothing
-end
-
-# Markers / lines
-function fixmarkercolor!(attr)
- u = ustripattribute!(attr, :marker_z)
- ustripattribute!(attr, :clims, u)
- u == NoUnits || append_unit_if_needed!(attr, :colorbar_title, u)
-end
-fixmarkersize!(attr) = ustripattribute!(attr, :markersize)
-fixlinecolor!(attr) = ustripattribute!(attr, :line_z)
-
-# strip unit from attribute[key]
-ustripattribute!(attr, key) =
- if haskey(attr, key)
- v = attr[key]
- u = _unit(eltype(v))
- attr[key] = _ustrip.(u, v)
- u
- else
- NoUnits
- end
-
-# if supplied, use the unit (optional 3rd argument)
-function ustripattribute!(attr, key, u)
- if haskey(attr, key)
- v = attr[key]
- if eltype(v) <: Quantity
- attr[key] = _ustrip.(u, v)
- end
- end
- u
-end
-
-#=======================================
-Label string containing unit information
-=======================================#
-
-abstract type AbstractProtectedString <: AbstractString end
-struct ProtectedString{S} <: AbstractProtectedString
- content::S
-end
-struct UnitfulString{S,U} <: AbstractProtectedString
- content::S
- unit::U
-end
-# Minimum required AbstractString interface to work with Plots
-const S = AbstractProtectedString
-Base.iterate(n::S) = iterate(n.content)
-Base.iterate(n::S, i::Integer) = iterate(n.content, i)
-Base.codeunit(n::S) = codeunit(n.content)
-Base.ncodeunits(n::S) = ncodeunits(n.content)
-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)
-
-#=====================================
-Append unit to labels when appropriate
-=====================================#
-
-append_unit_if_needed!(attr, key, u) =
- append_unit_if_needed!(attr, key, get(attr, key, nothing), u)
-# dispatch on the type of `label`
-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)
- 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(
- LaTeXString(
- format_unit_label(
- label,
- latexify(u),
- get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
- ),
- ),
- u,
- )
- else
- attr[key] = UnitfulString(
- S(
- format_unit_label(
- label,
- u,
- get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
- ),
- ),
- u,
- )
- end
-end
-
-#=============================================
-Surround unit string with specified delimiters
-=============================================#
-
-const UNIT_FORMATS = Dict(
- :round => ('(', ')'),
- :square => ('[', ']'),
- :curly => ('{', '}'),
- :angle => ('<', '>'),
- :slash => '/',
- :slashround => (" / (", ")"),
- :slashsquare => (" / [", "]"),
- :slashcurly => (" / {", "}"),
- :slashangle => (" / <", ">"),
- :verbose => " in units of ",
- :none => nothing,
-)
-
-format_unit_label(l, u, f::Nothing) = string(l, ' ', u)
-format_unit_label(l, u, f::Function) = f(l, u)
-format_unit_label(l, u, f::AbstractString) = string(l, f, u)
-format_unit_label(l, u, f::NTuple{2,<:AbstractString}) = string(l, f[1], u, f[2])
-format_unit_label(l, u, f::NTuple{3,<:AbstractString}) = string(f[1], l, f[2], u, f[3])
-format_unit_label(l, u, f::Char) = string(l, ' ', f, ' ', u)
-format_unit_label(l, u, f::NTuple{2,Char}) = string(l, ' ', f[1], u, f[2])
-format_unit_label(l, u, f::NTuple{3,Char}) = string(f[1], l, ' ', f[2], u, f[3])
-format_unit_label(l, u, f::Bool) = f ? format_unit_label(l, u, :round) : format_unit_label(l, u, nothing)
-format_unit_label(l, u, f::Symbol) = format_unit_label(l, u, UNIT_FORMATS[f])
-
-getaxisunit(::AbstractString) = NoUnits
-getaxisunit(s::UnitfulString) = s.unit
-getaxisunit(a::Axis) = getaxisunit(a[:guide])
-
-#==============
-Fix annotations
-===============#
-function Plots.locate_annotation(
- sp::Subplot,
- x::MissingOrQuantity,
- y::MissingOrQuantity,
- label::PlotText,
-)
- xunit = getaxisunit(sp.attr[:xaxis])
- yunit = getaxisunit(sp.attr[:yaxis])
- (_ustrip(xunit, x), _ustrip(yunit, y), label)
-end
-function Plots.locate_annotation(
- sp::Subplot,
- x::MissingOrQuantity,
- y::MissingOrQuantity,
- z::MissingOrQuantity,
- label::PlotText,
-)
- xunit = getaxisunit(sp.attr[:xaxis])
- yunit = getaxisunit(sp.attr[:yaxis])
- zunit = getaxisunit(sp.attr[:zaxis])
- (_ustrip(xunit, x), _ustrip(yunit, y), _ustrip(zunit, z), label)
-end
-function Plots.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)
-end
-
-#==================#
-# ticks and limits #
-#==================#
-Plots._transform_ticks(ticks::AbstractArray{T}, axis) where {T<:Quantity} =
- _ustrip.(getaxisunit(axis), ticks)
-Plots.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} =
- _ustrip.(getaxisunit(axis), lims)
-
-function _ustrip(u, x)
- u isa MixedUnits && return ustrip(uconvert(u, x))
- ustrip(u, x)
-end
-
-function _unit(x)
- (t = eltype(x)) <: LogScaled && return logunit(t)
- unit(x)
-end
-
-function Plots.pgfx_sanitize_string(s::UnitfulString)
- UnitfulString(Plots.pgfx_sanitize_string(s.content), s.unit)
-end
-
-end # module
diff --git a/src/Plots.jl b/src/Plots.jl
index e3610d9bd1..3495a138b6 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,
+import Reexport
+Reexport.@reexport using PlotsBase
- savefig,
- png,
- gui,
- inline,
- closeall,
-
- 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
+if PlotsBase.DEFAULT_BACKEND == "gr"
+ @debug "loading default GR"
+ import GR
+end
-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")
+function __init__()
+ ccall(:jl_generating_output, Cint, ()) == 1 && return
+ PlotsBase.default_backend()
+ return nothing
+end
end
diff --git a/src/arg_desc.jl b/src/arg_desc.jl
deleted file mode 100644
index 33eb6cd7cc..0000000000
--- a/src/arg_desc.jl
+++ /dev/null
@@ -1,204 +0,0 @@
-const AStr = AbstractString
-const ColorType = Union{Symbol,Colorant,PlotUtils.ColorSchemes.ColorScheme,Integer}
-const TicksType = Union{AVec{Real},Tuple{AVec{Real},AVec{AStr}},Symbol,Bool,Nothing}
-
-# NOTE: when updating `arg_desc`, don't forget to modify `PlotDocs.make_attr_df` accordingly.
-const _arg_desc = KW(
- # series args
- :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)"),
- :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)."),
- :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)."),
- :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."),
- :bins => (Union{Integer,NTuple{2,Integer},AVec,Symbol}, """
- Default is :auto (the Freedman-Diaconis rule). For histogram-types, defines the approximate number of bins to aim for, or the auto-binning algorithm to use (:sturges, :sqrt, :rice, :scott or :fd).
- For fine-grained control pass a Vector of break values, e.g. `range(minimum(x), stop = maximum(x), length = 25)`."""),
- :smooth => (Bool, "Add a regression line ?"),
- :group => (AVec, "Data is split into a separate series, one for each unique value in `group`."),
- :x => (Any, "Input data (first dimension)."),
- :y => (Any, "Input data (second dimension)."),
- :z => (Any, "Input data (third dimension). May be wrapped by a `Surface` for surface and heatmap types."),
- :marker_z => (Union{AVec,Function}, "z-values for each series data point, which correspond to the color to be used from a markercolor gradient (`f(x,y,z) -> z_value` or `f(x,y) -> z_value`)."),
- :line_z => (Union{AVec,Function}, "z-values for each series line segment, which correspond to the color to be used from a linecolor gradient. Note that for N points, only the first N-1 values are used (one per line-segment)."),
- :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_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)."),
- :ribbon => (Union{Real,AVec}, "Creates a fillrange around the data points."),
- :quiver => (Union{AVec,NTuple{2,AVec}}, "The directional vectors U,V which specify velocity/gradient vectors for a quiver plot."),
- :arrow => (Union{Bool,Arrow}, "Defines arrowheads that should be displayed at the end of path line segments (just before a NaN and the last non-NaN point). Used in quiverplot, streamplot, or similar."),
- :normalize => (Union{Bool,Symbol}, "Histogram normalization mode. Possible values are: false/:none (no normalization, default), true/:pdf (normalize to a discrete PDF, where the total area of the bins is 1), :probability (bin heights sum to 1) and :density (the area of each bin, rather than the height, is equal to the counts - useful for uneven bin sizes)."),
- :weights => (AVec, "Used in histogram types for weighted counts."),
- :show_empty_bins => (Bool, "Whether empty bins in a 2D histogram are colored as 0 (true), or transparent (the default)."),
- :contours => (Bool, "Add contours to the side-grids of 3D plots? Used in surface/wireframe."),
- :contour_labels => (Bool, "Show labels at the contour lines ?"),
- :match_dimensions => (Bool, "For heatmap types: should the first dimension of a matrix (rows) correspond to the first dimension of the plot (`x`-axis) ? Defaults to `false`, which matches the behavior of Matplotlib, Plotly, and others. Note: when passing a function for `z`, the function should still map `(x,y) -> z`."),
- :subplot => (Union{Integer,Subplot}, "The subplot that this series belongs to."),
- :series_annotations => (Union{AVec,AStr,PlotText}, "These are annotations which are mapped to data points/positions."),
- :primary => (Bool, "Does this count as a 'real series'? For example, you could have a path (primary), and a scatter (secondary) as two separate series, maybe with different data (see `sticks` recipe for an example). The secondary series will get the same color, etc as the primary."),
- :hover => (AVec{AStr}, "Text to display when hovering over each data point."),
- :colorbar_entry => (Bool, "Include this series in the color bar? Set to `false` to exclude."),
- :z_order => (Union{Symbol,Integer}, ":front (default), :back or index of position where 1 is furthest in the background."),
-
- # plot args
- :plot_title => (AStr, "Whole plot title (not to be confused with the title for individual subplots)."),
- :plot_titlevspan => (Real, "Vertical span of the whole plot title (fraction of the plot height)."),
- :background_color => (ColorType, " Base color for all backgrounds."),
- :background_color_outside => (ColorType, "Color outside the plot area(s) (`:match` matches `:background_color`)."),
- :foreground_color => (ColorType, "Base color for all foregrounds."),
- :size => (NTuple{2,Integer}, "(width_px, height_px) of the whole Plot."),
- :pos => (NTuple{2,Integer}, "(left_px, top_px) position of the GUI window (note: currently unimplemented)."),
- :window_title => (AStr, "Title of the standalone gui-window."),
- :show => (Bool, "Should this command open/refresh a GUI/display ? Allows to display plots in scripts or functions without explicitly calling `display`."),
- :layout => (Union{Integer,NTuple{2,Integer},AbstractLayout}, "Number of subplot, grid dimensions, layout (for example `grid(2,2)`), or the return from the `@layout` macro. This builds the layout of subplots."),
- :link => (Symbol, "How/whether to link axis limits between subplots. Values: `:none`, `:x` (x axes are linked by columns), `:y` (y axes are linked by rows), `:both` (x and y are linked), `:all` (every subplot is linked together regardless of layout position)."),
- :overwrite_figure => (Bool, "Should we reuse the same GUI window/figure when plotting (true) or open a new one (false)."),
- :html_output_format => (Symbol, "When writing html output, what is the format? `:png` and `:svg` are currently supported."),
- :tex_output_standalone => (Bool, "When writing tex output, should the source include a preamble for a standalone document class."),
- :inset_subplots => (AVec{NTuple{2,Any}}, "Optionally pass a vector of (parent,bbox) tuples which are the parent layout and the relative bounding box of inset subplots."),
- :dpi => (Real, "Dots Per Inch of output figures."),
- :thickness_scaling => (Real, "Scale for the thickness of all line elements like lines, borders, axes, grid lines, ... defaults to 1."),
- :display_type => (Symbol, "When supported, `display` will either open a GUI window or plot inline. Choose from (`:auto`, `:gui`, or `:inline`)."),
- :extra_kwargs => (Symbol, """
- Specify for which element extra keyword args are collected or a KW (Dict{Symbol,Any}) to pass a map of extra keyword args which may be specific to a backend. Choose from (`:plot`, `:subplot`, `:series`), defaults to `:series`.
- Example: `pgfplotsx(); scatter(1:5, extra_kwargs=Dict(:subplot=>Dict("axis line shift" => "10pt"))`."""),
- :fontfamily => (Union{AStr,Symbol}, "Default font family for title, legend entries, tick labels and guides."),
- :warn_on_unsupported => (Bool, "Warn on unsupported attributes, series types and marker shapes."),
-
- # subplot args
- :title => (AStr, "Subplot title."),
- :titlelocation => (Symbol, "Position of subplot title. Choose from (`:left`, `:center`, `:right`)."),
- :titlefontfamily => (Union{AStr,Symbol}, "Font family of subplot title."),
- :titlefontsize => (Integer, "Font pointsize of subplot title."),
- :titlefonthalign => (Symbol, "Font horizontal alignment of subplot title. Choose from (:hcenter, :left, :right, :center)."),
- :titlefontvalign => (Symbol, "Font vertical alignment of subplot title. Choose from (:vcenter, :top, :bottom, :center)."),
- :titlefontrotation => (Real, "Font rotation of subplot title."),
- :titlefontcolor => (ColorType, "Color Type. Font color of subplot title."),
- :background_color_subplot => (ColorType, "Base background color of the subplot (`:match` matches `:background_color`)."),
- :legend_background_color => (ColorType, "Background color of the legend (`:match` matches :background_color_subplot`)."),
- :background_color_inside => (ColorType, "Background color inside the plot area (under the grid) (`:match` matches :background_color_subplot`)."),
- :foreground_color_subplot => (ColorType, "Base foreground color of the subplot (`:match` matches :foreground_color`)."),
- :legend_foreground_color => (ColorType, "Foreground color of the legend (`:match` matches :foreground_color_subplot`)."),
- :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).
- 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_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."),
- :legend_font_halign => (Symbol, "Font horizontal alignment of legend entries. Choose from (:hcenter, :left, :right, :center)."),
- :legend_font_valign => (Symbol, "Font vertical alignment of legend entries. Choose from (:vcenter, :top, :bottom, :center)."),
- :legend_font_rotation => (Real, "Font rotation of legend entries."),
- :legend_title_font_color => (ColorType, "Font color of legend entries."),
- :legend_title => (AStr, "Legend title."),
- :legend_title_font_family => (Union{AStr,Symbol}, "Font family of the legend title."),
- :legend_title_font_pointsize => (Integer, "Font pointsize the legend title."),
- :legend_title_font_halign => (Symbol, "Font horizontal alignment of the legend title. Choose from (:hcenter, :left, :right, :center)."),
- :legend_title_font_valign => (Symbol, "Font vertical alignment of the legend title. Choose from (:vcenter, :top, :bottom, :center)."),
- :legend_title_font_rotation => (Real, "Font rotation of the legend title."),
- :legend_title_font_color => (ColorType, "Font color of the legend title."),
- :colorbar => (Union{Bool,Symbol}, "Show the colorbar ? A symbol specifies a colorbar position. Choose from (`:none`, `:best`, `:right`, `:left`, `:top`, `:bottom`, `:legend`): `legend` matches legend value (note: only some may be supported in each backend)."),
- :clims => (Union{NTuple{2,Real},Symbol,Function}, "Fixes the limits of the colorbar: values, `:auto`, or a function taking series data in and returning a NTuple{2,Real}."),
- :colorbar_fontfamily => (Union{AStr,Symbol}, "Font family of colorbar entries."),
- :colorbar_ticks => (TicksType, "Tick values, (tickvalues, ticklabels), `:auto`/`true`, or `:none`/`false`/`nothing` (ticks disabled)."),
- :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_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."),
- :annotations => (Union{AVec{Tuple},Tuple{Real,Real,Union{AStr,PlotText,Tuple}}}, "(x,y,text) tuple(s), where text can be String, PlotText (created with `text(args...)`), or a tuple of arguments to `text` (e.g., `(\"Label\", 8, :red, :top)`). Add one-off text annotations at the (x,y) coordinates."),
- :annotationfontfamily => (Union{AStr,Symbol}, "Font family of annotations."),
- :annotationfontsize => (Integer, "Font pointsize of annotations."),
- :annotationhalign => (Symbol, "horizontal alignment of annotations. Choose from (:hcenter, :left, :right, :center)."),
- :annotationvalign => (Symbol, "Vertical alignment of annotations. Choose from (:vcenter, :top, :bottom, :center)."),
- :annotationrotation => (Real, "Rotation of annotations in degrees."),
- :annotationcolor => (ColorType, "Annotations color."),
- :projection => (Union{AStr,Symbol}, "`3d` or `polar`."),
- :projection_type => (Symbol, "3d plots projection type: :auto (backend dependent), :persp(ective), :ortho(graphic)."),
- :aspect_ratio => (Union{Symbol,Real}, "Plot area is resized so that 1 y-unit is the same size as `aspect_ratio` x-units. With `:none`, images inherit aspect ratio of the plot area. Use `:equal` for unit aspect ratio."),
- :margin => (Union{Tuple,Real}, "Number multiplied by `mm`, `px`, etc... or Tuple `(0, :mm)`. Base for individual margins... not directly used. Specifies the extra padding around subplots."),
- :left_margin => (Union{Tuple,Real,Symbol}, "Specifies the extra padding to the left of the subplot (`:match` matches `:margin`)."),
- :top_margin => (Union{Tuple,Real,Symbol}, "Specifies the extra padding on the top of the subplot (`:match` matches `:margin`)."),
- :right_margin => (Union{Tuple,Real,Symbol}, "Specifies the extra padding to the right of the subplot (`:match` matches `:margin`)."),
- :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)."),
- :camera => (NTuple{2,Real}, "Sets the view angle (azimuthal, elevation) for 3D plots."),
-
- # axis args
- :guide => (AStr, "Axis guide (label)."),
- :guide_position => (Symbol, "Position of axis guides. Choose from (:top, :bottom, :left, :right)."),
- :lims => (Union{NTuple{2,Real},Symbol}, """
- Force axis limits. Only finite values are used (you can set only the right limit with `xlims = (-Inf, 2)` for example).
- `:round` widens the limit to the nearest round number, i.e. [0.1,3.6]=>[0.0,4.0].
- `: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)."),
- :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."),
- :tickfontfamily => (Union{AStr,Symbol}, "Font family of tick labels."),
- :tickfontsize => (Integer, "Font pointsize of tick labels."),
- :tickfonthalign => (Symbol, "Font horizontal alignment of tick labels. Choose from (:hcenter, :left, :right, :center)."),
- :tickfontvalign => (Symbol, "Font vertical alignment of tick labels. Choose from (:vcenter, :top, :bottom, :center)."),
- :tickfontrotation => (Real, "Font rotation of tick labels."),
- :tickfontcolor => (ColorType, "Font color of tick labels."),
- :guidefontfamily => (Union{AStr,Symbol}, "Font family of axes guides."),
- :guidefontsize => (Integer, "Font pointsize of axes guides."),
- :guidefonthalign => (Symbol, "Font horizontal alignment of axes guides. Choose from (:hcenter, :left, :right, :center)."),
- :guidefontvalign => (Symbol, "Font vertical alignment of axes guides. Choose from (:vcenter, :top, :bottom, :center)."),
- :guidefontrotation => (Real, "Font rotation of axes guides."),
- :guidefontcolor => (ColorType, "Font color of axes guides."),
- :foreground_color_axis => (ColorType, "Color of axis ticks (`:match` matches `:foreground_color_subplot`)."),
- :foreground_color_border => (ColorType, "Color of plot area border/spines (`:match` matches `:foreground_color_subplot`)."),
- :foreground_color_text => (ColorType, "Color of tick labels (`:match` matches `:foreground_color_subplot`)."),
- :foreground_color_guide => (ColorType, "Color of axis guides/labels (`:match` matches `:foreground_color_subplot`)."),
- :mirror => (Bool, "Switch the side of the tick labels (right or top)."),
- :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)."),
- :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)."),
- :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).
- 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."""),
- :draw_arrow => (Bool, "Draw arrow at the end of the axis."),
- :unitformat => (Union{Bool,Nothing,Symbol,Char,String,NTuple{<:Union{Char,String}},Function}, """Check examples in https://docs.juliaplots.org/stable/generated/unitfulext_examples/#Unit-formatting"""),
-)
diff --git a/src/args.jl b/src/args.jl
deleted file mode 100644
index 94c44f10b6..0000000000
--- 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 f9b733cd32..0000000000
--- 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 1a75a940a6..0000000000
--- 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 3e992dba89..0000000000
--- 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 4fc33cfd2e..0000000000
--- 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 07a435fa03..0000000000
--- 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 8e614a48ef..0000000000
--- 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 b5021087f7..0000000000
--- 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 665b518f3c..0000000000
--- 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 764ac3c7a8..0000000000
--- 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 a3f955be5a..0000000000
--- 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/layouts.jl b/src/layouts.jl
deleted file mode 100644
index 2eed33558e..0000000000
--- a/src/layouts.jl
+++ /dev/null
@@ -1,691 +0,0 @@
-
-# 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)
-
-Create a bounding box for plotting
-"""
-function bbox(x, y, w, h, oarg1::Symbol, originargs::Symbol...)
- oargs = vcat(oarg1, originargs...)
- orighor = :left
- origver = :top
- for oarg in oargs
- if oarg === :center
- orighor = origver = oarg
- elseif oarg in (:left, :right, :hcenter)
- orighor = oarg
- elseif oarg in (:top, :bottom, :vcenter)
- origver = oarg
- else
- @warn "Unused origin arg in bbox construction: $oarg"
- end
- end
- bbox(x, y, w, h; h_anchor = orighor, v_anchor = origver)
-end
-
-# create a new bbox
-function bbox(x, y, width, height; h_anchor = :left, v_anchor = :top)
- x = make_measure_hor(x)
- y = make_measure_vert(y)
- width = make_measure_hor(width)
- height = make_measure_vert(height)
- left = if h_anchor === :left
- x
- elseif h_anchor in (:center, :hcenter)
- 0.5w - 0.5width + x
- else
- 1w - x - width
- end
- top = if v_anchor === :top
- y
- elseif v_anchor in (:center, :vcenter)
- 0.5h - 0.5height + y
- else
- 1h - y - height
- end
- BoundingBox(left, top, width, height)
-end
-
-# 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))
-
-# padding_w(layout::AbstractLayout) = left_padding(layout) + right_padding(layout)
-# padding_h(layout::AbstractLayout) = bottom_padding(layout) + top_padding(layout)
-# padding(layout::AbstractLayout) = (padding_w(layout), padding_h(layout))
-
-update_position!(layout::AbstractLayout) = nothing
-update_child_bboxes!(
- layout::AbstractLayout,
- minimum_perimeter = [0mm, 0mm, 0mm, 0mm];
- 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
-
-_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)
-
-# 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)
-# for the whole layout tree. then we can compute the "padding borders" of this
-# layout as the biggest padding of the children on the perimeter. then we need to
-# recursively pass those borders back down the tree, one side at a time, but ONLY
-# to those perimeter children.
-
-function paddings(args...)
- funcs = (leftpad, toppad, rightpad, bottompad)
- args = length(args) == 1 ? ntuple(i -> first(args), Val(4)) : args
- map(i -> map(funcs[i], args[i]), Tuple(1:4))
-end
-
-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
- parent(isp) == sp || continue
- _update_min_padding!(isp)
- sp.minpad = max.(sp.minpad, isp.minpad)
- end
-
-# leftpad, toppad, rightpad, bottompad
-function _update_min_padding!(layout::GridLayout)
- map(_update_min_padding!, layout.grid)
- map(_update_inset_padding!, layout.grid)
- layout.minpad = compute_minpad(
- layout.grid[:, 1],
- layout.grid[1, :],
- layout.grid[:, end],
- layout.grid[end, :],
- )
- layout.minpad
-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
-function recompute_lengths(v)
- # dump(v)
- tot = 0pct
- cnt = 0
- for vi in v
- if vi == 0pct
- cnt += 1
- else
- tot += vi
- end
- end
- leftover = 1.0pct - tot
- if cnt > 1 && leftover.value <= 0
- error(
- "Not enough length left over in layout! v = $v, cnt = $cnt, leftover = $leftover",
- )
- end
-
- # now fill in the blanks
- map(x -> x == 0pct ? leftover / cnt : x, v)
-end
-
-# recursively compute the bounding boxes for the layout and plotarea (relative to canvas!)
-function update_child_bboxes!(layout::GridLayout, minimum_perimeter = [0mm, 0mm, 0mm, 0mm])
- nr, nc = size(layout)
-
- # create a matrix for each minimum padding direction
- minpad_left, minpad_top, minpad_right, minpad_bottom = paddings(layout.grid)
-
- # get the max horizontal (left and right) padding over columns,
- # and max vertical (bottom and top) padding over rows
- # TODO: add extra padding here
- pad_left = maximum(minpad_left, dims = 1)
- pad_top = maximum(minpad_top, dims = 2)
- pad_right = maximum(minpad_right, dims = 1)
- pad_bottom = maximum(minpad_bottom, dims = 2)
-
- # make sure the perimeter match the parent
- pad_left[1] = max(pad_left[1], leftpad(minimum_perimeter))
- pad_top[1] = max(pad_top[1], toppad(minimum_perimeter))
- pad_right[end] = max(pad_right[end], rightpad(minimum_perimeter))
- pad_bottom[end] = max(pad_bottom[end], bottompad(minimum_perimeter))
-
- # scale this up to the total padding in each direction, and limit padding to 95%
- total_pad_horizontal = min(0.95width(layout), sum(pad_left + pad_right))
- total_pad_vertical = min(0.95height(layout), sum(pad_top + pad_bottom))
-
- # now we can compute the total plot area in each direction
- total_plotarea_horizontal = width(layout) - total_pad_horizontal
- total_plotarea_vertical = height(layout) - total_pad_vertical
-
- @assert total_plotarea_horizontal > 0mm
- @assert total_plotarea_vertical > 0mm
-
- # recompute widths/heights
- layout.widths = recompute_lengths(layout.widths)
- 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
- child = layout[r, c]
-
- # get the top-left corner of this child... the first one is top-left of the parent (i.e. layout)
- child_left = c == 1 ? left(layout.bbox) : right(layout[r, c - 1].bbox)
- child_top = r == 1 ? top(layout.bbox) : bottom(layout[r - 1, c].bbox)
-
- # compute plot area
- plotarea_left = child_left + pad_left[c]
- plotarea_top = child_top + pad_top[r]
- plotarea_width = total_plotarea_horizontal * layout.widths[c]
- plotarea_height = total_plotarea_vertical * layout.heights[r]
-
- bb = BoundingBox(plotarea_left, plotarea_top, plotarea_width, plotarea_height)
- plotarea!(child, bb)
-
- # compute child bbox
- child_width = pad_left[c] + plotarea_width + pad_right[c]
- child_height = pad_top[r] + plotarea_height + pad_bottom[r]
- bbox!(child, BoundingBox(child_left, child_top, child_width, child_height))
-
- # this is the minimum perimeter as decided by this child's parent, so that
- # all children on this border have the same value
- min_child_perim = [
- c == 1 ? leftpad(layout) : pad_left[c],
- r == 1 ? toppad(layout) : pad_top[r],
- c == nc ? rightpad(layout) : pad_right[c],
- r == nr ? bottompad(layout) : pad_bottom[r],
- ]
- # recursively update the child's children
- update_child_bboxes!(child, min_child_perim)
- end
-end
-
-# for each inset (floating) subplot, resolve the relative position
-# to absolute canvas coordinates, relative to the parent's plotarea
-update_inset_bboxes!(plt::Plot) =
- for sp in plt.inset_subplots
- p_area = Measures.resolve(plotarea(sp.parent), sp[:relative_bbox])
- plotarea!(sp, p_area)
- # NOTE: `lens` example, `pgfplotsx` for non-regression
- bbox!(
- sp,
- bbox(
- left(p_area) - leftpad(sp),
- top(p_area) - toppad(sp),
- width(p_area) + leftpad(sp) + rightpad(sp),
- height(p_area) + toppad(sp) + bottompad(sp),
- ),
- )
- end
-# ----------------------------------------------------------------------
-
-calc_num_subplots(layout::AbstractLayout) = get(layout.attr, :blank, false) ? 0 : 1
-calc_num_subplots(layout::GridLayout) = sum(map(l -> calc_num_subplots(l), layout.grid))
-
-function compute_gridsize(numplts::Int, nr::Int, nc::Int)
- # figure out how many rows/columns we need
- if nr < 1
- if nc < 1
- nr = round(Int, sqrt(numplts))
- nc = ceil(Int, numplts / nr)
- else
- nr = ceil(Int, numplts / nc)
- end
- else
- nc = ceil(Int, numplts / nr)
- end
- nr, nc
-end
-
-# ----------------------------------------------------------------------
-# constructors
-
-# pass the layout arg through
-layout_args(plotattributes::AKW) = layout_args(plotattributes[:layout])
-
-function layout_args(plotattributes::AKW, n_override::Integer)
- layout, n = layout_args(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.",
- )
- end
- layout, n
-end
-
-function layout_args(n::Integer)
- nr, nc = compute_gridsize(n, -1, -1)
- GridLayout(nr, nc), n
-end
-
-function layout_args(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)
-
-function layout_args(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})
- nr = sztup[1]
- nc = ceil(Int, n / nr)
- GridLayout(nr, nc), n
-end
-
-function layout_args(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
-
-function layout_args(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]
- end
- layout_args(gl)
-end
-
-# recursively get the size of the grid
-layout_args(layout::GridLayout) = layout, calc_num_subplots(layout)
-
-layout_args(n_override::Integer, layout::Union{AbstractVecOrMat,GridLayout}) =
- layout_args(layout)
-
-# ----------------------------------------------------------------------
-
-function build_layout(args...)
- layout, n = layout_args(args...)
- build_layout(layout, n, Array{Plot}(undef, 0))
-end
-
-# n is the number of subplots...
-function build_layout(layout::GridLayout, n::Integer, plts::AVec{Plot})
- nr, nc = size(layout)
- subplots = Subplot[]
- spmap = SubplotMap()
- empty = isempty(plts)
- i = 0
- for r in 1:nr, c in 1:nc
- l = layout[r, c]
- if isa(l, EmptyLayout) && !get(l.attr, :blank, false)
- if empty
- # initialize the inner subplots recursively
- sp = Subplot(backend(), parent = layout)
- layout[r, c] = sp
- push!(subplots, sp)
- spmap[attr(l, :label, gensym())] = sp
- inc = 1
- else
- # build a layout from a list of existing Plot objects
- plt = popfirst!(plts) # grab the first plot out of the list
- layout[r, c] = plt.layout
- append!(subplots, plt.subplots)
- merge!(spmap, plt.spmap)
- inc = length(plt.subplots)
- end
- if get(l.attr, :width, :auto) !== :auto
- layout.widths[c] = attr(l, :width)
- end
- 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
- layout.widths[c] = attr(l, :width)
- end
- if get(l.attr, :height, :auto) !== :auto
- layout.heights[r] = attr(l, :height)
- end
- l, sps, m = build_layout(l, n - i, plts)
- append!(subplots, sps)
- merge!(spmap, m)
- i += length(sps)
- elseif isa(l, Subplot) && empty
- error("Subplot exists. Cannot re-use existing layout. Please make a new one.")
- end
- i ≥ n && break # only add n subplots
- end
-
- layout, subplots, spmap
-end
-
-# -------------------------------------------------------------------------
-
-# make all reference the same axis extrema/values.
-# merge subplot lists.
-function link_axes!(axes::Axis...)
- a1 = axes[1]
- for i in 2:length(axes)
- a2 = axes[i]
- expand_extrema!(a1, ignorenan_extrema(a2))
- for k in (: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
- sp in a1.sps || push!(a1.sps, sp)
- end
- a2.sps = a1.sps
- end
-end
-
-# figure out which subplots to link
-function link_subplots(a::AbstractArray{AbstractLayout}, axissym::Symbol)
- subplots = []
- for l in a
- if isa(l, Subplot)
- push!(subplots, l)
- elseif isa(l, GridLayout) && size(l) == (1, 1)
- push!(subplots, l[1, 1])
- end
- end
- subplots
-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]
- length(axes) > 0 && link_axes!(axes...)
-end
-
-# don't do anything for most layout types
-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
- end
- if link in (:y, :both)
- for r in 1:nr
- link_axes!(layout.grid[r, :], :yaxis)
- end
- 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
- end
- end
- if link === :all
- link_axes!(layout.grid, :xaxis)
- link_axes!(layout.grid, :yaxis)
- end
- foreach(l -> link_axes!(l, link), layout.grid)
-end
-
-# -------------------------------------------------------------------------
-
-function twin(sp, letter)
- plt = sp.plt
- orig_sp = first(plt.subplots)
- for letter in 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
- # incompatible with shared axes (see github.com/JuliaPlots/Plots.jl/issues/2894)
- orig_sp[:framestyle] = :axes
- end
- plot!(
- plt;
- inset = (sp[:subplot_index], bbox(0, 0, 1, 1)),
- left_margin = orig_sp[:left_margin],
- top_margin = orig_sp[:top_margin],
- right_margin = orig_sp[:right_margin],
- bottom_margin = orig_sp[:bottom_margin],
- )
- twin_sp = last(plt.subplots)
- letters = axes_letters(twin_sp, letter)
- tax, oax = map(l -> twin_sp[get_attr_symbol(l, :axis)], letters)
- tax[:grid] = false
- tax[:showaxis] = false
- tax[:ticks] = :none
- oax[:grid] = false
- oax[:mirror] = true
- twin_sp[:background_color_inside] = RGBA{Float64}(0, 0, 0, 0)
- link_axes!(sp[get_attr_symbol(letter, :axis)], tax)
- twin_sp
-end
-
-"""
- twinx(sp)
-
-Adds a new, empty subplot overlaid on top of `sp`, with a mirrored y-axis and linked x-axis.
-"""
-twinx(sp::Subplot) = twin(sp, :x)
-twinx(plt::Plot = current()) = twinx(first(plt))
-
-"""
- twiny(sp)
-
-Adds a new, empty subplot overlaid on top of `sp`, with a mirrored x-axis and linked y-axis.
-"""
-twiny(sp::Subplot) = twin(sp, :y)
-twiny(plt::Plot = current()) = twiny(first(plt))
diff --git a/src/legend.jl b/src/legend.jl
deleted file mode 100644
index 74dfd66cdb..0000000000
--- a/src/legend.jl
+++ /dev/null
@@ -1,57 +0,0 @@
-"""
-```julia
-legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax)
-```
-
-Return `(x,y)` at an angle `theta` degrees from
-`(xcenter,ycenter)` on a rectangle defined by (`xmin`, `xmax`, `ymin`, `ymax`).
-"""
-function legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax)
- (s, c) = sincosd(theta)
- x = c < 0 ? (xmin - xcenter) / c : (xmax - xcenter) / c
- y = s < 0 ? (ymin - ycenter) / s : (ymax - ycenter) / s
- A = min(x, y)
- return (xcenter + A * c, ycenter + A * s)
-end
-
-"""
-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
-end
-
-"""
-Turn legend argument into a (theta, :inner) or (theta, :outer) tuple.
-For backends where legend position is given in normal coordinates (0,0) -- (1,1),
-so :topleft exactly corresponds to (45, :inner) etc.
-
-If `leg` is a (::Real,::Real) tuple, keep it as is.
-"""
-legend_angle(leg::Real) = (leg, :inner)
-legend_angle(leg::Tuple{S,T}) where {S<:Real,T<:Real} = leg
-legend_angle(leg::Tuple{S,Symbol}) where {S<:Real} = leg
-legend_angle(leg::Symbol) = get(
- (
- topleft = (135, :inner),
- top = (90, :inner),
- topright = (45, :inner),
- left = (180, :inner),
- right = (0, :inner),
- bottomleft = (225, :inner),
- bottom = (270, :inner),
- bottomright = (315, :inner),
- outertopleft = (135, :outer),
- outertop = (90, :outer),
- outertopright = (45, :outer),
- outerleft = (180, :outer),
- outerright = (0, :outer),
- outerbottomleft = (225, :outer),
- outerbottom = (270, :outer),
- outerbottomright = (315, :outer),
- ),
- leg,
- (45, :inner),
-)
diff --git a/src/plotmeasures.jl b/src/plotmeasures.jl
deleted file mode 100644
index 7c92784933..0000000000
--- 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 6dc6067e2b..0000000000
--- 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 faf7b18809..0000000000
--- 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/templates/backends.jl b/templates/backends.jl
deleted file mode 100644
index 6d32207cda..0000000000
--- a/templates/backends.jl
+++ /dev/null
@@ -1,66 +0,0 @@
-
-# TODO: find/replace all [PkgName] with CamelCase
-
-# [ADD BACKEND WEBSITE]
-
-import [PkgName]
-export [PkgName]
-push!(_initialized_backends, [pgkname]::Symbol)
-
-# ---------------------------------------------------------------------------
-
-# Create the window/figure for this backend.
-function _create_backend_figure(plt::Plot{[PkgName]Backend})
- nothing
-end
-
-# Set up the subplot within the backend object.
-function _initialize_subplot(plt::Plot{[PkgName]Backend}, sp::Subplot{[PkgName]Backend})
-end
-
-# ---------------------------------------------------------------------------
-
-# Add one series to the underlying backend object.
-function _series_added(plt::Plot{[PkgName]Backend}, 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{[PkgName]Backend}, 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{[PkgName]Backend})
-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{[PkgName]Backend})
- sp.minpad = (20mm, 5mm, 2mm, 10mm)
-end
-
-
-# ----------------------------------------------------------------
-
-# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations])
-function _update_plot_object(plt::Plot{[PkgName]Backend})
-end
-
-# ----------------------------------------------------------------
-
-# Write a png to io. You could define methods for:
- # "application/eps" => "eps",
- # "image/eps" => "eps",
- # "application/pdf" => "pdf",
- # "image/png" => "png",
- # "application/postscript" => "ps",
- # "image/svg+xml" => "svg"
-function _show(io::IO, ::MIME"image/png", plt::Plot{[PkgName]Backend})
-end
-
-# Display/show the plot (open a GUI window, or browser page, for example).
-function _display(plt::Plot{[PkgName]Backend})
-end
diff --git a/test/Project.toml b/test/Project.toml
new file mode 100644
index 0000000000..7ef5cf5ff2
--- /dev/null
+++ b/test/Project.toml
@@ -0,0 +1,8 @@
+[deps]
+GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
+Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
+PlotsBase = "c52230a3-c5da-43a3-9e85-260fcdfdc737"
+PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
diff --git a/test/runtests.jl b/test/runtests.jl
index 72b7b4e771..57d1c3a981 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,68 +1,30 @@
-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 Pkg
+Pkg.status(; outdated = true, mode = Pkg.PKGMODE_MANIFEST)
-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 in 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 in 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 in TEST_PACKAGES
+ @testset "simple plots using $pkg" begin
+ @eval $(TEST_BACKENDS[pkg])()
+ pl = plot(1:2)
+ @test pl isa PlotsBase.Plot
+ show(devnull, pl)
end
end
diff --git a/test/test_backends.jl b/test/test_backends.jl
deleted file mode 100644
index e335cd6c83..0000000000
--- 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_plotly.jl b/test/test_plotly.jl
deleted file mode 100644
index b50c2323da..0000000000
--- a/test/test_plotly.jl
+++ /dev/null
@@ -1,66 +0,0 @@
-using Plots, Test
-Sys.isunix() && Plots.with(:plotly) do
- @testset "Basic" begin
- @test backend() == Plots.PlotlyBackend()
-
- pl = plot(rand(10))
- @test pl isa Plots.Plot
- @test_nowarn Plots.plotly_series(plot())
- @test !haskey(Plots.plotly_series(pl)[1], :zmax)
- end
-
- @testset "Contours" begin
- x = (-2π):0.1:(2π)
- y = (-π):0.1:π
- z = cos.(y) .* sin.(x')
-
- @testset "Contour numbers" begin
- @testset "Default" begin
- @test Plots.plotly_series(contour(x, y, z))[1][:ncontours] ==
- Plots._series_defaults[:levels] + 2
- end
- @testset "Specified number" begin
- cont = contour(x, y, z, levels = 10)
- @test Plots.plotly_series(cont)[1][:ncontours] == 12
- end
- end
-
- @testset "Contour values" begin
- @testset "Range" begin
- 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)
- end
-
- @testset "Set of contours" begin
- levels = [-1, -0.25, 0, 0.25, 1]
- levels_range =
- range(first(levels), stop = last(levels), length = length(levels))
- pl = contour(x, y, z, levels = levels)
- @test pl[1][1].plotattributes[:levels] == levels
- series_dict = @test_logs (
- :warn,
- """
- setting arbitrary contour levels with Plotly backend is not supported;
- use a range to set equally-spaced contours or an integer to set the
- approximate number of contours with the keyword `levels`.
- Setting levels to -1.0:0.5:1.0
- """,
- ) Plots.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)
- end
- end
- end
-
- @testset "Extra kwargs" begin
- pl = plot(1:5, test = "me")
- @test Plots.plotly_series(pl)[1][:test] == "me"
- pl = plot(1:5, test = "me", extra_kwargs = :plot)
- @test Plots.plotly_layout(pl)[:test] == "me"
- end
-end
diff --git a/test/test_quality.jl b/test/test_quality.jl
deleted file mode 100644
index d38c2b3311..0000000000
--- 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