├── .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 | [![PyPI Package](https://img.shields.io/pypi/v/branca.svg)](https://pypi.python.org/pypi/branca) 2 | [![Build Status](https://github.com/python-visualization/branca/actions/workflows/test_code.yml/badge.svg?branch=main)](https://github.com/python-visualization/branca/actions/workflows/test_code.yml) 3 | [![Gitter](https://badges.gitter.im/python-visualization/folium.svg)](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'' 200 | + "".join( 201 | [ 202 | ( 203 | '' 205 | ).format( 206 | i=i * 1, 207 | color=self.rgba_hex_str( 208 | self.vmin + (self.vmax - self.vmin) * i / (self.width - 1), 209 | ), 210 | ) 211 | for i in range(self.width) 212 | ], 213 | ) 214 | + ( 215 | '{}' 217 | ).format( 218 | self.text_color, 219 | self.vmin, 220 | ) 221 | + "".join( 222 | [ 223 | ( 224 | '{}' 226 | ).format(x_ticks[i], self.text_color, val_ticks[i]) 227 | for i in range(1, nb_ticks - 1) 228 | ], 229 | ) 230 | + ( 231 | '{}' 233 | ).format( 234 | self.width, 235 | self.text_color, 236 | self.vmax, 237 | ) 238 | + '{}'.format( 239 | self.text_color, 240 | self.caption, 241 | ) 242 | + "" 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 | 646 | {% endfor %}
{{key}}{{val._repr_html_()}}
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 | 669 | {% endfor %}
{{key}}{{val._repr_html_()}}
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 | "3.24.45.66.87.99.110.3Unemployment rate" 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 | --------------------------------------------------------------------------------