├── .github
└── workflows
│ ├── deploy-docs.yml
│ ├── pypi.yml
│ ├── test_code.yml
│ ├── test_code_notebooks.yml
│ ├── test_mypy.yml
│ └── test_selenium.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGES.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── branca
├── __init__.py
├── _cnames.json
├── _schemes.json
├── colormap.py
├── element.py
├── py.typed
├── scheme_base_codes.json
├── scheme_info.json
├── templates
│ └── color_scale.js
└── utilities.py
├── docs
├── Makefile
└── source
│ ├── colormap.rst
│ ├── conf.py
│ ├── element.rst
│ └── index.rst
├── examples
├── Custom_colormap.ipynb
└── Elements.ipynb
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
├── test_colormap.py
├── test_iframe.py
└── test_utilities.py
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Documentation
3 |
4 | on:
5 | pull_request:
6 | push:
7 | branches:
8 | - main
9 | release:
10 | types:
11 | - published
12 |
13 | jobs:
14 | build-docs:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: checkout
19 | uses: actions/checkout@v3
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Setup Micromamba
24 | uses: mamba-org/setup-micromamba@v1
25 | with:
26 | environment-name: TEST
27 | init-shell: bash
28 | create-args: >-
29 | python=3 --file requirements.txt --file requirements-dev.txt --channel conda-forge
30 |
31 | - name: Install branca
32 | shell: bash -l {0}
33 | run: |
34 | python -m pip install -e . --no-deps --force-reinstall
35 |
36 | - name: Build documentation
37 | shell: bash -l {0}
38 | run: >
39 | set -e
40 | && pushd docs
41 | && make clean html linkcheck
42 | && popd
43 |
44 | - name: Deploy
45 | if: success() && github.event_name == 'release'
46 | uses: peaceiris/actions-gh-pages@v3
47 | with:
48 | github_token: ${{ secrets.GITHUB_TOKEN }}
49 | publish_dir: docs/build/html
50 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Publish to PyPI
3 |
4 | on:
5 | pull_request:
6 | push:
7 | branches:
8 | - main
9 | release:
10 | types:
11 | - published
12 |
13 | defaults:
14 | run:
15 | shell: bash
16 |
17 | jobs:
18 | packages:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v3
22 |
23 | - name: Set up Python
24 | uses: actions/setup-python@v4
25 | with:
26 | python-version: "3.x"
27 |
28 | - name: Get tags
29 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
30 |
31 | - name: Install build tools
32 | run: |
33 | python -m pip install --upgrade pip build wheel twine check-manifest
34 |
35 | - name: Build binary wheel
36 | run: python -m build --sdist --wheel . --outdir dist
37 |
38 | - name: CheckFiles
39 | run: |
40 | check-manifest --verbose
41 | ls dist
42 |
43 | - name: Test wheels
44 | run: |
45 | cd dist && python -m pip install branca*.whl
46 | python -m twine check *
47 |
48 | - name: Publish a Python distribution to PyPI
49 | if: success() && github.event_name == 'release'
50 | uses: pypa/gh-action-pypi-publish@release/v1
51 | with:
52 | user: __token__
53 | password: ${{ secrets.PYPI_PASSWORD }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/test_code.yml:
--------------------------------------------------------------------------------
1 | name: Code Tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | run:
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | os: [ubuntu-latest, windows-latest]
15 | python-version: ["3.8", "3.12"]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Setup Micromamba Python ${{ matrix.python-version }}
21 | uses: mamba-org/setup-micromamba@v1
22 | with:
23 | environment-name: TEST
24 | init-shell: bash
25 | create-args: >-
26 | python=${{ matrix.python-version }} pip --file requirements.txt --file requirements-dev.txt --channel conda-forge
27 |
28 | - name: Install branca
29 | shell: bash -l {0}
30 | run: |
31 | python -m pip install -e . --no-deps --force-reinstall
32 |
33 | - name: Tests
34 | shell: bash -l {0}
35 | run: |
36 | python -m pytest -vv -rxs tests -m "not headless"
37 |
--------------------------------------------------------------------------------
/.github/workflows/test_code_notebooks.yml:
--------------------------------------------------------------------------------
1 | name: Notebook Tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | run:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Setup Micromamba Python 3
16 | uses: mamba-org/setup-micromamba@v1
17 | with:
18 | environment-name: TEST
19 | init-shell: bash
20 | create-args: >-
21 | python=3 pip --file requirements.txt --file requirements-dev.txt --channel conda-forge
22 |
23 | - name: Install branca
24 | shell: bash -l {0}
25 | run: |
26 | python -m pip install -e . --no-deps --force-reinstall
27 |
28 | - name: Notebook tests
29 | shell: bash -l {0}
30 | run: |
31 | python -m pytest --nbval-lax examples
32 |
--------------------------------------------------------------------------------
/.github/workflows/test_mypy.yml:
--------------------------------------------------------------------------------
1 | name: Mypy type hint checks
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | run:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Micromamba env
17 | uses: mamba-org/setup-micromamba@v1
18 | with:
19 | environment-name: TEST
20 | create-args: >-
21 | python=3
22 | --file requirements.txt
23 | --file requirements-dev.txt
24 |
25 | - name: Install branca from source
26 | shell: bash -l {0}
27 | run: |
28 | python -m pip install -e . --no-deps --force-reinstall
29 |
30 | - name: Mypy test
31 | shell: bash -l {0}
32 | run: |
33 | mypy branca
34 |
--------------------------------------------------------------------------------
/.github/workflows/test_selenium.yml:
--------------------------------------------------------------------------------
1 | name: Headless Tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | run:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Setup Micromamba Python 3
16 | uses: mamba-org/setup-micromamba@v1
17 | with:
18 | environment-name: TEST
19 | init-shell: bash
20 | create-args: >-
21 | python=3 pip --file requirements.txt --file requirements-dev.txt --channel conda-forge
22 |
23 | - name: Install branca
24 | shell: bash -l {0}
25 | run: |
26 | python -m pip install -e . --no-deps --force-reinstall
27 |
28 | - name: Tests
29 | shell: bash -l {0}
30 | run: |
31 | pytest -vv -rxs tests -m "headless"
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | include
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
38 | #Mac
39 | *.DS_Store
40 |
41 | # IPython Notebook Checkpoints
42 | .ipynb_checkpoints
43 |
44 | #Virtualenv
45 | ENV
46 | .env
47 |
48 | # Tests products
49 | .cache
50 | data.png
51 | map.html
52 | examples/foo.html
53 |
54 | # documentation builds
55 | docs/_build
56 |
57 | geckodriver.exe
58 | geckodriver.log
59 |
60 | # Pycharm
61 | .idea/
62 | branca/_version.py
63 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autofix_prs: false
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v5.0.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: check-ast
9 | - id: debug-statements
10 | - id: end-of-file-fixer
11 | - id: check-docstring-first
12 | - id: requirements-txt-fixer
13 | - id: file-contents-sorter
14 | files: requirements-dev.txt
15 |
16 | - repo: https://github.com/PyCQA/flake8
17 | rev: 7.2.0
18 | hooks:
19 | - id: flake8
20 | exclude: docs/source/conf.py
21 | args: [--max-line-length=105, "--ignore=E203,W503"]
22 |
23 | - repo: https://github.com/pycqa/isort
24 | rev: 6.0.1
25 | hooks:
26 | - id: isort
27 | additional_dependencies: [toml]
28 | args: ["--profile", "black", "--filter-files"]
29 |
30 | - repo: https://github.com/psf/black
31 | rev: 25.1.0
32 | hooks:
33 | - id: black
34 | language_version: python3
35 |
36 | - repo: https://github.com/keewis/blackdoc
37 | rev: v0.3.9
38 | hooks:
39 | - id: blackdoc
40 |
41 | - repo: https://github.com/codespell-project/codespell
42 | rev: v2.4.1
43 | hooks:
44 | - id: codespell
45 | args:
46 | - --ignore-words-list=thex
47 |
48 | - repo: https://github.com/asottile/pyupgrade
49 | rev: v3.20.0
50 | hooks:
51 | - id: pyupgrade
52 | args:
53 | - --py36-plus
54 |
55 | - repo: https://github.com/asottile/add-trailing-comma
56 | rev: v3.2.0
57 | hooks:
58 | - id: add-trailing-comma
59 |
--------------------------------------------------------------------------------
/CHANGES.txt:
--------------------------------------------------------------------------------
1 | 0.7.0
2 | ~~~~~
3 | - Make all Element with Template pickable natively (@BastienGauthier #144)
4 | - Make _parse_size robust to already parsed values (@BastienGauthier #142)
5 | - StepColormap: inclusive lower bound (@MxMartin #141)
6 | - Add color schemes: plasma, inferno, magma (@FlorinAndrei #131)
7 | - Allow branca ColorMap in write_png (@Conengmo #126)
8 | - More flexible _parse_size (@Conengmo #125)
9 |
10 |
11 | 0.6.0
12 | ~~~~~
13 | - Properly escape colormap caption (@Conengmo #117)
14 | - Multiple fixes in color_brewer (@ajabep #115)
15 | - Expose colorbar size variables (@Conengmo #77)
16 | - Proper html tags in Figure template (@desrod #67)
17 | - Make Element class pickleable (@bwest2397 #99)
18 | - Improve colorbar representation in notebooks (@HaudinFlorence #110)
19 | - Allow custom ticks on colorbar (@kota7 #113)
20 |
21 |
22 | 0.5.0
23 | ~~~~~
24 | - Support for Pathlib when saving an `Element` (@wd60622 #103)
25 | - Faster UUID generation for `Element` id (@bwest2397 #101)
26 | - Store html content in `srcdoc` instead of `data-html` (@dstein64 #96)
27 | - Add `max_labels` argument to color maps (@martinfleis #90)
28 | - Pass caption when converting colormap to steps (@ndswaef #87)
29 |
30 | 0.4.2
31 | ~~~~~
32 | - Fix special char encoding in notebooks, store as percent-encoded (@conengmo #76)
33 |
34 | 0.4.1
35 | ~~~~~
36 | - Prompt Jupyter users to trust notebook (@conengmo #75)
37 | - Removed Python 2 specific code (@ocefpaf #69)
38 |
39 | 0.4.0
40 | ~~~~~
41 | - Dropped Python 2 support
42 | - Store html content in a data-html attribute (#66)
43 | - Colormap alpha #64
44 | - Fix caption being propagated in scale functions #62
45 | - Assert color type in color_brewer #52
46 |
47 | 0.3.1
48 | ~~~~~
49 | - Added viridis scheme #47 (GillesC)
50 | - Fixed testing, auto PyPI upload, and docs
51 |
52 | 0.3.0
53 | ~~~~~
54 | - Add title to Figure (@fitoprincipe #33 and #39)
55 | - Move templates to class attributes (@psarka #34 and #38)
56 | - Explicit color support for range of ``n``
57 | and diverging colormaps (@nanodan #29)
58 | - Added class for hosting step colormap (@matsavage #25)
59 |
60 | 0.2.0
61 | ~~~~~
62 | - Correct rendering utf-8 IFrame (@knil-sama #18)
63 | - Remove embedded IFrame's border (@deelaka #17)
64 | - Let IFrame contents go fullscreen (@sanga #13)
65 | - Add HTML Popup Class to element.py (@samchorlton #6)
66 |
67 | 0.1.0
68 | ~~~~~
69 | - Separate branca from folium (@bibmartin d678357)
70 | - Enable HTML embedding in Html (@samchorlton 90f6b13)
71 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2013, Martin Journois
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | include README.md
3 | include branca/_cnames.json
4 | include branca/_schemes.json
5 | include branca/scheme_info.json
6 | include branca/scheme_base_codes.json
7 | include pyproject.toml
8 |
9 | graft branca
10 |
11 | prune docs
12 | prune tests
13 | prune examples
14 | prune *.egg-info
15 |
16 | exclude *.yml
17 | exclude .pre-commit-config.yaml
18 | exclude .gitignore
19 | exclude .isort.cfg
20 | exclude branca/_version.py
21 | exclude .github
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://pypi.python.org/pypi/branca)
2 | [](https://github.com/python-visualization/branca/actions/workflows/test_code.yml)
3 | [](https://gitter.im/python-visualization/folium)
4 |
5 | # Branca
6 |
7 | This library is a spinoff from [folium](https://github.com/python-visualization/folium). It can be used to generate HTML + JS. It is based on Jinja2.
8 |
9 | - Documentation: https://python-visualization.github.io/branca/
10 | - Examples: https://nbviewer.org/github/python-visualization/branca/tree/main/examples/
11 |
--------------------------------------------------------------------------------
/branca/__init__.py:
--------------------------------------------------------------------------------
1 | import branca.colormap as colormap
2 | import branca.element as element
3 |
4 | try:
5 | from ._version import __version__
6 | except ImportError:
7 | __version__ = "unknown"
8 |
9 |
10 | __all__ = [
11 | "colormap",
12 | "element",
13 | ]
14 |
--------------------------------------------------------------------------------
/branca/_cnames.json:
--------------------------------------------------------------------------------
1 | {"indigo": "#4B0082", "gold": "#FFD700", "hotpink": "#FF69B4", "firebrick": "#B22222", "indianred": "#CD5C5C", "sage": "#87AE73", "yellow": "#FFFF00", "mistyrose": "#FFE4E1", "darkolivegreen": "#556B2F", "olive": "#808000", "darkseagreen": "#8FBC8F", "pink": "#FFC0CB", "tomato": "#FF6347", "lightcoral": "#F08080", "orangered": "#FF4500", "navajowhite": "#FFDEAD", "lime": "#00FF00", "palegreen": "#98FB98", "greenyellow": "#ADFF2F", "burlywood": "#DEB887", "seashell": "#FFF5EE", "mediumspringgreen": "#00FA9A", "fuchsia": "#FF00FF", "papayawhip": "#FFEFD5", "blanchedalmond": "#FFEBCD", "chartreuse": "#7FFF00", "dimgray": "#696969", "black": "#000000", "peachpuff": "#FFDAB9", "springgreen": "#00FF7F", "aquamarine": "#7FFFD4", "white": "#FFFFFF", "b": "#0000FF", "orange": "#FFA500", "lightsalmon": "#FFA07A", "darkslategray": "#2F4F4F", "brown": "#A52A2A", "ivory": "#FFFFF0", "dodgerblue": "#1E90FF", "peru": "#CD853F", "lawngreen": "#7CFC00", "chocolate": "#D2691E", "crimson": "#DC143C", "forestgreen": "#228B22", "slateblue": "#6A5ACD", "lightseagreen": "#20B2AA", "cyan": "#00FFFF", "mintcream": "#F5FFFA", "silver": "#C0C0C0", "antiquewhite": "#FAEBD7", "mediumorchid": "#BA55D3", "skyblue": "#87CEEB", "gray": "#808080", "darkturquoise": "#00CED1", "goldenrod": "#DAA520", "darkgreen": "#006400", "floralwhite": "#FFFAF0", "darkviolet": "#9400D3", "darkgray": "#A9A9A9", "moccasin": "#FFE4B5", "saddlebrown": "#8B4513", "darkslateblue": "#483D8B", "lightskyblue": "#87CEFA", "lightpink": "#FFB6C1", "mediumvioletred": "#C71585", "r": "#FF0000", "red": "#FF0000", "deeppink": "#FF1493", "limegreen": "#32CD32", "k": "#000000", "darkmagenta": "#8B008B", "palegoldenrod": "#EEE8AA", "plum": "#DDA0DD", "turquoise": "#40E0D0", "m": "#FF00FF", "lightgoldenrodyellow": "#FAFAD2", "darkgoldenrod": "#B8860B", "lavender": "#E6E6FA", "maroon": "#800000", "yellowgreen": "#9ACD32", "sandybrown": "#FAA460", "thistle": "#D8BFD8", "violet": "#EE82EE", "navy": "#000080", "magenta": "#FF00FF", "tan": "#D2B48C", "rosybrown": "#BC8F8F", "olivedrab": "#6B8E23", "blue": "#0000FF", "lightblue": "#ADD8E6", "ghostwhite": "#F8F8FF", "honeydew": "#F0FFF0", "cornflowerblue": "#6495ED", "linen": "#FAF0E6", "darkblue": "#00008B", "powderblue": "#B0E0E6", "seagreen": "#2E8B57", "darkkhaki": "#BDB76B", "snow": "#FFFAFA", "sienna": "#A0522D", "mediumblue": "#0000CD", "royalblue": "#4169E1", "lightcyan": "#E0FFFF", "green": "#008000", "mediumpurple": "#9370DB", "midnightblue": "#191970", "cornsilk": "#FFF8DC", "paleturquoise": "#AFEEEE", "bisque": "#FFE4C4", "slategray": "#708090", "darkcyan": "#008B8B", "khaki": "#F0E68C", "wheat": "#F5DEB3", "teal": "#008080", "darkorchid": "#9932CC", "deepskyblue": "#00BFFF", "salmon": "#FA8072", "y": "#FFFF00", "darkred": "#8B0000", "steelblue": "#4682B4", "g": "#008000", "palevioletred": "#DB7093", "lightslategray": "#778899", "aliceblue": "#F0F8FF", "lightgreen": "#90EE90", "orchid": "#DA70D6", "gainsboro": "#DCDCDC", "mediumseagreen": "#3CB371", "lightgray": "#D3D3D3", "c": "#00FFFF", "mediumturquoise": "#48D1CC", "darksage": "#598556", "lemonchiffon": "#FFFACD", "cadetblue": "#5F9EA0", "lightyellow": "#FFFFE0", "lavenderblush": "#FFF0F5", "coral": "#FF7F50", "purple": "#800080", "aqua": "#00FFFF", "lightsage": "#BCECAC", "whitesmoke": "#F5F5F5", "mediumslateblue": "#7B68EE", "darkorange": "#FF8C00", "mediumaquamarine": "#66CDAA", "darksalmon": "#E9967A", "beige": "#F5F5DC", "w": "#FFFFFF", "blueviolet": "#8A2BE2", "azure": "#F0FFFF", "lightsteelblue": "#B0C4DE", "oldlace": "#FDF5E6"}
2 |
--------------------------------------------------------------------------------
/branca/_schemes.json:
--------------------------------------------------------------------------------
1 | {"viridis":["#440154","#481567","#482677","#453781","#404788","#39568C","#33638D","#2D708E","#287D8E","#238A8D","#1F968B","#20A387","#29AF7F","#3CBB75","#55C667","#73D055","#95D840","#B8DE29","#DCE319","#FDE725"],"plasma":["#0D0887","#2C0594","#43039E","#5901A5","#6E00A8","#8305A7","#9511A1","#A72197","#B6308B","#C5407E","#D14E72","#DD5E66","#E76E5B","#F07F4F","#F79044","#FCA338","#FEB72D","#FCCD25","#F7E225","#F0F921"],"inferno":["#000004","#08051D","#180C3C","#2F0A5B","#450A69","#5C126E","#71196E","#87216B","#9B2964","#B1325A","#C43C4E","#D74B3F","#E55C30","#F1711F","#F8870E","#FCA108","#FBBA1F","#F6D543","#F1ED71","#FCFFA4"],"magma":["#000004","#07061C","#150E38","#29115A","#3F0F72","#56147D","#6A1C81","#802582","#942C80","#AB337C","#C03A76","#D6456C","#E85362","#F4695C","#FA815F","#FD9B6B","#FEB47B","#FECD90","#FDE5A7","#FCFDBF"],"Pastel1_03":["#fbb4ae","#b3cde3","#ccebc5"],"Pastel1_05":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6"],"Pastel1_04":["#fbb4ae","#b3cde3","#ccebc5","#decbe4"],"Pastel1_07":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd"],"YlOrRd_04":["#ffffb2","#fecc5c","#fd8d3c","#e31a1c"],"Pastel1_09":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"],"Pastel1_08":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec"],"Spectral_07":["#d53e4f","#fc8d59","#fee08b","#ffffbf","#e6f598","#99d594","#3288bd"],"RdYlBu_05":["#d7191c","#fdae61","#ffffbf","#abd9e9","#2c7bb6"],"PuBuGn_03":["#ece2f0","#a6bddb","#1c9099"],"Set1_08":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf"],"PuBuGn_05":["#f6eff7","#bdc9e1","#67a9cf","#1c9099","#016c59"],"PuBuGn_04":["#f6eff7","#bdc9e1","#67a9cf","#02818a"],"PuBuGn_07":["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"],"PuBuGn_06":["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#1c9099","#016c59"],"PuBuGn_09":["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"],"PuBuGn_08":["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"],"YlOrBr_04":["#ffffd4","#fed98e","#fe9929","#cc4c02"],"YlOrBr_05":["#ffffd4","#fed98e","#fe9929","#d95f0e","#993404"],"Set1_07":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628"],"YlOrBr_03":["#fff7bc","#fec44f","#d95f0e"],"Set1_05":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00"],"YlOrRd_03":["#ffeda0","#feb24c","#f03b20"],"PuOr_06":["#b35806","#f1a340","#fee0b6","#d8daeb","#998ec3","#542788"],"PuOr_07":["#b35806","#f1a340","#fee0b6","#f7f7f7","#d8daeb","#998ec3","#542788"],"PuOr_04":["#e66101","#fdb863","#b2abd2","#5e3c99"],"PuOr_05":["#e66101","#fdb863","#f7f7f7","#b2abd2","#5e3c99"],"PuOr_03":["#f1a340","#f7f7f7","#998ec3"],"Purples_09":["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"],"Set2_06":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"],"RdYlBu_11":["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],"PuOr_08":["#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788"],"PuOr_09":["#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788"],"Paired_03":["#a6cee3","#1f78b4","#b2df8a"],"RdBu_03":["#ef8a62","#f7f7f7","#67a9cf"],"RdYlBu_10":["#a50026","#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],"Paired_07":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f"],"Paired_06":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c"],"Paired_05":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99"],"Paired_04":["#a6cee3","#1f78b4","#b2df8a","#33a02c"],"Paired_09":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6"],"Paired_08":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00"],"RdGy_03":["#ef8a62","#ffffff","#999999"],"PiYG_04":["#d01c8b","#f1b6da","#b8e186","#4dac26"],"Accent_03":["#7fc97f","#beaed4","#fdc086"],"BuGn_08":["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"],"BuGn_09":["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"],"BuGn_04":["#edf8fb","#b2e2e2","#66c2a4","#238b45"],"BuGn_05":["#edf8fb","#b2e2e2","#66c2a4","#2ca25f","#006d2c"],"BuGn_06":["#edf8fb","#ccece6","#99d8c9","#66c2a4","#2ca25f","#006d2c"],"BuGn_07":["#edf8fb","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"],"BuGn_03":["#e5f5f9","#99d8c9","#2ca25f"],"YlGnBu_07":["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"],"YlGnBu_06":["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"],"YlGnBu_05":["#ffffcc","#a1dab4","#41b6c4","#2c7fb8","#253494"],"YlGnBu_04":["#ffffcc","#a1dab4","#41b6c4","#225ea8"],"YlGnBu_03":["#edf8b1","#7fcdbb","#2c7fb8"],"RdBu_06":["#b2182b","#ef8a62","#fddbc7","#d1e5f0","#67a9cf","#2166ac"],"RdBu_05":["#ca0020","#f4a582","#f7f7f7","#92c5de","#0571b0"],"RdBu_04":["#ca0020","#f4a582","#92c5de","#0571b0"],"Accent_08":["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"],"RdBu_09":["#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac"],"RdBu_08":["#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac"],"Set2_04":["#66c2a5","#fc8d62","#8da0cb","#e78ac3"],"YlGnBu_09":["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"],"YlGnBu_08":["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"],"Blues_08":["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"],"Blues_09":["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"],"RdPu_09":["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"],"RdPu_08":["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"],"Set3_07":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69"],"Set3_06":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462"],"RdPu_05":["#feebe2","#fbb4b9","#f768a1","#c51b8a","#7a0177"],"RdPu_04":["#feebe2","#fbb4b9","#f768a1","#ae017e"],"RdPu_07":["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"],"RdPu_06":["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#c51b8a","#7a0177"],"Blues_06":["#eff3ff","#c6dbef","#9ecae1","#6baed6","#3182bd","#08519c"],"Blues_07":["#eff3ff","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"],"RdPu_03":["#fde0dd","#fa9fb5","#c51b8a"],"Blues_05":["#eff3ff","#bdd7e7","#6baed6","#3182bd","#08519c"],"Paired_10":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a"],"Paired_11":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99"],"Paired_12":["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"],"PuBu_06":["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#2b8cbe","#045a8d"],"PuBu_07":["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"],"PuBu_04":["#f1eef6","#bdc9e1","#74a9cf","#0570b0"],"PuBu_05":["#f1eef6","#bdc9e1","#74a9cf","#2b8cbe","#045a8d"],"PuRd_05":["#f1eef6","#d7b5d8","#df65b0","#dd1c77","#980043"],"PuBu_03":["#ece7f2","#a6bddb","#2b8cbe"],"PuRd_07":["#f1eef6","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"],"PuRd_06":["#f1eef6","#d4b9da","#c994c7","#df65b0","#dd1c77","#980043"],"PuRd_09":["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"],"PuRd_08":["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"],"Set2_07":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494"],"PuBu_08":["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"],"PuBu_09":["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"],"RdBu_10":["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],"RdBu_11":["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],"Accent_06":["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f"],"Set3_03":["#8dd3c7","#ffffb3","#bebada"],"Set3_05":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3"],"Set3_12":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"],"Set3_10":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd"],"Set3_04":["#8dd3c7","#ffffb3","#bebada","#fb8072"],"RdGy_11":["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],"RdGy_10":["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],"Set1_03":["#e41a1c","#377eb8","#4daf4a"],"Set1_09":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"],"Set3_09":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9"],"BuPu_08":["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"],"BuPu_09":["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"],"RdYlGn_11":["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],"Blues_03":["#deebf7","#9ecae1","#3182bd"],"Set2_05":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854"],"BuPu_03":["#e0ecf4","#9ebcda","#8856a7"],"BuPu_06":["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8856a7","#810f7c"],"BuPu_07":["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"],"BuPu_04":["#edf8fb","#b3cde3","#8c96c6","#88419d"],"BuPu_05":["#edf8fb","#b3cde3","#8c96c6","#8856a7","#810f7c"],"Accent_04":["#7fc97f","#beaed4","#fdc086","#ffff99"],"YlOrRd_05":["#ffffb2","#fecc5c","#fd8d3c","#f03b20","#bd0026"],"YlOrBr_08":["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"],"Oranges_08":["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"],"Oranges_09":["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"],"Oranges_06":["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#e6550d","#a63603"],"Oranges_07":["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"],"Oranges_04":["#feedde","#fdbe85","#fd8d3c","#d94701"],"YlOrBr_09":["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"],"Oranges_03":["#fee6ce","#fdae6b","#e6550d"],"YlOrBr_06":["#ffffd4","#fee391","#fec44f","#fe9929","#d95f0e","#993404"],"Dark2_06":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02"],"Blues_04":["#eff3ff","#bdd7e7","#6baed6","#2171b5"],"YlOrBr_07":["#ffffd4","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"],"RdYlGn_05":["#d7191c","#fdae61","#ffffbf","#a6d96a","#1a9641"],"Set3_08":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5"],"YlOrRd_06":["#ffffb2","#fed976","#feb24c","#fd8d3c","#f03b20","#bd0026"],"Dark2_03":["#1b9e77","#d95f02","#7570b3"],"Accent_05":["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0"],"RdYlGn_08":["#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850"],"RdYlGn_09":["#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850"],"PuOr_11":["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],"YlOrRd_07":["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],"Spectral_11":["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],"RdGy_08":["#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d"],"RdGy_09":["#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d"],"RdGy_06":["#b2182b","#ef8a62","#fddbc7","#e0e0e0","#999999","#4d4d4d"],"RdGy_07":["#b2182b","#ef8a62","#fddbc7","#ffffff","#e0e0e0","#999999","#4d4d4d"],"RdGy_04":["#ca0020","#f4a582","#bababa","#404040"],"RdGy_05":["#ca0020","#f4a582","#ffffff","#bababa","#404040"],"RdYlGn_04":["#d7191c","#fdae61","#a6d96a","#1a9641"],"PiYG_09":["#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221"],"RdYlGn_06":["#d73027","#fc8d59","#fee08b","#d9ef8b","#91cf60","#1a9850"],"RdYlGn_07":["#d73027","#fc8d59","#fee08b","#ffffbf","#d9ef8b","#91cf60","#1a9850"],"Spectral_04":["#d7191c","#fdae61","#abdda4","#2b83ba"],"Spectral_05":["#d7191c","#fdae61","#ffffbf","#abdda4","#2b83ba"],"Spectral_06":["#d53e4f","#fc8d59","#fee08b","#e6f598","#99d594","#3288bd"],"PiYG_08":["#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221"],"Set2_03":["#66c2a5","#fc8d62","#8da0cb"],"Spectral_03":["#fc8d59","#ffffbf","#99d594"],"Reds_08":["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"],"Set1_04":["#e41a1c","#377eb8","#4daf4a","#984ea3"],"Spectral_08":["#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd"],"Spectral_09":["#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd"],"Set2_08":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],"Reds_09":["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"],"Greys_07":["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"],"Greys_06":["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#636363","#252525"],"Greys_05":["#f7f7f7","#cccccc","#969696","#636363","#252525"],"Greys_04":["#f7f7f7","#cccccc","#969696","#525252"],"Greys_03":["#f0f0f0","#bdbdbd","#636363"],"PuOr_10":["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],"Accent_07":["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17"],"Reds_06":["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#de2d26","#a50f15"],"Greys_09":["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"],"Greys_08":["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"],"Reds_07":["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"],"RdYlBu_08":["#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4"],"RdYlBu_09":["#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4"],"BrBG_09":["#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e"],"BrBG_08":["#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e"],"BrBG_07":["#8c510a","#d8b365","#f6e8c3","#f5f5f5","#c7eae5","#5ab4ac","#01665e"],"BrBG_06":["#8c510a","#d8b365","#f6e8c3","#c7eae5","#5ab4ac","#01665e"],"BrBG_05":["#a6611a","#dfc27d","#f5f5f5","#80cdc1","#018571"],"BrBG_04":["#a6611a","#dfc27d","#80cdc1","#018571"],"BrBG_03":["#d8b365","#f5f5f5","#5ab4ac"],"PiYG_06":["#c51b7d","#e9a3c9","#fde0ef","#e6f5d0","#a1d76a","#4d9221"],"Reds_03":["#fee0d2","#fc9272","#de2d26"],"Set3_11":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5"],"Set1_06":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33"],"PuRd_03":["#e7e1ef","#c994c7","#dd1c77"],"PiYG_07":["#c51b7d","#e9a3c9","#fde0ef","#f7f7f7","#e6f5d0","#a1d76a","#4d9221"],"RdBu_07":["#b2182b","#ef8a62","#fddbc7","#f7f7f7","#d1e5f0","#67a9cf","#2166ac"],"Pastel1_06":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc"],"Spectral_10":["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],"PuRd_04":["#f1eef6","#d7b5d8","#df65b0","#ce1256"],"OrRd_03":["#fee8c8","#fdbb84","#e34a33"],"PiYG_03":["#e9a3c9","#f7f7f7","#a1d76a"],"Oranges_05":["#feedde","#fdbe85","#fd8d3c","#e6550d","#a63603"],"OrRd_07":["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"],"OrRd_06":["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#e34a33","#b30000"],"OrRd_05":["#fef0d9","#fdcc8a","#fc8d59","#e34a33","#b30000"],"OrRd_04":["#fef0d9","#fdcc8a","#fc8d59","#d7301f"],"Reds_04":["#fee5d9","#fcae91","#fb6a4a","#cb181d"],"Reds_05":["#fee5d9","#fcae91","#fb6a4a","#de2d26","#a50f15"],"OrRd_09":["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"],"OrRd_08":["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"],"BrBG_10":["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],"BrBG_11":["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],"PiYG_05":["#d01c8b","#f1b6da","#f7f7f7","#b8e186","#4dac26"],"YlOrRd_08":["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],"GnBu_04":["#f0f9e8","#bae4bc","#7bccc4","#2b8cbe"],"GnBu_05":["#f0f9e8","#bae4bc","#7bccc4","#43a2ca","#0868ac"],"GnBu_06":["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#43a2ca","#0868ac"],"GnBu_07":["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"],"Purples_08":["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"],"GnBu_03":["#e0f3db","#a8ddb5","#43a2ca"],"Purples_06":["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#756bb1","#54278f"],"Purples_07":["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"],"Purples_04":["#f2f0f7","#cbc9e2","#9e9ac8","#6a51a3"],"Purples_05":["#f2f0f7","#cbc9e2","#9e9ac8","#756bb1","#54278f"],"GnBu_08":["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"],"GnBu_09":["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"],"YlOrRd_09":["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"],"Purples_03":["#efedf5","#bcbddc","#756bb1"],"RdYlBu_04":["#d7191c","#fdae61","#abd9e9","#2c7bb6"],"PRGn_09":["#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837"],"PRGn_08":["#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837"],"PRGn_07":["#762a83","#af8dc3","#e7d4e8","#f7f7f7","#d9f0d3","#7fbf7b","#1b7837"],"PRGn_06":["#762a83","#af8dc3","#e7d4e8","#d9f0d3","#7fbf7b","#1b7837"],"PRGn_05":["#7b3294","#c2a5cf","#f7f7f7","#a6dba0","#008837"],"PRGn_04":["#7b3294","#c2a5cf","#a6dba0","#008837"],"PRGn_03":["#af8dc3","#f7f7f7","#7fbf7b"],"RdYlBu_06":["#d73027","#fc8d59","#fee090","#e0f3f8","#91bfdb","#4575b4"],"RdYlGn_10":["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],"YlGn_08":["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"],"YlGn_09":["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"],"RdYlBu_07":["#d73027","#fc8d59","#fee090","#ffffbf","#e0f3f8","#91bfdb","#4575b4"],"PiYG_10":["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],"PiYG_11":["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],"YlGn_03":["#f7fcb9","#addd8e","#31a354"],"YlGn_04":["#ffffcc","#c2e699","#78c679","#238443"],"YlGn_05":["#ffffcc","#c2e699","#78c679","#31a354","#006837"],"YlGn_06":["#ffffcc","#d9f0a3","#addd8e","#78c679","#31a354","#006837"],"YlGn_07":["#ffffcc","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"],"Dark2_05":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e"],"Dark2_04":["#1b9e77","#d95f02","#7570b3","#e7298a"],"Dark2_07":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d"],"Pastel2_03":["#b3e2cd","#fdcdac","#cbd5e8"],"Pastel2_04":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4"],"Pastel2_05":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9"],"Pastel2_06":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae"],"Pastel2_07":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc"],"Pastel2_08":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"],"RdYlBu_03":["#fc8d59","#ffffbf","#91bfdb"],"Dark2_08":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"],"RdYlGn_03":["#fc8d59","#ffffbf","#91cf60"],"PRGn_11":["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],"Greens_08":["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"],"Greens_09":["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"],"Greens_06":["#edf8e9","#c7e9c0","#a1d99b","#74c476","#31a354","#006d2c"],"Greens_07":["#edf8e9","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"],"Greens_04":["#edf8e9","#bae4b3","#74c476","#238b45"],"Greens_05":["#edf8e9","#bae4b3","#74c476","#31a354","#006d2c"],"PRGn_10":["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],"Greens_03":["#e5f5e0","#a1d99b","#31a354"]}
2 |
--------------------------------------------------------------------------------
/branca/colormap.py:
--------------------------------------------------------------------------------
1 | """
2 | Colormap
3 | --------
4 |
5 | Utility module for dealing with colormaps.
6 |
7 | """
8 |
9 | import json
10 | import math
11 | import os
12 | from typing import Dict, List, Optional, Sequence, Tuple, Union
13 |
14 | from jinja2 import Template
15 |
16 | from branca.element import ENV, Figure, JavascriptLink, MacroElement
17 | from branca.utilities import legend_scaler
18 |
19 | rootpath: str = os.path.abspath(os.path.dirname(__file__))
20 |
21 | with open(os.path.join(rootpath, "_cnames.json")) as f:
22 | _cnames: Dict[str, str] = json.loads(f.read())
23 |
24 | with open(os.path.join(rootpath, "_schemes.json")) as f:
25 | _schemes: Dict[str, List[str]] = json.loads(f.read())
26 |
27 |
28 | TypeRGBInts = Tuple[int, int, int]
29 | TypeRGBFloats = Tuple[float, float, float]
30 | TypeRGBAInts = Tuple[int, int, int, int]
31 | TypeRGBAFloats = Tuple[float, float, float, float]
32 | TypeAnyColorType = Union[TypeRGBInts, TypeRGBFloats, TypeRGBAInts, TypeRGBAFloats, str]
33 |
34 |
35 | def _is_hex(x: str) -> bool:
36 | return x.startswith("#") and len(x) == 7
37 |
38 |
39 | def _parse_hex(color_code: str) -> TypeRGBAFloats:
40 | return (
41 | _color_int_to_float(int(color_code[1:3], 16)),
42 | _color_int_to_float(int(color_code[3:5], 16)),
43 | _color_int_to_float(int(color_code[5:7], 16)),
44 | 1.0,
45 | )
46 |
47 |
48 | def _color_int_to_float(x: int) -> float:
49 | """Convert an integer between 0 and 255 to a float between 0. and 1.0"""
50 | return x / 255.0
51 |
52 |
53 | def _color_float_to_int(x: float) -> int:
54 | """Convert a float between 0. and 1.0 to an integer between 0 and 255"""
55 | return int(x * 255.9999)
56 |
57 |
58 | def _parse_color(x: Union[tuple, list, str]) -> TypeRGBAFloats:
59 | if isinstance(x, (tuple, list)):
60 | return tuple(tuple(x) + (1.0,))[:4] # type: ignore
61 | elif isinstance(x, str) and _is_hex(x):
62 | return _parse_hex(x)
63 | elif isinstance(x, str):
64 | cname = _cnames.get(x.lower(), None)
65 | if cname is None:
66 | raise ValueError(f"Unknown color {cname!r}.")
67 | return _parse_hex(cname)
68 | else:
69 | raise ValueError(f"Unrecognized color code {x!r}")
70 |
71 |
72 | def _base(x: float) -> float:
73 | if x > 0:
74 | base = pow(10, math.floor(math.log10(x)))
75 | return round(x / base) * base
76 | else:
77 | return 0
78 |
79 |
80 | class ColorMap(MacroElement):
81 | """A generic class for creating colormaps.
82 |
83 | Parameters
84 | ----------
85 | vmin: float
86 | The left bound of the color scale.
87 | vmax: float
88 | The right bound of the color scale.
89 | caption: str
90 | A caption to draw with the colormap.
91 | text_color: str, default "black"
92 | The color for the text.
93 | max_labels : int, default 10
94 | Maximum number of legend tick labels
95 | """
96 |
97 | _template: Template = ENV.get_template("color_scale.js")
98 |
99 | def __init__(
100 | self,
101 | vmin: float = 0.0,
102 | vmax: float = 1.0,
103 | caption: str = "",
104 | text_color: str = "black",
105 | max_labels: int = 10,
106 | ):
107 | super().__init__()
108 | self._name = "ColorMap"
109 |
110 | self.vmin = vmin
111 | self.vmax = vmax
112 | self.caption = caption
113 | self.text_color = text_color
114 | self.index: List[float] = [vmin, vmax]
115 | self.max_labels = max_labels
116 | self.tick_labels: Optional[Sequence[Union[float, str]]] = None
117 |
118 | self.width = 450
119 | self.height = 40
120 |
121 | def render(self, **kwargs):
122 | """Renders the HTML representation of the element."""
123 | self.color_domain = [
124 | float(self.vmin + (self.vmax - self.vmin) * k / 499.0) for k in range(500)
125 | ]
126 | self.color_range = [self.__call__(x) for x in self.color_domain]
127 |
128 | # sanitize possible numpy floats to native python floats
129 | self.index = [float(i) for i in self.index]
130 |
131 | if self.tick_labels is None:
132 | self.tick_labels = legend_scaler(self.index, self.max_labels)
133 |
134 | super().render(**kwargs)
135 |
136 | figure = self.get_root()
137 | assert isinstance(figure, Figure), (
138 | "You cannot render this Element " "if it is not in a Figure."
139 | )
140 |
141 | figure.header.add_child(
142 | JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"),
143 | name="d3",
144 | ) # noqa
145 |
146 | def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
147 | """
148 | This class has to be implemented for each class inheriting from
149 | Colormap. This has to be a function of the form float ->
150 | (float, float, float, float) describing for each input float x,
151 | the output color in RGBA format;
152 | Each output value being between 0 and 1.
153 | """
154 | raise NotImplementedError
155 |
156 | def rgba_bytes_tuple(self, x: float) -> TypeRGBAInts:
157 | """Provides the color corresponding to value `x` in the
158 | form of a tuple (R,G,B,A) with int values between 0 and 255.
159 | """
160 | return tuple(_color_float_to_int(u) for u in self.rgba_floats_tuple(x)) # type: ignore
161 |
162 | def rgb_bytes_tuple(self, x: float) -> TypeRGBInts:
163 | """Provides the color corresponding to value `x` in the
164 | form of a tuple (R,G,B) with int values between 0 and 255.
165 | """
166 | return self.rgba_bytes_tuple(x)[:3]
167 |
168 | def rgb_hex_str(self, x: float) -> str:
169 | """Provides the color corresponding to value `x` in the
170 | form of a string of hexadecimal values "#RRGGBB".
171 | """
172 | return "#%02x%02x%02x" % self.rgb_bytes_tuple(x)
173 |
174 | def rgba_hex_str(self, x: float) -> str:
175 | """Provides the color corresponding to value `x` in the
176 | form of a string of hexadecimal values "#RRGGBBAA".
177 | """
178 | return "#%02x%02x%02x%02x" % self.rgba_bytes_tuple(x)
179 |
180 | def __call__(self, x: float) -> str:
181 | """Provides the color corresponding to value `x` in the
182 | form of a string of hexadecimal values "#RRGGBBAA".
183 | """
184 | return self.rgba_hex_str(x)
185 |
186 | def _repr_html_(self) -> str:
187 | """Display the colormap in a Jupyter Notebook.
188 |
189 | Does not support all the class arguments.
190 |
191 | """
192 | nb_ticks = 7
193 | delta_x = math.floor(self.width / (nb_ticks - 1))
194 | x_ticks = [(i) * delta_x for i in range(0, nb_ticks)]
195 | delta_val = delta_x * (self.vmax - self.vmin) / self.width
196 | val_ticks = [round(self.vmin + (i) * delta_val, 1) for i in range(0, nb_ticks)]
197 |
198 | return (
199 | f'"
243 | )
244 |
245 |
246 | class LinearColormap(ColorMap):
247 | """Creates a ColorMap based on linear interpolation of a set of colors
248 | over a given index.
249 |
250 | Parameters
251 | ----------
252 |
253 | colors : list-like object with at least two colors.
254 | The set of colors to be used for interpolation.
255 | Colors can be provided in the form:
256 | * tuples of RGBA ints between 0 and 255 (e.g: `(255, 255, 0)` or
257 | `(255, 255, 0, 255)`)
258 | * tuples of RGBA floats between 0. and 1. (e.g: `(1.,1.,0.)` or
259 | `(1., 1., 0., 1.)`)
260 | * HTML-like string (e.g: `"#ffff00`)
261 | * a color name or shortcut (e.g: `"y"` or `"yellow"`)
262 | index : list of floats, default None
263 | The values corresponding to each color.
264 | It has to be sorted, and have the same length as `colors`.
265 | If None, a regular grid between `vmin` and `vmax` is created.
266 | vmin : float, default 0.
267 | The minimal value for the colormap.
268 | Values lower than `vmin` will be bound directly to `colors[0]`.
269 | vmax : float, default 1.
270 | The maximal value for the colormap.
271 | Values higher than `vmax` will be bound directly to `colors[-1]`.
272 | caption: str
273 | A caption to draw with the colormap.
274 | text_color: str, default "black"
275 | The color for the text.
276 | max_labels : int, default 10
277 | Maximum number of legend tick labels
278 | tick_labels: list of floats, default None
279 | If given, used as the positions of ticks."""
280 |
281 | def __init__(
282 | self,
283 | colors: Sequence[TypeAnyColorType],
284 | index: Optional[Sequence[float]] = None,
285 | vmin: float = 0.0,
286 | vmax: float = 1.0,
287 | caption: str = "",
288 | text_color: str = "black",
289 | max_labels: int = 10,
290 | tick_labels: Optional[Sequence[float]] = None,
291 | ):
292 | super().__init__(
293 | vmin=vmin,
294 | vmax=vmax,
295 | caption=caption,
296 | text_color=text_color,
297 | max_labels=max_labels,
298 | )
299 | self.tick_labels: Optional[Sequence[float]] = tick_labels
300 |
301 | n = len(colors)
302 | if n < 2:
303 | raise ValueError("You must provide at least 2 colors.")
304 | if index is None:
305 | self.index = [vmin + (vmax - vmin) * i * 1.0 / (n - 1) for i in range(n)]
306 | else:
307 | self.index = list(index)
308 | self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
309 |
310 | def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
311 | """Provides the color corresponding to value `x` in the
312 | form of a tuple (R,G,B,A) with float values between 0. and 1.
313 | """
314 | if x <= self.index[0]:
315 | return self.colors[0]
316 | if x >= self.index[-1]:
317 | return self.colors[-1]
318 |
319 | i = len([u for u in self.index if u < x]) # 0 < i < n.
320 | if self.index[i - 1] < self.index[i]:
321 | p = (x - self.index[i - 1]) * 1.0 / (self.index[i] - self.index[i - 1])
322 | elif self.index[i - 1] == self.index[i]:
323 | p = 1.0
324 | else:
325 | raise ValueError("Thresholds are not sorted.")
326 |
327 | return tuple( # type: ignore
328 | (1.0 - p) * self.colors[i - 1][j] + p * self.colors[i][j] for j in range(4)
329 | )
330 |
331 | def to_step(
332 | self,
333 | n: Optional[int] = None,
334 | index: Optional[Sequence[float]] = None,
335 | data: Optional[Sequence[float]] = None,
336 | method: str = "linear",
337 | quantiles: Optional[Sequence[float]] = None,
338 | round_method: Optional[str] = None,
339 | max_labels: int = 10,
340 | ) -> "StepColormap":
341 | """Splits the LinearColormap into a StepColormap.
342 |
343 | Parameters
344 | ----------
345 | n : int, default None
346 | The number of expected colors in the output StepColormap.
347 | This will be ignored if `index` is provided.
348 | index : list of floats, default None
349 | The values corresponding to each color bounds.
350 | It has to be sorted.
351 | If None, a regular grid between `vmin` and `vmax` is created.
352 | data : list of floats, default None
353 | A sample of data to adapt the color map to.
354 | method : str, default 'linear'
355 | The method used to create data-based colormap.
356 | It can be 'linear' for linear scale, 'log' for logarithmic,
357 | or 'quant' for data's quantile-based scale.
358 | quantiles : list of floats, default None
359 | Alternatively, you can provide explicitly the quantiles you
360 | want to use in the scale.
361 | round_method : str, default None
362 | The method used to round thresholds.
363 | * If 'int', all values will be rounded to the nearest integer.
364 | * If 'log10', all values will be rounded to the nearest
365 | order-of-magnitude integer. For example, 2100 is rounded to
366 | 2000, 2790 to 3000.
367 | max_labels : int, default 10
368 | Maximum number of legend tick labels
369 |
370 | Returns
371 | -------
372 | A StepColormap with `n=len(index)-1` colors.
373 |
374 | Examples:
375 | >> lc.to_step(n=12)
376 | >> lc.to_step(index=[0, 2, 4, 6, 8, 10])
377 | >> lc.to_step(data=some_list, n=12)
378 | >> lc.to_step(data=some_list, n=12, method='linear')
379 | >> lc.to_step(data=some_list, n=12, method='log')
380 | >> lc.to_step(data=some_list, n=12, method='quantiles')
381 | >> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1])
382 | >> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1],
383 | ... round_method='log10')
384 |
385 | """
386 | msg = "You must specify either `index` or `n`"
387 | if index is None:
388 | if data is None:
389 | if n is None:
390 | raise ValueError(msg)
391 | else:
392 | index = [
393 | self.vmin + (self.vmax - self.vmin) * i * 1.0 / n
394 | for i in range(1 + n)
395 | ]
396 | scaled_cm = self
397 | else:
398 | max_ = max(data)
399 | min_ = min(data)
400 | scaled_cm = self.scale(vmin=min_, vmax=max_)
401 | method = "quantiles" if quantiles is not None else method
402 | if method.lower().startswith("lin"):
403 | if n is None:
404 | raise ValueError(msg)
405 | index = [min_ + i * (max_ - min_) * 1.0 / n for i in range(1 + n)]
406 | elif method.lower().startswith("log"):
407 | if n is None:
408 | raise ValueError(msg)
409 | if min_ <= 0:
410 | msg = "Log-scale works only with strictly " "positive values."
411 | raise ValueError(msg)
412 | index = [
413 | math.exp(
414 | math.log(min_)
415 | + i * (math.log(max_) - math.log(min_)) * 1.0 / n,
416 | )
417 | for i in range(1 + n)
418 | ]
419 | elif method.lower().startswith("quant"):
420 | if quantiles is None:
421 | if n is None:
422 | msg = (
423 | "You must specify either `index`, `n` or" "`quantiles`."
424 | )
425 | raise ValueError(msg)
426 | else:
427 | quantiles = [i * 1.0 / n for i in range(1 + n)]
428 | p = len(data) - 1
429 | s = sorted(data)
430 | index = [
431 | s[int(q * p)] * (1.0 - (q * p) % 1)
432 | + s[min(int(q * p) + 1, p)] * ((q * p) % 1)
433 | for q in quantiles
434 | ]
435 | else:
436 | raise ValueError(f"Unknown method {method}")
437 | else:
438 | scaled_cm = self.scale(vmin=min(index), vmax=max(index))
439 |
440 | n = len(index) - 1
441 |
442 | if round_method == "int":
443 | index = [round(x) for x in index]
444 |
445 | if round_method == "log10":
446 | index = [_base(x) for x in index]
447 |
448 | colors = [
449 | scaled_cm.rgba_floats_tuple(
450 | index[i] * (1.0 - i / (n - 1.0)) + index[i + 1] * i / (n - 1.0),
451 | )
452 | for i in range(n)
453 | ]
454 |
455 | caption = self.caption
456 | text_color = self.text_color
457 |
458 | return StepColormap(
459 | colors,
460 | index=index,
461 | vmin=index[0],
462 | vmax=index[-1],
463 | caption=caption,
464 | text_color=text_color,
465 | max_labels=max_labels,
466 | tick_labels=self.tick_labels,
467 | )
468 |
469 | def scale(
470 | self,
471 | vmin: float = 0.0,
472 | vmax: float = 1.0,
473 | max_labels: int = 10,
474 | ) -> "LinearColormap":
475 | """Transforms the colorscale so that the minimal and maximal values
476 | fit the given parameters.
477 | """
478 | return LinearColormap(
479 | self.colors,
480 | index=[
481 | vmin + (vmax - vmin) * (x - self.vmin) * 1.0 / (self.vmax - self.vmin)
482 | for x in self.index
483 | ], # noqa
484 | vmin=vmin,
485 | vmax=vmax,
486 | caption=self.caption,
487 | text_color=self.text_color,
488 | max_labels=max_labels,
489 | )
490 |
491 |
492 | class StepColormap(ColorMap):
493 | """Creates a ColorMap based on linear interpolation of a set of colors
494 | over a given index.
495 |
496 | Parameters
497 | ----------
498 | colors : list-like object
499 | The set of colors to be used for interpolation.
500 | Colors can be provided in the form:
501 | * tuples of int between 0 and 255 (e.g: `(255,255,0)` or
502 | `(255, 255, 0, 255)`)
503 | * tuples of floats between 0. and 1. (e.g: `(1.,1.,0.)` or
504 | `(1., 1., 0., 1.)`)
505 | * HTML-like string (e.g: `"#ffff00`)
506 | * a color name or shortcut (e.g: `"y"` or `"yellow"`)
507 | index : list of floats, default None
508 | The bounds of the colors. The lower value is inclusive,
509 | the upper value is exclusive.
510 | It has to be sorted, and have the same length as `colors`.
511 | If None, a regular grid between `vmin` and `vmax` is created.
512 | vmin : float, default 0.
513 | The minimal value for the colormap.
514 | Values lower than `vmin` will be bound directly to `colors[0]`.
515 | vmax : float, default 1.
516 | The maximal value for the colormap.
517 | Values higher than `vmax` will be bound directly to `colors[-1]`.
518 | caption: str
519 | A caption to draw with the colormap.
520 | text_color: str, default "black"
521 | The color for the text.
522 | max_labels : int, default 10
523 | Maximum number of legend tick labels
524 | tick_labels: list of floats, default None
525 | If given, used as the positions of ticks.
526 | """
527 |
528 | def __init__(
529 | self,
530 | colors: Sequence[TypeAnyColorType],
531 | index: Optional[Sequence[float]] = None,
532 | vmin: float = 0.0,
533 | vmax: float = 1.0,
534 | caption: str = "",
535 | text_color: str = "black",
536 | max_labels: int = 10,
537 | tick_labels: Optional[Sequence[float]] = None,
538 | ):
539 | super().__init__(
540 | vmin=vmin,
541 | vmax=vmax,
542 | caption=caption,
543 | text_color=text_color,
544 | max_labels=max_labels,
545 | )
546 | self.tick_labels = tick_labels
547 |
548 | n = len(colors)
549 | if n < 1:
550 | raise ValueError("You must provide at least 1 colors.")
551 | if index is None:
552 | self.index = [vmin + (vmax - vmin) * i * 1.0 / n for i in range(n + 1)]
553 | else:
554 | self.index = list(index)
555 | self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
556 |
557 | def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
558 | """
559 | Provides the color corresponding to value `x` in the
560 | form of a tuple (R,G,B,A) with float values between 0. and 1.
561 |
562 | """
563 | if x <= self.index[0]:
564 | return self.colors[0]
565 | if x >= self.index[-1]:
566 | return self.colors[-1]
567 |
568 | i = len([u for u in self.index if u <= x]) # 0 < i < n.
569 | return self.colors[i - 1]
570 |
571 | def to_linear(
572 | self,
573 | index: Optional[Sequence[float]] = None,
574 | max_labels: int = 10,
575 | ) -> LinearColormap:
576 | """
577 | Transforms the StepColormap into a LinearColormap.
578 |
579 | Parameters
580 | ----------
581 | index : list of floats, default None
582 | The values corresponding to each color in the output colormap.
583 | It has to be sorted.
584 | If None, a regular grid between `vmin` and `vmax` is created.
585 | max_labels : int, default 10
586 | Maximum number of legend tick labels
587 |
588 | """
589 | if index is None:
590 | n = len(self.index) - 1
591 | index = [
592 | self.index[i] * (1.0 - i / (n - 1.0))
593 | + self.index[i + 1] * i / (n - 1.0)
594 | for i in range(n)
595 | ]
596 |
597 | colors = [self.rgba_floats_tuple(x) for x in index]
598 | return LinearColormap(
599 | colors,
600 | index=index,
601 | vmin=self.vmin,
602 | vmax=self.vmax,
603 | caption=self.caption,
604 | text_color=self.text_color,
605 | max_labels=max_labels,
606 | )
607 |
608 | def scale(
609 | self,
610 | vmin: float = 0.0,
611 | vmax: float = 1.0,
612 | max_labels: int = 10,
613 | ) -> "StepColormap":
614 | """Transforms the colorscale so that the minimal and maximal values
615 | fit the given parameters.
616 | """
617 | return StepColormap(
618 | self.colors,
619 | index=[
620 | vmin + (vmax - vmin) * (x - self.vmin) * 1.0 / (self.vmax - self.vmin)
621 | for x in self.index
622 | ], # noqa
623 | vmin=vmin,
624 | vmax=vmax,
625 | caption=self.caption,
626 | text_color=self.text_color,
627 | max_labels=max_labels,
628 | )
629 |
630 |
631 | class _LinearColormaps:
632 | """A class for hosting the list of built-in linear colormaps."""
633 |
634 | def __init__(self):
635 | self._schemes = _schemes.copy()
636 | self._colormaps = {key: LinearColormap(val) for key, val in _schemes.items()}
637 | for key, val in _schemes.items():
638 | setattr(self, key, LinearColormap(val))
639 |
640 | def _repr_html_(self) -> str:
641 | return Template(
642 | """
643 |
644 | {% for key,val in this._colormaps.items() %}
645 | {{key}} | {{val._repr_html_()}} |
646 | {% endfor %}
647 | """,
648 | ).render(this=self)
649 |
650 |
651 | linear = _LinearColormaps()
652 |
653 |
654 | class _StepColormaps:
655 | """A class for hosting the list of built-in step colormaps."""
656 |
657 | def __init__(self):
658 | self._schemes = _schemes.copy()
659 | self._colormaps = {key: StepColormap(val) for key, val in _schemes.items()}
660 | for key, val in _schemes.items():
661 | setattr(self, key, StepColormap(val))
662 |
663 | def _repr_html_(self) -> str:
664 | return Template(
665 | """
666 |
667 | {% for key,val in this._colormaps.items() %}
668 | {{key}} | {{val._repr_html_()}} |
669 | {% endfor %}
670 | """,
671 | ).render(this=self)
672 |
673 |
674 | step = _StepColormaps()
675 |
--------------------------------------------------------------------------------
/branca/element.py:
--------------------------------------------------------------------------------
1 | """
2 | Element
3 | -------
4 |
5 | A generic class for creating Elements.
6 |
7 | """
8 |
9 | import base64
10 | import json
11 | import warnings
12 | from binascii import hexlify
13 | from collections import OrderedDict
14 | from html import escape
15 | from os import urandom
16 | from pathlib import Path
17 | from typing import BinaryIO, List, Optional, Tuple, Type, Union
18 | from urllib.request import urlopen
19 |
20 | from jinja2 import Environment, PackageLoader, Template
21 |
22 | from .utilities import TypeParseSize, _camelify, _parse_size, none_max, none_min
23 |
24 | ENV = Environment(loader=PackageLoader("branca", "templates"))
25 |
26 |
27 | class Element:
28 | """Basic Element object that does nothing.
29 | Other Elements may inherit from this one.
30 |
31 | Parameters
32 | ----------
33 | template : str, default None
34 | A jinaj2-compatible template string for rendering the element.
35 | If None, template will be:
36 |
37 | .. code-block:: jinja
38 |
39 | {% for name, element in this._children.items() %}
40 | {{element.render(**kwargs)}}
41 | {% endfor %}
42 |
43 | so that all the element's children are rendered.
44 | template_name : str, default None
45 | If no template is provided, you can also provide a filename.
46 |
47 | """
48 |
49 | _template: Template = Template(
50 | "{% for name, element in this._children.items() %}\n"
51 | " {{element.render(**kwargs)}}"
52 | "{% endfor %}",
53 | )
54 |
55 | def __init__(
56 | self,
57 | template: Optional[str] = None,
58 | template_name: Optional[str] = None,
59 | ):
60 | self._name: str = "Element"
61 | self._id: str = hexlify(urandom(16)).decode()
62 | self._children: OrderedDict[str, Element] = OrderedDict()
63 | self._parent: Optional[Element] = None
64 | self._template_str: Optional[str] = template
65 | self._template_name: Optional[str] = template_name
66 |
67 | if template is not None:
68 | self._template = Template(template)
69 | elif template_name is not None:
70 | self._template = ENV.get_template(template_name)
71 |
72 | def __getstate__(self) -> dict:
73 | """Modify object state when pickling the object.
74 |
75 | jinja2 Templates cannot be pickled, so remove the instance attribute
76 | if it exists. It will be added back when unpickling (see __setstate__).
77 | """
78 | state: dict = self.__dict__.copy()
79 | state.pop("_template", None)
80 | return state
81 |
82 | def __setstate__(self, state: dict):
83 | """Re-add _template instance attribute when unpickling"""
84 | if state["_template_str"] is not None:
85 | state["_template"] = Template(state["_template_str"])
86 | elif state["_template_name"] is not None:
87 | state["_template"] = ENV.get_template(state["_template_name"])
88 |
89 | self.__dict__.update(state)
90 |
91 | def get_name(self) -> str:
92 | """Returns a string representation of the object.
93 | This string has to be unique and to be a python and
94 | javascript-compatible
95 | variable name.
96 | """
97 | return _camelify(self._name) + "_" + self._id
98 |
99 | def _get_self_bounds(self) -> List[List[Optional[float]]]:
100 | """Computes the bounds of the object itself (not including it's children)
101 | in the form [[lat_min, lon_min], [lat_max, lon_max]]
102 | """
103 | return [[None, None], [None, None]]
104 |
105 | def get_bounds(self) -> List[List[Optional[float]]]:
106 | """Computes the bounds of the object and all it's children
107 | in the form [[lat_min, lon_min], [lat_max, lon_max]].
108 | """
109 | bounds = self._get_self_bounds()
110 |
111 | for child in self._children.values():
112 | child_bounds = child.get_bounds()
113 | bounds = [
114 | [
115 | none_min(bounds[0][0], child_bounds[0][0]),
116 | none_min(bounds[0][1], child_bounds[0][1]),
117 | ],
118 | [
119 | none_max(bounds[1][0], child_bounds[1][0]),
120 | none_max(bounds[1][1], child_bounds[1][1]),
121 | ],
122 | ]
123 | return bounds
124 |
125 | def add_children(
126 | self,
127 | child: "Element",
128 | name: Optional[str] = None,
129 | index: Optional[int] = None,
130 | ) -> "Element":
131 | """Add a child."""
132 | warnings.warn(
133 | "Method `add_children` is deprecated. Please use `add_child` instead.",
134 | FutureWarning,
135 | stacklevel=2,
136 | )
137 | return self.add_child(child, name=name, index=index)
138 |
139 | def add_child(
140 | self,
141 | child: "Element",
142 | name: Optional[str] = None,
143 | index: Optional[int] = None,
144 | ) -> "Element":
145 | """Add a child."""
146 | if name is None:
147 | name = child.get_name()
148 | if index is None:
149 | self._children[name] = child
150 | else:
151 | items = [item for item in self._children.items() if item[0] != name]
152 | items.insert(int(index), (name, child))
153 | self._children = OrderedDict(items)
154 | child._parent = self
155 | return self
156 |
157 | def add_to(
158 | self,
159 | parent: "Element",
160 | name: Optional[str] = None,
161 | index: Optional[int] = None,
162 | ) -> "Element":
163 | """Add element to a parent."""
164 | parent.add_child(self, name=name, index=index)
165 | return self
166 |
167 | def to_dict(
168 | self,
169 | depth: int = -1,
170 | ordered: bool = True,
171 | **kwargs,
172 | ) -> Union[dict, OrderedDict]:
173 | """Returns a dict representation of the object."""
174 | dict_fun: Type[Union[dict, OrderedDict]]
175 | if ordered:
176 | dict_fun = OrderedDict
177 | else:
178 | dict_fun = dict
179 | out = dict_fun()
180 | out["name"] = self._name
181 | out["id"] = self._id
182 | if depth != 0:
183 | out["children"] = dict_fun(
184 | [
185 | (name, child.to_dict(depth=depth - 1))
186 | for name, child in self._children.items()
187 | ],
188 | )
189 | return out
190 |
191 | def to_json(self, depth: int = -1, **kwargs) -> str:
192 | """Returns a JSON representation of the object."""
193 | return json.dumps(self.to_dict(depth=depth, ordered=True), **kwargs)
194 |
195 | def get_root(self) -> "Element":
196 | """Returns the root of the elements tree."""
197 | if self._parent is None:
198 | return self
199 | else:
200 | return self._parent.get_root()
201 |
202 | def render(self, **kwargs) -> str:
203 | """Renders the HTML representation of the element."""
204 | return self._template.render(this=self, kwargs=kwargs)
205 |
206 | def save(
207 | self,
208 | outfile: Union[str, bytes, Path, BinaryIO],
209 | close_file: bool = True,
210 | **kwargs,
211 | ):
212 | """Saves an Element into a file.
213 |
214 | Parameters
215 | ----------
216 | outfile : str or file object
217 | The file (or filename) where you want to output the html.
218 | close_file : bool, default True
219 | Whether the file has to be closed after write.
220 | """
221 | fid: BinaryIO
222 | if isinstance(outfile, (str, bytes, Path)):
223 | fid = open(outfile, "wb")
224 | else:
225 | fid = outfile
226 |
227 | root = self.get_root()
228 | html = root.render(**kwargs)
229 | fid.write(html.encode("utf8"))
230 | if close_file:
231 | fid.close()
232 |
233 |
234 | class Link(Element):
235 | """An abstract class for embedding a link in the HTML."""
236 |
237 | def __init__(self, url: str, download: bool = False):
238 | super().__init__()
239 | self.url = url
240 | self.code: Optional[bytes] = None
241 | if download:
242 | self.get_code()
243 |
244 | def get_code(self) -> bytes:
245 | """Opens the link and returns the response's content."""
246 | if self.code is None:
247 | self.code = urlopen(self.url).read()
248 | return self.code
249 |
250 | def to_dict(
251 | self,
252 | depth: int = -1,
253 | ordered: bool = True,
254 | **kwargs,
255 | ) -> Union[dict, OrderedDict]:
256 | """Returns a dict representation of the object."""
257 | out = super().to_dict(depth=depth, ordered=ordered, **kwargs)
258 | out["url"] = self.url
259 | return out
260 |
261 |
262 | class JavascriptLink(Link):
263 | """Create a JavascriptLink object based on a url.
264 |
265 | Parameters
266 | ----------
267 | url : str
268 | The url to be linked
269 | download : bool, default False
270 | Whether the target document shall be loaded right now.
271 |
272 | """
273 |
274 | _template = Template(
275 | '{% if kwargs.get("embedded",False) %}'
276 | ""
277 | "{% else %}"
278 | ''
279 | "{% endif %}",
280 | )
281 |
282 | def __init__(self, url: str, download: bool = False):
283 | super().__init__(url=url, download=download)
284 | self._name = "JavascriptLink"
285 |
286 |
287 | class CssLink(Link):
288 | """Create a CssLink object based on a url.
289 |
290 | Parameters
291 | ----------
292 | url : str
293 | The url to be linked
294 | download : bool, default False
295 | Whether the target document shall be loaded right now.
296 |
297 | """
298 |
299 | _template = Template(
300 | '{% if kwargs.get("embedded",False) %}'
301 | ""
302 | "{% else %}"
303 | ''
304 | "{% endif %}",
305 | )
306 |
307 | def __init__(self, url: str, download: bool = False):
308 | super().__init__(url=url, download=download)
309 | self._name = "CssLink"
310 |
311 |
312 | class Figure(Element):
313 | """Create a Figure object, to plot things into it.
314 |
315 | Parameters
316 | ----------
317 | width : str, default "100%"
318 | The width of the Figure.
319 | It may be a percentage or pixel value (like "300px").
320 | height : str, default None
321 | The height of the Figure.
322 | It may be a percentage or a pixel value (like "300px").
323 | ratio : str, default "60%"
324 | A percentage defining the aspect ratio of the Figure.
325 | It will be ignored if height is not None.
326 | title : str, default None
327 | Figure title.
328 | figsize : tuple of two int, default None
329 | If you're a matplotlib addict, you can overwrite width and
330 | height. Values will be converted into pixels in using 60 dpi.
331 | For example figsize=(10, 5) will result in
332 | width="600px", height="300px".
333 | """
334 |
335 | _template = Template(
336 | "\n"
337 | "\n"
338 | "\n"
339 | "{% if this.title %}{{this.title}}{% endif %}"
340 | " {{this.header.render(**kwargs)}}\n"
341 | "\n"
342 | "\n"
343 | " {{this.html.render(**kwargs)}}\n"
344 | "\n"
345 | "\n"
348 | "\n",
349 | )
350 |
351 | def __init__(
352 | self,
353 | width: str = "100%",
354 | height: Optional[str] = None,
355 | ratio: str = "60%",
356 | title: Optional[str] = None,
357 | figsize: Optional[Tuple[int, int]] = None,
358 | ):
359 | super().__init__()
360 | self._name = "Figure"
361 | self.header = Element()
362 | self.html = Element()
363 | self.script = Element()
364 |
365 | self.header._parent = self
366 | self.html._parent = self
367 | self.script._parent = self
368 |
369 | self.width = width
370 | self.height = height
371 | self.ratio = ratio
372 | self.title = title
373 | if figsize is not None:
374 | self.width = str(60 * figsize[0]) + "px"
375 | self.height = str(60 * figsize[1]) + "px"
376 |
377 | # Create the meta tag.
378 | self.header.add_child(
379 | Element(
380 | '',
381 | ), # noqa
382 | name="meta_http",
383 | )
384 |
385 | def to_dict(
386 | self,
387 | depth: int = -1,
388 | ordered: bool = True,
389 | **kwargs,
390 | ) -> Union[dict, OrderedDict]:
391 | """Returns a dict representation of the object."""
392 | out = super().to_dict(depth=depth, **kwargs)
393 | out["header"] = self.header.to_dict(depth=depth - 1, **kwargs)
394 | out["html"] = self.html.to_dict(depth=depth - 1, **kwargs)
395 | out["script"] = self.script.to_dict(depth=depth - 1, **kwargs)
396 | return out
397 |
398 | def get_root(self) -> "Figure":
399 | """Returns the root of the elements tree."""
400 | return self
401 |
402 | def render(self, **kwargs) -> str:
403 | """Renders the HTML representation of the element."""
404 | for name, child in self._children.items():
405 | child.render(**kwargs)
406 | return self._template.render(this=self, kwargs=kwargs)
407 |
408 | def _repr_html_(self, **kwargs) -> str:
409 | """Displays the Figure in a Jupyter notebook."""
410 | html = escape(self.render(**kwargs))
411 | if self.height is None:
412 | iframe = (
413 | ''
414 | '
' # noqa
415 | 'Make this Notebook Trusted to load map: File -> Trust Notebook' # noqa
416 | '"
420 | "
"
421 | ).format(html=html, width=self.width, ratio=self.ratio)
422 | else:
423 | iframe = (
424 | '"
428 | ).format(html=html, width=self.width, height=self.height)
429 | return iframe
430 |
431 | def add_subplot(self, x: int, y: int, n: int, margin: float = 0.05) -> "Div":
432 | """Creates a div child subplot in a matplotlib.figure.add_subplot style.
433 |
434 | Parameters
435 | ----------
436 | x : int
437 | The number of rows in the grid.
438 | y : int
439 | The number of columns in the grid.
440 | n : int
441 | The cell number in the grid, counted from 1 to x*y.
442 | margin : float, default 0.05
443 | Factor to add to the left, top, width and height parameters.
444 |
445 | Example
446 | -------
447 | >>> fig.add_subplot(3, 2, 5)
448 | # Create a div in the 5th cell of a 3rows x 2columns
449 | grid(bottom-left corner).
450 | """
451 | width = 1.0 / y
452 | height = 1.0 / x
453 | left = ((n - 1) % y) * width
454 | top = ((n - 1) // y) * height
455 |
456 | left = left + width * margin
457 | top = top + height * margin
458 | width = width * (1 - 2.0 * margin)
459 | height = height * (1 - 2.0 * margin)
460 |
461 | div = Div(
462 | position="absolute",
463 | width=f"{100.0 * width}%",
464 | height=f"{100.0 * height}%",
465 | left=f"{100.0 * left}%",
466 | top=f"{100.0 * top}%",
467 | )
468 | self.add_child(div)
469 | return div
470 |
471 |
472 | class Html(Element):
473 | """Create an HTML div object for embedding data.
474 |
475 | Parameters
476 | ----------
477 | data : str
478 | The HTML data to be embedded.
479 | script : bool
480 | If True, data will be embedded without escaping
481 | (suitable for embedding html-ready code)
482 | width : int or str, default '100%'
483 | The width of the output div element.
484 | Ex: 120 , '80%'
485 | height : int or str, default '100%'
486 | The height of the output div element.
487 | Ex: 120 , '80%'
488 | """
489 |
490 | _template = Template(
491 | '' # noqa
493 | "{% if this.script %}{{this.data}}{% else %}{{this.data|e}}{% endif %}
",
494 | )
495 |
496 | def __init__(
497 | self,
498 | data: str,
499 | script: bool = False,
500 | width: TypeParseSize = "100%",
501 | height: TypeParseSize = "100%",
502 | ):
503 | super().__init__()
504 | self._name = "Html"
505 | self.script = script
506 | self.data = data
507 |
508 | self.width = _parse_size(width)
509 | self.height = _parse_size(height)
510 |
511 |
512 | class Div(Figure):
513 | """Create a Div to be embedded in a Figure.
514 |
515 | Parameters
516 | ----------
517 | width: int or str, default '100%'
518 | The width of the div in pixels (int) or percentage (str).
519 | height: int or str, default '100%'
520 | The height of the div in pixels (int) or percentage (str).
521 | left: int or str, default '0%'
522 | The left-position of the div in pixels (int) or percentage (str).
523 | top: int or str, default '0%'
524 | The top-position of the div in pixels (int) or percentage (str).
525 | position: str, default 'relative'
526 | The position policy of the div.
527 | Usual values are 'relative', 'absolute', 'fixed', 'static'.
528 | """
529 |
530 | _template = Template(
531 | "{% macro header(this, kwargs) %}"
532 | ""
539 | "{% endmacro %}"
540 | "{% macro html(this, kwargs) %}"
541 | '{{this.html.render(**kwargs)}}
'
542 | "{% endmacro %}",
543 | )
544 |
545 | def __init__(
546 | self,
547 | width: TypeParseSize = "100%",
548 | height: TypeParseSize = "100%",
549 | left: TypeParseSize = "0%",
550 | top: TypeParseSize = "0%",
551 | position: str = "relative",
552 | ):
553 | super(Figure, self).__init__()
554 | self._name = "Div"
555 |
556 | # Size Parameters.
557 | self.width = _parse_size(width) # type: ignore
558 | self.height = _parse_size(height) # type: ignore
559 | self.left = _parse_size(left)
560 | self.top = _parse_size(top)
561 | self.position = position
562 |
563 | self.header = Element()
564 | self.html = Element(
565 | "{% for name, element in this._children.items() %}"
566 | "{{element.render(**kwargs)}}"
567 | "{% endfor %}",
568 | )
569 | self.script = Element()
570 |
571 | self.header._parent = self
572 | self.html._parent = self
573 | self.script._parent = self
574 |
575 | def get_root(self) -> "Div":
576 | """Returns the root of the elements tree."""
577 | return self
578 |
579 | def render(self, **kwargs):
580 | """Renders the HTML representation of the element."""
581 | figure = self._parent
582 | assert isinstance(figure, Figure), (
583 | "You cannot render this Element " "if it is not in a Figure."
584 | )
585 |
586 | for name, element in self._children.items():
587 | element.render(**kwargs)
588 |
589 | for name, element in self.header._children.items():
590 | figure.header.add_child(element, name=name)
591 |
592 | for name, element in self.script._children.items():
593 | figure.script.add_child(element, name=name)
594 |
595 | header = self._template.module.__dict__.get("header", None)
596 | if header is not None:
597 | figure.header.add_child(Element(header(self, kwargs)), name=self.get_name())
598 |
599 | html = self._template.module.__dict__.get("html", None)
600 | if html is not None:
601 | figure.html.add_child(Element(html(self, kwargs)), name=self.get_name())
602 |
603 | script = self._template.module.__dict__.get("script", None)
604 | if script is not None:
605 | figure.script.add_child(Element(script(self, kwargs)), name=self.get_name())
606 |
607 | def _repr_html_(self, **kwargs) -> str:
608 | """Displays the Div in a Jupyter notebook."""
609 | if self._parent is None:
610 | self.add_to(Figure())
611 | out = self._parent._repr_html_(**kwargs) # type: ignore
612 | self._parent = None
613 | else:
614 | out = self._parent._repr_html_(**kwargs) # type: ignore
615 | return out
616 |
617 |
618 | class IFrame(Element):
619 | """Create a Figure object, to plot things into it.
620 |
621 | Parameters
622 | ----------
623 | html : str, default None
624 | Eventual HTML code that you want to put in the frame.
625 | width : str, default "100%"
626 | The width of the Figure.
627 | It may be a percentage or pixel value (like "300px").
628 | height : str, default None
629 | The height of the Figure.
630 | It may be a percentage or a pixel value (like "300px").
631 | ratio : str, default "60%"
632 | A percentage defining the aspect ratio of the Figure.
633 | It will be ignored if height is not None.
634 | figsize : tuple of two int, default None
635 | If you're a matplotlib addict, you can overwrite width and
636 | height. Values will be converted into pixels in using 60 dpi.
637 | For example figsize=(10, 5) will result in
638 | width="600px", height="300px".
639 | """
640 |
641 | def __init__(
642 | self,
643 | html: Optional[Union[str, Element]] = None,
644 | width: str = "100%",
645 | height: Optional[str] = None,
646 | ratio: str = "60%",
647 | figsize: Optional[Tuple[int, int]] = None,
648 | ):
649 | super().__init__()
650 | self._name = "IFrame"
651 |
652 | self.width = width
653 | self.height = height
654 | self.ratio = ratio
655 | if figsize is not None:
656 | self.width = str(60 * figsize[0]) + "px"
657 | self.height = str(60 * figsize[1]) + "px"
658 |
659 | if isinstance(html, str):
660 | self.add_child(Element(html))
661 | elif html is not None:
662 | self.add_child(html)
663 |
664 | def render(self, **kwargs) -> str:
665 | """Renders the HTML representation of the element."""
666 | html = super().render(**kwargs)
667 | html = "data:text/html;charset=utf-8;base64," + base64.b64encode(
668 | html.encode("utf8"),
669 | ).decode("utf8")
670 |
671 | if self.height is None:
672 | iframe = (
673 | ''
674 | '
' # noqa
675 | '"
678 | "
"
679 | ).format(html=html, width=self.width, ratio=self.ratio)
680 | else:
681 | iframe = (
682 | ''
684 | ).format(html=html, width=self.width, height=self.height)
685 | return iframe
686 |
687 |
688 | class MacroElement(Element):
689 | """This is a parent class for Elements defined by a macro template.
690 | To compute your own element, all you have to do is:
691 |
692 | * To inherit from this class
693 | * Overwrite the '_name' attribute
694 | * Overwrite the '_template' attribute with something of the form::
695 |
696 | {% macro header(this, kwargs) %}
697 | ...
698 | {% endmacro %}
699 |
700 | {% macro html(this, kwargs) %}
701 | ...
702 | {% endmacro %}
703 |
704 | {% macro script(this, kwargs) %}
705 | ...
706 | {% endmacro %}
707 |
708 | """
709 |
710 | _template = Template("")
711 |
712 | def __init__(self):
713 | super().__init__()
714 | self._name = "MacroElement"
715 |
716 | def render(self, **kwargs):
717 | """Renders the HTML representation of the element."""
718 | figure = self.get_root()
719 | assert isinstance(figure, Figure), (
720 | "You cannot render this Element " "if it is not in a Figure."
721 | )
722 |
723 | header = self._template.module.__dict__.get("header", None)
724 | if header is not None:
725 | figure.header.add_child(Element(header(self, kwargs)), name=self.get_name())
726 |
727 | html = self._template.module.__dict__.get("html", None)
728 | if html is not None:
729 | figure.html.add_child(Element(html(self, kwargs)), name=self.get_name())
730 |
731 | script = self._template.module.__dict__.get("script", None)
732 | if script is not None:
733 | figure.script.add_child(Element(script(self, kwargs)), name=self.get_name())
734 |
735 | for name, element in self._children.items():
736 | element.render(**kwargs)
737 |
--------------------------------------------------------------------------------
/branca/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-visualization/branca/d4bbe7af6ed08feabfe397381e2f40b17f8f5554/branca/py.typed
--------------------------------------------------------------------------------
/branca/scheme_base_codes.json:
--------------------------------------------------------------------------------
1 | {"codes": ["viridis", "plasma", "inferno", "magma", "Spectral", "RdYlGn", "PuBu", "Accent", "OrRd", "Set1", "Set2", "Set3", "BuPu", "Dark2", "RdBu", "Oranges", "BuGn", "PiYG", "YlOrBr", "YlGn", "Pastel2", "RdPu", "Greens", "PRGn", "YlGnBu", "RdYlBu", "Paired", "BrBG", "Purples", "Reds", "Pastel1", "GnBu", "Greys", "RdGy", "YlOrRd", "PuOr", "PuRd", "Blues", "PuBuGn"]}
2 |
--------------------------------------------------------------------------------
/branca/scheme_info.json:
--------------------------------------------------------------------------------
1 | {"Spectral": "Diverging", "RdYlGn": "Diverging", "Set2": "Qualitative", "Accent": "Qualitative", "OrRd": "Sequential", "Set1": "Qualitative", "PuBu": "Sequential", "Set3": "Qualitative", "BuPu": "Sequential", "Dark2": "Qualitative", "RdBu": "Diverging", "BuGn": "Sequential", "PiYG": "Diverging", "YlOrBr": "Sequential", "YlGn": "Sequential", "RdPu": "Sequential", "PRGn": "Diverging", "YlGnBu": "Sequential", "RdYlBu": "Diverging", "Paired": "Qualitative", "Pastel2": "Qualitative", "Pastel1": "Qualitative", "GnBu": "Sequential", "RdGy": "Diverging", "YlOrRd": "Sequential", "PuOr": "Diverging", "PuRd": "Sequential", "BrBG": "Diverging", "PuBuGn": "Sequential", "Greens": "Sequential", "viridis": "Sequential", "plasma": "Sequential", "inferno": "Sequential", "magma": "Sequential", "Oranges": "Sequential", "Blues": "Sequential", "Greys": "Sequential", "Reds": "Sequential", "Purples": "Sequential"}
2 |
--------------------------------------------------------------------------------
/branca/templates/color_scale.js:
--------------------------------------------------------------------------------
1 | {% macro script(this, kwargs) %}
2 | var {{this.get_name()}} = {};
3 |
4 | {%if this.color_range %}
5 | {{this.get_name()}}.color = d3.scale.threshold()
6 | .domain({{this.color_domain}})
7 | .range({{this.color_range}});
8 | {%else%}
9 | {{this.get_name()}}.color = d3.scale.threshold()
10 | .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}])
11 | .range(['{{ this.fill_color }}', '{{ this.fill_color }}']);
12 | {%endif%}
13 |
14 | {{this.get_name()}}.x = d3.scale.linear()
15 | .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}])
16 | .range([0, {{ this.width }} - 50]);
17 |
18 | {{this.get_name()}}.legend = L.control({position: 'topright'});
19 | {{this.get_name()}}.legend.onAdd = function (map) {var div = L.DomUtil.create('div', 'legend'); return div};
20 | {{this.get_name()}}.legend.addTo({{this._parent.get_name()}});
21 |
22 | {{this.get_name()}}.xAxis = d3.svg.axis()
23 | .scale({{this.get_name()}}.x)
24 | .orient("top")
25 | .tickSize(1)
26 | .tickValues({{ this.tick_labels }});
27 |
28 | {{this.get_name()}}.svg = d3.select(".legend.leaflet-control").append("svg")
29 | .attr("id", 'legend')
30 | .attr("width", {{ this.width }})
31 | .attr("height", {{ this.height }});
32 |
33 | {{this.get_name()}}.g = {{this.get_name()}}.svg.append("g")
34 | .attr("class", "key")
35 | .attr("fill", {{ this.text_color | tojson }})
36 | .attr("transform", "translate(25,16)");
37 |
38 | {{this.get_name()}}.g.selectAll("rect")
39 | .data({{this.get_name()}}.color.range().map(function(d, i) {
40 | return {
41 | x0: i ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i - 1]) : {{this.get_name()}}.x.range()[0],
42 | x1: i < {{this.get_name()}}.color.domain().length ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i]) : {{this.get_name()}}.x.range()[1],
43 | z: d
44 | };
45 | }))
46 | .enter().append("rect")
47 | .attr("height", {{ this.height }} - 30)
48 | .attr("x", function(d) { return d.x0; })
49 | .attr("width", function(d) { return d.x1 - d.x0; })
50 | .style("fill", function(d) { return d.z; });
51 |
52 | {{this.get_name()}}.g.call({{this.get_name()}}.xAxis).append("text")
53 | .attr("class", "caption")
54 | .attr("y", 21)
55 | .attr("fill", {{ this.text_color | tojson }})
56 | .text({{ this.caption|tojson }});
57 | {% endmacro %}
58 |
--------------------------------------------------------------------------------
/branca/utilities.py:
--------------------------------------------------------------------------------
1 | """
2 | Utilities
3 | -------
4 |
5 | Utility module for Folium helper functions.
6 |
7 | """
8 |
9 | import base64
10 | import json
11 | import math
12 | import os
13 | import re
14 | import struct
15 | import typing
16 | import zlib
17 | from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
18 |
19 | from jinja2 import Environment, PackageLoader
20 |
21 | try:
22 | import numpy as np
23 | except ImportError:
24 | np = None # type: ignore
25 |
26 | if typing.TYPE_CHECKING:
27 | from branca.colormap import ColorMap
28 |
29 |
30 | rootpath: str = os.path.abspath(os.path.dirname(__file__))
31 |
32 |
33 | TypeParseSize = Union[int, float, str, Tuple[float, str]]
34 |
35 |
36 | def get_templates() -> Environment:
37 | """Get Jinja templates."""
38 | return Environment(loader=PackageLoader("branca", "templates"))
39 |
40 |
41 | def legend_scaler(
42 | legend_values: Sequence[float],
43 | max_labels: int = 10,
44 | ) -> List[Union[float, str]]:
45 | """
46 | Downsamples the number of legend values so that there isn't a collision
47 | of text on the legend colorbar (within reason). The colorbar seems to
48 | support ~10 entries as a maximum.
49 |
50 | """
51 | legend_ticks: List[Union[float, str]]
52 | if len(legend_values) < max_labels:
53 | legend_ticks = list(legend_values)
54 | else:
55 | spacer = int(math.ceil(len(legend_values) / max_labels))
56 | legend_ticks = []
57 | for i in legend_values[::spacer]:
58 | legend_ticks += [i]
59 | legend_ticks += [""] * (spacer - 1)
60 | return legend_ticks
61 |
62 |
63 | def linear_gradient(hexList: List[str], nColors: int) -> List[str]:
64 | """
65 | Given a list of hexcode values, will return a list of length
66 | nColors where the colors are linearly interpolated between the
67 | (r, g, b) tuples that are given.
68 | """
69 |
70 | def _scale(start, finish, length, i):
71 | """
72 | Return the value correct value of a number that is in between start
73 | and finish, for use in a loop of length *length*.
74 |
75 | """
76 | base = 16
77 |
78 | fraction = float(i) / (length - 1)
79 | raynge = int(finish, base) - int(start, base)
80 | thex = hex(int(int(start, base) + fraction * raynge)).split("x")[-1]
81 | if len(thex) != 2:
82 | thex = "0" + thex
83 | return thex
84 |
85 | allColors: List[str] = []
86 | # Separate (R, G, B) pairs.
87 | for start, end in zip(hexList[:-1], hexList[1:]):
88 | # Linearly interpolate between pair of hex ###### values and
89 | # add to list.
90 | nInterpolate = 765
91 | for index in range(nInterpolate):
92 | r = _scale(start[1:3], end[1:3], nInterpolate, index)
93 | g = _scale(start[3:5], end[3:5], nInterpolate, index)
94 | b = _scale(start[5:7], end[5:7], nInterpolate, index)
95 | allColors.append("".join(["#", r, g, b]))
96 |
97 | # Pick only nColors colors from the total list.
98 | result: List[str] = []
99 | for counter in range(nColors):
100 | fraction = float(counter) / (nColors - 1)
101 | index = int(fraction * (len(allColors) - 1))
102 | result.append(allColors[index])
103 | return result
104 |
105 |
106 | def color_brewer(color_code: str, n: int = 6) -> List[str]:
107 | """
108 | Generate a colorbrewer color scheme of length 'len', type 'scheme.
109 | Live examples can be seen at http://colorbrewer2.org/
110 |
111 | """
112 | maximum_n = 253
113 | minimum_n = 3
114 |
115 | if not isinstance(n, int):
116 | raise TypeError("n has to be an int, not a %s" % type(n))
117 |
118 | # Raise an error if the n requested is greater than the maximum.
119 | if n > maximum_n:
120 | raise ValueError(
121 | "The maximum number of colors in a"
122 | " ColorBrewer sequential color series is 253",
123 | )
124 | if n < minimum_n:
125 | raise ValueError(
126 | "The minimum number of colors in a"
127 | " ColorBrewer sequential color series is 3",
128 | )
129 |
130 | if not isinstance(color_code, str):
131 | raise ValueError(f"color should be a string, not a {type(color_code)}.")
132 | if color_code[-2:] == "_r":
133 | base_code = color_code[:-2]
134 | core_color_code = base_code + "_" + str(n).zfill(2)
135 | color_reverse = True
136 | else:
137 | base_code = color_code
138 | core_color_code = base_code + "_" + str(n).zfill(2)
139 | color_reverse = False
140 |
141 | with open(os.path.join(rootpath, "_schemes.json")) as f:
142 | schemes = json.loads(f.read())
143 |
144 | with open(os.path.join(rootpath, "scheme_info.json")) as f:
145 | scheme_info = json.loads(f.read())
146 |
147 | with open(os.path.join(rootpath, "scheme_base_codes.json")) as f:
148 | core_schemes = json.loads(f.read())["codes"]
149 |
150 | if base_code not in core_schemes:
151 | raise ValueError(base_code + " is not a valid ColorBrewer code")
152 |
153 | explicit_scheme = True
154 | if schemes.get(core_color_code) is None:
155 | explicit_scheme = False
156 |
157 | # Only if n is greater than the scheme length do we interpolate values.
158 | if not explicit_scheme:
159 | # Check to make sure that it is not a qualitative scheme.
160 | if scheme_info[base_code] == "Qualitative":
161 | matching_quals = []
162 | for key in schemes:
163 | if base_code + "_" in key:
164 | matching_quals.append(int(key.split("_")[1]))
165 |
166 | raise ValueError(
167 | "Expanded color support is not available"
168 | " for Qualitative schemes; restrict the"
169 | " number of colors for the "
170 | + base_code
171 | + " code to between "
172 | + str(min(matching_quals))
173 | + " and "
174 | + str(max(matching_quals)),
175 | )
176 | else:
177 | longest_scheme_name = base_code
178 | longest_scheme_n = 0
179 | for sn_name in schemes.keys():
180 | if "_" not in sn_name:
181 | continue
182 | if sn_name.split("_")[0] != base_code:
183 | continue
184 | if int(sn_name.split("_")[1]) > longest_scheme_n:
185 | longest_scheme_name = sn_name
186 | longest_scheme_n = int(sn_name.split("_")[1])
187 |
188 | if not color_reverse:
189 | color_scheme = linear_gradient(schemes.get(longest_scheme_name), n)
190 | else:
191 | color_scheme = linear_gradient(
192 | schemes.get(longest_scheme_name)[::-1],
193 | n,
194 | )
195 | else:
196 | if not color_reverse:
197 | color_scheme = schemes.get(core_color_code, None)
198 | else:
199 | color_scheme = schemes.get(core_color_code, None)[::-1]
200 | return color_scheme
201 |
202 |
203 | def image_to_url(
204 | image: Any,
205 | colormap: Union["ColorMap", Callable, None] = None,
206 | origin: str = "upper",
207 | ) -> str:
208 | """Infers the type of an image argument and transforms it into a URL.
209 |
210 | Parameters
211 | ----------
212 | image: string, file or array-like object
213 | * If string, it will be written directly in the output file.
214 | * If file, it's content will be converted as embedded in the
215 | output file.
216 | * If array-like, it will be converted to PNG base64 string and
217 | embedded in the output.
218 | origin : ['upper' | 'lower'], optional, default 'upper'
219 | Place the [0, 0] index of the array in the upper left or
220 | lower left corner of the axes.
221 | colormap : ColorMap or callable, used only for `mono` image.
222 | Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
223 | for transforming a mono image into RGB.
224 | It must output iterables of length 3 or 4, with values between
225 | 0. and 1. Hint : you can use colormaps from `matplotlib.cm`.
226 | """
227 | if hasattr(image, "read"):
228 | # We got an image file.
229 | if hasattr(image, "name"):
230 | # We try to get the image format from the file name.
231 | fileformat = image.name.lower().split(".")[-1]
232 | else:
233 | fileformat = "png"
234 | url = "data:image/{};base64,{}".format(
235 | fileformat,
236 | base64.b64encode(image.read()).decode("utf-8"),
237 | )
238 | elif (not (isinstance(image, str) or isinstance(image, bytes))) and hasattr(
239 | image,
240 | "__iter__",
241 | ):
242 | # We got an array-like object.
243 | png = write_png(image, origin=origin, colormap=colormap)
244 | url = "data:image/png;base64," + base64.b64encode(png).decode("utf-8")
245 | else:
246 | # We got an URL.
247 | url = json.loads(json.dumps(image))
248 |
249 | return url.replace("\n", " ")
250 |
251 |
252 | def write_png(
253 | data: Any,
254 | origin: str = "upper",
255 | colormap: Union["ColorMap", Callable, None] = None,
256 | ) -> bytes:
257 | """
258 | Transform an array of data into a PNG string.
259 | This can be written to disk using binary I/O, or encoded using base64
260 | for an inline PNG like this:
261 |
262 | >>> png_str = write_png(array)
263 | >>> "data:image/png;base64," + png_str.encode("base64")
264 |
265 | Inspired from
266 | http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
267 |
268 | Parameters
269 | ----------
270 | data: numpy array or equivalent list-like object.
271 | Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA)
272 | origin : ['upper' | 'lower'], optional, default 'upper'
273 | Place the [0,0] index of the array in the upper left or lower left
274 | corner of the axes.
275 | colormap : ColorMap subclass or callable, optional
276 | Only needed to transform mono images into RGB. You have three options:
277 | - use a subclass of `ColorMap` like `LinearColorMap`
278 | - use a colormap from `matplotlib.cm`
279 | - use a custom function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)].
280 | It must output iterables of length 3 or 4 with values between 0 and 1.
281 |
282 | Returns
283 | -------
284 | PNG formatted byte string
285 | """
286 | from branca.colormap import ColorMap
287 |
288 | if np is None:
289 | raise ImportError("The NumPy package is required" " for this functionality")
290 |
291 | if isinstance(colormap, ColorMap):
292 | colormap_callable = colormap.rgba_floats_tuple
293 | elif callable(colormap):
294 | colormap_callable = colormap
295 | else:
296 | colormap_callable = lambda x: (x, x, x, 1) # noqa E731
297 |
298 | array = np.atleast_3d(data)
299 | height, width, nblayers = array.shape
300 |
301 | if nblayers not in [1, 3, 4]:
302 | raise ValueError("Data must be NxM (mono), " "NxMx3 (RGB), or NxMx4 (RGBA)")
303 | assert array.shape == (height, width, nblayers)
304 |
305 | if nblayers == 1:
306 | array = np.array(list(map(colormap_callable, array.ravel())))
307 | nblayers = array.shape[1]
308 | if nblayers not in [3, 4]:
309 | raise ValueError(
310 | "colormap must provide colors of" "length 3 (RGB) or 4 (RGBA)",
311 | )
312 | array = array.reshape((height, width, nblayers))
313 | assert array.shape == (height, width, nblayers)
314 |
315 | if nblayers == 3:
316 | array = np.concatenate((array, np.ones((height, width, 1))), axis=2)
317 | nblayers = 4
318 | assert array.shape == (height, width, nblayers)
319 | assert nblayers == 4
320 |
321 | # Normalize to uint8 if it isn't already.
322 | if array.dtype != "uint8":
323 | with np.errstate(divide="ignore", invalid="ignore"):
324 | array = array * 255.0 / array.max(axis=(0, 1)).reshape((1, 1, 4))
325 | array[~np.isfinite(array)] = 0
326 | array = array.astype("uint8")
327 |
328 | # Eventually flip the image.
329 | if origin == "lower":
330 | array = array[::-1, :, :]
331 |
332 | # Transform the array to bytes.
333 | raw_data = b"".join([b"\x00" + array[i, :, :].tobytes() for i in range(height)])
334 |
335 | def png_pack(png_tag, data):
336 | chunk_head = png_tag + data
337 | return (
338 | struct.pack("!I", len(data))
339 | + chunk_head
340 | + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
341 | )
342 |
343 | return b"".join(
344 | [
345 | b"\x89PNG\r\n\x1a\n",
346 | png_pack(b"IHDR", struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
347 | png_pack(b"IDAT", zlib.compress(raw_data, 9)),
348 | png_pack(b"IEND", b""),
349 | ],
350 | )
351 |
352 |
353 | def _camelify(out: str) -> str:
354 | return (
355 | (
356 | "".join(
357 | [
358 | (
359 | "_" + x.lower()
360 | if i < len(out) - 1 and x.isupper() and out[i + 1].islower()
361 | else (
362 | x.lower() + "_"
363 | if i < len(out) - 1 and x.islower() and out[i + 1].isupper()
364 | else x.lower()
365 | )
366 | )
367 | for i, x in enumerate(list(out))
368 | ],
369 | )
370 | )
371 | .lstrip("_")
372 | .replace("__", "_")
373 | )
374 |
375 |
376 | def _parse_size(value: TypeParseSize) -> Tuple[float, str]:
377 | if isinstance(value, (int, float)):
378 | return float(value), "px"
379 | elif isinstance(value, str):
380 | # match digits or a point, possibly followed by a space,
381 | # followed by a unit: either 1 to 5 letters or a percent sign
382 | match = re.fullmatch(r"([\d.]+)\s?(\w{1,5}|%)", value.strip())
383 | if match:
384 | return float(match.group(1)), match.group(2)
385 | else:
386 | raise ValueError(
387 | f"Cannot parse {value!r}, it should be a number followed by a unit.",
388 | )
389 | elif (
390 | isinstance(value, tuple)
391 | and isinstance(value[0], (int, float))
392 | and isinstance(value[1], str)
393 | ):
394 | # value had been already parsed
395 | return (float(value[0]), value[1])
396 | else:
397 | raise TypeError(
398 | f"Cannot parse {value!r}, it should be a number or a string containing a number and a unit.",
399 | )
400 |
401 |
402 | def _locations_mirror(x):
403 | """Mirrors the points in a list-of-list-of-...-of-list-of-points.
404 | For example:
405 | >>> _locations_mirror([[[1, 2], [3, 4]], [5, 6], [7, 8]])
406 | [[[2, 1], [4, 3]], [6, 5], [8, 7]]
407 |
408 | """
409 | if hasattr(x, "__iter__"):
410 | if hasattr(x[0], "__iter__"):
411 | return list(map(_locations_mirror, x))
412 | else:
413 | return list(x[::-1])
414 | else:
415 | return x
416 |
417 |
418 | def _locations_tolist(x):
419 | """Transforms recursively a list of iterables into a list of list."""
420 | if hasattr(x, "__iter__"):
421 | return list(map(_locations_tolist, x))
422 | else:
423 | return x
424 |
425 |
426 | def none_min(x: Optional[float], y: Optional[float]) -> Optional[float]:
427 | if x is None:
428 | return y
429 | elif y is None:
430 | return x
431 | else:
432 | return min(x, y)
433 |
434 |
435 | def none_max(x: Optional[float], y: Optional[float]) -> Optional[float]:
436 | if x is None:
437 | return y
438 | elif y is None:
439 | return x
440 | else:
441 | return max(x, y)
442 |
443 |
444 | def iter_points(x: Union[List, Tuple]) -> list:
445 | """Iterates over a list representing a feature, and returns a list of points,
446 | whatever the shape of the array (Point, MultiPolyline, etc).
447 | """
448 | if isinstance(x, (list, tuple)):
449 | if len(x):
450 | if isinstance(x[0], (list, tuple)):
451 | out = []
452 | for y in x:
453 | out += iter_points(y)
454 | return out
455 | else:
456 | return [x]
457 | else:
458 | return []
459 | else:
460 | raise ValueError(f"List/tuple type expected. Got {x!r}.")
461 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SOURCEDIR = source
8 | BUILDDIR = build
9 |
10 | # Put it first so that "make" without argument is like "make help".
11 | help:
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13 |
14 | .PHONY: help Makefile
15 |
16 | # Catch-all target: route all unknown targets to Sphinx using the new
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18 | %: Makefile
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
20 |
--------------------------------------------------------------------------------
/docs/source/colormap.rst:
--------------------------------------------------------------------------------
1 | :mod:`branca.colormap`
2 | ----------------------
3 |
4 | .. automodule:: branca.colormap
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | #
2 | # Configuration file for the Sphinx documentation builder.
3 | #
4 | # This file does only contain a selection of the most common options. For a
5 | # full list see the documentation:
6 | # http://www.sphinx-doc.org/en/master/config
7 |
8 | # -- Path setup --------------------------------------------------------------
9 |
10 | # If extensions (or modules to document with autodoc) are in another directory,
11 | # add these directories to sys.path here. If the directory is relative to the
12 | # documentation root, use os.path.abspath to make it absolute, like shown here.
13 | #
14 | # import os
15 | # import sys
16 | # sys.path.insert(0, os.path.abspath('.'))
17 |
18 |
19 | # -- Project information -----------------------------------------------------
20 |
21 | project = "branca"
22 | copyright = "2018, Filipe Fernandes"
23 | author = "Filipe Fernandes"
24 |
25 | import branca
26 |
27 | version = release = branca.__version__
28 |
29 |
30 | # -- General configuration ---------------------------------------------------
31 |
32 | # If your documentation needs a minimal Sphinx version, state it here.
33 | #
34 | # needs_sphinx = '1.0'
35 |
36 | # Add any Sphinx extension module names here, as strings. They can be
37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
38 | # ones.
39 | extensions = [
40 | "sphinx.ext.autodoc",
41 | "sphinx.ext.mathjax",
42 | "sphinx.ext.githubpages",
43 | "sphinx.ext.viewcode",
44 | "sphinx.ext.napoleon",
45 | "nbsphinx",
46 | ]
47 |
48 | # Add any paths that contain templates here, relative to this directory.
49 | templates_path = ["_templates"]
50 |
51 | # The suffix(es) of source filenames.
52 | # You can specify multiple suffix as a list of string:
53 | #
54 | # source_suffix = ['.rst', '.md']
55 | source_suffix = ".rst"
56 |
57 | # The master toctree document.
58 | master_doc = "index"
59 |
60 | # The language for content autogenerated by Sphinx. Refer to documentation
61 | # for a list of supported languages.
62 | #
63 | # This is also used if you do content translation via gettext catalogs.
64 | # Usually you set "language" from the command line for these cases.
65 | language = "en"
66 |
67 | # List of patterns, relative to source directory, that match files and
68 | # directories to ignore when looking for source files.
69 | # This pattern also affects html_static_path and html_extra_path.
70 | exclude_patterns = []
71 |
72 | # The name of the Pygments (syntax highlighting) style to use.
73 | pygments_style = None
74 |
75 |
76 | # -- Options for HTML output -------------------------------------------------
77 |
78 | # The theme to use for HTML and HTML Help pages. See the documentation for
79 | # a list of builtin themes.
80 | #
81 | html_theme = "alabaster"
82 |
83 | # Theme options are theme-specific and customize the look and feel of a theme
84 | # further. For a list of options available for each theme, see the
85 | # documentation.
86 | #
87 | # html_theme_options = {}
88 |
89 | # Add any paths that contain custom static files (such as style sheets) here,
90 | # relative to this directory. They are copied after the builtin static files,
91 | # so a file named "default.css" will overwrite the builtin "default.css".
92 | html_static_path = ["_static"]
93 |
94 | # Custom sidebar templates, must be a dictionary that maps document names
95 | # to template names.
96 | #
97 | # The default sidebars (for documents that don't match any pattern) are
98 | # defined by theme itself. Builtin themes are using these templates by
99 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
100 | # 'searchbox.html']``.
101 | #
102 | # html_sidebars = {}
103 |
104 |
105 | # -- Options for HTMLHelp output ---------------------------------------------
106 |
107 | # Output file base name for HTML help builder.
108 | htmlhelp_basename = "brancadoc"
109 |
110 |
111 | # -- Options for LaTeX output ------------------------------------------------
112 |
113 | latex_elements = {
114 | # The paper size ('letterpaper' or 'a4paper').
115 | #
116 | # 'papersize': 'letterpaper',
117 | # The font size ('10pt', '11pt' or '12pt').
118 | #
119 | # 'pointsize': '10pt',
120 | # Additional stuff for the LaTeX preamble.
121 | #
122 | # 'preamble': '',
123 | # Latex figure (float) alignment
124 | #
125 | # 'figure_align': 'htbp',
126 | }
127 |
128 | # Grouping the document tree into LaTeX files. List of tuples
129 | # (source start file, target name, title,
130 | # author, documentclass [howto, manual, or own class]).
131 | latex_documents = [
132 | (master_doc, "branca.tex", "branca Documentation", "Filipe Fernandes", "manual"),
133 | ]
134 |
135 |
136 | # -- Options for manual page output ------------------------------------------
137 |
138 | # One entry per manual page. List of tuples
139 | # (source start file, name, description, authors, manual section).
140 | man_pages = [(master_doc, "branca", "branca Documentation", [author], 1)]
141 |
142 |
143 | # -- Options for Texinfo output ----------------------------------------------
144 |
145 | # Grouping the document tree into Texinfo files. List of tuples
146 | # (source start file, target name, title, author,
147 | # dir menu entry, description, category)
148 | texinfo_documents = [
149 | (
150 | master_doc,
151 | "branca",
152 | "branca Documentation",
153 | author,
154 | "branca",
155 | "One line description of project.",
156 | "Miscellaneous",
157 | ),
158 | ]
159 |
160 |
161 | # -- Options for Epub output -------------------------------------------------
162 |
163 | # Bibliographic Dublin Core info.
164 | epub_title = project
165 |
166 | # The unique identifier of the text. This can be a ISBN number
167 | # or the project homepage.
168 | #
169 | # epub_identifier = ''
170 |
171 | # A unique identification for the text.
172 | #
173 | # epub_uid = ''
174 |
175 | # A list of files that should not be packed into the epub file.
176 | epub_exclude_files = ["search.html"]
177 |
178 |
179 | # -- Extension configuration -------------------------------------------------
180 |
--------------------------------------------------------------------------------
/docs/source/element.rst:
--------------------------------------------------------------------------------
1 | :mod:`branca.element`
2 | ---------------------
3 |
4 | .. automodule:: branca.element
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. branca documentation master file, created by
2 | sphinx-quickstart on Mon Nov 5 13:22:48 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to branca's documentation!
7 | ==================================
8 |
9 | .. toctree::
10 | :maxdepth: 3
11 | :caption: Contents:
12 |
13 | element
14 | colormap
15 |
16 | Indices and tables
17 | ==================
18 |
19 | * :ref:`genindex`
20 | * :ref:`modindex`
21 | * :ref:`search`
22 |
--------------------------------------------------------------------------------
/examples/Custom_colormap.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "id": "1d544550",
7 | "metadata": {},
8 | "outputs": [
9 | {
10 | "data": {
11 | "text/html": [
12 | ""
13 | ],
14 | "text/plain": [
15 | ""
16 | ]
17 | },
18 | "execution_count": 1,
19 | "metadata": {},
20 | "output_type": "execute_result"
21 | }
22 | ],
23 | "source": [
24 | "from branca.colormap import linear\n",
25 | "\n",
26 | "colormap_choice = linear.YlOrRd_04\n",
27 | "vmin = 3.2\n",
28 | "vmax = 10.3\n",
29 | "colormap = colormap_choice.scale(vmin, vmax)\n",
30 | "colormap.caption = 'Unemployment rate'\n",
31 | "colormap.text_color = 'cyan'\n",
32 | "colormap"
33 | ]
34 | }
35 | ],
36 | "metadata": {
37 | "kernelspec": {
38 | "display_name": "Python 3 (ipykernel)",
39 | "language": "python",
40 | "name": "python3"
41 | },
42 | "language_info": {
43 | "codemirror_mode": {
44 | "name": "ipython",
45 | "version": 3
46 | },
47 | "file_extension": ".py",
48 | "mimetype": "text/x-python",
49 | "name": "python",
50 | "nbconvert_exporter": "python",
51 | "pygments_lexer": "ipython3",
52 | "version": "3.10.12"
53 | }
54 | },
55 | "nbformat": 4,
56 | "nbformat_minor": 5
57 | }
58 |
--------------------------------------------------------------------------------
/examples/Elements.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import sys\n",
10 | "sys.path.insert(0, '..')\n",
11 | "\n",
12 | "from branca.element import *"
13 | ]
14 | },
15 | {
16 | "cell_type": "markdown",
17 | "metadata": {},
18 | "source": [
19 | "## Element"
20 | ]
21 | },
22 | {
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "This is the base brick of `branca`. You can create an `Element` in providing a template string:"
27 | ]
28 | },
29 | {
30 | "cell_type": "code",
31 | "execution_count": 2,
32 | "metadata": {},
33 | "outputs": [],
34 | "source": [
35 | "e = Element(\"This is fancy text\")"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "Each element has an attribute `_name` and a unique `_id`. You also have a method `get_name` to get a unique string representation of the element."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": 3,
48 | "metadata": {},
49 | "outputs": [
50 | {
51 | "name": "stdout",
52 | "output_type": "stream",
53 | "text": [
54 | "Element a1d0f648f7444f96b526931944247fd6\n",
55 | "element_a1d0f648f7444f96b526931944247fd6\n"
56 | ]
57 | }
58 | ],
59 | "source": [
60 | "print(e._name, e._id)\n",
61 | "print(e.get_name())"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "You can render an `Element` using the method `render`:"
69 | ]
70 | },
71 | {
72 | "cell_type": "code",
73 | "execution_count": 4,
74 | "metadata": {},
75 | "outputs": [
76 | {
77 | "data": {
78 | "text/plain": [
79 | "'This is fancy text'"
80 | ]
81 | },
82 | "execution_count": 4,
83 | "metadata": {},
84 | "output_type": "execute_result"
85 | }
86 | ],
87 | "source": [
88 | "e.render()"
89 | ]
90 | },
91 | {
92 | "cell_type": "markdown",
93 | "metadata": {},
94 | "source": [
95 | "In the template, you can use keyword `this` for accessing the object itself ; and the keyword `kwargs` for accessing any keyword argument provided in the `render` method:"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": 5,
101 | "metadata": {},
102 | "outputs": [
103 | {
104 | "data": {
105 | "text/plain": [
106 | "'Hello World, my name is `element_6f17661abddb45c7bf2aa794cadd327d`.'"
107 | ]
108 | },
109 | "execution_count": 5,
110 | "metadata": {},
111 | "output_type": "execute_result"
112 | }
113 | ],
114 | "source": [
115 | "e = Element(\"Hello {{kwargs['you']}}, my name is `{{this.get_name()}}`.\")\n",
116 | "e.render(you='World')"
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {},
122 | "source": [
123 | "Well, this is not really cool for now. What makes elements useful lies in the fact that you can create trees out of them. To do so, you can either use the method `add_child` or the method `add_to`."
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": 6,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "child = Element('This is the child.')\n",
133 | "parent = Element('This is the parent.').add_child(child)\n",
134 | "\n",
135 | "parent = Element('This is the parent.')\n",
136 | "child = Element('This is the child.').add_to(parent)"
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {},
142 | "source": [
143 | "Now in the example above, embedding the one in the other does not change anything."
144 | ]
145 | },
146 | {
147 | "cell_type": "code",
148 | "execution_count": 7,
149 | "metadata": {},
150 | "outputs": [
151 | {
152 | "name": "stdout",
153 | "output_type": "stream",
154 | "text": [
155 | "This is the parent. This is the child.\n"
156 | ]
157 | }
158 | ],
159 | "source": [
160 | "print(parent.render(), child.render())"
161 | ]
162 | },
163 | {
164 | "cell_type": "markdown",
165 | "metadata": {},
166 | "source": [
167 | "But you can use the tree structure in the template."
168 | ]
169 | },
170 | {
171 | "cell_type": "code",
172 | "execution_count": 8,
173 | "metadata": {},
174 | "outputs": [
175 | {
176 | "data": {
177 | "text/plain": [
178 | "''"
179 | ]
180 | },
181 | "execution_count": 8,
182 | "metadata": {},
183 | "output_type": "execute_result"
184 | }
185 | ],
186 | "source": [
187 | "parent = Element(\"{% for child in this._children.values() %}{{child.render()}}{% endfor %}\")\n",
188 | "Element('').add_to(parent)\n",
189 | "Element('').add_to(parent)\n",
190 | "parent.render()"
191 | ]
192 | },
193 | {
194 | "cell_type": "markdown",
195 | "metadata": {},
196 | "source": [
197 | "As you can see, the child of an element are referenced in the `_children` attribute in the form of an `OrderedDict`. You can choose the key of each child in specifying a `name` in the `add_child` (or `add_to`) method:"
198 | ]
199 | },
200 | {
201 | "cell_type": "code",
202 | "execution_count": 9,
203 | "metadata": {},
204 | "outputs": [
205 | {
206 | "data": {
207 | "text/plain": [
208 | "OrderedDict([('child_1', )])"
209 | ]
210 | },
211 | "execution_count": 9,
212 | "metadata": {},
213 | "output_type": "execute_result"
214 | }
215 | ],
216 | "source": [
217 | "parent = Element(\"{% for child in this._children.values() %}{{child.render()}}{% endfor %}\")\n",
218 | "Element('').add_to(parent, name='child_1')\n",
219 | "parent._children"
220 | ]
221 | },
222 | {
223 | "cell_type": "markdown",
224 | "metadata": {},
225 | "source": [
226 | "That way, it's possible to overwrite a child in specifying the same name:"
227 | ]
228 | },
229 | {
230 | "cell_type": "code",
231 | "execution_count": 10,
232 | "metadata": {},
233 | "outputs": [
234 | {
235 | "data": {
236 | "text/plain": [
237 | "''"
238 | ]
239 | },
240 | "execution_count": 10,
241 | "metadata": {},
242 | "output_type": "execute_result"
243 | }
244 | ],
245 | "source": [
246 | "Element('').add_to(parent, name='child_1')\n",
247 | "parent.render()"
248 | ]
249 | },
250 | {
251 | "cell_type": "markdown",
252 | "metadata": {},
253 | "source": [
254 | "I hope you start to find it useful.\n",
255 | "\n",
256 | "In fact, the real interest of `Element` lies in the classes that inherit from it. The most important one is `Figure` described in the next section."
257 | ]
258 | },
259 | {
260 | "cell_type": "markdown",
261 | "metadata": {},
262 | "source": [
263 | "## Figure\n",
264 | "\n",
265 | "A `Figure` represents an HTML document. It's composed of 3 parts (attributes):\n",
266 | "\n",
267 | "* `header` : corresponds to the `` part of the HTML document,\n",
268 | "* `html` : corresponds to the `` part,\n",
269 | "* `script` : corresponds to a `\n"
289 | ]
290 | }
291 | ],
292 | "source": [
293 | "f = Figure()\n",
294 | "print(f.render())"
295 | ]
296 | },
297 | {
298 | "cell_type": "markdown",
299 | "metadata": {},
300 | "source": [
301 | "You can for example create a beautiful cyan \"hello-world\" webpage in doing:"
302 | ]
303 | },
304 | {
305 | "cell_type": "code",
306 | "execution_count": 12,
307 | "metadata": {},
308 | "outputs": [
309 | {
310 | "name": "stdout",
311 | "output_type": "stream",
312 | "text": [
313 | "\n",
314 | " \n",
315 | " \n",
316 | " \n",
317 | "\n",
318 | " \n",
319 | " Hello world
\n",
320 | "\n",
321 | "\n"
323 | ]
324 | }
325 | ],
326 | "source": [
327 | "f.header.add_child(Element(\"\"))\n",
328 | "f.html.add_child(Element(\"Hello world
\"))\n",
329 | "print(f.render())"
330 | ]
331 | },
332 | {
333 | "cell_type": "markdown",
334 | "metadata": {},
335 | "source": [
336 | "You can simply save the content of the `Figure` to a file, thanks to the `save` method:"
337 | ]
338 | },
339 | {
340 | "cell_type": "code",
341 | "execution_count": 13,
342 | "metadata": {},
343 | "outputs": [
344 | {
345 | "name": "stdout",
346 | "output_type": "stream",
347 | "text": [
348 | "\n",
349 | " \n",
350 | " \n",
351 | " \n",
352 | "\n",
353 | " \n",
354 | " Hello world
\n",
355 | "\n",
356 | "\n"
358 | ]
359 | }
360 | ],
361 | "source": [
362 | "f.save('foo.html')\n",
363 | "print(open('foo.html').read())"
364 | ]
365 | },
366 | {
367 | "cell_type": "markdown",
368 | "metadata": {},
369 | "source": [
370 | "If you want to visualize it in the notebook, you can let `Figure._repr_html_` method do it's job in typing: "
371 | ]
372 | },
373 | {
374 | "cell_type": "code",
375 | "execution_count": 14,
376 | "metadata": {},
377 | "outputs": [
378 | {
379 | "data": {
380 | "text/html": [
381 | ""
382 | ],
383 | "text/plain": [
384 | ""
385 | ]
386 | },
387 | "execution_count": 14,
388 | "metadata": {},
389 | "output_type": "execute_result"
390 | }
391 | ],
392 | "source": [
393 | "f"
394 | ]
395 | },
396 | {
397 | "cell_type": "markdown",
398 | "metadata": {},
399 | "source": [
400 | "If this rendering is too large for you, you can force it's width and height:"
401 | ]
402 | },
403 | {
404 | "cell_type": "code",
405 | "execution_count": 15,
406 | "metadata": {},
407 | "outputs": [
408 | {
409 | "data": {
410 | "text/html": [
411 | ""
412 | ],
413 | "text/plain": [
414 | ""
415 | ]
416 | },
417 | "execution_count": 15,
418 | "metadata": {},
419 | "output_type": "execute_result"
420 | }
421 | ],
422 | "source": [
423 | "f.width = 300\n",
424 | "f.height = 200\n",
425 | "f"
426 | ]
427 | },
428 | {
429 | "cell_type": "markdown",
430 | "metadata": {},
431 | "source": [
432 | "Note that you can also define a `Figure`'s size in a matplotlib way:"
433 | ]
434 | },
435 | {
436 | "cell_type": "code",
437 | "execution_count": 16,
438 | "metadata": {},
439 | "outputs": [
440 | {
441 | "data": {
442 | "text/html": [
443 | ""
444 | ],
445 | "text/plain": [
446 | ""
447 | ]
448 | },
449 | "execution_count": 16,
450 | "metadata": {},
451 | "output_type": "execute_result"
452 | }
453 | ],
454 | "source": [
455 | "Figure(figsize=(5,5))"
456 | ]
457 | },
458 | {
459 | "cell_type": "markdown",
460 | "metadata": {},
461 | "source": [
462 | "## MacroElement"
463 | ]
464 | },
465 | {
466 | "cell_type": "markdown",
467 | "metadata": {},
468 | "source": [
469 | "It happens you need to create elements that have multiple effects on a Figure. For this, you can use `MacroElement` whose template contains macros ; each macro writes something into the parent Figure's header, body and script."
470 | ]
471 | },
472 | {
473 | "cell_type": "code",
474 | "execution_count": 17,
475 | "metadata": {},
476 | "outputs": [
477 | {
478 | "name": "stdout",
479 | "output_type": "stream",
480 | "text": [
481 | "\n",
482 | " \n",
483 | " \n",
484 | " This is header of macro_element_ea36a310ab8a4212a8c7ca754a4140fc\n",
485 | "\n",
486 | " \n",
487 | " This is html of macro_element_ea36a310ab8a4212a8c7ca754a4140fc\n",
488 | "\n",
489 | "\n"
492 | ]
493 | }
494 | ],
495 | "source": [
496 | "macro = MacroElement()\n",
497 | "macro._template = Template(\n",
498 | " '{% macro header(this, kwargs) %}'\n",
499 | " 'This is header of {{this.get_name()}}'\n",
500 | " '{% endmacro %}'\n",
501 | "\n",
502 | " '{% macro html(this, kwargs) %}'\n",
503 | " 'This is html of {{this.get_name()}}'\n",
504 | " '{% endmacro %}'\n",
505 | "\n",
506 | " '{% macro script(this, kwargs) %}'\n",
507 | " 'This is script of {{this.get_name()}}'\n",
508 | " '{% endmacro %}'\n",
509 | " )\n",
510 | "\n",
511 | "print(Figure().add_child(macro).render())"
512 | ]
513 | },
514 | {
515 | "cell_type": "markdown",
516 | "metadata": {},
517 | "source": [
518 | "## Link"
519 | ]
520 | },
521 | {
522 | "cell_type": "markdown",
523 | "metadata": {},
524 | "source": [
525 | "To embed javascript and css links in the header, you can use these class:"
526 | ]
527 | },
528 | {
529 | "cell_type": "code",
530 | "execution_count": 18,
531 | "metadata": {},
532 | "outputs": [
533 | {
534 | "data": {
535 | "text/plain": [
536 | "''"
537 | ]
538 | },
539 | "execution_count": 18,
540 | "metadata": {},
541 | "output_type": "execute_result"
542 | }
543 | ],
544 | "source": [
545 | "js_link = JavascriptLink('https://example.com/javascript.js')\n",
546 | "js_link.render()"
547 | ]
548 | },
549 | {
550 | "cell_type": "code",
551 | "execution_count": 19,
552 | "metadata": {},
553 | "outputs": [
554 | {
555 | "data": {
556 | "text/plain": [
557 | "''"
558 | ]
559 | },
560 | "execution_count": 19,
561 | "metadata": {},
562 | "output_type": "execute_result"
563 | }
564 | ],
565 | "source": [
566 | "css_link = CssLink('https://example.com/style.css')\n",
567 | "css_link.render()"
568 | ]
569 | },
570 | {
571 | "cell_type": "markdown",
572 | "metadata": {},
573 | "source": [
574 | "## Html"
575 | ]
576 | },
577 | {
578 | "cell_type": "markdown",
579 | "metadata": {},
580 | "source": [
581 | "An `Html` element enables you to create custom div to put in the *body* of your page."
582 | ]
583 | },
584 | {
585 | "cell_type": "code",
586 | "execution_count": 26,
587 | "metadata": {},
588 | "outputs": [
589 | {
590 | "data": {
591 | "text/plain": [
592 | "'Hello world
'"
593 | ]
594 | },
595 | "execution_count": 26,
596 | "metadata": {},
597 | "output_type": "execute_result"
598 | }
599 | ],
600 | "source": [
601 | "html = Html('Hello world')\n",
602 | "html.render()"
603 | ]
604 | },
605 | {
606 | "cell_type": "markdown",
607 | "metadata": {},
608 | "source": [
609 | "It's designed to render the text *as you gave it*, so it won't work directly it you want to embed HTML code inside the div."
610 | ]
611 | },
612 | {
613 | "cell_type": "code",
614 | "execution_count": 25,
615 | "metadata": {},
616 | "outputs": [
617 | {
618 | "data": {
619 | "text/plain": [
620 | "'<b>Hello world</b>
'"
621 | ]
622 | },
623 | "execution_count": 25,
624 | "metadata": {},
625 | "output_type": "execute_result"
626 | }
627 | ],
628 | "source": [
629 | "Html('Hello world').render()"
630 | ]
631 | },
632 | {
633 | "cell_type": "markdown",
634 | "metadata": {},
635 | "source": [
636 | "For this, you have to set `script=True` and it will work:"
637 | ]
638 | },
639 | {
640 | "cell_type": "code",
641 | "execution_count": 28,
642 | "metadata": {},
643 | "outputs": [
644 | {
645 | "data": {
646 | "text/plain": [
647 | "'Hello world
'"
648 | ]
649 | },
650 | "execution_count": 28,
651 | "metadata": {},
652 | "output_type": "execute_result"
653 | }
654 | ],
655 | "source": [
656 | "Html('Hello world', script=True).render()"
657 | ]
658 | },
659 | {
660 | "cell_type": "markdown",
661 | "metadata": {},
662 | "source": [
663 | "## IFrame"
664 | ]
665 | },
666 | {
667 | "cell_type": "markdown",
668 | "metadata": {},
669 | "source": [
670 | "If you need to embed a full webpage (with separate javascript environment), you can use `IFrame`."
671 | ]
672 | },
673 | {
674 | "cell_type": "code",
675 | "execution_count": 21,
676 | "metadata": {},
677 | "outputs": [
678 | {
679 | "data": {
680 | "text/plain": [
681 | "''"
682 | ]
683 | },
684 | "execution_count": 21,
685 | "metadata": {},
686 | "output_type": "execute_result"
687 | }
688 | ],
689 | "source": [
690 | "iframe = IFrame('Hello World')\n",
691 | "iframe.render()"
692 | ]
693 | },
694 | {
695 | "cell_type": "markdown",
696 | "metadata": {},
697 | "source": [
698 | "As you can see, it will embed the full content of the iframe in a *base64* string so that the output looks like:"
699 | ]
700 | },
701 | {
702 | "cell_type": "code",
703 | "execution_count": 22,
704 | "metadata": {},
705 | "outputs": [
706 | {
707 | "data": {
708 | "text/html": [
709 | ""
710 | ],
711 | "text/plain": [
712 | ""
713 | ]
714 | },
715 | "execution_count": 22,
716 | "metadata": {},
717 | "output_type": "execute_result"
718 | }
719 | ],
720 | "source": [
721 | "f = Figure(height=180)\n",
722 | "f.html.add_child(Element(\"Before the frame\"))\n",
723 | "f.html.add_child(IFrame('In the frame', height='100px'))\n",
724 | "f.html.add_child(Element(\"After the frame\"))\n",
725 | "f"
726 | ]
727 | },
728 | {
729 | "cell_type": "markdown",
730 | "metadata": {},
731 | "source": [
732 | "## Div"
733 | ]
734 | },
735 | {
736 | "cell_type": "markdown",
737 | "metadata": {},
738 | "source": [
739 | "At last, you have the `Div` element that behaves almost like `Html` with a few differences:\n",
740 | "\n",
741 | "* The style is put in the header, while `Html`'s style is embedded inline.\n",
742 | "* `Div` inherits from `MacroElement` so that:\n",
743 | " * It cannot be rendered unless it's embedded in a `Figure`.\n",
744 | " * It is a useful object toinherit from when you create new classes."
745 | ]
746 | },
747 | {
748 | "cell_type": "code",
749 | "execution_count": 29,
750 | "metadata": {},
751 | "outputs": [
752 | {
753 | "name": "stdout",
754 | "output_type": "stream",
755 | "text": [
756 | "\n",
757 | " \n",
758 | " \n",
759 | " \n",
766 | "\n",
767 | " \n",
768 | " Hello world
\n",
769 | "\n",
770 | "\n"
772 | ]
773 | }
774 | ],
775 | "source": [
776 | "div = Div()\n",
777 | "div.html.add_child(Element('Hello world'))\n",
778 | "print(Figure().add_child(div).render())"
779 | ]
780 | }
781 | ],
782 | "metadata": {
783 | "kernelspec": {
784 | "display_name": "Python 3 (ipykernel)",
785 | "language": "python",
786 | "name": "python3"
787 | },
788 | "language_info": {
789 | "codemirror_mode": {
790 | "name": "ipython",
791 | "version": 3
792 | },
793 | "file_extension": ".py",
794 | "mimetype": "text/x-python",
795 | "name": "python",
796 | "nbconvert_exporter": "python",
797 | "pygments_lexer": "ipython3",
798 | "version": "3.11.0"
799 | }
800 | },
801 | "nbformat": 4,
802 | "nbformat_minor": 1
803 | }
804 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=41.2", "setuptools_scm"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.mypy]
6 | ignore_missing_imports = true
7 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | black
2 | check-manifest
3 | flake8
4 | flake8-builtins
5 | flake8-comprehensions
6 | flake8-mutable
7 | flake8-print
8 | isort
9 | jupyter
10 | mypy
11 | nbsphinx
12 | nbval
13 | numpy
14 | pre-commit
15 | pylint
16 | pytest
17 | pytest-cov
18 | pytest-flake8
19 | pytest-xdist
20 | selenium
21 | setuptools_scm
22 | sphinx
23 | twine
24 | wheel
25 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jinja2>=3
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [tool:pytest]
2 | flake8-max-line-length = 105
3 | flake8-ignore =
4 | docs/* ALL
5 | versioneer.py ALL
6 | branca/_version.py ALL
7 | markers =
8 | headless: mark headless tests (deselect with '-m "not headless"')
9 |
10 | [metadata]
11 | long_description = README.md
12 | license_files = LICENSE.txt
13 |
14 | [check-manifest]
15 | ignore =
16 | .*.yml
17 | .coveragerc
18 | docs
19 | docs/*
20 | examples
21 | examples/*
22 | tests
23 | tests/*
24 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import setup
4 |
5 | rootpath = os.path.abspath(os.path.dirname(__file__))
6 |
7 |
8 | def read(*parts):
9 | return open(os.path.join(rootpath, *parts)).read()
10 |
11 |
12 | def walk_subpkg(name):
13 | data_files = []
14 | package_dir = "branca"
15 | for parent, dirs, files in os.walk(os.path.join(package_dir, name)):
16 | # Remove package_dir from the path.
17 | sub_dir = os.sep.join(parent.split(os.sep)[1:])
18 | for f in files:
19 | data_files.append(os.path.join(sub_dir, f))
20 | return data_files
21 |
22 |
23 | pkg_data = {
24 | "": [
25 | "*.js",
26 | "plugins/*.js",
27 | "plugins/*.html",
28 | "plugins/*.css",
29 | "plugins/*.tpl",
30 | "templates/*.html",
31 | "templates/*.js",
32 | "templates/*.txt",
33 | "py.typed",
34 | ],
35 | }
36 | pkgs = ["branca"]
37 |
38 | LICENSE = "MIT"
39 | long_description = "{}".format(read("README.md"))
40 |
41 | # Dependencies.
42 | with open("requirements.txt") as f:
43 | tests_require = f.readlines()
44 | install_requires = [t.strip() for t in tests_require]
45 |
46 |
47 | setup(
48 | name="branca",
49 | description="Generate complex HTML+JS pages with Python",
50 | long_description=long_description,
51 | long_description_content_type="text/markdown",
52 | author="Martin Journois",
53 | url="https://github.com/python-visualization/branca",
54 | keywords="data visualization",
55 | classifiers=[
56 | "Programming Language :: Python :: 3",
57 | "Programming Language :: Python :: 3.8",
58 | "Programming Language :: Python :: 3.9",
59 | "Programming Language :: Python :: 3.10",
60 | "Programming Language :: Python :: 3.11",
61 | "Programming Language :: Python :: 3.12",
62 | "License :: OSI Approved :: MIT License",
63 | "Development Status :: 5 - Production/Stable",
64 | ],
65 | packages=pkgs,
66 | package_data=pkg_data,
67 | include_package_data=True,
68 | use_scm_version={
69 | "write_to": "branca/_version.py",
70 | "write_to_template": '__version__ = "{version}"',
71 | "tag_regex": r"^(?Pv)?(?P[^\+]+)(?P.*)?$",
72 | },
73 | tests_require=["pytest"],
74 | license=LICENSE,
75 | install_requires=install_requires,
76 | python_requires=">=3.7",
77 | zip_safe=False,
78 | )
79 |
--------------------------------------------------------------------------------
/tests/test_colormap.py:
--------------------------------------------------------------------------------
1 | """ "
2 | Folium Colormap Module
3 | ----------------------
4 | """
5 |
6 | import pytest
7 |
8 | import branca.colormap as cm
9 |
10 |
11 | def test_simple_step():
12 | step = cm.StepColormap(
13 | ["green", "yellow", "red"],
14 | vmin=3.0,
15 | vmax=10.0,
16 | index=[3, 4, 8, 10],
17 | caption="step",
18 | )
19 | step = cm.StepColormap(["r", "y", "g", "c", "b", "m"])
20 | step._repr_html_()
21 |
22 |
23 | def test_simple_linear():
24 | linear = cm.LinearColormap(["green", "yellow", "red"], vmin=3.0, vmax=10.0)
25 | linear = cm.LinearColormap(
26 | ["red", "orange", "yellow", "green"],
27 | index=[0, 0.1, 0.9, 1.0],
28 | )
29 | linear._repr_html_()
30 |
31 |
32 | black = "#000000ff"
33 | red = "#ff0000ff"
34 | green = "#00ff00ff"
35 | blue = "#0000ffff"
36 |
37 |
38 | def test_step_color_indexing():
39 | step = cm.StepColormap(colors=["black", "red", "lime", "blue"], index=[1, 2, 4, 5])
40 | assert step(0.99) == black
41 | assert step(1) == black
42 | assert step(1.01) == black
43 | assert step(1.99) == black
44 | assert step(2) == red
45 | assert step(2.01) == red
46 | assert step(3.99) == red
47 | assert step(4) == green
48 | assert step(4.01) == green
49 | assert step(4.99) == green
50 | assert step(5) == blue
51 | assert step(5.01) == blue
52 |
53 |
54 | def test_step_color_indexing_larger_index():
55 | # add an upper bound to the last color, which doesn't do much but shouldn't fail
56 | step = cm.StepColormap(
57 | colors=["black", "red", "lime", "blue"],
58 | index=[1, 2, 4, 5, 10],
59 | )
60 | assert step(4.99) == green
61 | assert step(5) == blue
62 | assert step(10) == blue
63 | assert step(20) == blue
64 |
65 |
66 | def test_linear_color_indexing():
67 | linear = cm.LinearColormap(
68 | colors=["black", "red", "lime", "blue"],
69 | index=[1, 2, 4, 5],
70 | )
71 | assert linear(1) == black
72 | assert linear(2) == red
73 | assert linear(4) == green
74 | assert linear(5) == blue
75 | assert linear(3) == "#7f7f00ff"
76 |
77 |
78 | def test_linear_to_step():
79 | some_list = [30.6, 50, 51, 52, 53, 54, 55, 60, 70, 100]
80 | lc = cm.linear.YlOrRd_06
81 | lc.to_step(n=12)
82 | lc.to_step(index=[0, 2, 4, 6, 8, 10])
83 | lc.to_step(data=some_list, n=12)
84 | lc.to_step(data=some_list, n=12, method="linear")
85 | lc.to_step(data=some_list, n=12, method="log")
86 | lc.to_step(data=some_list, n=30, method="quantiles")
87 | lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1])
88 | lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1], round_method="int")
89 | lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1], round_method="log10")
90 |
91 |
92 | def test_step_to_linear():
93 | step = cm.StepColormap(
94 | ["green", "yellow", "red"],
95 | vmin=3.0,
96 | vmax=10.0,
97 | index=[3, 4, 8, 10],
98 | caption="step",
99 | )
100 | step.to_linear()
101 |
102 |
103 | def test_linear_object():
104 | cm.linear.OrRd_06._repr_html_()
105 | cm.linear.PuBu_06.to_step(12)
106 | cm.linear.YlGn_06.scale(3, 12)
107 | cm.linear._repr_html_()
108 |
109 |
110 | def test_step_object():
111 | cm.step.OrRd_06._repr_html_()
112 | cm.step.PuBu_06.to_linear()
113 | cm.step.YlGn_06.scale(3, 12)
114 | cm.step._repr_html_()
115 |
116 |
117 | @pytest.mark.parametrize(
118 | "max_labels,expected",
119 | [
120 | (10, [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]),
121 | (5, [0.0, "", 2.0, "", 4.0, "", 6.0, "", 8.0, ""]),
122 | (3, [0.0, "", "", "", 4.0, "", "", "", 8.0, "", "", ""]),
123 | ],
124 | )
125 | def test_max_labels_linear(max_labels, expected):
126 | colorbar = cm.LinearColormap(["red"] * 10, vmin=0, vmax=9, max_labels=max_labels)
127 | try:
128 | colorbar.render()
129 | except AssertionError: # rendering outside parent Figure raises error
130 | pass
131 | assert colorbar.tick_labels == expected
132 |
133 |
134 | @pytest.mark.parametrize(
135 | "max_labels,expected",
136 | [
137 | (10, [0.0, "", 2.0, "", 4.0, "", 6.0, "", 8.0, "", 10.0, ""]),
138 | (5, [0.0, "", "", 3.0, "", "", 6.0, "", "", 9.0, "", ""]),
139 | (3, [0.0, "", "", "", 4.0, "", "", "", 8.0, "", "", ""]),
140 | ],
141 | )
142 | def test_max_labels_step(max_labels, expected):
143 | colorbar = cm.StepColormap(
144 | ["red", "blue"] * 5,
145 | vmin=0,
146 | vmax=10,
147 | max_labels=max_labels,
148 | )
149 | try:
150 | colorbar.render()
151 | except AssertionError: # rendering outside parent Figure raises error
152 | pass
153 | assert colorbar.tick_labels == expected
154 |
--------------------------------------------------------------------------------
/tests/test_iframe.py:
--------------------------------------------------------------------------------
1 | """
2 | Folium Element Module class IFrame
3 | ----------------------
4 | """
5 |
6 | import os
7 |
8 | import pytest
9 | from selenium.webdriver import Firefox
10 | from selenium.webdriver.common.by import By
11 | from selenium.webdriver.firefox.options import Options
12 |
13 | import branca.element as elem
14 |
15 |
16 | def test_create_empty_iframe():
17 | iframe = elem.IFrame()
18 | iframe.render()
19 |
20 |
21 | def test_create_iframe():
22 | iframe = elem.IFrame(html="test content
", width=60, height=45)
23 | iframe.render()
24 |
25 |
26 | @pytest.mark.headless
27 | def test_rendering_utf8_iframe():
28 | iframe = elem.IFrame(html="
Cerrahpaşa Tıp Fakültesi
")
29 |
30 | options = Options()
31 | options.add_argument("-headless")
32 | driver = Firefox(options=options)
33 |
34 | driver.get("data:text/html," + iframe.render())
35 | driver.switch_to.frame(0)
36 | assert "Cerrahpaşa Tıp Fakültesi" in driver.page_source
37 |
38 |
39 | @pytest.mark.headless
40 | def test_rendering_figure_notebook():
41 | """Verify special characters are correctly rendered in Jupyter notebooks."""
42 | text = '5/7 %, Линейная улица, "\u00e9 Berdsk"'
43 | figure = elem.Figure()
44 | elem.Html(text).add_to(figure.html)
45 | html = figure._repr_html_()
46 |
47 | filepath = "temp_test_rendering_figure_notebook.html"
48 | filepath = os.path.abspath(filepath)
49 | with open(filepath, "w") as f:
50 | f.write(html)
51 |
52 | options = Options()
53 | options.add_argument("-headless")
54 | driver = Firefox(options=options)
55 | try:
56 | driver.get("file://" + filepath)
57 | driver.switch_to.frame(0)
58 | text_div = driver.find_element(By.CSS_SELECTOR, "div")
59 | assert text_div.text == text
60 | finally:
61 | os.remove(filepath)
62 | driver.quit()
63 |
--------------------------------------------------------------------------------
/tests/test_utilities.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from pathlib import Path
4 |
5 | import pytest
6 |
7 | import branca.utilities as ut
8 | from branca.colormap import LinearColormap
9 |
10 | rootpath = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / "branca"
11 | color_brewer_minimum_n = 3
12 | color_brewer_maximum_n = 253 # Why this limitation @ branca/utilities.py:108 ?
13 |
14 |
15 | # Loads schemes and their meta-data
16 | with open(rootpath / "_schemes.json") as f:
17 | schemes = json.loads(f.read())
18 | with open(rootpath / "scheme_info.json") as f:
19 | scheme_info = json.loads(f.read())
20 | with open(rootpath / "scheme_base_codes.json") as f:
21 | core_schemes = json.loads(f.read())["codes"]
22 |
23 |
24 | def test_color_brewer_base():
25 | scheme = ut.color_brewer("YlGnBu", 9)
26 | assert scheme == [
27 | "#ffffd9",
28 | "#edf8b1",
29 | "#c7e9b4",
30 | "#7fcdbb",
31 | "#41b6c4",
32 | "#1d91c0",
33 | "#225ea8",
34 | "#253494",
35 | "#081d58",
36 | ]
37 |
38 |
39 | def test_color_brewer_reverse():
40 | scheme = ut.color_brewer("YlGnBu")
41 | scheme_r = ut.color_brewer("YlGnBu_r")
42 | assert scheme[::-1] == scheme_r
43 |
44 |
45 | def test_color_brewer_extendability():
46 | """
47 | The non-qualitative schemes should be extendable.
48 |
49 | :see https://github.com/python-visualization/branca/issues/104
50 | :see https://github.com/python-visualization/branca/issues/114
51 |
52 | Moreover, the following error was not reported via issues:
53 | * TypeError in the linear_gradient function when trying to extend any scheme.
54 | Indeed, in color_brewer, the key searched in the scheme database was not found,
55 | thus, it was passing `None` instead of a real scheme vector to linear_gradient.
56 | """
57 | for sname in core_schemes:
58 | for n in range(color_brewer_minimum_n, color_brewer_maximum_n + 1):
59 | try:
60 | scheme = ut.color_brewer(sname, n=n)
61 | except Exception as e:
62 | if scheme_info[sname] == "Qualitative" and isinstance(e, ValueError):
63 | continue
64 | raise
65 |
66 | assert len(scheme) == n
67 |
68 | # When we try to extend a scheme,
69 | # the reverse is not always the exact reverse vector of the original one.
70 | # Thus, we do not test this property!
71 | _ = ut.color_brewer(sname + "_r", n=n)
72 |
73 |
74 | def test_color_avoid_unexpected_error():
75 | """
76 | We had unexpected errors by providing some scheme name with unexpected value of `n`.
77 | This function tests them.
78 |
79 | Identified errors which was not reported via issues:
80 | * The scheme 'viridis' was not in the base_codes JSON;
81 | * Multiple scheme hadn't any metadata in scheme_info JSON;
82 | * When a `n` value provided to `color_scheme` was a float,
83 | it tried to select an unknown scheme without raising the right Exception type.
84 | """
85 |
86 | # Verify that every scheme has is present in base codes
87 | scheme_names = set()
88 | for sname in schemes.keys():
89 | scheme_names.add(sname.split("_")[0])
90 | assert scheme_names == set(core_schemes)
91 |
92 | # Verify that every scheme has a metadata
93 | assert scheme_names == set(scheme_info.keys())
94 |
95 | # Verify that every scheme can be generated in color_brewer using exotic value of `n`.
96 | # Note that big but valid values are generated by test_color_brewer_extendability.
97 | for sname in scheme_names:
98 | for n in (
99 | [-10]
100 | + list(range(-1, color_brewer_minimum_n))
101 | + list(range(color_brewer_maximum_n + 1, color_brewer_maximum_n + 10))
102 | ):
103 | with pytest.raises(ValueError):
104 | ut.color_brewer(sname, n)
105 | for n in [str(color_brewer_minimum_n), float(color_brewer_minimum_n), "abc"]:
106 | with pytest.raises(TypeError):
107 | ut.color_brewer(sname, n)
108 |
109 |
110 | @pytest.mark.parametrize(
111 | "value,result",
112 | [
113 | (1, (1.0, "px")),
114 | ("1 px", (1.0, "px")),
115 | ("80 % ", (80.0, "%")),
116 | ("100% ", (100.0, "%")),
117 | ("3 vw", (3.0, "vw")),
118 | ("3.14 rem", (3.14, "rem")),
119 | ((1, "px"), (1.0, "px")),
120 | ((80.0, "%"), (80.0, "%")),
121 | ],
122 | )
123 | def test_parse_size(value, result):
124 | assert ut._parse_size(value) == result
125 |
126 |
127 | @pytest.mark.parametrize(
128 | "value",
129 | [
130 | "what?",
131 | "1.21 jigawatts",
132 | ut._parse_size,
133 | (1.21, 4.9),
134 | ],
135 | )
136 | def test_parse_size_exceptions(value):
137 | with pytest.raises((ValueError, TypeError)):
138 | ut._parse_size(value)
139 |
140 |
141 | def test_write_png_mono():
142 | mono_image = [
143 | [0.24309289, 0.75997446, 0.02971671, 0.52830537],
144 | [0.62339252, 0.65369358, 0.41545387, 0.03307279],
145 | ]
146 |
147 | mono_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00)IDATx\xdac\x08\x0c\x0c\xfc\x0f\x02\x9c\x9c\x9c\xff7n\xdc\xf8\x9f\xe1\xe2\xc5\x8b\xffo\xdf\xbe\xfd\xbf\xbb\xbb\xfb?77\xf7\x7f\x00f\x87\x14\xdd\x0c\r;\xc0\x00\x00\x00\x00IEND\xaeB`\x82" # noqa E501
148 | assert ut.write_png(mono_image) == mono_png
149 |
150 | colormap = LinearColormap(colors=["red", "yellow", "green"])
151 | color_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00)IDATx\xdac\xf8_\xcf\xf0\xbf\xea\x10\xc3\xff\xff\xfc\x0c\xff?\xfcg\xf8\xcfp\xe0\x19\xc3\xff\r\xf7\x80\x02\xb7\x80X\x90\xe1?\x00N\xca\x13\xcd\xfb\xad\r\xb8\x00\x00\x00\x00IEND\xaeB`\x82" # noqa E501
152 | assert ut.write_png(mono_image, colormap=colormap) == color_png
153 |
154 |
155 | def test_write_png_rgb():
156 | image_rgb = [
157 | [
158 | [0.8952778565195247, 0.6196806506704735, 0.2696137085302287],
159 | [0.3940794236804127, 0.9432178293916365, 0.16500617914697335],
160 | [0.5566755388192485, 0.10469673377265687, 0.27346260130585975],
161 | [0.2029951628162342, 0.5357152681832641, 0.13692921080346832],
162 | ],
163 | [
164 | [0.5186482474007286, 0.8625240370164696, 0.6965561989987038],
165 | [0.04425586727957387, 0.45448042432657076, 0.8552600511205423],
166 | [0.696453974598333, 0.7508742900711168, 0.9646572952994652],
167 | [0.7471809029502141, 0.3218907599994758, 0.789193070740859],
168 | ],
169 | ]
170 | png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00-IDATx\xda\x01"\x00\xdd\xff\x00\xff\xa7G\xffp\xff+\xff\x9e\x1cH\xff9\x90$\xff\x00\x93\xe9\xb8\xff\x0cz\xe2\xff\xc6\xca\xff\xff\xd4W\xd0\xffYw\x15\x95\xcf\xb9@D\x00\x00\x00\x00IEND\xaeB`\x82' # noqa E501
171 | assert ut.write_png(image_rgb) == png
172 |
--------------------------------------------------------------------------------