├── tests
├── data
│ └── warning_list.txt
├── __init__.py
├── test_ipygee.py
├── conftest.py
└── check_warnings.py
├── docs
├── usage.rst
├── _static
│ ├── logo.png
│ ├── custom.css
│ └── custom-icon.js
├── contribute.rst
├── _template
│ └── pypackage-credit.html
├── index.rst
├── conf.py
└── usage
│ └── plot
│ ├── index.rst
│ ├── map-featurecollection.ipynb
│ ├── map-image.ipynb
│ ├── plot-imagecollection.ipynb
│ └── plot-featurecollection.ipynb
├── ipygee
├── py.typed
├── js
│ └── jupyter_clip.js
├── type.py
├── map.py
├── __init__.py
├── sidecar.py
├── decorator.py
├── task.py
├── ee_feature_collection.py
├── plotting.py
├── ee_image.py
├── ee_image_collection.py
└── asset.py
├── codecov.yml
├── CITATION.cff
├── .readthedocs.yaml
├── .copier-answers.yml
├── .devcontainer
└── devcontainer.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ └── pr_template.md
│ └── feature_request.md
└── workflows
│ ├── release.yaml
│ ├── pypackage_check.yaml
│ └── unit.yaml
├── AUTHORS.rst
├── LICENSE
├── .pre-commit-config.yaml
├── .gitignore
├── noxfile.py
├── pyproject.toml
├── README.rst
├── example.ipynb
├── CONTRIBUTING.rst
└── CODE_OF_CONDUCT.rst
/tests/data/warning_list.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """make test folder a package for coverage."""
2 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | Usage
2 | =====
3 |
4 | **ipygee** usage documentation.
5 |
--------------------------------------------------------------------------------
/ipygee/py.typed:
--------------------------------------------------------------------------------
1 | # Marker file for PEP 561. The mypy package uses inline types.
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | # disable the treemap comment and report in PRs
2 | comment: false
3 |
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gee-community/ipygee/main/docs/_static/logo.png
--------------------------------------------------------------------------------
/docs/contribute.rst:
--------------------------------------------------------------------------------
1 | Contribute
2 | ==========
3 |
4 | .. include:: ../CONTRIBUTING.rst
5 | :start-line: 3
6 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | /* add dollar sign in console code-block */
2 | div.highlight-console pre span.go::before {
3 | content: "$";
4 | margin-right: 10px;
5 | margin-left: 5px;
6 | }
7 |
--------------------------------------------------------------------------------
/docs/_template/pypackage-credit.html:
--------------------------------------------------------------------------------
1 |
2 | From
3 | @12rambau/pypackage
4 | 0.1.16 Copier project.
5 |
6 |
--------------------------------------------------------------------------------
/tests/test_ipygee.py:
--------------------------------------------------------------------------------
1 | """Test the ipygee package."""
2 |
3 | import ee
4 |
5 |
6 | def test_gee_connection():
7 | """Test the geeconnection is working."""
8 | assert ee.Number(1).getInfo() == 1
9 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Pytest session configuration."""
2 |
3 | import pytest_gee
4 |
5 |
6 | def pytest_configure():
7 | """Configure test environment."""
8 | pytest_gee.init_ee_from_service_account()
9 |
--------------------------------------------------------------------------------
/ipygee/js/jupyter_clip.js:
--------------------------------------------------------------------------------
1 | var tempInput = document.createElement("input");
2 | tempInput.value = _txt;
3 | document.body.appendChild(tempInput);
4 | tempInput.focus();
5 | tempInput.select();
6 | document.execCommand("copy");
7 | document.body.removeChild(tempInput);
8 |
--------------------------------------------------------------------------------
/ipygee/type.py:
--------------------------------------------------------------------------------
1 | """The different types created for this specific package."""
2 |
3 | import os
4 | from pathlib import Path
5 | from typing import Union
6 |
7 | import ee
8 | import geetools # noqa: F401
9 |
10 | pathlike = Union[os.PathLike, Path, ee.Asset]
11 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | cff-version: "1.2.0"
2 | message: "If you use this software, please cite it as below."
3 | authors:
4 | - family-names: "Rambaud"
5 | given-names: "Pierrick"
6 | orcid: "https://orcid.org/"
7 | title: "ipygee"
8 | version: "0.0.0"
9 | doi: ""
10 | date-released: "2023-10-12"
11 | url: "https://github.com/12rambau/ipygee"
12 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
2 |
3 | version: 2
4 |
5 | build:
6 | os: ubuntu-22.04
7 | tools:
8 | python: "3.10"
9 |
10 | sphinx:
11 | configuration: docs/conf.py
12 |
13 | python:
14 | install:
15 | - method: pip
16 | path: .
17 | extra_requirements:
18 | - doc
19 |
--------------------------------------------------------------------------------
/ipygee/map.py:
--------------------------------------------------------------------------------
1 | """All the map related widgets and functions are here."""
2 |
3 | from __future__ import annotations
4 |
5 | from geemap import core
6 |
7 | from .sidecar import HasSideCar
8 |
9 |
10 | class Map(core.Map, HasSideCar):
11 | """A subclass of geemap.Map with a sidecar method."""
12 |
13 | sidecar_title = "Map"
14 | "title of the sidecar"
15 |
--------------------------------------------------------------------------------
/ipygee/__init__.py:
--------------------------------------------------------------------------------
1 | """The init file of the package."""
2 |
3 | __version__ = "0.0.0"
4 | __author__ = "Pierrick Rambaud"
5 | __email__ = "pierrick.rambaud49@gmail.com"
6 |
7 | # import in the main class the extensions built for the different GEE native classes
8 | from .ee_feature_collection import FeatureCollectionAccessor
9 | from .ee_image import ImageAccessor
10 | from .ee_image_collection import ImageCollectionAccessor
11 |
--------------------------------------------------------------------------------
/.copier-answers.yml:
--------------------------------------------------------------------------------
1 | # Changes here will be overwritten by Copier
2 | _commit: 0.1.16
3 | _src_path: gh:12rambau/pypackage
4 | author_email: pierrick.rambaud49@gmail.com
5 | author_first_name: Pierrick
6 | author_last_name: Rambaud
7 | author_orcid: ""
8 | creation_year: "2020"
9 | github_repo_name: ipygee
10 | github_user: 12rambau
11 | project_name: ipygee
12 | project_slug: ipygee
13 | short_description: widgets to interact with GEE API
14 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3",
3 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
4 | "features": {
5 | "ghcr.io/devcontainers/features/python:1": {
6 | "installJupyterlab": true
7 | },
8 | "ghcr.io/devcontainers-contrib/features/nox:2": {},
9 | "ghcr.io/devcontainers-contrib/features/pre-commit:2": {}
10 | },
11 | "postCreateCommand": "python -m pip install commitizen && pre-commit install"
12 | }
13 |
--------------------------------------------------------------------------------
/ipygee/sidecar.py:
--------------------------------------------------------------------------------
1 | """A meta class to define DOM element that can be send to the sidecar."""
2 |
3 | from __future__ import annotations
4 |
5 | from IPython.display import display
6 | from sidecar import Sidecar
7 |
8 |
9 | class HasSideCar:
10 | """MetClass to define the to_sidecar method."""
11 |
12 | sidecar_title = "Sidecar"
13 | "The title of the sidecar"
14 |
15 | def to_sidecar(self):
16 | """Send the widget to a sidecar."""
17 | with Sidecar(title=self.sidecar_title):
18 | display(self)
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Additional context**
24 | Add any other context about the problem here.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE/pr_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Pull request template
3 | about: Create a pull request
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | ## reference the related issue
10 |
11 | PR should answer problem stated in the issue tracker. please open one before starting a PR
12 |
13 | ## description of the changes
14 |
15 | Describe the changes you propose
16 |
17 | ## mention
18 |
19 | @mentions of the person or team responsible for reviewing proposed changes
20 |
21 | ## comments
22 |
23 | any other comments we should pay attention to
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | :html_theme.sidebar_secondary.remove:
2 |
3 |
4 | ipygee
5 | ======
6 |
7 | .. toctree::
8 | :hidden:
9 |
10 | usage
11 | contribute
12 |
13 | Documentation contents
14 | ----------------------
15 |
16 | The documentation contains 3 main sections:
17 |
18 | .. grid:: 1 2 3 3
19 |
20 | .. grid-item::
21 |
22 | .. card:: Usage
23 | :link: usage.html
24 |
25 | Usage and installation
26 |
27 | .. grid-item::
28 |
29 | .. card:: Contribute
30 | :link: contribute.html
31 |
32 | Help us improve the lib.
33 |
34 | .. grid-item::
35 |
36 | .. card:: API
37 | :link: autoapi/index.html
38 |
39 | Discover the lib API.
40 |
--------------------------------------------------------------------------------
/AUTHORS.rst:
--------------------------------------------------------------------------------
1 | Thanks goes to these wonderful people (`emoji key `_):
2 |
3 | .. raw:: html
4 |
5 |
18 |
19 | This project follows the `all-contributors `_ specification.
20 |
21 | Contributions of any kind are welcome!
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | env:
8 | PIP_ROOT_USER_ACTION: ignore
9 |
10 | jobs:
11 | tests:
12 | uses: ./.github/workflows/unit.yaml
13 |
14 | deploy:
15 | needs: [tests]
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-python@v5
20 | with:
21 | python-version: "3.11"
22 | - name: Install dependencies
23 | run: pip install twine build nox[uv]
24 | - name: update citation date
25 | run: nox -s release-date
26 | - name: Build and publish
27 | env:
28 | TWINE_USERNAME: __token__
29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
30 | run: python -m build && twine upload dist/*
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Pierrick Rambaud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_install_hook_types: [pre-commit, commit-msg]
2 |
3 | repos:
4 | - repo: "https://github.com/commitizen-tools/commitizen"
5 | rev: "v2.18.0"
6 | hooks:
7 | - id: commitizen
8 | stages: [commit-msg]
9 |
10 | - repo: "https://github.com/kynan/nbstripout"
11 | rev: "0.5.0"
12 | hooks:
13 | - id: nbstripout
14 | stages: [pre-commit]
15 |
16 | - repo: "https://github.com/pycontribs/mirrors-prettier"
17 | rev: "v3.4.2"
18 | hooks:
19 | - id: prettier
20 | stages: [pre-commit]
21 | exclude: tests\/test_.+\.
22 |
23 | - repo: https://github.com/charliermarsh/ruff-pre-commit
24 | rev: "v0.7.0"
25 | hooks:
26 | - id: ruff
27 | stages: [pre-commit]
28 | - id: ruff-format
29 | stages: [pre-commit]
30 |
31 | - repo: https://github.com/sphinx-contrib/sphinx-lint
32 | rev: "v1.0.0"
33 | hooks:
34 | - id: sphinx-lint
35 | stages: [pre-commit]
36 |
37 | - repo: https://github.com/codespell-project/codespell
38 | rev: v2.2.4
39 | hooks:
40 | - id: codespell
41 | stages: [pre-commit]
42 | additional_dependencies:
43 | - tomli
44 |
45 | # Prevent committing inline conflict markers
46 | - repo: https://github.com/pre-commit/pre-commit-hooks
47 | rev: v4.3.0
48 | hooks:
49 | - id: check-merge-conflict
50 | stages: [pre-commit]
51 | args: [--assume-in-merge]
52 |
--------------------------------------------------------------------------------
/tests/check_warnings.py:
--------------------------------------------------------------------------------
1 | """Check the warnings from doc builds."""
2 |
3 | import sys
4 | from pathlib import Path
5 |
6 |
7 | def check_warnings(file: Path) -> int:
8 | """Check the list of warnings produced by the CI tests.
9 |
10 | Raises errors if there are unexpected ones and/or if some are missing.
11 |
12 | Args:
13 | file: the path to the generated warning.txt file from
14 | the CI build
15 |
16 | Returns:
17 | 0 if the warnings are all there
18 | 1 if some warning are not registered or unexpected
19 | """
20 | # print some log
21 | print("\n=== Sphinx Warnings test ===\n")
22 |
23 | # find the file where all the known warnings are stored
24 | warning_file = Path(__file__).parent / "data" / "warning_list.txt"
25 |
26 | test_warnings = file.read_text().strip().split("\n")
27 | ref_warnings = warning_file.read_text().strip().split("\n")
28 |
29 | print(
30 | f'Checking build warnings in file: "{file}" and comparing to expected '
31 | f'warnings defined in "{warning_file}"\n\n'
32 | )
33 |
34 | # find all the missing warnings
35 | missing_warnings = []
36 | for wa in ref_warnings:
37 | index = [i for i, twa in enumerate(test_warnings) if wa in twa]
38 | if len(index) == 0:
39 | missing_warnings += [wa]
40 | print(f"Warning was not raised: {wa}")
41 | else:
42 | test_warnings.pop(index[0])
43 |
44 | # the remaining one are unexpected
45 | for twa in test_warnings:
46 | print(f"Unexpected warning: {twa}")
47 |
48 | # delete the tmp warnings file
49 | file.unlink()
50 |
51 | return len(missing_warnings) != 0 or len(test_warnings) != 0
52 |
53 |
54 | if __name__ == "__main__":
55 | # cast the file to path and resolve to an absolute one
56 | file = Path.cwd() / "warnings.txt"
57 |
58 | # execute the test
59 | sys.exit(check_warnings(file))
60 |
--------------------------------------------------------------------------------
/ipygee/decorator.py:
--------------------------------------------------------------------------------
1 | """Decorators used in ipygee.
2 |
3 | ported from https://github.com/12rambau/sepal_ui/blob/main/sepal_ui/scripts/decorator.py
4 | """
5 |
6 | from __future__ import annotations
7 |
8 | from functools import wraps
9 | from typing import Any, Optional
10 |
11 |
12 | def switch(*params, member: Optional[str] = None, debug: bool = True) -> Any:
13 | r"""Decorator to switch the state of input boolean parameters on class widgets or the class itself.
14 |
15 | If ``widget`` is defined, it will switch the state of every widget parameter, otherwise
16 | it will change the state of the class (self). You can also set two decorators on the same
17 | function, one could affect the class and other the widget.
18 |
19 | Args:
20 | *params: any boolean member of a Widget.
21 | member: THe widget on which the member are switched. Default to self.
22 | debug: Whether trigger or not an Exception if the decorated function fails.
23 |
24 | Returns:
25 | The return statement of the decorated method
26 | """
27 |
28 | def decorator_switch(func):
29 | @wraps(func)
30 | def wrapper_switch(self, *args, **kwargs):
31 | # set the widget to work with. if nothing is set it will be self
32 | widget = getattr(self, member) if member else self
33 |
34 | # create the list of target values based on the initial values
35 | targets = [bool(getattr(widget, p)) for p in params]
36 | not_targets = [not t for t in targets]
37 |
38 | # assgn the parameters to the target inverse
39 | [setattr(widget, p, t) for p, t in zip(params, not_targets)]
40 |
41 | # execute the function and catch errors
42 | try:
43 | func(self, *args, **kwargs)
44 | except Exception as e:
45 | if debug:
46 | raise e
47 | finally:
48 | [setattr(widget, p, t) for p, t in zip(params, targets)]
49 |
50 | return wrapper_switch
51 |
52 | return decorator_switch
53 |
--------------------------------------------------------------------------------
/.github/workflows/pypackage_check.yaml:
--------------------------------------------------------------------------------
1 | name: template update check
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | env:
7 | PIP_ROOT_USER_ACTION: ignore
8 |
9 | jobs:
10 | check_version:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-python@v5
15 | with:
16 | python-version: "3.10"
17 | - name: install dependencies
18 | run: pip install requests
19 | - name: get latest pypackage release
20 | id: get_latest_release
21 | run: |
22 | RELEASE=$(curl -s https://api.github.com/repos/12rambau/pypackage/releases | jq -r '.[0].tag_name')
23 | echo "latest=$RELEASE" >> $GITHUB_OUTPUT
24 | echo "latest release: $RELEASE"
25 | - name: get current pypackage version
26 | id: get_current_version
27 | run: |
28 | RELEASE=$(yq -r "._commit" .copier-answers.yml)
29 | echo "current=$RELEASE" >> $GITHUB_OUTPUT
30 | echo "current release: $RELEASE"
31 | - name: open issue
32 | if: steps.get_current_version.outputs.current != steps.get_latest_release.outputs.latest
33 | uses: rishabhgupta/git-action-issue@v2
34 | with:
35 | token: ${{ secrets.GITHUB_TOKEN }}
36 | title: "Update template to ${{ steps.get_latest_release.outputs.latest }}"
37 | body: |
38 | The package is based on the ${{ steps.get_current_version.outputs.current }} version of [@12rambau/pypackage](https://github.com/12rambau/pypackage).
39 |
40 | The latest version of the template is ${{ steps.get_latest_release.outputs.latest }}.
41 |
42 | Please consider updating the template to the latest version to include all the latest developments.
43 |
44 | Run the following code in your project directory to update the template:
45 |
46 | ```
47 | copier update --trust --defaults --vcs-ref ${{ steps.get_latest_release.outputs.latest }}
48 | ```
49 |
50 | > **Note**
51 | > You may need to reinstall ``copier`` and ``jinja2-time`` if they are not available in your environment.
52 |
53 | After solving the merging issues you can push back the changes to your main branch.
54 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | """Configuration file for the Sphinx documentation builder.
2 |
3 | This file only contains a selection of the most common options. For a full
4 | list see the documentation:
5 | https://www.sphinx-doc.org/en/master/usage/configuration.html
6 | """
7 |
8 | # -- Path setup ----------------------------------------------------------------
9 | from datetime import datetime
10 |
11 | # -- Project information -------------------------------------------------------
12 | project = "ipygee"
13 | author = "Pierrick Rambaud"
14 | copyright = f"2020-{datetime.now().year}, {author}"
15 | release = "0.0.0"
16 |
17 | # -- General configuration -----------------------------------------------------
18 | extensions = [
19 | "sphinx_copybutton",
20 | "sphinx.ext.napoleon",
21 | "sphinx.ext.viewcode",
22 | "sphinx.ext.intersphinx",
23 | "sphinx_design",
24 | "autoapi.extension",
25 | ]
26 | exclude_patterns = ["**.ipynb_checkpoints"]
27 | templates_path = ["_template"]
28 |
29 | # -- Options for HTML output ---------------------------------------------------
30 | html_theme = "pydata_sphinx_theme"
31 | html_static_path = ["_static"]
32 | html_logo = "_static/logo.png"
33 | html_theme_options = {
34 | "logo": {
35 | "text": project,
36 | },
37 | "use_edit_page_button": True,
38 | "footer_end": ["theme-version", "pypackage-credit"],
39 | "icon_links": [
40 | {
41 | "name": "GitHub",
42 | "url": "https://github.com/12rambau/ipygee",
43 | "icon": "fa-brands fa-github",
44 | },
45 | {
46 | "name": "Pypi",
47 | "url": "https://pypi.org/project/ipygee/",
48 | "icon": "fa-brands fa-python",
49 | },
50 | {
51 | "name": "Conda",
52 | "url": "https://anaconda.org/conda-forge/ipygee",
53 | "icon": "fa-custom fa-conda",
54 | "type": "fontawesome",
55 | },
56 | ],
57 | }
58 | html_context = {
59 | "github_user": "gee-community",
60 | "github_repo": "ipygee",
61 | "github_version": "",
62 | "doc_path": "docs",
63 | }
64 | html_css_files = ["custom.css"]
65 |
66 | # -- Options for autosummary/autodoc output ------------------------------------
67 | autodoc_typehints = "description"
68 | autoapi_dirs = ["../ipygee"]
69 | autoapi_python_class_content = "init"
70 | autoapi_member_order = "groupwise"
71 |
72 | # -- Options for intersphinx output --------------------------------------------
73 | intersphinx_mapping = {}
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | .ruff_cache/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 | docs/api/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97 | __pypackages__/
98 |
99 | # Celery stuff
100 | celerybeat-schedule
101 | celerybeat.pid
102 |
103 | # SageMath parsed files
104 | *.sage.py
105 |
106 | # Environments
107 | .env
108 | .venv
109 | env/
110 | venv/
111 | ENV/
112 | env.bak/
113 | venv.bak/
114 |
115 | # Spyder project settings
116 | .spyderproject
117 | .spyproject
118 |
119 | # Rope project settings
120 | .ropeproject
121 |
122 | # mkdocs documentation
123 | /site
124 |
125 | # mypy
126 | .mypy_cache/
127 | .dmypy.json
128 | dmypy.json
129 |
130 | # Pyre type checker
131 | .pyre/
132 |
133 | # system IDE
134 | .vscode/
135 |
136 | # image tmp file
137 | *Zone.Identifier
138 |
139 | # debugging notebooks
140 | test.ipynb
141 |
--------------------------------------------------------------------------------
/docs/_static/custom-icon.js:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Set a custom icon for pypi as it's not available in the fa built-in brands
3 | */
4 | FontAwesome.library.add(
5 | (faListOldStyle = {
6 | prefix: "fa-custom",
7 | iconName: "conda",
8 | icon: [
9 | 24, // viewBox width
10 | 24, // viewBox height
11 | [], // ligature
12 | "e001", // unicode codepoint - private use area
13 | "M12.045.033a12.181 12.182 0 00-1.361.078 17.512 17.513 0 011.813 1.433l.48.438-.465.45a15.047 15.048 0 00-1.126 1.205l-.178.215a8.527 8.527 0 01.86-.05 8.154 8.155 0 11-4.286 15.149 15.764 15.765 0 01-1.841.106h-.86a21.847 21.848 0 00.264 2.866 11.966 11.967 0 106.7-21.89zM8.17.678a12.181 12.182 0 00-2.624 1.275 15.506 15.507 0 011.813.43A18.551 18.552 0 018.17.678zM9.423.75a16.237 16.238 0 00-.995 1.998 16.15 16.152 0 011.605.66 6.98 6.98 0 01.43-.509c.234-.286.472-.559.716-.817A15.047 15.048 0 009.423.75zM4.68 2.949a14.969 14.97 0 000 2.336c.587-.065 1.196-.1 1.812-.107a16.617 16.617 0 01.48-1.748 16.48 16.481 0 00-2.292-.481zM3.62 3.5A11.938 11.938 0 001.762 5.88a17.004 17.004 0 011.877-.444A17.39 17.391 0 013.62 3.5zm4.406.287c-.143.437-.265.888-.38 1.347a8.255 8.255 0 011.67-.803c-.423-.2-.845-.38-1.29-.544zM6.3 6.216a14.051 14.052 0 00-1.555.108c.064.523.157 1.038.272 1.554a8.39 8.391 0 011.283-1.662zm-2.55.137a15.313 15.313 0 00-2.602.716h-.078v.079a17.104 17.105 0 001.267 2.544l.043.071.072-.049a16.309 16.31 0 011.734-1.083l.057-.035V8.54a16.867 16.868 0 01-.408-2.094v-.092zM.644 8.095l-.063.2A11.844 11.845 0 000 11.655v.209l.143-.152a17.706 17.707 0 011.584-1.447l.057-.043-.043-.064a16.18 16.18 0 01-1.025-1.87zm3.77 1.253l-.18.1c-.465.273-.93.573-1.375.889l-.065.05.05.064c.309.437.645.867.996 1.276l.137.165v-.208a8.176 8.176 0 01.364-2.15zM2.2 10.853l-.072.05a16.574 16.574 0 00-1.813 1.734l-.058.058.066.057a15.449 15.45 0 001.991 1.483l.072.05.043-.08a16.738 16.74 0 011.053-1.64v-.05l-.043-.05a16.99 16.99 0 01-1.19-1.54zm1.855 2.071l-.121.172a15.363 15.363 0 00-.917 1.433l-.043.072.071.043a16.61 16.61 0 001.562.766l.193.086-.086-.193a8.04 8.04 0 01-.66-2.172zm-3.976.48v.2a11.758 11.759 0 00.946 3.326l.078.186.072-.194a16.215 16.216 0 01.845-2l.057-.063-.064-.043a17.197 17.198 0 01-1.776-1.284zm2.543 1.805l-.035.08a15.764 15.765 0 00-.983 2.479v.08h.086a16.15 16.152 0 002.688.5l.072.007v-.086a17.562 17.563 0 01.164-2.056v-.065H4.55a16.266 16.266 0 01-1.849-.896zm2.544 1.169v.114a17.254 17.255 0 00-.151 1.828v.078h.931c.287 0 .624.014.946 0h.209l-.166-.129a8.011 8.011 0 01-1.64-1.834zm-3.29 2.1l.115.172a11.988 11.988 0 002.502 2.737l.157.129v-.201a22.578 22.58 0 01-.2-2.336v-.071h-.072a16.23 16.23 0 01-2.3-.387z", // svg path (https://simpleicons.org/icons/anaconda.svg)
14 | ],
15 | }),
16 | );
17 |
--------------------------------------------------------------------------------
/.github/workflows/unit.yaml:
--------------------------------------------------------------------------------
1 | name: Unit tests
2 |
3 | on:
4 | workflow_call:
5 | push:
6 | branches:
7 | - main
8 | pull_request:
9 |
10 | env:
11 | EARTHENGINE_PROJECT: ${{ secrets.EARTHENGINE_PROJECT }}
12 | EARTHENGINE_SERVICE_ACCOUNT: ${{ secrets.EARTHENGINE_SERVICE_ACCOUNT }}
13 | FORCE_COLOR: 1
14 | PIP_ROOT_USER_ACTION: ignore
15 |
16 | jobs:
17 | lint:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-python@v5
22 | with:
23 | python-version: "3.11"
24 | - uses: pre-commit/action@v3.0.0
25 |
26 | mypy:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v4
30 | - uses: actions/setup-python@v5
31 | with:
32 | python-version: "3.11"
33 | - name: Install nox
34 | run: pip install nox[uv]
35 | - name: run mypy checks
36 | run: nox -s mypy
37 |
38 | docs:
39 | needs: [lint, mypy]
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: actions/checkout@v4
43 | - uses: actions/setup-python@v5
44 | with:
45 | python-version: "3.11"
46 | - name: Install nox
47 | run: pip install nox[uv]
48 | - name: build static docs
49 | run: nox -s docs
50 |
51 | build:
52 | needs: [lint, mypy]
53 | strategy:
54 | fail-fast: true
55 | matrix:
56 | os: [ubuntu-latest]
57 | python-version: ["3.8", "3.9", "3.10", "3.11"]
58 | include:
59 | - os: macos-latest # macos test
60 | python-version: "3.11"
61 | - os: windows-latest # windows test
62 | python-version: "3.11"
63 | runs-on: ${{ matrix.os }}
64 | steps:
65 | - uses: actions/checkout@v4
66 | - name: Set up Python ${{ matrix.python-version }}
67 | uses: actions/setup-python@v5
68 | with:
69 | python-version: ${{ matrix.python-version }}
70 | - name: Install nox
71 | run: pip install nox[uv]
72 | - name: test with pytest
73 | run: nox -s ci-test
74 | - name: assess dead fixtures
75 | if: ${{ matrix.python-version == '3.10' }}
76 | shell: bash
77 | run: nox -s dead-fixtures
78 | - uses: actions/upload-artifact@v4
79 | if: ${{ matrix.python-version == '3.10' }}
80 | with:
81 | name: coverage
82 | path: coverage.xml
83 |
84 | coverage:
85 | needs: [build]
86 | runs-on: ubuntu-latest
87 | steps:
88 | - uses: actions/download-artifact@v4
89 | with:
90 | name: coverage
91 | path: coverage.xml
92 | - name: codecov
93 | uses: codecov/codecov-action@v4
94 | with:
95 | file: ./coverage.xml
96 | token: ${{ secrets.CODECOV_TOKEN }}
97 | verbose: true
98 |
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | """All the process that can be run using nox.
2 |
3 | The nox run are build in isolated environment that will be stored in .nox. to force the venv update, remove the .nox/xxx folder.
4 | """
5 |
6 | import datetime
7 | import fileinput
8 |
9 | import nox
10 |
11 | nox.options.sessions = ["lint", "test", "docs", "mypy"]
12 |
13 |
14 | @nox.session(reuse_venv=True, venv_backend="uv")
15 | def lint(session):
16 | """Apply the pre-commits."""
17 | session.install("pre-commit")
18 | session.run("pre-commit", "run", "--all-files", *session.posargs)
19 |
20 |
21 | @nox.session(reuse_venv=True, venv_backend="uv")
22 | def test(session):
23 | """Run the selected tests and report coverage in html."""
24 | session.install("--reinstall", ".[test]")
25 | test_files = session.posargs or ["tests"]
26 | session.run("pytest", "--cov", "--cov-report=html", *test_files)
27 |
28 |
29 | @nox.session(reuse_venv=True, name="ci-test", venv_backend="uv")
30 | def ci_test(session):
31 | """Run all the test and report coverage in xml."""
32 | session.install("--reinstall", ".[test]")
33 | session.run("pytest", "--cov", "--cov-report=xml")
34 |
35 |
36 | @nox.session(reuse_venv=True, name="dead-fixtures", venv_backend="uv")
37 | def dead_fixtures(session):
38 | """Check for dead fixtures within the tests."""
39 | session.install("--reinstall", ".[test]")
40 | session.run("pytest", "--dead-fixtures")
41 |
42 |
43 | @nox.session(reuse_venv=True, venv_backend="uv")
44 | def docs(session):
45 | """Build the documentation."""
46 | build = session.posargs.pop() if session.posargs else "html"
47 | session.install("--reinstall", ".[doc]")
48 | dst, warn = f"docs/_build/{build}", "warnings.txt"
49 | session.run("sphinx-build", "-v", "-b", build, "docs", dst, "-w", warn)
50 | session.run("python", "tests/check_warnings.py")
51 |
52 |
53 | @nox.session(name="mypy", reuse_venv=True, venv_backend="uv")
54 | def mypy(session):
55 | """Run a mypy check of the lib."""
56 | session.install("mypy")
57 | session.install("types-requests")
58 | test_files = session.posargs or ["ipygee"]
59 | session.run("mypy", *test_files)
60 |
61 |
62 | @nox.session(reuse_venv=True, venv_backend="uv")
63 | def stubgen(session):
64 | """Generate stub files for the lib but requires human attention before merge."""
65 | session.install("mypy")
66 | package = session.posargs or ["ipygee"]
67 | session.run("stubgen", "-p", package[0], "-o", "stubs", "--include-private")
68 |
69 |
70 | @nox.session(name="release-date", reuse_venv=True, venv_backend="uv")
71 | def release_date(session):
72 | """Update the release date of the citation file."""
73 | current_date = datetime.datetime.now().strftime("%Y-%m-%d")
74 |
75 | with fileinput.FileInput("CITATION.cff", inplace=True) as file:
76 | for line in file:
77 | if line.startswith("date-released:"):
78 | print(f'date-released: "{current_date}"')
79 | else:
80 | print(line, end="")
81 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "ipygee"
7 | version = "0.0.0"
8 | description = "widgets to interact with GEE API"
9 | keywords = [
10 | "skeleton",
11 | "Python"
12 | ]
13 | classifiers = [
14 | "Development Status :: 3 - Alpha",
15 | "Intended Audience :: Developers",
16 | "License :: OSI Approved :: MIT License",
17 | "Programming Language :: Python :: 3.8",
18 | "Programming Language :: Python :: 3.9",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | ]
22 | requires-python = ">=3.8"
23 | dependencies = [
24 | "deprecated>=1.2.14",
25 | "earthengine-api",
26 | "ipyvuetify",
27 | "natsort",
28 | "sidecar",
29 | "geemap[core]",
30 | "bokeh",
31 | "jupyter_bokeh",
32 | ]
33 |
34 | [[project.authors]]
35 | name = "Pierrick Rambaud"
36 | email = "pierrick.rambaud49@gmail.com"
37 |
38 | [project.license]
39 | text = "MIT"
40 |
41 | [project.readme]
42 | file = "README.rst"
43 | content-type = "text/x-rst"
44 |
45 | [project.urls]
46 | Homepage = "https://github.com/gee-community/ipygee"
47 |
48 | [project.optional-dependencies]
49 | test = [
50 | "pytest",
51 | "pytest-cov",
52 | "pytest-deadfixtures",
53 | "httplib2",
54 | "pytest-gee",
55 | ]
56 | doc = [
57 | "sphinx>=6.2.1",
58 | "pydata-sphinx-theme",
59 | "sphinx-copybutton",
60 | "sphinx-design",
61 | "sphinx-autoapi",
62 | "jupyter-sphinx",
63 | "httplib2",
64 | ]
65 |
66 | [tool.hatch.build.targets.wheel]
67 | only-include = ["ipygee"]
68 |
69 | [tool.hatch.envs.default]
70 | dependencies = [
71 | "pre-commit",
72 | "commitizen",
73 | "nox[uv]"
74 | ]
75 | post-install-commands = ["pre-commit install"]
76 |
77 | [tool.commitizen]
78 | tag_format = "v$major.$minor.$patch$prerelease"
79 | update_changelog_on_bump = false
80 | version = "0.0.0"
81 | version_files = [
82 | "pyproject.toml:version",
83 | "ipygee/__init__.py:__version__",
84 | "docs/conf.py:release",
85 | "CITATION.cff:version"
86 | ]
87 |
88 | [tool.pytest.ini_options]
89 | testpaths = "tests"
90 |
91 | [tool.ruff]
92 | line-length = 105 # small margin for long lines
93 | fix = true
94 |
95 | [tool.ruff.lint]
96 | select = ["E", "F", "W", "I", "D", "RUF"]
97 | ignore = [
98 | "E501", # line too long | Black take care of it
99 | "D212", # Multi-line docstring | We use D213
100 | "D101", # Missing docstring in public class | We use D106
101 | ]
102 |
103 | [tool.ruff.lint.flake8-quotes]
104 | docstring-quotes = "double"
105 |
106 | [tool.ruff.lint.pydocstyle]
107 | convention = "google"
108 |
109 | [tool.ruff.lint.extend-per-file-ignores]
110 | "__init__.py" = ["F401"] # Unused import | We use it to expose API extension
111 |
112 | [tool.coverage.run]
113 | source = ["ipygee"]
114 |
115 | [tool.mypy]
116 | scripts_are_modules = true
117 | ignore_missing_imports = true
118 | install_types = true
119 | non_interactive = true
120 | warn_redundant_casts = true
121 |
122 | [tool.licensecheck]
123 | using = "PEP631"
124 |
125 | [tool.codespell]
126 | skip = "**/*.ipynb,**/*.yml,**/*.svg"
127 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | ipygee
3 | ======
4 |
5 | .. |license| image:: https://img.shields.io/badge/License-MIT-yellow.svg?logo=opensourceinitiative&logoColor=white
6 | :target: LICENSE
7 | :alt: License: MIT
8 |
9 | .. |commit| image:: https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git&logoColor=white
10 | :target: https://conventionalcommits.org
11 | :alt: conventional commit
12 |
13 | .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
14 | :target: https://github.com/astral-sh/ruff
15 | :alt: ruff badge
16 |
17 | .. |prettier| image:: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier&logoColor=white
18 | :target: https://github.com/prettier/prettier
19 | :alt: prettier badge
20 |
21 | .. |pre-commmit| image:: https://img.shields.io/badge/pre--commit-active-yellow?logo=pre-commit&logoColor=white
22 | :target: https://pre-commit.com/
23 | :alt: pre-commit
24 |
25 | .. |pypi| image:: https://img.shields.io/pypi/v/ipygee?color=blue&logo=pypi&logoColor=white
26 | :target: https://pypi.org/project/ipygee/
27 | :alt: PyPI version
28 |
29 | .. |build| image:: https://img.shields.io/github/actions/workflow/status/12rambau/ipygee/unit.yaml?logo=github&logoColor=white
30 | :target: https://github.com/12rambau/ipygee/actions/workflows/unit.yaml
31 | :alt: build
32 |
33 | .. |coverage| image:: https://img.shields.io/codecov/c/github/12rambau/ipygee?logo=codecov&logoColor=white
34 | :target: https://codecov.io/gh/12rambau/ipygee
35 | :alt: Test Coverage
36 |
37 | .. |docs| image:: https://img.shields.io/readthedocs/ipygee?logo=readthedocs&logoColor=white
38 | :target: https://ipygee.readthedocs.io/en/latest/
39 | :alt: Documentation Status
40 |
41 | |license| |commit| |ruff| |prettier| |pre-commmit| |pypi| |build| |coverage| |docs|
42 |
43 | Overview
44 | --------
45 |
46 | .. image:: https://raw.githubusercontent.com/gee-community/ipygee/main/docs/_static/logo.svg
47 | :width: 20%
48 | :align: right
49 |
50 | This package provides interactive widgets object to interact with Google Earth engine from Python interactive environment.
51 |
52 | TODO
53 | ----
54 |
55 | This package is still a work in progress but beta testers are welcome. here are the different object that I would like to see implemented in this package:
56 |
57 | - A very basic map with the most critical functions available (basically geemap core and that's all)
58 | - A Task manager widget reproducing all the task manager features (and more?)
59 | - A asset manager to manipulate GEE assets in an interactive window
60 | - A bokhe interface (pure python) for all the geetools plotting capabilities
61 | - A Map inspector (need the map first)
62 | - A dataset Explorer (based on the geemap one but with an increased interactivity)
63 | - All should be wired to JupyterLab sidecar for now, other IDE support will be welcome
64 | - A complete and easy to use documentation
65 | - If possible some CI tests (complicated for UI)
66 |
67 | Credits
68 | -------
69 |
70 | This package was created with `Copier `__ and the `@12rambau/pypackage `__ 0.1.16 project template.
71 |
--------------------------------------------------------------------------------
/example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ee\n",
10 | "\n",
11 | "ee.Authenticate()"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": null,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "ee.Initialize(project=\"ee-borntobealive\")"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": null,
26 | "metadata": {},
27 | "outputs": [],
28 | "source": [
29 | "from ipygee.asset import AssetManager\n",
30 | "from ipygee.map import Map"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": null,
36 | "metadata": {},
37 | "outputs": [],
38 | "source": [
39 | "am = AssetManager()\n",
40 | "am.to_sidecar()"
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": null,
46 | "metadata": {},
47 | "outputs": [],
48 | "source": [
49 | "# tm = TaskManager()\n",
50 | "# tm.to_sidecar()"
51 | ]
52 | },
53 | {
54 | "cell_type": "code",
55 | "execution_count": null,
56 | "metadata": {},
57 | "outputs": [],
58 | "source": [
59 | "m = Map()\n",
60 | "m.to_sidecar()"
61 | ]
62 | },
63 | {
64 | "cell_type": "code",
65 | "execution_count": null,
66 | "metadata": {},
67 | "outputs": [],
68 | "source": [
69 | "dataset = ee.ImageCollection(\"ECMWF/ERA5_LAND/HOURLY\").filter(ee.Filter.date(\"2020-07-01\", \"2020-07-02\"))\n",
70 | "\n",
71 | "visualization = {\n",
72 | " \"bands\": [\"temperature_2m\"],\n",
73 | " \"min\": 250.0,\n",
74 | " \"max\": 320.0,\n",
75 | " \"palette\": [\n",
76 | " \"000080\",\n",
77 | " \"0000d9\",\n",
78 | " \"4000ff\",\n",
79 | " \"8000ff\",\n",
80 | " \"0080ff\",\n",
81 | " \"00ffff\",\n",
82 | " \"00ff80\",\n",
83 | " \"80ff00\",\n",
84 | " \"daff00\",\n",
85 | " \"ffff00\",\n",
86 | " \"fff500\",\n",
87 | " \"ffda00\",\n",
88 | " \"ffb000\",\n",
89 | " \"ffa400\",\n",
90 | " \"ff4f00\",\n",
91 | " \"ff2500\",\n",
92 | " \"ff0a00\",\n",
93 | " \"ff00ff\",\n",
94 | " ],\n",
95 | "}\n",
96 | "\n",
97 | "m.addLayer(dataset, visualization, \"Air temperature [K] at 2m height\")"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": null,
103 | "metadata": {},
104 | "outputs": [],
105 | "source": []
106 | },
107 | {
108 | "cell_type": "code",
109 | "execution_count": null,
110 | "metadata": {},
111 | "outputs": [],
112 | "source": []
113 | }
114 | ],
115 | "metadata": {
116 | "kernelspec": {
117 | "display_name": "Python 3 (ipykernel)",
118 | "language": "python",
119 | "name": "python3"
120 | },
121 | "language_info": {
122 | "codemirror_mode": {
123 | "name": "ipython",
124 | "version": 3
125 | },
126 | "file_extension": ".py",
127 | "mimetype": "text/x-python",
128 | "name": "python",
129 | "nbconvert_exporter": "python",
130 | "pygments_lexer": "ipython3",
131 | "version": "3.12.6"
132 | }
133 | },
134 | "nbformat": 4,
135 | "nbformat_minor": 4
136 | }
137 |
--------------------------------------------------------------------------------
/docs/usage/plot/index.rst:
--------------------------------------------------------------------------------
1 | Plotting
2 | ========
3 |
4 | We embed some plotting capabilities in the library to help you visualize your data. For simplicity we decided to map all the plotting function to the :doc:`matplotlib ` library as it's the most used static plotting library in the Python ecosystem.
5 |
6 | .. toctree::
7 | :hidden:
8 | :maxdepth: 1
9 |
10 | plot-featurecollection
11 | plot-image
12 | plot-imagecollection
13 | map-image
14 | map-featurecollection
15 |
16 | .. grid:: 1 2 3 3
17 |
18 | .. grid-item::
19 |
20 | .. card:: :icon:`fa-solid fa-chart-simple` FeatureCollection
21 | :link: plot-featurecollection.html
22 |
23 | .. grid-item::
24 |
25 | .. card:: :icon:`fa-solid fa-chart-simple` Image
26 | :link: plot-image.html
27 |
28 | .. grid-item::
29 |
30 | .. card:: :icon:`fa-solid fa-chart-simple` ImageCollection
31 | :link: plot-imagecollection.html
32 |
33 | .. grid-item::
34 |
35 | .. card:: :icon:`fa-solid fa-image` Image
36 | :link: map-image.html
37 |
38 | .. grid-item::
39 |
40 | .. card:: :icon:`fa-solid fa-map` FeatureCollection
41 | :link: map-featurecollection.html
42 |
43 |
44 |
45 | In all these examples we will use the object interface of matplotlib creating the :py:class:`Figure ` and :py:class:`Axes ` object before plotting the data. This is the recommended way to use matplotlib as it gives you more control over the plot and the figure.
46 |
47 | .. code-block:: python
48 |
49 | # custom image for this specific chart
50 | modisSr = (
51 | ee.ImageCollection("MODIS/061/MOD09A1")
52 | .filter(ee.Filter.date("2018-06-01", "2018-09-01"))
53 | .select(["sur_refl_b01", "sur_refl_b02", "sur_refl_b06"])
54 | .mean()
55 | )
56 | histRegion = ee.Geometry.Rectangle([-112.60, 40.60, -111.18, 41.22])
57 |
58 | #create a matplotlib figure
59 | fig, ax = plt.subplots(figsize=(10, 4))
60 |
61 | # plot the histogram of the reds
62 | modisSr.geetools.plot_hist(
63 | bands = ["sur_refl_b01", "sur_refl_b02", "sur_refl_b06"],
64 | labels = [['Red', 'NIR', 'SWIR']],
65 | colors = ["#cf513e", "#1d6b99", "#f0af07"],
66 | ax = ax,
67 | bins = 100,
68 | scale = 500,
69 | region = histRegion,
70 | )
71 |
72 | # once created the axes can be modified as needed using pure matplotlib functions
73 | ax.set_title("Modis SR Reflectance Histogram")
74 | ax.set_xlabel("Reflectance (x1e4)")
75 |
76 | .. image:: ../../_static/usage/plot/index/histogram.png
77 | :alt: Modis SR Reflectance Histogram
78 | :align: center
79 |
80 | If you are used to the :py:mod:`pyplot ` interface of matplotlib you can still use it with the state-base module of matplotlib. Just be aware that the module is a stateful interface and you will have less control over the figure and the plot.
81 |
82 | .. code-block:: python
83 |
84 | # get all hydroshed from the the south amercias within the WWF/HydroATLAS dataset.
85 | region = ee.Geometry.BBox(-80, -60, -20, 20);
86 | fc = ee.FeatureCollection('WWF/HydroATLAS/v1/Basins/level04').filterBounds(region)
87 |
88 | # create the plot
89 | fc.geetools.plot(property="UP_AREA", cmap="viridis")
90 |
91 | # Customized display
92 | plt.colorbar(ax.collections[0], label="Upstream area (km²)")
93 | plt.title("HydroATLAS basins of level4")
94 | plt.xlabel("Longitude (°)")
95 | plt.ylabel("Latitude (°)")
96 |
97 | .. image:: ../../_static/usage/plot/index/hydroshed.png
98 | :alt: HydroATLAS basins of level4
99 | :align: center
100 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Contribute
2 | ==========
3 |
4 | Thank you for your help improving **ipygee**!
5 |
6 | **ipygee** uses `nox `__ to automate several development-related tasks.
7 | Currently, the project uses four automation processes (called sessions) in ``noxfile.py``:
8 |
9 | - ``mypy``: to perform a mypy check on the lib;
10 | - ``test``: to run the test with pytest;
11 | - ``docs``: to build the documentation in the ``build`` folder;
12 | - ``lint``: to run the pre-commits in an isolated environment
13 |
14 | Every nox session is run in its own virtual environment, and the dependencies are installed automatically.
15 |
16 | To run a specific nox automation process, use the following command:
17 |
18 | .. code-block:: console
19 |
20 | nox -s
21 |
22 | For example: ``nox -s test`` or ``nox -s docs``.
23 |
24 | Workflow for contributing changes
25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 |
27 | We follow a typical GitHub workflow of:
28 |
29 | - Create a personal fork of this repo
30 | - Create a branch
31 | - Open a pull request
32 | - Fix findings of various linters and checks
33 | - Work through code review
34 |
35 | See the following sections for more details.
36 |
37 | Clone the repository
38 | ^^^^^^^^^^^^^^^^^^^^
39 |
40 | First off, you'll need your own copy of **ipygee** codebase. You can clone it for local development like so:
41 |
42 | Fork the repository so you have your own copy on GitHub. See the `GitHub forking guide for more information `__.
43 |
44 | Then, clone the repository locally so that you have a local copy to work on:
45 |
46 | .. code-block:: console
47 |
48 | git clone https://github.com//ipygee
49 | cd ipygee
50 |
51 | Then install the development version of the extension:
52 |
53 | .. code-block:: console
54 |
55 | pip install -e .[dev]
56 |
57 | This will install the **ipygee** library, together with two additional tools:
58 | - `pre-commit `__ for automatically enforcing code standards and quality checks before commits.
59 | - `nox `__, for automating common development tasks.
60 |
61 | Lastly, activate the pre-commit hooks by running:
62 |
63 | .. code-block:: console
64 |
65 | pre-commit install
66 |
67 | This will install the necessary dependencies to run pre-commit every time you make a commit with Git.
68 |
69 | Contribute to the codebase
70 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
71 |
72 | Any larger updates to the codebase should include tests and documentation. The tests are located in the ``tests`` folder, and the documentation is located in the ``docs`` folder.
73 |
74 | To run the tests locally, use the following command:
75 |
76 | .. code-block:: console
77 |
78 | nox -s test
79 |
80 | See :ref:`below ` for more information on how to update the documentation.
81 |
82 | .. _contributing-docs:
83 |
84 | Contribute to the docs
85 | ^^^^^^^^^^^^^^^^^^^^^^
86 |
87 | The documentation is built using `Sphinx `__ and deployed to `Read the Docs `__.
88 |
89 | To build the documentation locally, use the following command:
90 |
91 | .. code-block:: console
92 |
93 | nox -s docs
94 |
95 | For each pull request, the documentation is built and deployed to make it easier to review the changes in the PR. To access the docs build from a PR, click on the "Read the Docs" preview in the CI/CD jobs.
96 |
97 | Release new version
98 | ^^^^^^^^^^^^^^^^^^^
99 |
100 | To release a new version, start by pushing a new bump from the local directory:
101 |
102 | .. code-block::
103 |
104 | cz bump
105 |
106 | The commitizen-tool will detect the semantic version name based on the existing commits messages.
107 |
108 | Then push to Github. In Github design a new release using the same tag name nad the ``release.yaml`` job will send it to pipy.
109 |
--------------------------------------------------------------------------------
/ipygee/task.py:
--------------------------------------------------------------------------------
1 | """he task manager widget and functionalitites."""
2 |
3 | from __future__ import annotations
4 |
5 | from datetime import datetime as dt
6 | from pathlib import Path
7 | from typing import List
8 |
9 | import ee
10 | import ipyvuetify as v
11 |
12 | from .decorator import switch
13 | from .sidecar import HasSideCar
14 |
15 | ICON_TYPE = {
16 | "EXPORT_IMAGE": "mdi-image-outline",
17 | "EXPORT_FEATURES": "mdi-table",
18 | }
19 |
20 | ICON_STATUS = {
21 | "RUNNING": ["mdi-cog", "primary"],
22 | "SUCCEEDED": ["mdi-check", "success"],
23 | "FAILED": ["mdi-alert", "error"],
24 | }
25 |
26 | DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
27 |
28 |
29 | class TaskManager(v.Flex, HasSideCar):
30 | """A task manager widget."""
31 |
32 | # -- Variables -------------------------------------------------------------
33 |
34 | sidecar_title = "Tasks"
35 | "The title of the sidecar"
36 |
37 | # -- Widgets ---------------------------------------------------------------
38 |
39 | w_reload: v.Btn
40 | "The reload btn at the top of the Task manager"
41 |
42 | w_search: v.Btn
43 | "The search button to crowl into the existing tasks"
44 |
45 | w_list: v.List
46 | "The list of tasks displayed in the Task manager"
47 |
48 | w_card: v.Card
49 | "The card hosting the list of tasks"
50 |
51 | def __init__(self):
52 | """Initialize the widget."""
53 | # start by defining al the widgets
54 | # We deactivated the formatting to define each one of them on 1 single line
55 | # fmt: off
56 |
57 |
58 | # add a line of buttons to reload and saerch items
59 | self.w_reload = v.Btn(children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1")
60 | self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1")
61 | w_main_line = v.Flex(children=[self.w_reload, self.w_search])
62 |
63 | # generate the list of tasks
64 | w_group = v.ListItemGroup(children=self.get_tasks(), v_model="")
65 | self.w_list = v.List(dense=True, v_model=True, children=[w_group], outlined=True)
66 | self.w_card = v.Card(children=[self.w_list], class_="ma-1", elevation=0)
67 |
68 | super().__init__(children=[w_main_line, self.w_card], v_model="", class_="ma-1")
69 | # fmt: on
70 |
71 | # javascrit interaction
72 | self.w_reload.on_event("click", self.on_reload)
73 |
74 | def get_tasks(self) -> List[v.ListItem]:
75 | """Create the list of tasks from the current user."""
76 | # get all the tasks
77 | tasks = ee.data.listOperations()
78 |
79 | # build the listItems from the information
80 | task_list = []
81 | for t in tasks:
82 | # build a dictionary of metadata for the expansion panel
83 | state = t["metadata"]["state"]
84 | metadata = {
85 | "id": Path(t["name"]).name,
86 | "phase": state,
87 | "attempted": f"{t['metadata']['attempt']} time",
88 | }
89 |
90 | # add time information if the task computed
91 | if state in ["SUCCEEDED", "FAILED"]:
92 | start = dt.strptime(t["metadata"]["startTime"], DATE_FORMAT)
93 | end = dt.strptime(t["metadata"]["endTime"], DATE_FORMAT)
94 | runtime = (end - start).seconds
95 | hours, r = divmod(runtime, 3600)
96 | minutes, seconds = divmod(r, 60)
97 | metadata.update(runtime=f"{hours:02d}:{minutes:02d}:{seconds:02d}")
98 |
99 | # add EECCU consumption if the task was a success
100 | if state == "SUCCEEDED":
101 | metadata.update(consumption=f"{t['metadata']['batchEecuUsageSeconds']:4f} EECU/s")
102 |
103 | # display the information in a list
104 | ul_content = []
105 | for k, m in metadata.items():
106 | title = v.Html(tag="b", children=[f"{k}: "])
107 | text = v.Html(tag="span", children=[m])
108 | ul_content.append(v.Html(tag="li", children=[title, text]))
109 | content_list = v.Html(tag="ul", children=ul_content, style_="list-style-type: none")
110 |
111 | # the header will include multiple information from the task:
112 | # status, type of output and name
113 | icon, color = ICON_STATUS[state]
114 | w_status = v.Icon(children=[icon], color=color, class_="mr-1")
115 | w_type = v.Icon(children=[ICON_TYPE[t["metadata"]["type"]]], class_="mr-1")
116 | name = t["metadata"]["description"]
117 | header_row = v.Flex(children=[w_status, w_type, name])
118 |
119 | # finally we build each individual expansion panels
120 | content = v.ExpansionPanelContent(children=[content_list])
121 | header = v.ExpansionPanelHeader(children=[header_row])
122 | expansion_panel = v.ExpansionPanel(children=[header, content])
123 |
124 | task_list.append(v.ExpansionPanels(children=[expansion_panel], class_="pa-1"))
125 |
126 | return task_list
127 |
128 | @switch("loading", "disabled", member="w_card")
129 | def on_reload(self, *args):
130 | """Reload the list of tasks."""
131 | self.w_list.children = [v.ListItemGroup(children=self.get_tasks(), v_model="")]
132 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.rst:
--------------------------------------------------------------------------------
1 | Contributor Covenant Code of Conduct
2 | ====================================
3 |
4 | Our Pledge
5 | ----------
6 |
7 | We as members, contributors, and leaders pledge to make participation in our
8 | community a harassment-free experience for everyone, regardless of age, body
9 | size, visible or invisible disability, ethnicity, sex characteristics, gender
10 | identity and expression, level of experience, education, socio-economic status,
11 | nationality, personal appearance, race, religion, or sexual identity
12 | and orientation.
13 |
14 | We pledge to act and interact in ways that contribute to an open, welcoming,
15 | diverse, inclusive, and healthy community.
16 |
17 | Our Standards
18 | -------------
19 |
20 | Examples of behavior that contributes to a positive environment for our
21 | community include:
22 |
23 | * Demonstrating empathy and kindness toward other people
24 | * Being respectful of differing opinions, viewpoints, and experiences
25 | * Giving and gracefully accepting constructive feedback
26 | * Accepting responsibility and apologizing to those affected by our mistakes,
27 | and learning from the experience
28 | * Focusing on what is best not just for us as individuals, but for the
29 | overall community
30 |
31 | Examples of unacceptable behavior include:
32 |
33 | * The use of sexualized language or imagery, and sexual attention or
34 | advances of any kind
35 | * Trolling, insulting or derogatory comments, and personal or political attacks
36 | * Public or private harassment
37 | * Publishing others' private information, such as a physical or email
38 | address, without their explicit permission
39 | * Other conduct which could reasonably be considered inappropriate in a
40 | professional setting
41 |
42 | Enforcement Responsibilities
43 | ----------------------------
44 |
45 | Community leaders are responsible for clarifying and enforcing our standards of
46 | acceptable behavior and will take appropriate and fair corrective action in
47 | response to any behavior that they deem inappropriate, threatening, offensive,
48 | or harmful.
49 |
50 | Community leaders have the right and responsibility to remove, edit, or reject
51 | comments, commits, code, wiki edits, issues, and other contributions that are
52 | not aligned to this Code of Conduct, and will communicate reasons for moderation
53 | decisions when appropriate.
54 |
55 | Scope
56 | -----
57 |
58 | This Code of Conduct applies within all community spaces, and also applies when
59 | an individual is officially representing the community in public spaces.
60 | Examples of representing our community include using an official e-mail address,
61 | posting via an official social media account, or acting as an appointed
62 | representative at an online or offline event.
63 |
64 | Enforcement
65 | -----------
66 |
67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
68 | reported to the FAO team responsible for enforcement at
69 | pierrick.rambaud49@gmail.com.
70 | All complaints will be reviewed and investigated promptly and fairly.
71 |
72 | All community leaders are obligated to respect the privacy and security of the
73 | reporter of any incident.
74 |
75 | Enforcement Guidelines
76 | ----------------------
77 |
78 | Community leaders will follow these Community Impact Guidelines in determining
79 | the consequences for any action they deem in violation of this Code of Conduct:
80 |
81 | Correction
82 | ^^^^^^^^^^
83 |
84 | **Community Impact**: Use of inappropriate language or other behavior deemed
85 | unprofessional or unwelcome in the community.
86 |
87 | **Consequence**: A private, written warning from community leaders, providing
88 | clarity around the nature of the violation and an explanation of why the
89 | behavior was inappropriate. A public apology may be requested.
90 |
91 | Warning
92 | ^^^^^^^
93 |
94 | **Community Impact**: A violation through a single incident or series
95 | of actions.
96 |
97 | **Consequence**: A warning with consequences for continued behavior. No
98 | interaction with the people involved, including unsolicited interaction with
99 | those enforcing the Code of Conduct, for a specified period of time. This
100 | includes avoiding interactions in community spaces as well as external channels
101 | like social media. Violating these terms may lead to a temporary or
102 | permanent ban.
103 |
104 | Temporary Ban
105 | ^^^^^^^^^^^^^
106 |
107 | **Community Impact**: A serious violation of community standards, including
108 | sustained inappropriate behavior.
109 |
110 | **Consequence**: A temporary ban from any sort of interaction or public
111 | communication with the community for a specified period of time. No public or
112 | private interaction with the people involved, including unsolicited interaction
113 | with those enforcing the Code of Conduct, is allowed during this period.
114 | Violating these terms may lead to a permanent ban.
115 |
116 | Permanent Ban
117 | ^^^^^^^^^^^^^
118 |
119 | **Community Impact**: Demonstrating a pattern of violation of community
120 | standards, including sustained inappropriate behavior, harassment of an
121 | individual, or aggression toward or disparagement of classes of individuals.
122 |
123 | **Consequence**: A permanent ban from any sort of public interaction within
124 | the community.
125 |
126 | Attribution
127 | -----------
128 |
129 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
130 | version 2.0, available at
131 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
132 |
133 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
134 | enforcement ladder](https://github.com/mozilla/diversity).
135 |
136 | [homepage]: https://www.contributor-covenant.org
137 |
138 | For answers to common questions about this code of conduct, see the FAQ at
139 | https://www.contributor-covenant.org/faq. Translations are available at
140 | https://www.contributor-covenant.org/translations.
141 |
--------------------------------------------------------------------------------
/docs/usage/plot/map-featurecollection.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Map FeatureCollection\n",
8 | "\n",
9 | "The `geetools` extension contains a set of functions for rendering maps from `ee.FeatureCollection` objects. Use the following function descriptions and examples to determine the best function and chart type for your purpose."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {
16 | "tags": [
17 | "remove-cell"
18 | ]
19 | },
20 | "outputs": [],
21 | "source": [
22 | "import ee\n",
23 | "from geetools.utils import initialize_documentation\n",
24 | "\n",
25 | "initialize_documentation()"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "[](https://github.com/gee-community/geetools/blob/main/docs/usage/plot/map-featurecollection.ipynb)\n",
33 | "[](https://colab.research.google.com/github/gee-community/geetools/blob/main/docs/usage/plot/map-featurecollection.ipynb)"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "## Set up environment\n",
41 | "\n",
42 | "Install all the required packages and perform the import statement upstream."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "# uncomment if installation of libs is necessary\n",
52 | "# !pip install earthengine-api geetools"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "from matplotlib import pyplot as plt"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": null,
67 | "metadata": {},
68 | "outputs": [],
69 | "source": [
70 | "# uncomment if authetication to GEE is needed\n",
71 | "# ee.Authenticate()\n",
72 | "# ee.Intialize(project=\"\")"
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {},
78 | "source": [
79 | "## Example data \n",
80 | "\n",
81 | "The following examples rely on a `ee.FeatureCollection` composed of all the hydroshed bassins from south america."
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "region = ee.Geometry.BBox(-80, -60, -20, 20)\n",
91 | "fc = ee.FeatureCollection(\"WWF/HydroATLAS/v1/Basins/level04\").filterBounds(region)"
92 | ]
93 | },
94 | {
95 | "cell_type": "markdown",
96 | "metadata": {},
97 | "source": [
98 | "## Map Vector\n",
99 | "\n",
100 | "```{api}\n",
101 | "{docstring}`ee.FeatureCollection.geetools.plot`\n",
102 | "```\n",
103 | "\n",
104 | "An `ee.FeatureCollection` is a vector representation of geographical properties. A user can be interested by either the property evolution across the landscape or the geometries associated with it. The {py:meth}`plot ` is coverinig both use cases. \n",
105 | "\n",
106 | "### Map a property\n",
107 | "\n",
108 | "A single property can be ploted on a map using matplotlib. The following example is showing the bassin area in km².\n",
109 | "\n",
110 | "First create a matplotlib figure and axis, then you can add the bassins to the map using the `plot` method. By default it will display the first property of the features. In our case we will opt to display the area of the bassins in km² i.e. the \"UP_AREA\" property. Finally that we have the plot, we can customize it with matplotlib. For example, we can add a title and a colorbar."
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": null,
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "# create the plot\n",
120 | "fig, ax = plt.subplots(figsize=(10, 10))\n",
121 | "\n",
122 | "# generate the graph\n",
123 | "fc.geetools.plot(ax=ax, property=\"UP_AREA\", cmap=\"viridis\")\n",
124 | "\n",
125 | "# you can then customize the figure as you would for any other matplotlib object\n",
126 | "fig.colorbar(ax.collections[0], label=\"Upstream area (km²)\")\n",
127 | "ax.set_title(\"HydroATLAS basins of level4\")\n",
128 | "ax.set_xlabel(\"Longitude (°)\")\n",
129 | "ax.set_ylabel(\"Latitude (°)\")\n",
130 | "\n",
131 | "plt.show()"
132 | ]
133 | },
134 | {
135 | "cell_type": "markdown",
136 | "metadata": {},
137 | "source": [
138 | "### Map geometries\n",
139 | "\n",
140 | "Alternatively if you only want to plot the geometries of the featurecollection on a map, you can use the `plot` method with the `boundares` parameter set to `True`.\n",
141 | "\n",
142 | "Similarly to the previous example we start by creating a pyplot figure and axis, then you can start plotting the geometries and finally customize the plot."
143 | ]
144 | },
145 | {
146 | "cell_type": "code",
147 | "execution_count": null,
148 | "metadata": {},
149 | "outputs": [],
150 | "source": [
151 | "plt.ioff() # remove interactive for the sake of the example\n",
152 | "fig, ax = plt.subplots(figsize=(10, 10))\n",
153 | "\n",
154 | "# create the graph\n",
155 | "fc.geetools.plot(ax=ax, boundaries=True)\n",
156 | "\n",
157 | "# you can then customize the figure as you would for any other matplotlib object\n",
158 | "ax.set_title(\"Borders of the HydroATLAS basins of level4\")\n",
159 | "ax.set_xlabel(\"Longitude (°)\")\n",
160 | "ax.set_ylabel(\"Latitude (°)\")\n",
161 | "\n",
162 | "plt.show()"
163 | ]
164 | },
165 | {
166 | "cell_type": "code",
167 | "execution_count": null,
168 | "metadata": {},
169 | "outputs": [],
170 | "source": []
171 | }
172 | ],
173 | "metadata": {
174 | "kernelspec": {
175 | "display_name": "geetools",
176 | "language": "python",
177 | "name": "python3"
178 | },
179 | "language_info": {
180 | "codemirror_mode": {
181 | "name": "ipython",
182 | "version": 3
183 | },
184 | "file_extension": ".py",
185 | "mimetype": "text/x-python",
186 | "name": "python",
187 | "nbconvert_exporter": "python",
188 | "pygments_lexer": "ipython3",
189 | "version": "3.11.9"
190 | }
191 | },
192 | "nbformat": 4,
193 | "nbformat_minor": 2
194 | }
195 |
--------------------------------------------------------------------------------
/docs/usage/plot/map-image.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Map Image\n",
8 | "\n",
9 | "The `geetools` extension contains a set of functions for rendering maps from `ee.Image` objects. Use the following function descriptions and examples to determine the best function and chart type for your purpose."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {
16 | "tags": [
17 | "remove-cell"
18 | ]
19 | },
20 | "outputs": [],
21 | "source": [
22 | "import ee\n",
23 | "from geetools.utils import initialize_documentation\n",
24 | "\n",
25 | "initialize_documentation()"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "[](https://github.com/gee-community/geetools/blob/main/docs/usage/plot/map-image.ipynb)\n",
33 | "[](https://colab.research.google.com/github/gee-community/geetools/blob/main/docs/usage/plot/map-image.ipynb)"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "## Set up environment\n",
41 | "\n",
42 | "Install the required packages and authenticate your Earth Engine account."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "# uncomment if installation of libs is necessary\n",
52 | "# !pip install earthengine-api geetools"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "from matplotlib import pyplot as plt"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": null,
67 | "metadata": {},
68 | "outputs": [],
69 | "source": [
70 | "# uncomment if authetication to GEE is needed\n",
71 | "# ee.Authenticate()\n",
72 | "# ee.Intialize(project=\"\")"
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {},
78 | "source": [
79 | "## Example data \n",
80 | "\n",
81 | "The following examples rely on the \"COPERNICUS/S2_HARMONIZED\" `ee.ImageCollection` filtered between 2022-06-01 and 2022-06-30. We then build the NDVI spectral indice and use mosaic to get an `ee.Image` object. This object is clipped over the Vatican city as it's one of the smallest country in the world."
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "# load the vatican\n",
91 | "level0 = ee.FeatureCollection(\"FAO/GAUL/2015/level0\")\n",
92 | "vatican = level0.filter(ee.Filter.eq(\"ADM0_NAME\", \"Holy See\"))\n",
93 | "\n",
94 | "# pre-process the imagecollection and mosaic the month of June 2022\n",
95 | "image = (\n",
96 | " ee.ImageCollection(\"COPERNICUS/S2_HARMONIZED\")\n",
97 | " .filterDate(\"2022-06-01\", \"2022-06-30\")\n",
98 | " .filterBounds(vatican)\n",
99 | " .geetools.maskClouds()\n",
100 | " .geetools.spectralIndices(\"NDVI\")\n",
101 | " .mosaic()\n",
102 | ")"
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "metadata": {},
108 | "source": [
109 | "## Map Raster\n",
110 | "\n",
111 | "```{api}\n",
112 | "{py:meth}`plot `: \n",
113 | " {docstring}`geetools.ImageAccessor.plot`\n",
114 | "```\n",
115 | "\n",
116 | "An `ee.image` is a raster representation of the Earth's surface. The `plot` function allows you to visualize the raster data on a map. The function provides options to customize the visualization, such as the color palette, opacity, and the visualization range.\n",
117 | "\n",
118 | "### Map pseudo color\n",
119 | "\n",
120 | "A pseudo-color image is a single-band raster image that uses a color palette to represent the data. The following example demonstrates how to plot the NDVI pseudo-color image using the `plot` function.\n",
121 | "\n",
122 | "First create a matplotlib figure and axis. Then you can add the map to the axis. Provide a single element list in the bands parameter to plot the NDVI image. \n",
123 | "As per interactive representation an image needs to be reduced to a region, here \"Vatican City\". In this example we also select a pseudo-mercator projection and we displayed the `ee.FeatureCollection` on top of it. Now that we have the plot, we can customize it with matplotlib. For example, we can add a title and a colorbar. Now that we have the plot, we can customize it with matplotlib. For example, we can add a title and a colorbar."
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "fig, ax = plt.subplots()\n",
133 | "\n",
134 | "image.geetools.plot(\n",
135 | " bands=[\"NDVI\"],\n",
136 | " ax=ax,\n",
137 | " region=vatican.geometry(),\n",
138 | " crs=\"EPSG:3857\",\n",
139 | " scale=10,\n",
140 | " fc=vatican,\n",
141 | " cmap=\"viridis\",\n",
142 | " color=\"k\",\n",
143 | ")\n",
144 | "\n",
145 | "# as it's a figure you can then edit the information as you see fit\n",
146 | "ax.set_title(\"NDVI in Vatican City\")\n",
147 | "ax.set_xlabel(\"x coordinates (m)\")\n",
148 | "ax.set_ylabel(\"y coordinates (m)\")\n",
149 | "fig.colorbar(ax.images[0], label=\"NDVI\")\n",
150 | "\n",
151 | "plt.show()"
152 | ]
153 | },
154 | {
155 | "cell_type": "markdown",
156 | "metadata": {},
157 | "source": [
158 | "### Map RGB combo\n",
159 | "\n",
160 | "An RGB image is a three-band raster image that uses the red, green, and blue bands to represent the data. The following example demonstrates how to plot the RGB image using the `plot` function.\n",
161 | "\n",
162 | "First create a matplotlib figure and axis. Then you can add the map to the axis. Provide a 3 elements list in the bands parameter to plot the NDVI image. \n",
163 | "As per interactive representation an image needs to be reduced to a region, here \"Vatican City\". In this example we displayed the `ee.FeatureCollection` on top of it. Finally customize the plot."
164 | ]
165 | },
166 | {
167 | "cell_type": "code",
168 | "execution_count": null,
169 | "metadata": {},
170 | "outputs": [],
171 | "source": [
172 | "# Create the plot figure\n",
173 | "fig, ax = plt.subplots()\n",
174 | "\n",
175 | "# Create the graph\n",
176 | "image.geetools.plot(bands=[\"B4\", \"B3\", \"B2\"], ax=ax, region=vatican.geometry(), fc=vatican, color=\"k\")\n",
177 | "\n",
178 | "# as it's a figure you can then edit the information as you see fit\n",
179 | "ax.set_title(\"Sentinel 2 composite in Vatican City\")\n",
180 | "ax.set_xlabel(\"longitude (°)\")\n",
181 | "ax.set_ylabel(\"latitude (°)\")\n",
182 | "\n",
183 | "plt.show()"
184 | ]
185 | }
186 | ],
187 | "metadata": {
188 | "kernelspec": {
189 | "display_name": "Python 3",
190 | "language": "python",
191 | "name": "python3"
192 | },
193 | "language_info": {
194 | "codemirror_mode": {
195 | "name": "ipython",
196 | "version": 3
197 | },
198 | "file_extension": ".py",
199 | "mimetype": "text/x-python",
200 | "name": "python",
201 | "nbconvert_exporter": "python",
202 | "pygments_lexer": "ipython3",
203 | "version": "3.11.10"
204 | }
205 | },
206 | "nbformat": 4,
207 | "nbformat_minor": 2
208 | }
209 |
--------------------------------------------------------------------------------
/ipygee/ee_feature_collection.py:
--------------------------------------------------------------------------------
1 | """Toolbox for the :py:class:`ee.FeatureCollection` class."""
2 |
3 | from __future__ import annotations
4 |
5 | import ee
6 | import geetools # noqa: F401
7 | import numpy as np
8 | from bokeh import plotting
9 | from geetools.accessors import register_class_accessor
10 | from matplotlib import pyplot as plt
11 |
12 | from .plotting import plot_data
13 |
14 |
15 | @register_class_accessor(ee.FeatureCollection, "bokeh")
16 | class FeatureCollectionAccessor:
17 | """Toolbox for the :py:class:`ee.FeatureCollection` class."""
18 |
19 | def __init__(self, obj: ee.FeatureCollection):
20 | """Initialize the FeatureCollectionAccessor class."""
21 | self._obj = obj
22 |
23 | def plot_by_features(
24 | self,
25 | type: str = "bar",
26 | featureId: str = "system:index",
27 | properties: list[str] | None = None,
28 | labels: list[str] | None = None,
29 | colors: list[str] | None = None,
30 | figure: plotting.figure | None = None,
31 | **kwargs,
32 | ) -> plotting.figure:
33 | """Plot the values of a :py:class:`ee.FeatureCollection` by feature.
34 |
35 | Each feature property selected in properties will be plotted using the ``featureId`` as the x-axis.
36 | If no ``properties`` are provided, all properties will be plotted.
37 | If no ``featureId`` is provided, the ``"system:index"`` property will be used.
38 |
39 | Warning:
40 | This function is a client-side function.
41 |
42 | Args:
43 | type: The type of plot to use. Defaults to ``"bar"``. can be any type of plot from the python lib ``matplotlib.pyplot``. If the one you need is missing open an issue!
44 | featureId: The property to use as the x-axis (name the features). Defaults to ``"system:index"``.
45 | properties: A list of properties to plot. Defaults to all properties.
46 | labels: A list of labels to use for plotting the properties. If not provided, the default labels will be used. It needs to match the properties' length.
47 | colors: A list of colors to use for plotting the properties. If not provided, the default colors from the matplotlib library will be used.
48 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
49 | kwargs: Additional arguments from the ``pyplot`` type selected.
50 |
51 | Examples:
52 | .. jupyter-execute::
53 |
54 | import ee, geetools
55 | from geetools.utils import initialize_documentation
56 | from matplotlib import pyplot as plt
57 |
58 | initialize_documentation()
59 |
60 | # start a plot object from matplotlib library
61 | fig, ax = plt.subplots(figsize=(10, 5))
62 |
63 | # plot on this object the 10 first items of the FAO GAUL level 2 feature collection
64 | # for each one of them (marked with it's "ADM0_NAME" property) we plot the value of the "ADM1_CODE" and "ADM2_CODE" properties
65 | fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10)
66 | fc.geetools.plot_by_features(featureId="ADM2_NAME", properties=["ADM1_CODE", "ADM2_CODE"], colors=["#61A0D4", "#D49461"], ax=ax)
67 |
68 | # Modify the rotation of existing x-axis tick labels
69 | for label in ax.get_xticklabels():
70 | label.set_rotation(45)
71 | """
72 | # Get the features and properties
73 | props = (
74 | ee.List(properties)
75 | if properties is not None
76 | else self._obj.first().propertyNames().getInfo()
77 | )
78 | props = props.remove(featureId)
79 |
80 | # get the data from server
81 | data = self._obj.geetools.byProperties(featureId, props, labels).getInfo()
82 |
83 | # reorder the data according to the labels or properties set by the user
84 | labels = labels if labels is not None else props.getInfo()
85 | data = {k: data[k] for k in labels}
86 |
87 | return plot_data(
88 | type=type, data=data, label_name=featureId, colors=colors, figure=figure, **kwargs
89 | )
90 |
91 | def plot_by_properties(
92 | self,
93 | type: str = "bar",
94 | featureId: str = "system:index",
95 | properties: list[str] | ee.List | None = None,
96 | labels: list[str] | None = None,
97 | colors: list[str] | None = None,
98 | figure: plotting.Feature | None = None,
99 | **kwargs,
100 | ) -> plotting.Feature:
101 | """Plot the values of a :py:class:`ee.FeatureCollection` by property.
102 |
103 | Each features will be represented by a color and each property will be a bar of the bar chart.
104 |
105 | Warning:
106 | This function is a client-side function.
107 |
108 | Args:
109 | type: The type of plot to use. Defaults to ``"bar"``. can be any type of plot from the python lib ``matplotlib.pyplot``. If the one you need is missing open an issue!
110 | featureId: The property to use as the y-axis (name the features). Defaults to ``"system:index"``.
111 | properties: A list of properties to plot. Defaults to all properties.
112 | labels: A list of labels to use for plotting the properties. If not provided, the default labels will be used. It needs to match the properties' length.
113 | colors: A list of colors to use for plotting the properties. If not provided, the default colors from the matplotlib library will be used.
114 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
115 | kwargs: Additional arguments from the ``pyplot`` function.
116 |
117 | Examples:
118 | .. jupyter-execute::
119 |
120 | import ee, ipygee
121 | from geetools.utils import initialize_documentation
122 | from matplotlib import pyplot as plt
123 |
124 | initialize_documentation()
125 |
126 | # start a plot object from matplotlib library
127 | fig, ax = plt.subplots(figsize=(10, 5))
128 |
129 | # plot on this object the 10 first items of the FAO GAUL level 2 feature collection
130 | # for each one of them (marked with it's "ADM2_NAME" property) we plot the value of the "ADM1_CODE" property
131 | fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10)
132 | fc.bokeh.plot_by_properties(featureId="ADM2_NAME", properties=["ADM1_CODE"], ax=ax)
133 | """
134 | # Get the features and properties
135 | fc = self._obj
136 | props = ee.List(properties) if properties is not None else fc.first().propertyNames()
137 | props = props.remove(featureId)
138 |
139 | # get the data from server
140 | data = self._obj.geetools.byFeatures(featureId, props, labels).getInfo()
141 |
142 | # reorder the data according to the lapbes or properties set by the user
143 | labels = labels if labels is not None else props.getInfo()
144 | data = {f: {k: data[f][k] for k in labels} for f in data.keys()}
145 |
146 | return plot_data(
147 | type=type, data=data, label_name=featureId, colors=colors, figure=figure, **kwargs
148 | )
149 |
150 | def plot_hist(
151 | self,
152 | property: str | ee.String,
153 | label: str = "",
154 | figure: plotting.figure | None = None,
155 | color: str | None = None,
156 | **kwargs,
157 | ) -> plotting.figure:
158 | """Plot the histogram of a specific property.
159 |
160 | Warning:
161 | This function is a client-side function.
162 |
163 | Args:
164 | property: The property to display
165 | label: The label to use for the property. If not provided, the property name will be used.
166 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
167 | color: The color to use for the plot. If not provided, the default colors from the matplotlib library will be used.
168 | **kwargs: Additional arguments from the :py:func:`matplotlib.pyplot.hist` function.
169 |
170 | Examples:
171 | .. jupyter-execute::
172 |
173 | import ee, ipygee
174 | from geetools.utils import initialize_documentation
175 | from matplotlib import pyplot as plt
176 |
177 | initialize_documentation()
178 |
179 | # start a plot object from matplotlib library
180 | fig, ax = plt.subplots(figsize=(10, 5))
181 | ax.set_title('Histogram of Precipitation in July')
182 | ax.set_xlabel('Precipitation (mm)')
183 |
184 |
185 | # build the histogram of the precipitation band for the month of july in the PRISM dataset
186 | normClim = ee.ImageCollection('OREGONSTATE/PRISM/Norm81m').toBands()
187 | region = ee.Geometry.Rectangle(-123.41, 40.43, -116.38, 45.14)
188 | climSamp = normClim.sample(region, 5000)
189 | climSamp.ipygee.plot_hist("07_ppt", ax=ax, bins=20)
190 |
191 | fig.show()
192 | """
193 | # gather the data from parameters
194 | properties, labels = ee.List([property]), ee.List([label])
195 |
196 | # get the data from the server
197 | data = self._obj.geetools.byProperties(properties=properties, labels=labels).getInfo()
198 |
199 | # create the graph objcet if not provided
200 | figure = plotting.figure() if figure is None else figure
201 |
202 | # gather the data from the data variable
203 | labels = list(data.keys())
204 | if len(labels) != 1:
205 | raise ValueError("histogram chart can only be used with one property")
206 |
207 | color = color or plt.get_cmap("tab10").colors[0]
208 | y, x = np.histogram(list(data[labels[0]].values()), **kwargs)
209 | figure.vbar(x=x, top=y, width=0.9, color=color)
210 |
211 | # customize the layout of the axis
212 | figure.xaxis.axis_label = labels[0]
213 | figure.yaxis.axis_label = "frequency"
214 | figure.xgrid.grid_line_color = None
215 | figure.outline_line_color = None
216 |
217 | return figure
218 |
--------------------------------------------------------------------------------
/ipygee/plotting.py:
--------------------------------------------------------------------------------
1 | """The extensive plotting function for bokhe binding."""
2 |
3 | from __future__ import annotations
4 |
5 | from datetime import datetime as dt
6 | from math import pi
7 |
8 | import numpy as np
9 | from bokeh import plotting
10 | from bokeh.layouts import column
11 | from bokeh.models import RangeTool
12 | from bokeh.models.layouts import Column
13 | from matplotlib import pyplot as plt
14 | from matplotlib.colors import to_hex
15 |
16 |
17 | def plot_data(
18 | type: str,
19 | data: dict,
20 | label_name: str,
21 | colors: list[str] | None = None,
22 | figure: plotting.figure | None = None,
23 | ax: plt.Axes | None = None,
24 | **kwargs,
25 | ) -> plotting.figure | Column:
26 | """Plotting mechanism used in all the plotting functions.
27 |
28 | It binds the bokeh capabilities with the data aggregated by different axes.
29 | the shape of the data should as follows:
30 |
31 | .. code-block::
32 |
33 | {
34 | "label1": {"properties1": value1, "properties2": value2, ...}
35 | "label2": {"properties1": value1, "properties2": value2, ...},
36 | ...
37 | }
38 |
39 | Args:
40 | type: The type of plot to use. can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue!
41 | data: the data to use as inputs of the graph. Please follow the format specified in the documentation.
42 | label_name: The name of the property that was used to generate the labels
43 | property_names: The list of names that was used to name the values. They will be used to order the keys of the data dictionary.
44 | colors: A list of colors to use for the plot. If not provided, the default colors from the matplotlib library will be used.
45 | figure: The bokeh figure to use. If not provided, the plot will be sent to a new figure.
46 | ax: The matplotlib axis to use. If not provided, the plot will be sent to a new axis.
47 | kwargs: Additional arguments from the ``figure`` chart type selected.
48 |
49 | Returns:
50 | The bokeh figure or the column of figure for time series.
51 | """
52 | # define the ax if not provided by the user
53 | figure = plotting.figure(match_aspect=True) if figure is None else figure
54 |
55 | # gather the data from parameters
56 | labels = list(data.keys())
57 | props = list(data[labels[0]].keys())
58 | colors = colors if colors else plt.get_cmap("tab10").colors
59 |
60 | # convert the colors to hexadecimal representation
61 | colors = [to_hex(c) for c in colors]
62 |
63 | # draw the chart based on the type
64 | if type == "plot":
65 | ticker_values = list(range(len(props)))
66 | for i, label in enumerate(labels):
67 | kwargs.update(color=colors[i], legend_label=label)
68 | figure.line(x=ticker_values, y=list(data[label].values()), **kwargs)
69 | figure.xaxis.ticker = ticker_values
70 | figure.xaxis.major_label_overrides = {i: p for i, p in enumerate(props)}
71 | figure.yaxis.axis_label = props[0] if len(props) == 1 else "Properties values"
72 | figure.xaxis.axis_label = f"Features (labeled by {label_name})"
73 | figure.xgrid.grid_line_color = None
74 | figure.outline_line_color = None
75 | return figure
76 |
77 | elif type == "scatter":
78 | ticker_values = list(range(len(props)))
79 | for i, label in enumerate(labels):
80 | kwargs.update(color=colors[i], legend_label=label)
81 | figure.scatter(x=ticker_values, y=list(data[label].values()), **kwargs)
82 | figure.xaxis.ticker = ticker_values
83 | figure.xaxis.major_label_overrides = {i: p for i, p in enumerate(props)}
84 | figure.yaxis.axis_label = props[0] if len(props) == 1 else "Properties values"
85 | figure.xaxis.axis_label = f"Features (labeled by {label_name})"
86 | figure.xgrid.grid_line_color = None
87 | figure.outline_line_color = None
88 | return figure
89 |
90 | elif type == "fill_between":
91 | ticker_values = list(range(len(props)))
92 | for i, label in enumerate(labels):
93 | values = list(data[label].values())
94 | bottom = [0] * len(values)
95 | kwargs.update(color=colors[i], legend_label=label)
96 | figure.varea(x=ticker_values, y1=bottom, y2=values, alpha=0.2, **kwargs)
97 | figure.line(x=ticker_values, y=values, **kwargs)
98 | figure.xaxis.ticker = ticker_values
99 | figure.xaxis.major_label_overrides = {i: p for i, p in enumerate(props)}
100 | figure.yaxis.axis_label = props[0] if len(props) == 1 else "Properties values"
101 | figure.xaxis.axis_label = f"Features (labeled by {label_name})"
102 | figure.xgrid.grid_line_color = None
103 | figure.outline_line_color = None
104 | return figure
105 |
106 | elif type == "bar":
107 | ticker_values = list(range(len(props)))
108 | data.update(props=ticker_values)
109 |
110 | x = np.arange(len(props))
111 | width = 1 / (len(labels) + 0.8)
112 | margin = width / 10
113 | ticks_value = x + width * len(labels) / 2
114 | figure.xaxis.ticker = ticks_value
115 | figure.xaxis.major_label_overrides = dict(zip(ticks_value, props))
116 | for i, label in enumerate(labels):
117 | values = list(data[label].values())
118 | kwargs.update(legend_label=label, color=colors[i])
119 | figure.vbar(x=x + width * i, top=values, width=width - margin, **kwargs)
120 | figure.xgrid.grid_line_color = None
121 | figure.outline_line_color = None
122 | return figure
123 |
124 | elif type == "barh":
125 | y = np.arange(len(props))
126 | height = 1 / (len(labels) + 0.8)
127 | margin = height / 10
128 | ticks_value = y + height * len(labels) / 2
129 | figure.yaxis.ticker = ticks_value
130 | figure.yaxis.major_label_overrides = dict(zip(ticks_value, props))
131 | for i, label in enumerate(labels):
132 | values = list(data[label].values())
133 | kwargs.update(legend_label=label, color=colors[i])
134 | figure.hbar(y=y + height * i, right=values, height=height - margin, **kwargs)
135 | figure.ygrid.grid_line_color = None
136 | figure.outline_line_color = None
137 | return figure
138 |
139 | elif type == "stacked":
140 | for label in labels:
141 | data[label] = [data[label][p] for p in props]
142 | ticker_values = list(range(len(props)))
143 | data.update(props=ticker_values)
144 | kwargs.update(color=colors, legend_label=labels, width=0.9)
145 | figure.vbar_stack(labels, x="props", source=data, **kwargs)
146 | figure.xaxis.ticker = ticker_values
147 | figure.xaxis.major_label_overrides = {i: p for i, p in enumerate(props)}
148 | figure.xgrid.grid_line_color = None
149 | return figure
150 |
151 | elif type == "pie":
152 | if len(labels) != 1:
153 | raise ValueError("Pie chart can only be used with one property")
154 | total = sum([data[labels[0]][p] for p in props])
155 | kwargs.update(x=0, y=0, radius=1)
156 | start_angle = 0
157 | for i, p in enumerate(props):
158 | kwargs.update(color=colors[i], legend_label=p)
159 | end_angle = start_angle + data[labels[0]][p] / total * 2 * pi
160 | figure.wedge(start_angle=start_angle, end_angle=end_angle, **kwargs)
161 | start_angle = end_angle
162 | figure.axis.visible = False
163 | figure.x_range.start, figure.y_range.start = -1.5, -1.5
164 | figure.x_range.end, figure.y_range.end = 1.5, 1.5
165 | figure.grid.grid_line_color = None
166 | figure.outline_line_color = None
167 | return figure
168 |
169 | elif type == "donut":
170 | if len(labels) != 1:
171 | raise ValueError("Pie chart can only be used with one property")
172 | total = sum([data[labels[0]][p] for p in props])
173 | kwargs.update(x=0, y=0, inner_radius=0.5, outer_radius=1)
174 | start_angle = 0
175 | for i, p in enumerate(props):
176 | kwargs.update(color=colors[i], legend_label=p)
177 | end_angle = start_angle + data[labels[0]][p] / total * 2 * pi
178 | figure.annular_wedge(start_angle=start_angle, end_angle=end_angle, **kwargs)
179 | start_angle = end_angle
180 | figure.axis.visible = False
181 | figure.x_range.start, figure.y_range.start = -1.5, -1.5
182 | figure.x_range.end, figure.y_range.end = 1.5, 1.5
183 | figure.grid.grid_line_color = None
184 | figure.outline_line_color = None
185 | return figure
186 |
187 | elif type == "date":
188 | # get the original height and width
189 | height, width = figure.height, figure.width
190 |
191 | # create the 2 figures that will be displayed in the column
192 | main = plotting.figure(
193 | height=int(height * 0.8), width=width, x_axis_type="datetime", x_axis_location="above"
194 | )
195 | main.outline_line_color = None
196 |
197 | # create the select item
198 | select = plotting.figure(
199 | height=int(height * 0.3),
200 | width=width,
201 | y_range=main.y_range,
202 | x_axis_type="datetime",
203 | y_axis_type=None,
204 | tools="",
205 | )
206 | select.title.text = "Drag the middle and edges of the selection box to change the range above"
207 | select.ygrid.grid_line_color = None
208 | select.outline_line_color = None
209 |
210 | # draw the curves on both figures
211 | for i, label in enumerate(labels):
212 | kwargs.update(color=colors[i], legend_label=label)
213 | x, y = list(data[label].keys()), list(data[label].values())
214 | main.line(x, y, color=colors[i], legend_label=label)
215 | select.line(x, y, color=colors[i])
216 |
217 | # add the range tool to the select figure
218 | range_tool = RangeTool(x_range=main.x_range)
219 | select.add_tools(range_tool)
220 |
221 | return column(main, select)
222 |
223 | elif type == "doy":
224 | xmin, xmax = 366, 0 # inverted initialization to get the first iteration values
225 | for i, label in enumerate(labels):
226 | x, y = list(data[label].keys()), list(data[label].values())
227 | figure.line(x, y, color=colors[i], legend_label=label)
228 | xmin, xmax = min(xmin, min(x)), max(xmax, max(x))
229 | dates = [dt(2023, i + 1, 1) for i in range(12)]
230 | idates = [int(d.strftime("%j")) - 1 for d in dates]
231 | ndates = [d.strftime("%B")[:3] for d in dates]
232 | figure.xaxis.ticker = idates
233 | figure.xaxis.major_label_overrides = dict(zip(idates, ndates))
234 | figure.xaxis.axis_label = "Day of year"
235 | figure.x_range.start = xmin - 5
236 | figure.x_range.end = xmax + 5
237 | return figure
238 |
239 | else:
240 | raise ValueError(f"Type {type} is not (yet?) supported")
241 |
--------------------------------------------------------------------------------
/ipygee/ee_image.py:
--------------------------------------------------------------------------------
1 | """Toolbox for the :py:class:`ee.Image` class."""
2 |
3 | from __future__ import annotations
4 |
5 | import ee
6 | import geetools # noqa: F401
7 | from bokeh import plotting
8 | from geetools.accessors import register_class_accessor
9 | from matplotlib import pyplot as plt
10 | from matplotlib.colors import to_hex
11 |
12 | from .plotting import plot_data
13 |
14 |
15 | @register_class_accessor(ee.Image, "bokeh")
16 | class ImageAccessor:
17 | """Toolbox for the :py:class:`ee.Image` class."""
18 |
19 | def __init__(self, obj: ee.Image):
20 | """Initialize the Image class."""
21 | self._obj = obj
22 |
23 | def plot_by_regions(
24 | self,
25 | type: str,
26 | regions: ee.FeatureCollection,
27 | reducer: str | ee.Reducer = "mean",
28 | bands: list[str] | None = None,
29 | regionId: str = "system:index",
30 | labels: list[str] | None = None,
31 | colors: list[str] | None = None,
32 | figure: plotting.figure | None = None,
33 | scale: int = 10000,
34 | crs: str | None = None,
35 | crsTransform: list | None = None,
36 | tileScale: float = 1,
37 | ) -> plotting.figure:
38 | """Plot the reduced values for each region.
39 |
40 | Each region will be plotted using the ``regionId`` as x-axis label defaulting to "system:index" if not provided.
41 | If no ``bands`` are provided, all bands will be plotted.
42 | If no ``labels`` are provided, the band names will be used.
43 |
44 | Warning:
45 | This method is client-side.
46 |
47 | Parameters:
48 | type: The type of plot to use. Defaults to ``"bar"``. can be any type of plot from the python lib ``matplotlib.pyplot``. If the one you need is missing open an issue!
49 | regions: The regions to compute the reducer in.
50 | reducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
51 | bands: The bands to compute the reducer on. Default to all bands.
52 | regionId: The property used to label region. Defaults to ``"system:index"``.
53 | labels: The labels to use for the output dictionary. Default to the band names.
54 | colors: The colors to use for the plot. Default to the default matplotlib colors.
55 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
56 | scale: The scale to use for the computation. Default is 10000m.
57 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
58 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
59 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
60 |
61 | Returns:
62 | The bokeh figure with the plot.
63 |
64 | Examples:
65 | .. code-block:: python
66 |
67 | import ee, ipygee
68 |
69 | ee.Initialize()
70 |
71 | ecoregions = ee.FeatureCollection("projects/google/charts_feature_example").select(["label", "value","warm"])
72 | normClim = ee.ImageCollection('OREGONSTATE/PRISM/Norm91m').toBands()
73 |
74 | normClim.bokeh.plot_by_regions(ecoregions, ee.Reducer.mean(), scale=10000)
75 | """
76 | # get the data from the server
77 | data = self._obj.geetools.byBands(
78 | regions=regions,
79 | reducer=reducer,
80 | bands=bands,
81 | regionId=regionId,
82 | labels=labels,
83 | scale=scale,
84 | crs=crs,
85 | crsTransform=crsTransform,
86 | tileScale=tileScale,
87 | ).getInfo()
88 |
89 | # get all the id values, they must be string so we are forced to cast them manually
90 | # the default casting is broken from Python side: https://issuetracker.google.com/issues/329106322
91 | features = regions.aggregate_array(regionId)
92 | isString = lambda i: ee.Algorithms.ObjectType(i).compareTo("String").eq(0) # noqa: E731
93 | features = features.map(lambda i: ee.Algorithms.If(isString(i), i, ee.Number(i).format()))
94 | features = features.getInfo()
95 |
96 | # extract the labels from the parameters
97 | eeBands = ee.List(bands) if bands is not None else self._obj.bandNames()
98 | labels = labels if labels is not None else eeBands.getInfo()
99 |
100 | # reorder the data according to the labels id set by the user
101 | data = {b: {f: data[b][f] for f in features} for b in labels}
102 |
103 | ax = plot_data(type=type, data=data, label_name=regionId, colors=colors, figure=figure)
104 |
105 | return ax
106 |
107 | def plot_by_bands(
108 | self,
109 | type: str,
110 | regions: ee.FeatureCollection,
111 | reducer: str | ee.Reducer = "mean",
112 | bands: list[str] | None = None,
113 | regionId: str = "system:index",
114 | labels: list[str] | None = None,
115 | colors: list[str] | None = None,
116 | figure: plotting.figure | None = None,
117 | scale: int = 10000,
118 | crs: str | None = None,
119 | crsTransform: list | None = None,
120 | tileScale: float = 1,
121 | ) -> plotting.figure:
122 | """Plot the reduced values for each band.
123 |
124 | Each band will be plotted using the ``labels`` as x-axis label defaulting to band names if not provided.
125 | If no ``bands`` are provided, all bands will be plotted.
126 | If no ``regionId`` are provided, the ``"system:index"`` property will be used.
127 |
128 | Warning:
129 | This method is client-side.
130 |
131 | Parameters:
132 | type: The type of plot to use. Defaults to ``"bar"``. can be any type of plot from the python lib ``matplotlib.pyplot``. If the one you need is missing open an issue!
133 | regions: The regions to compute the reducer in.
134 | reducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
135 | bands: The bands to compute the reducer on. Default to all bands.
136 | regionId: The property used to label region. Defaults to ``"system:index"``.
137 | labels: The labels to use for the output dictionary. Default to the band names.
138 | colors: The colors to use for the plot. Default to the default matplotlib colors.
139 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
140 | scale: The scale to use for the computation. Default is 10000m.
141 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
142 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
143 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
144 |
145 | Returns:
146 | The bokeh figure with the plot
147 |
148 | Examples:
149 | .. code-block:: python
150 |
151 | import ee, ipygee
152 |
153 | ee.Initialize()
154 |
155 | ecoregions = ee.FeatureCollection("projects/google/charts_feature_example").select(["label", "value","warm"])
156 | normClim = ee.ImageCollection('OREGONSTATE/PRISM/Norm91m').toBands()
157 |
158 | normClim.bokeh.plot_by_bands(ecoregions, ee.Reducer.mean(), scale=10000)
159 | """
160 | # get the data from the server
161 | data = self._obj.geetools.byRegions(
162 | regions=regions,
163 | reducer=reducer,
164 | bands=bands,
165 | regionId=regionId,
166 | labels=labels,
167 | scale=scale,
168 | crs=crs,
169 | crsTransform=crsTransform,
170 | tileScale=tileScale,
171 | ).getInfo()
172 |
173 | # get all the id values, they must be string so we are forced to cast them manually
174 | # the default casting is broken from Python side: https://issuetracker.google.com/issues/329106322
175 | features = regions.aggregate_array(regionId)
176 | isString = lambda i: ee.Algorithms.ObjectType(i).compareTo("String").eq(0) # noqa: E731
177 | features = features.map(lambda i: ee.Algorithms.If(isString(i), i, ee.Number(i).format()))
178 | features = features.getInfo()
179 |
180 | # extract the labels from the parameters
181 | eeBands = ee.List(bands) if bands is not None else self._obj.bandNames()
182 | labels = labels if labels is not None else eeBands.getInfo()
183 |
184 | # reorder the data according to the labels id set by the user
185 | data = {f: {b: data[f][b] for b in labels} for f in features}
186 |
187 | ax = plot_data(type=type, data=data, label_name=regionId, colors=colors, figure=figure)
188 |
189 | return ax
190 |
191 | def plot_hist(
192 | self,
193 | bins: int = 30,
194 | region: ee.Geometry | None = None,
195 | bands: list[str] | None = None,
196 | labels: list[str] | None = None,
197 | colors: list[str] | None = None,
198 | precision: int = 2,
199 | figure: plotting.figure | None = None,
200 | scale: int = 10000,
201 | crs: str | None = None,
202 | crsTransform: list | None = None,
203 | bestEffort: bool = False,
204 | maxPixels: int = 10**7,
205 | tileScale: float = 1,
206 | **kwargs,
207 | ) -> plotting.figure:
208 | """Plot the histogram of the image bands.
209 |
210 | Parameters:
211 | bins: The number of bins to use for the histogram. Default is 30.
212 | region: The region to compute the histogram in. Default is the image geometry.
213 | bands: The bands to plot the histogram for. Default to all bands.
214 | labels: The labels to use for the output dictionary. Default to the band names.
215 | colors: The colors to use for the plot. Default to the default matplotlib colors.
216 | precision: The number of decimal to keep for the histogram bins values. Default is 2.
217 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
218 | scale: The scale to use for the computation. Default is 10,000m.
219 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
220 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
221 | bestEffort: If the polygon would contain too many pixels at the given scale, compute and use a larger scale which would allow the operation to succeed.
222 | maxPixels: The maximum number of pixels to reduce. default to 10**7.
223 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
224 | **kwargs: Keyword arguments passed to the `matplotlib.fill_between() `_ function.
225 |
226 | Returns:
227 | The bokeh figure with the plot.
228 |
229 | Examples:
230 | .. code-block:: python
231 |
232 | import ee, ipygee
233 |
234 | ee.Initialize()
235 |
236 | normClim = ee.ImageCollection('OREGONSTATE/PRISM/Norm91m').toBands()
237 | normClim.bokeh.plot_hist()
238 | """
239 | # extract the bands from the image
240 | eeBands = ee.List(bands) if bands is not None else self._obj.bandNames()
241 | eeLabels = ee.List(labels).flatten() if labels is not None else eeBands
242 | new_labels: list[str] = eeLabels.getInfo()
243 | new_colors: list[str] = colors if colors is not None else plt.get_cmap("tab10").colors
244 |
245 | # retrieve the region from the parameters
246 | region = region if region is not None else self._obj.geometry()
247 |
248 | # extract the data from the server
249 | image = self._obj.select(eeBands).rename(eeLabels).clip(region)
250 |
251 | # set the common parameters of the 3 reducers
252 | params = {
253 | "geometry": region,
254 | "scale": scale,
255 | "crs": crs,
256 | "crsTransform": crsTransform,
257 | "bestEffort": bestEffort,
258 | "maxPixels": maxPixels,
259 | "tileScale": tileScale,
260 | }
261 |
262 | # compute the min and max values of the bands so w can scale the bins of the histogram
263 | min = image.reduceRegion(**{"reducer": ee.Reducer.min(), **params})
264 | min = min.values().reduce(ee.Reducer.min())
265 |
266 | max = image.reduceRegion(**{"reducer": ee.Reducer.max(), **params})
267 | max = max.values().reduce(ee.Reducer.max())
268 |
269 | # compute the histogram. The result is a dictionary with each band as key and the histogram
270 | # as values. The histograp is a list of [start of bin, value] pairs
271 | reducer = ee.Reducer.fixedHistogram(min, max, bins)
272 | raw_data = image.reduceRegion(**{"reducer": reducer, **params}).getInfo()
273 |
274 | # massage raw data to reshape them as usable source for an Axes plot
275 | # first extract the x coordinates of the plot as a list of bins borders
276 | # every value is duplicated but the first one to create a scale like display.
277 | # the values are treated the same way we simply drop the last duplication to get the same size.
278 | p = 10**precision # multiplier use to truncate the float values
279 | x = [int(d[0] * p) / p for d in raw_data[new_labels[0]] for _ in range(2)][1:]
280 | data = {lbl: [int(d[1]) for d in raw_data[lbl] for _ in range(2)][:-1] for lbl in new_labels}
281 |
282 | # create the graph objcet if not provided
283 | figure = plotting.figure() if figure is None else figure
284 |
285 | # display the histogram as a fill_between plot to respect GEE lib design
286 | for i, label in enumerate(new_labels):
287 | y = data[label]
288 | bottom = [0] * len(data[label])
289 | color = to_hex(new_colors[i])
290 | figure.varea(x=x, y1=bottom, y2=y, legend_label=label, color=color, alpha=0.2, **kwargs)
291 | figure.line(x=x, y=y, legend_label=label, color=color)
292 |
293 | # customize the layout of the axis
294 | figure.yaxis.axis_label = "Count"
295 | figure.xgrid.grid_line_color = None
296 | figure.outline_line_color = None
297 |
298 | return figure
299 |
--------------------------------------------------------------------------------
/docs/usage/plot/plot-imagecollection.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Plot ImageCollection\n",
8 | "\n",
9 | "The `geetools` extention contains a set of functions for rendering charts from the results of spatiotemporal reduction of images within an `ee.ImageCollection`. The choice of function dictates the arrangement of data in the chart, i.e., what defines x- and y-axis values and what defines the series. Use the following function descriptions and examples to determine the best function for your purpose."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {
16 | "tags": [
17 | "remove-cell"
18 | ]
19 | },
20 | "outputs": [],
21 | "source": [
22 | "import ee\n",
23 | "from geetools.utils import initialize_documentation\n",
24 | "\n",
25 | "initialize_documentation()"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "[](https://github.com/gee-community/ipygee/blob/main/docs/usage/plot/plot-imagecollection.ipynb)\n",
33 | "[](https://colab.research.google.com/github/gee-community/ipygee/blob/main/docs/usage/plot/plot-imagecollection.ipynb)"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "## Set up environment\n",
41 | "\n",
42 | "Install all the required libs if necessary and perform the import satements upstream."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "# uncomment if installation of libs is necessary\n",
52 | "# !pip install earthengine-api geetools"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "from bokeh.io import output_notebook\n",
62 | "\n",
63 | "import ipygee # noqa: F401\n",
64 | "\n",
65 | "output_notebook()"
66 | ]
67 | },
68 | {
69 | "cell_type": "code",
70 | "execution_count": null,
71 | "metadata": {},
72 | "outputs": [],
73 | "source": [
74 | "# uncomment if authetication to GEE is needed\n",
75 | "# ee.Authenticate()\n",
76 | "# ee.Initialize(project=\"\")"
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "metadata": {},
82 | "source": [
83 | "## Example data \n",
84 | "\n",
85 | "The following examples rely on a `ee.FeatureCollection` composed of three ecoregion features that define regions by which to reduce image data. The ImageCollection data loads the modis vegetation indicies and subset the 2010 2020 decade of images."
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "## Import the example feature collection and drop the data property.\n",
95 | "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\").select(\n",
96 | " [\"label\", \"value\", \"warm\"]\n",
97 | ")\n",
98 | "\n",
99 | "\n",
100 | "## Load MODIS vegetation indices data and subset a decade of images.\n",
101 | "vegIndices = (\n",
102 | " ee.ImageCollection(\"MODIS/061/MOD13A1\")\n",
103 | " .filter(ee.Filter.date(\"2010-01-01\", \"2020-01-01\"))\n",
104 | " .select([\"NDVI\", \"EVI\"])\n",
105 | ")"
106 | ]
107 | },
108 | {
109 | "cell_type": "markdown",
110 | "metadata": {},
111 | "source": [
112 | "## Plot dates\n",
113 | "\n",
114 | "The `plot_dates*` methods will plot the values of the image collection using their dates as x-axis values."
115 | ]
116 | },
117 | {
118 | "cell_type": "markdown",
119 | "metadata": {},
120 | "source": [
121 | "### series by bands \n",
122 | "\n",
123 | "Image date is plotted along the x-axis according to the `dateProperty` property. Series are defined by image bands. Y-axis values are the reduction of images, by date, for a single region."
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {
130 | "tags": [
131 | "remove-input"
132 | ]
133 | },
134 | "outputs": [],
135 | "source": [
136 | "from bokeh.plotting import figure, show\n",
137 | "\n",
138 | "fig = figure(width=800, height=400)\n",
139 | "\n",
140 | "# Sample data (replace these with your actual data)\n",
141 | "dates = [\"date1\", \"date2\", \"date3\"]\n",
142 | "ticker_values = list(range(len(dates)))\n",
143 | "b1 = [1, 2, 1]\n",
144 | "b2 = [2, 3, 2]\n",
145 | "b3 = [3, 4, 3]\n",
146 | "\n",
147 | "# Create the plot\n",
148 | "fig.line(x=ticker_values, y=b1, legend_label=\"b1\", color=\"#1d6b99\")\n",
149 | "fig.line(x=ticker_values, y=b2, legend_label=\"b2\", color=\"#cf513e\")\n",
150 | "fig.line(x=ticker_values, y=b3, legend_label=\"b3\", color=\"#f0af07\")\n",
151 | "\n",
152 | "# Add titles and labels\n",
153 | "fig.title.text = \"Single-region spatial reduction\"\n",
154 | "fig.xaxis.axis_label = \"Image date\"\n",
155 | "fig.yaxis.axis_label = \"Spatial reduction\"\n",
156 | "fig.y_range.start = 0\n",
157 | "fig.y_range.end = 5\n",
158 | "fig.legend.title = \"Band names\"\n",
159 | "fig.legend.location = \"top_right\"\n",
160 | "fig.xaxis.ticker = ticker_values\n",
161 | "fig.xaxis.major_label_overrides = {i: date for i, date in enumerate(dates)}\n",
162 | "fig.xgrid.grid_line_color = None\n",
163 | "fig.legend.orientation = \"horizontal\"\n",
164 | "fig.outline_line_color = None\n",
165 | "\n",
166 | "show(fig)"
167 | ]
168 | },
169 | {
170 | "cell_type": "markdown",
171 | "metadata": {},
172 | "source": [
173 | "Use `plot_series_by_bands` to display an image time series for a given region; each image band is presented as a unique series. It is useful for comparing the time series of individual image bands. Here, a MODIS image collection with bands representing NDVI and EVI vegetation indices are plotted. The date of every image observation is included along the x-axis, while the mean reduction of pixels intersecting a forest ecoregion defines the y-axis."
174 | ]
175 | },
176 | {
177 | "cell_type": "code",
178 | "execution_count": null,
179 | "metadata": {},
180 | "outputs": [],
181 | "source": [
182 | "fig = figure(width=800, height=400)\n",
183 | "\n",
184 | "region = ecoregions.filter(ee.Filter.eq(\"label\", \"Forest\"))\n",
185 | "col = vegIndices.bokeh.plot_dates_by_bands(\n",
186 | " region=region.geometry(),\n",
187 | " reducer=\"mean\",\n",
188 | " scale=500,\n",
189 | " bands=[\"NDVI\", \"EVI\"],\n",
190 | " figure=fig,\n",
191 | " dateProperty=\"system:time_start\",\n",
192 | ")\n",
193 | "\n",
194 | "# once created the figure can be modified as needed using pure bokeh members\n",
195 | "col.children[0].yaxis.axis_label = \"Vegetation indices (x1e4)\"\n",
196 | "col.children[0].title.text = \"Average Vegetation index Values by date in the Forest ecoregion\"\n",
197 | "\n",
198 | "show(col)"
199 | ]
200 | },
201 | {
202 | "cell_type": "markdown",
203 | "metadata": {},
204 | "source": [
205 | "### Plot series by region\n",
206 | "\n",
207 | "Image date is plotted along the x-axis according to the `dateProperty` property. Series are defined by regions. Y-axis values are the reduction of images, by date, for a single image band."
208 | ]
209 | },
210 | {
211 | "cell_type": "code",
212 | "execution_count": null,
213 | "metadata": {
214 | "tags": [
215 | "remove-input"
216 | ]
217 | },
218 | "outputs": [],
219 | "source": [
220 | "from bokeh.plotting import figure, show\n",
221 | "\n",
222 | "fig = figure(width=800, height=400)\n",
223 | "\n",
224 | "# Sample data (replace these with your actual data)\n",
225 | "dates = [\"date1\", \"date2\", \"date3\"]\n",
226 | "ticker_values = list(range(len(dates)))\n",
227 | "r1 = [1, 2, 1]\n",
228 | "r2 = [2, 3, 2]\n",
229 | "r3 = [3, 4, 3]\n",
230 | "\n",
231 | "# Create the plot\n",
232 | "fig.line(x=ticker_values, y=r1, legend_label=\"r1\", color=\"#1d6b99\")\n",
233 | "fig.line(x=ticker_values, y=r2, legend_label=\"r2\", color=\"#cf513e\")\n",
234 | "fig.line(x=ticker_values, y=r3, legend_label=\"r3\", color=\"#f0af07\")\n",
235 | "\n",
236 | "# Add titles and labels\n",
237 | "fig.title.text = \"Single-band spatial reduction\"\n",
238 | "fig.xaxis.axis_label = \"Image date\"\n",
239 | "fig.yaxis.axis_label = \"Spatial reduction\"\n",
240 | "fig.y_range.start = 0\n",
241 | "fig.y_range.end = 5\n",
242 | "fig.legend.title = \"Regions\"\n",
243 | "fig.legend.location = \"top_right\"\n",
244 | "fig.xaxis.ticker = ticker_values\n",
245 | "fig.xaxis.major_label_overrides = {i: date for i, date in enumerate(dates)}\n",
246 | "fig.xgrid.grid_line_color = None\n",
247 | "fig.legend.orientation = \"horizontal\"\n",
248 | "fig.outline_line_color = None\n",
249 | "\n",
250 | "show(fig)"
251 | ]
252 | },
253 | {
254 | "cell_type": "markdown",
255 | "metadata": {},
256 | "source": [
257 | "Use `plot_dates_by_regions` to display a single image band time series for multiple regions; each region is presented as a unique series. It is useful for comparing the time series of a single band among several regions. Here, a MODIS image collection representing an NDVI time series is plotted for three ecoregions. The date of every image observation is included along the x-axis, while mean reduction of pixels intersecting forest, desert, and grasslands ecoregions define y-axis series."
258 | ]
259 | },
260 | {
261 | "cell_type": "code",
262 | "execution_count": null,
263 | "metadata": {},
264 | "outputs": [],
265 | "source": [
266 | "fig = figure(width=800, height=400)\n",
267 | "\n",
268 | "region = ecoregions.filter(ee.Filter.eq(\"label\", \"Forest\"))\n",
269 | "col = vegIndices.bokeh.plot_dates_by_regions(\n",
270 | " band=\"NDVI\",\n",
271 | " regions=ecoregions,\n",
272 | " label=\"label\",\n",
273 | " reducer=\"mean\",\n",
274 | " scale=500,\n",
275 | " figure=fig,\n",
276 | " dateProperty=\"system:time_start\",\n",
277 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
278 | ")\n",
279 | "\n",
280 | "# once created the axes can be modified as needed using pure matplotlib functions\n",
281 | "col.children[0].yaxis.axis_label = \"Vegetation indices (x1e4)\"\n",
282 | "col.children[0].title.text = \"Average Vegetation index Values by date in the Forest ecoregion\"\n",
283 | "\n",
284 | "show(col)"
285 | ]
286 | },
287 | {
288 | "cell_type": "markdown",
289 | "metadata": {},
290 | "source": [
291 | "## PLot DOY\n",
292 | "\n",
293 | "DOY stands for day of year. The `plot_doyseries*` methods will plot the values of the image collection using the day of year as x-axis values.\n",
294 | "\n",
295 | "Note that `.plot_doyseries*` functions take two reducers: one for region reduction (`regionReducer`) and another for intra-annual coincident day-of-year reduction (`yearReducer`). Examples in the following sections use `ee.Reducer.mean()` as the argument for both of these parameters."
296 | ]
297 | },
298 | {
299 | "cell_type": "markdown",
300 | "metadata": {},
301 | "source": [
302 | "### Plot DOY by bands \n",
303 | "\n",
304 | "Image day-of-year is plotted along the x-axis according to the `dateProperty` property. Series are defined by image bands. Y-axis values are the reduction of image pixels in a given region, grouped by day-of-year."
305 | ]
306 | },
307 | {
308 | "cell_type": "code",
309 | "execution_count": null,
310 | "metadata": {
311 | "tags": [
312 | "remove-input"
313 | ]
314 | },
315 | "outputs": [],
316 | "source": [
317 | "from bokeh.plotting import figure, show\n",
318 | "\n",
319 | "fig = figure(width=800, height=400)\n",
320 | "\n",
321 | "# Sample data (replace these with your actual data)\n",
322 | "dates = [\"doy1\", \"doy2\", \"doy3\"]\n",
323 | "ticker_values = list(range(len(dates)))\n",
324 | "b1 = [1, 2, 1]\n",
325 | "b2 = [2, 3, 2]\n",
326 | "b3 = [3, 4, 3]\n",
327 | "\n",
328 | "# Create the plot\n",
329 | "fig.line(x=ticker_values, y=b1, legend_label=\"b1\", color=\"#1d6b99\")\n",
330 | "fig.line(x=ticker_values, y=b2, legend_label=\"b2\", color=\"#cf513e\")\n",
331 | "fig.line(x=ticker_values, y=b3, legend_label=\"b3\", color=\"#f0af07\")\n",
332 | "\n",
333 | "# Add titles and labels\n",
334 | "fig.title.text = \"Single-band spatiotemporal reduction\"\n",
335 | "fig.xaxis.axis_label = \"Image date\"\n",
336 | "fig.yaxis.axis_label = \"Reduced values\"\n",
337 | "fig.y_range.start = 0\n",
338 | "fig.y_range.end = 5\n",
339 | "fig.legend.title = \"Band names\"\n",
340 | "fig.legend.location = \"top_right\"\n",
341 | "fig.xaxis.ticker = ticker_values\n",
342 | "fig.xaxis.major_label_overrides = {i: date for i, date in enumerate(dates)}\n",
343 | "fig.xgrid.grid_line_color = None\n",
344 | "fig.legend.orientation = \"horizontal\"\n",
345 | "fig.outline_line_color = None\n",
346 | "\n",
347 | "show(fig)"
348 | ]
349 | },
350 | {
351 | "cell_type": "markdown",
352 | "metadata": {},
353 | "source": [
354 | "Use `plot_doy_by_bands` to display a day-of-year time series for a given region; each image band is presented as a unique series. It is useful for reducing observations occurring on the same day-of-year, across multiple years, to compare e.g. average annual NDVI and EVI profiles from MODIS, as in this example."
355 | ]
356 | },
357 | {
358 | "cell_type": "code",
359 | "execution_count": null,
360 | "metadata": {},
361 | "outputs": [],
362 | "source": [
363 | "fig = figure(width=800, height=400)\n",
364 | "\n",
365 | "vegIndices.bokeh.plot_doy_by_bands(\n",
366 | " region=ecoregions.filter(ee.Filter.eq(\"label\", \"Grassland\")).geometry(),\n",
367 | " spatialReducer=\"mean\",\n",
368 | " timeReducer=\"mean\",\n",
369 | " scale=500,\n",
370 | " bands=[\"NDVI\", \"EVI\"],\n",
371 | " figure=fig,\n",
372 | " dateProperty=\"system:time_start\",\n",
373 | " colors=[\"#e37d05\", \"#1d6b99\"],\n",
374 | ")\n",
375 | "\n",
376 | "# once created the axes can be modified as needed using pure matplotlib functions\n",
377 | "fig.yaxis.axis_label = \"Vegetation indices (x1e4)\"\n",
378 | "fig.title.text = \"Average Vegetation index Values by doy in the Grassland ecoregion\"\n",
379 | "\n",
380 | "show(fig)"
381 | ]
382 | },
383 | {
384 | "cell_type": "markdown",
385 | "metadata": {},
386 | "source": [
387 | "### Plot doy by regions \n",
388 | "\n",
389 | "Image day-of-year is plotted along the x-axis according to the `dateProperty` property. Series are defined by regions. Y-axis values are the reduction of image pixels in a given region, grouped by day-of-year, for a selected image band.\n"
390 | ]
391 | },
392 | {
393 | "cell_type": "code",
394 | "execution_count": null,
395 | "metadata": {
396 | "tags": [
397 | "remove-input"
398 | ]
399 | },
400 | "outputs": [],
401 | "source": [
402 | "from bokeh.plotting import figure, show\n",
403 | "\n",
404 | "fig = figure(width=800, height=400)\n",
405 | "\n",
406 | "# Sample data (replace these with your actual data)\n",
407 | "dates = [\"doy1\", \"doy2\", \"doy3\"]\n",
408 | "ticker_values = list(range(len(dates)))\n",
409 | "r1 = [1, 2, 1]\n",
410 | "r2 = [2, 3, 2]\n",
411 | "r3 = [3, 4, 3]\n",
412 | "\n",
413 | "# Create the plot\n",
414 | "fig.line(x=ticker_values, y=r1, legend_label=\"r1\", color=\"#1d6b99\")\n",
415 | "fig.line(x=ticker_values, y=r2, legend_label=\"r2\", color=\"#cf513e\")\n",
416 | "fig.line(x=ticker_values, y=r3, legend_label=\"r3\", color=\"#f0af07\")\n",
417 | "\n",
418 | "# Add titles and labels\n",
419 | "fig.title.text = \"Single-region spatiotemporal reduction\"\n",
420 | "fig.xaxis.axis_label = \"Image date\"\n",
421 | "fig.yaxis.axis_label = \"Reduced values\"\n",
422 | "fig.y_range.start = 0\n",
423 | "fig.y_range.end = 5\n",
424 | "fig.legend.title = \"Region names\"\n",
425 | "fig.legend.location = \"top_right\"\n",
426 | "fig.xaxis.ticker = ticker_values\n",
427 | "fig.xaxis.major_label_overrides = {i: date for i, date in enumerate(dates)}\n",
428 | "fig.xgrid.grid_line_color = None\n",
429 | "fig.legend.orientation = \"horizontal\"\n",
430 | "fig.outline_line_color = None\n",
431 | "\n",
432 | "show(fig)"
433 | ]
434 | },
435 | {
436 | "cell_type": "markdown",
437 | "metadata": {},
438 | "source": [
439 | "Use `plot_doy_by_regions` to display a single image band day-of-year time series for multiple regions, where each distinct region is presented as a unique series. It is useful for comparing annual single-band time series among regions. For instance, in this example, annual MODIS-derived NDVI profiles for forest, desert, and grassland ecoregions are plotted, providing a convenient comparison of NDVI response by region. Note that intra-annual observations occurring on the same day-of-year are reduced by their mean."
440 | ]
441 | },
442 | {
443 | "cell_type": "code",
444 | "execution_count": null,
445 | "metadata": {},
446 | "outputs": [],
447 | "source": [
448 | "fig = figure(width=800, height=400)\n",
449 | "\n",
450 | "vegIndices.bokeh.plot_doy_by_regions(\n",
451 | " regions=ecoregions,\n",
452 | " label=\"label\",\n",
453 | " spatialReducer=\"mean\",\n",
454 | " timeReducer=\"mean\",\n",
455 | " scale=500,\n",
456 | " band=\"NDVI\",\n",
457 | " figure=fig,\n",
458 | " dateProperty=\"system:time_start\",\n",
459 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
460 | ")\n",
461 | "\n",
462 | "# once created the axes can be modified as needed using pure matplotlib functions\n",
463 | "fig.yaxis.axis_label = \"NDVI (x1e4)\"\n",
464 | "fig.title.text = \"Average NDVI Values by doy in each ecoregion\"\n",
465 | "\n",
466 | "show(fig)"
467 | ]
468 | },
469 | {
470 | "cell_type": "markdown",
471 | "metadata": {},
472 | "source": [
473 | "### plot doy by seasons \n",
474 | "\n",
475 | "In case the observation you want to analyse are only meaningful on a subset of the year a variant of the previous method allows you to plot the data by season. The season is defined by the `seasonStart` and `seasonEnd` parameters, which are 2 numbers between 1 and 366 representing the start and end of the season. To set them, the user can use the {py:method}`ee.Date.getRelative` or {py:class}`time.struct_time` method to get the day of the year. \n",
476 | "\n",
477 | "```{note} \n",
478 | "The default season is a year (1, 366).\n",
479 | "```"
480 | ]
481 | },
482 | {
483 | "cell_type": "code",
484 | "execution_count": null,
485 | "metadata": {},
486 | "outputs": [],
487 | "source": [
488 | "# reduce the regions to grassland\n",
489 | "grassland = ecoregions.filter(ee.Filter.eq(\"label\", \"Grassland\"))\n",
490 | "\n",
491 | "# for plot speed and lisibility only keep 2 years (2010 and 2020) for the example\n",
492 | "indices = vegIndices.filter(\n",
493 | " ee.Filter.Or(\n",
494 | " ee.Filter.date(\"2012-01-01\", \"2012-12-31\"),\n",
495 | " ee.Filter.date(\"2019-01-01\", \"2019-12-31\"),\n",
496 | " )\n",
497 | ")"
498 | ]
499 | },
500 | {
501 | "cell_type": "code",
502 | "execution_count": null,
503 | "metadata": {},
504 | "outputs": [],
505 | "source": [
506 | "fig = figure(width=800, height=400)\n",
507 | "\n",
508 | "indices.bokeh.plot_doy_by_seasons(\n",
509 | " band=\"NDVI\",\n",
510 | " region=grassland.geometry(),\n",
511 | " seasonStart=ee.Date(\"2022-04-15\").getRelative(\"day\", \"year\"),\n",
512 | " seasonEnd=ee.Date(\"2022-09-15\").getRelative(\"day\", \"year\"),\n",
513 | " reducer=\"mean\",\n",
514 | " scale=500,\n",
515 | " figure=fig,\n",
516 | " colors=[\"#39a8a7\", \"#9c4f97\"],\n",
517 | ")\n",
518 | "\n",
519 | "# once created the axes can be modified as needed using pure matplotlib functions\n",
520 | "fig.yaxis.axis_label = \"NDVI (x1e4)\"\n",
521 | "fig.title.text = \"Average NDVI Values during growing season in Grassland\"\n",
522 | "\n",
523 | "show(fig)"
524 | ]
525 | },
526 | {
527 | "cell_type": "code",
528 | "execution_count": null,
529 | "metadata": {},
530 | "outputs": [],
531 | "source": [
532 | "fig = figure(width=800, height=400)\n",
533 | "\n",
534 | "indices.bokeh.plot_doy_by_seasons(\n",
535 | " band=\"NDVI\",\n",
536 | " region=grassland.geometry(),\n",
537 | " reducer=\"mean\",\n",
538 | " scale=500,\n",
539 | " figure=fig,\n",
540 | " colors=[\"#39a8a7\", \"#9c4f97\"],\n",
541 | ")\n",
542 | "\n",
543 | "# once created the axes can be modified as needed using pure matplotlib functions\n",
544 | "fig.yaxis.axis_label = \"NDVI (x1e4)\"\n",
545 | "fig.title.text = \"Average NDVI Values by years\"\n",
546 | "\n",
547 | "show(fig)"
548 | ]
549 | },
550 | {
551 | "cell_type": "code",
552 | "execution_count": null,
553 | "metadata": {},
554 | "outputs": [],
555 | "source": []
556 | }
557 | ],
558 | "metadata": {
559 | "kernelspec": {
560 | "display_name": "ipygee",
561 | "language": "python",
562 | "name": "python3"
563 | },
564 | "language_info": {
565 | "codemirror_mode": {
566 | "name": "ipython",
567 | "version": 3
568 | },
569 | "file_extension": ".py",
570 | "mimetype": "text/x-python",
571 | "name": "python",
572 | "nbconvert_exporter": "python",
573 | "pygments_lexer": "ipython3",
574 | "version": "3.10.16"
575 | }
576 | },
577 | "nbformat": 4,
578 | "nbformat_minor": 2
579 | }
580 |
--------------------------------------------------------------------------------
/ipygee/ee_image_collection.py:
--------------------------------------------------------------------------------
1 | """Toolbox for the :py:class:`ee.ImageCollection` class."""
2 |
3 | from __future__ import annotations
4 |
5 | from datetime import datetime as dt
6 |
7 | import ee
8 | import geetools # noqa: F401
9 | from bokeh import plotting
10 | from geetools.accessors import register_class_accessor
11 |
12 | from .plotting import plot_data
13 |
14 | PY_DATE_FORMAT = "%Y-%m-%dT%H-%M-%S"
15 | "The python format to use to parse dates coming from GEE."
16 |
17 | EE_DATE_FORMAT = "YYYY-MM-dd'T'HH-mm-ss"
18 | "The javascript format to use to burn date object in GEE."
19 |
20 |
21 | @register_class_accessor(ee.ImageCollection, "bokeh")
22 | class ImageCollectionAccessor:
23 | """Toolbox for the :py:class:`ee.ImageCollection` class."""
24 |
25 | def __init__(self, obj: ee.ImageCollection):
26 | """Initialize the ImageCollectionAccessor class."""
27 | self._obj = obj
28 |
29 | def plot_dates_by_bands(
30 | self,
31 | region: ee.Geometry,
32 | reducer: str | ee.Reducer = "mean",
33 | dateProperty: str = "system:time_start",
34 | bands: list[str] | None = None,
35 | labels: list[str] | None = None,
36 | colors: list[str] | None = None,
37 | figure: plotting.figure | None = None,
38 | scale: int = 10000,
39 | crs: str | None = None,
40 | crsTransform: list | None = None,
41 | bestEffort: bool = False,
42 | maxPixels: int | None = 10**7,
43 | tileScale: float = 1,
44 | ) -> plotting.figure:
45 | """Plot the reduced data for each image in the collection by bands on a specific region.
46 |
47 | This method is plotting the reduced data for each image in the collection by bands on a specific region.
48 |
49 | Parameters:
50 | region: The region to reduce the data on.
51 | reducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
52 | dateProperty: The property to use as date for each image. Default is ``"system:time_start"``.
53 | bands: The bands to reduce. If empty, all bands are reduced.
54 | labels: The labels to use for the bands. If empty, the bands names are used.
55 | colors: The colors to use for the bands. If empty, the default colors are used.
56 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
57 | scale: The scale in meters to use for the reduction. default is 10000m
58 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
59 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
60 | bestEffort: If the polygon would contain too many pixels at the given scale, compute and use a larger scale which would allow the operation to succeed.
61 | maxPixels: The maximum number of pixels to reduce. Defaults to 1e7.
62 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
63 |
64 | Returns:
65 | A bokeh figure with the reduced values for each band and each date.
66 |
67 | Examples:
68 | .. code-block:: python
69 |
70 | import ee, geetools
71 |
72 | ee.Initialize()
73 |
74 | collection = (
75 | ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
76 | .filterBounds(ee.Geometry.Point(-122.262, 37.8719))
77 | .filterDate("2014-01-01", "2014-12-31")
78 | )
79 |
80 | region = ee.Geometry.Point(-122.262, 37.8719).buffer(10000)
81 | collection.geetools.plot_dates_by_bands(region, "mean", 10000, "system:time_start")
82 | """
83 | # get the reduced data
84 | raw_data = self._obj.geetools.datesByBands(
85 | region=region,
86 | reducer=reducer,
87 | dateProperty=dateProperty,
88 | bands=bands,
89 | labels=labels,
90 | scale=scale,
91 | crs=crs,
92 | crsTransform=crsTransform,
93 | bestEffort=bestEffort,
94 | maxPixels=maxPixels,
95 | tileScale=tileScale,
96 | ).getInfo()
97 |
98 | # transform all the dates int datetime objects
99 | def to_date(dict):
100 | return {dt.strptime(d, PY_DATE_FORMAT): v for d, v in dict.items()}
101 |
102 | data = {lbl: to_date(dict) for lbl, dict in raw_data.items()}
103 |
104 | # create the plot
105 | figure = plot_data(type="date", data=data, label_name="Date", colors=colors, figure=figure)
106 |
107 | return figure
108 |
109 | def plot_dates_by_regions(
110 | self,
111 | band: str,
112 | regions: ee.FeatureCollection,
113 | label: str = "system:index",
114 | reducer: str | ee.Reducer = "mean",
115 | dateProperty: str = "system:time_start",
116 | colors: list[str] | None = None,
117 | figure: plotting.figure | None = None,
118 | scale: int = 10000,
119 | crs: str | None = None,
120 | crsTransform: list | None = None,
121 | tileScale: float = 1,
122 | ) -> plotting.figure:
123 | """Plot the reduced data for each image in the collection by regions for a single band.
124 |
125 | This method is plotting the reduced data for each image in the collection by regions for a single band.
126 |
127 | Parameters:
128 | band: The band to reduce.
129 | regions: The regions to reduce the data on.
130 | label: The property to use as label for each region. Default is ``"system:index"``.
131 | reducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
132 | dateProperty: The property to use as date for each image. Default is ``"system:time_start"``.
133 | colors: The colors to use for the regions. If empty, the default colors are used.
134 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
135 | scale: The scale in meters to use for the reduction. default is 10000m
136 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
137 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
138 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
139 |
140 | Returns:
141 | A bokeh figure with the reduced values for each region and each date.
142 |
143 | Examples:
144 | .. code-block:: python
145 |
146 | import ee, geetools
147 |
148 | ee.Initialize()
149 |
150 | collection = (
151 | ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
152 | .filterBounds(ee.Geometry.Point(-122.262, 37.8719))
153 | .filterDate("2014-01-01", "2014-12-31")
154 | )
155 |
156 | regions = ee.FeatureCollection([
157 | ee.Feature(ee.Geometry.Point(-122.262, 37.8719).buffer(10000), {"name": "region1"}),
158 | ee.Feature(ee.Geometry.Point(-122.262, 37.8719).buffer(20000), {"name": "region2"})
159 | ])
160 |
161 | collection.geetools.plot_dates_by_regions("B1", regions, "name", "mean", 10000, "system:time_start")
162 | """
163 | # get the reduced data
164 | raw_data = self._obj.geetools.datesByRegions(
165 | band=band,
166 | regions=regions,
167 | label=label,
168 | reducer=reducer,
169 | dateProperty=dateProperty,
170 | scale=scale,
171 | crs=crs,
172 | crsTransform=crsTransform,
173 | tileScale=tileScale,
174 | ).getInfo()
175 |
176 | # transform all the dates int datetime objects
177 | def to_date(dict):
178 | return {dt.strptime(d, PY_DATE_FORMAT): v for d, v in dict.items()}
179 |
180 | data = {lbl: to_date(dict) for lbl, dict in raw_data.items()}
181 |
182 | # create the plot
183 | figure = plot_data("date", data, "Date", colors, figure)
184 |
185 | return figure
186 |
187 | def plot_doy_by_bands(
188 | self,
189 | region: ee.Geometry,
190 | spatialReducer: str | ee.Reducer = "mean",
191 | timeReducer: str | ee.Reducer = "mean",
192 | dateProperty: str = "system:time_start",
193 | bands: list[str] | None = None,
194 | labels: list[str] | None = None,
195 | colors: list[str] | None = None,
196 | figure: plotting.figure | None = None,
197 | scale: int = 10000,
198 | crs: str | None = None,
199 | crsTransform: list | None = None,
200 | bestEffort: bool = False,
201 | maxPixels: int | None = 10**7,
202 | tileScale: float = 1,
203 | ) -> plotting.figure:
204 | """Plot the reduced data for each image in the collection by bands on a specific region.
205 |
206 | This method is plotting the reduced data for each image in the collection by bands on a specific region.
207 |
208 | Parameters:
209 | region: The region to reduce the data on.
210 | spatialReducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
211 | timeReducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
212 | dateProperty: The property to use as date for each image. Default is ``"system:time_start"``.
213 | bands: The bands to reduce. If empty, all bands are reduced.
214 | labels: The labels to use for the bands. If empty, the bands names are used.
215 | colors: The colors to use for the bands. If empty, the default colors are used.
216 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
217 | scale: The scale in meters to use for the reduction. default is 10000m
218 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
219 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
220 | bestEffort: If the polygon would contain too many pixels at the given scale, compute and use a larger scale which would allow the operation to succeed.
221 | maxPixels: The maximum number of pixels to reduce. Defaults to 1e7.
222 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
223 |
224 | Returns:
225 | A bokeh figure with the reduced values for each band and each day.
226 |
227 | Examples:
228 | .. code-block:: python
229 |
230 | import ee, geetools
231 |
232 | ee.Initialize()
233 |
234 | collection = (
235 | ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
236 | .filterBounds(ee.Geometry.Point(-122.262, 37.8719))
237 | .filterDate("2014-01-01", "2014-12-31")
238 | )
239 |
240 | region = ee.Geometry.Point(-122.262, 37.8719).buffer(10000)
241 | collection.geetools.plot_doy_by_bands(region, "mean", "mean", 10000, "system:time_start")
242 | """
243 | # get the reduced data
244 | raw_data = self._obj.geetools.doyByBands(
245 | region=region,
246 | spatialReducer=spatialReducer,
247 | timeReducer=timeReducer,
248 | dateProperty=dateProperty,
249 | bands=bands,
250 | labels=labels,
251 | scale=scale,
252 | crs=crs,
253 | crsTransform=crsTransform,
254 | bestEffort=bestEffort,
255 | maxPixels=maxPixels,
256 | tileScale=tileScale,
257 | ).getInfo()
258 |
259 | # transform all the dates strings into int object and reorder the dictionary
260 | def to_int(d):
261 | return {int(k): v for k, v in d.items()}
262 |
263 | data = {lbl: dict(sorted(to_int(raw_data[lbl]).items())) for lbl in raw_data}
264 |
265 | # create the plot
266 | figure = plot_data("doy", data, "Day of Year", colors, figure)
267 |
268 | return figure
269 |
270 | def plot_doy_by_regions(
271 | self,
272 | band: str,
273 | regions: ee.FeatureCollection,
274 | label: str = "system:index",
275 | spatialReducer: str | ee.Reducer = "mean",
276 | timeReducer: str | ee.Reducer = "mean",
277 | dateProperty: str = "system:time_start",
278 | colors: list[str] | None = None,
279 | figure: plotting.figure | None = None,
280 | scale: int = 10000,
281 | crs: str | None = None,
282 | crsTransform: list | None = None,
283 | tileScale: float = 1,
284 | ) -> plotting.figure:
285 | """Plot the reduced data for each image in the collection by regions for a single band.
286 |
287 | This method is plotting the reduced data for each image in the collection by regions for a single band.
288 |
289 | Parameters:
290 | band: The band to reduce.
291 | regions: The regions to reduce the data on.
292 | label: The property to use as label for each region. Default is ``"system:index"``.
293 | spatialReducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
294 | timeReducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
295 | dateProperty: The property to use as date for each image. Default is ``"system:time_start"``.
296 | colors: The colors to use for the regions. If empty, the default colors are used.
297 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
298 | scale: The scale in meters to use for the reduction. default is 10000m
299 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
300 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
301 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
302 |
303 | Returns:
304 | A bokeh figure with the reduced values for each region and each day.
305 |
306 | Examples:
307 | .. code-block:: python
308 |
309 | import ee, geetools
310 |
311 | ee.Initialize()
312 |
313 | collection = (
314 | ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
315 | .filterBounds(ee.Geometry.Point(-122.262, 37.8719))
316 | .filterDate("2014-01-01", "2014-12-31")
317 | )
318 |
319 | regions = ee.FeatureCollection([
320 | ee.Feature(ee.Geometry.Point(-122.262, 37.8719).buffer(10000), {"name": "region1"}),
321 | ee.Feature(ee.Geometry.Point(-122.262, 37.8719).buffer(20000), {"name": "region2"})
322 | ])
323 |
324 | collection.geetools.plot_doy_by_regions("B1", regions, "name", "mean", "mean", 10000, "system:time_start")
325 | """
326 | # get the reduced data
327 | raw_data = self._obj.geetools.doyByRegions(
328 | band=band,
329 | regions=regions,
330 | label=label,
331 | spatialReducer=spatialReducer,
332 | timeReducer=timeReducer,
333 | dateProperty=dateProperty,
334 | scale=scale,
335 | crs=crs,
336 | crsTransform=crsTransform,
337 | tileScale=tileScale,
338 | ).getInfo()
339 |
340 | # transform all the dates strings into int object and reorder the dictionary
341 | def to_int(d):
342 | return {int(k): v for k, v in d.items()}
343 |
344 | data = {lbl: dict(sorted(to_int(raw_data[lbl]).items())) for lbl in raw_data}
345 |
346 | # create the plot
347 | figure = plot_data("doy", data, "Day of Year", colors, figure)
348 |
349 | return figure
350 |
351 | def plot_doy_by_seasons(
352 | self,
353 | band: str,
354 | region: ee.Geometry,
355 | seasonStart: int | ee.Number = 1,
356 | seasonEnd: int | ee.Number = 366,
357 | reducer: str | ee.Reducer = "mean",
358 | dateProperty: str = "system:time_start",
359 | colors: list[str] | None = None,
360 | figure: plotting.figure | None = None,
361 | scale: int = 10000,
362 | crs: str | None = None,
363 | crsTransform: list | None = None,
364 | bestEffort: bool = False,
365 | maxPixels: int | None = 10**7,
366 | tileScale: float = 1,
367 | ) -> plotting.figure:
368 | """Plot the reduced data for each image in the collection by years for a single band.
369 |
370 | This method is plotting the reduced data for each image in the collection by years for a single band.
371 | To set the start and end of the season, use the :py:meth:`ee.Date.getRelative` or :py:class:`time.struct_time` method to get the day of the year.
372 |
373 | Parameters:
374 | band: The band to reduce.
375 | region: The region to reduce the data on.
376 | seasonStart: The day of the year that marks the start of the season.
377 | seasonEnd: The day of the year that marks the end of the season.
378 | reducer: The name of the reducer or a reducer object to use. Default is ``"mean"``.
379 | dateProperty: The property to use as date for each image. Default is ``"system:time_start"``.
380 | colors: The colors to use for the regions. If empty, the default colors are used.
381 | figure: The bokeh figure to plot the data on. If None, a new figure is created.
382 | scale: The scale in meters to use for the reduction. default is 10000m
383 | crs: The projection to work in. If unspecified, the projection of the image's first band is used. If specified in addition to scale, rescaled to the specified scale.
384 | crsTransform: The list of CRS transform values. This is a row-major ordering of the 3x2 transform matrix. This option is mutually exclusive with 'scale', and replaces any transform already set on the projection.
385 | bestEffort: If the polygon would contain too many pixels at the given scale, compute and use a larger scale which would allow the operation to succeed.
386 | maxPixels: The maximum number of pixels to reduce. Defaults to 1e7.
387 | tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
388 |
389 | Returns:
390 | A bokeh figure with the reduced values for each year and each day.
391 |
392 | Examples:
393 | .. jupyter-execute::
394 |
395 | import ee, geetools
396 | from geetools.utils import initialize_documentation
397 |
398 | initialize_documentation()
399 |
400 | collection = (
401 | ee.ImageCollection("LANDSAT/LC08/C02/T1_TOA")
402 | .filterBounds(ee.Geometry.Point(-122.262, 37.8719))
403 | .filter(ee.Filter.Or(
404 | ee.Filter.date("2022-01-01", "2022-12-31"),
405 | ee.Filter.date("2016-01-01", "2016-12-31"),
406 | ))
407 | .map(lambda i: ee.Image(i).addBands(
408 | ee.Image(i)
409 | .normalizedDifference(["B5", "B4"])
410 | .rename("NDVI")
411 | ))
412 | )
413 |
414 | collection.geetools.plot_doy_by_seasons(
415 | band = "NDVI",
416 | region = ee.Geometry.Point(-122.262, 37.8719).buffer(1000),
417 | seasonStart = ee.Date("2016-05-01").getRelative("day", "year"),
418 | seasonEnd = ee.Date("2016-10-31").getRelative("day", "year"),
419 | reducer = "mean",
420 | dateProperty = "system:time_start",
421 | scale = 10000
422 | )
423 | """
424 | # get the reduced data
425 | raw_data = self._obj.geetools.doyBySeasons(
426 | band=band,
427 | region=region,
428 | seasonStart=seasonStart,
429 | seasonEnd=seasonEnd,
430 | reducer=reducer,
431 | dateProperty=dateProperty,
432 | scale=scale,
433 | crs=crs,
434 | crsTransform=crsTransform,
435 | bestEffort=bestEffort,
436 | maxPixels=maxPixels,
437 | tileScale=tileScale,
438 | ).getInfo()
439 |
440 | # transform all the dates strings into int object and reorder the dictionary
441 | def to_int(d):
442 | return {int(k): v for k, v in d.items()}
443 |
444 | data = {lbl: dict(sorted(to_int(raw_data[lbl]).items())) for lbl in raw_data}
445 |
446 | # create the plot
447 | figure = plot_data("doy", data, "Day of Year", colors, figure)
448 |
449 | return figure
450 |
--------------------------------------------------------------------------------
/docs/usage/plot/plot-featurecollection.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Plot FeatureCollection\n",
8 | "\n",
9 | "The `geetools` extension contains a set of functions for rendering charts from `ee.FeatureCollection` objects. The choice of function determines the arrangement of data in the chart, i.e., what defines x- and y-axis values and what defines the series. Use the following function descriptions and examples to determine the best function and chart type for your purpose."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {
16 | "tags": [
17 | "remove-cell"
18 | ]
19 | },
20 | "outputs": [],
21 | "source": [
22 | "import ee\n",
23 | "from geetools.utils import initialize_documentation\n",
24 | "\n",
25 | "initialize_documentation()"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "[](https://github.com/gee-community/ipygee/blob/main/docs/usage/plot/plot-featurecollection.ipynb)\n",
33 | "[](https://colab.research.google.com/github/gee-community/ipygee/blob/main/docs/usage/plot/plot-featurecollection.ipynb)"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "## Set up environment\n",
41 | "\n",
42 | "Install all the required libs if necessary and perform the import statements upstream."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "# uncomment if installation of libs is necessary\n",
52 | "# !pip install earthengine-api geetools"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "from bokeh.io import output_notebook\n",
62 | "\n",
63 | "import ipygee # noqa: F401\n",
64 | "\n",
65 | "output_notebook()"
66 | ]
67 | },
68 | {
69 | "cell_type": "code",
70 | "execution_count": null,
71 | "metadata": {},
72 | "outputs": [],
73 | "source": [
74 | "# uncomment if authetication to GEE is needed\n",
75 | "# ee.Authenticate()\n",
76 | "# ee.Initialize(project=\"\")"
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "metadata": {},
82 | "source": [
83 | "## Example data\n",
84 | "\n",
85 | "The following examples rely on a FeatureCollection composed of three ecoregion features with properties that describe climate normals."
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "# Import the example feature collection.\n",
95 | "ecoregions = ee.FeatureCollection(\"projects/google/charts_feature_example\")"
96 | ]
97 | },
98 | {
99 | "cell_type": "markdown",
100 | "metadata": {},
101 | "source": [
102 | "## Plot by features\n",
103 | "\n",
104 | "Features are plotted along the x-axis by values of a selected property. Series are defined by a list of property names whose values are plotted along the y-axis. The type of produced chart can be controlled by the `type` parameter as shown in the following examples.\n",
105 | "\n",
106 | "If you want to use another plotting library you can get the raw data using the `byFeatures` function."
107 | ]
108 | },
109 | {
110 | "cell_type": "code",
111 | "execution_count": null,
112 | "metadata": {
113 | "tags": [
114 | "remove-input"
115 | ]
116 | },
117 | "outputs": [],
118 | "source": [
119 | "import numpy as np\n",
120 | "from bokeh.plotting import figure, show\n",
121 | "\n",
122 | "# Data for the chart\n",
123 | "features = [\"f1\", \"f2\", \"f3\"]\n",
124 | "p1_values = [0.5, 2.5, 4.5]\n",
125 | "p2_values = [1.5, 3.5, 5.5]\n",
126 | "p3_values = [2.5, 4.0, 6.5]\n",
127 | "\n",
128 | "# Set the width of the bars\n",
129 | "bar_width = 0.25\n",
130 | "index = np.arange(len(features))\n",
131 | "offset = 0.02\n",
132 | "\n",
133 | "# Create the plot\n",
134 | "fig = figure(width=800, height=400)\n",
135 | "\n",
136 | "# Plotting the bars\n",
137 | "rects1 = fig.vbar(x=index, top=p1_values, width=bar_width, legend_label=\"p1\", color=\"#1d6b99\")\n",
138 | "rects2 = fig.vbar(\n",
139 | " x=index + (bar_width + offset), top=p2_values, width=bar_width, legend_label=\"p2\", color=\"#cf513e\"\n",
140 | ")\n",
141 | "rects3 = fig.vbar(\n",
142 | " x=index + 2 * (bar_width + offset),\n",
143 | " top=p3_values,\n",
144 | " width=bar_width,\n",
145 | " legend_label=\"p3\",\n",
146 | " color=\"#f0af07\",\n",
147 | ")\n",
148 | "\n",
149 | "# Add labels, title, and custom x-axis tick labels\n",
150 | "fig.yaxis.axis_label = \"Series property value\"\n",
151 | "fig.xaxis.axis_label = \"Features by property value\"\n",
152 | "fig.outline_line_color = None\n",
153 | "fig.legend.title = \"Property names\"\n",
154 | "fig.legend.location = \"top_left\"\n",
155 | "fig.xaxis.ticker = index + (bar_width + offset)\n",
156 | "fig.xaxis.major_label_overrides = dict(zip(index + (bar_width + offset), features))\n",
157 | "fig.xgrid.grid_line_color = None\n",
158 | "fig.legend.orientation = \"horizontal\"\n",
159 | "\n",
160 | "# Show the plot\n",
161 | "show(fig)"
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "metadata": {},
167 | "source": [
168 | "### Column chart\n",
169 | "\n",
170 | "Features are plotted along the x-axis, labeled by values of a selected property. Series are represented by adjacent columns defined by a list of property names whose values are plotted along the y-axis."
171 | ]
172 | },
173 | {
174 | "cell_type": "code",
175 | "execution_count": null,
176 | "metadata": {},
177 | "outputs": [],
178 | "source": [
179 | "fig = figure(width=800, height=400)\n",
180 | "\n",
181 | "# initialize the plot with the ecoregions data\n",
182 | "ecoregions.bokeh.plot_by_features(\n",
183 | " type=\"bar\",\n",
184 | " featureId=\"label\",\n",
185 | " properties=[\n",
186 | " \"01_tmean\",\n",
187 | " \"02_tmean\",\n",
188 | " \"03_tmean\",\n",
189 | " \"04_tmean\",\n",
190 | " \"05_tmean\",\n",
191 | " \"06_tmean\",\n",
192 | " \"07_tmean\",\n",
193 | " \"08_tmean\",\n",
194 | " \"09_tmean\",\n",
195 | " \"10_tmean\",\n",
196 | " \"11_tmean\",\n",
197 | " \"12_tmean\",\n",
198 | " ],\n",
199 | " labels=[\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"],\n",
200 | " colors=[\n",
201 | " \"#604791\",\n",
202 | " \"#1d6b99\",\n",
203 | " \"#39a8a7\",\n",
204 | " \"#0f8755\",\n",
205 | " \"#76b349\",\n",
206 | " \"#f0af07\",\n",
207 | " \"#e37d05\",\n",
208 | " \"#cf513e\",\n",
209 | " \"#96356f\",\n",
210 | " \"#724173\",\n",
211 | " \"#9c4f97\",\n",
212 | " \"#696969\",\n",
213 | " ],\n",
214 | " figure=fig,\n",
215 | ")\n",
216 | "\n",
217 | "# once created the figure can be modified as needed using pure bokeh members\n",
218 | "fig.title.text = \"Average Monthly Temperature by Ecoregion\"\n",
219 | "fig.xaxis.axis_label = \"Ecoregion\"\n",
220 | "fig.yaxis.axis_label = \"Temperature (°C)\"\n",
221 | "\n",
222 | "show(fig)"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "metadata": {},
228 | "source": [
229 | "### Stacked column chart\n",
230 | "\n",
231 | "Features are plotted along the x-axis, labeled by values of a selected property. Series are represented by stacked columns defined by a list of property names whose values are plotted along the y-axis as the cumulative series sum."
232 | ]
233 | },
234 | {
235 | "cell_type": "code",
236 | "execution_count": null,
237 | "metadata": {},
238 | "outputs": [],
239 | "source": [
240 | "fig = figure(width=800, height=400)\n",
241 | "\n",
242 | "# initialize theplot with the ecoregions data\n",
243 | "ecoregions.bokeh.plot_by_features(\n",
244 | " type=\"stacked\",\n",
245 | " featureId=\"label\",\n",
246 | " properties=[\n",
247 | " \"01_ppt\",\n",
248 | " \"02_ppt\",\n",
249 | " \"03_ppt\",\n",
250 | " \"04_ppt\",\n",
251 | " \"05_ppt\",\n",
252 | " \"06_ppt\",\n",
253 | " \"07_ppt\",\n",
254 | " \"08_ppt\",\n",
255 | " \"09_ppt\",\n",
256 | " \"10_ppt\",\n",
257 | " \"11_ppt\",\n",
258 | " \"12_ppt\",\n",
259 | " ],\n",
260 | " labels=[\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"],\n",
261 | " colors=[\n",
262 | " \"#604791\",\n",
263 | " \"#1d6b99\",\n",
264 | " \"#39a8a7\",\n",
265 | " \"#0f8755\",\n",
266 | " \"#76b349\",\n",
267 | " \"#f0af07\",\n",
268 | " \"#e37d05\",\n",
269 | " \"#cf513e\",\n",
270 | " \"#96356f\",\n",
271 | " \"#724173\",\n",
272 | " \"#9c4f97\",\n",
273 | " \"#696969\",\n",
274 | " ],\n",
275 | " figure=fig,\n",
276 | ")\n",
277 | "\n",
278 | "# once created the figure can be modified as needed using pure bokeh members\n",
279 | "fig.title.text = \"Average Monthly Precipitation by Ecoregion\"\n",
280 | "fig.xaxis.axis_label = \"Ecoregion\"\n",
281 | "fig.yaxis.axis_label = \"Precipitation (mm)\"\n",
282 | "\n",
283 | "show(fig)"
284 | ]
285 | },
286 | {
287 | "cell_type": "markdown",
288 | "metadata": {},
289 | "source": [
290 | "### Scatter chart\n",
291 | "\n",
292 | "Features are plotted along the x-axis, labeled by values of a selected property. Series are represented by points defined by a list of property names whose values are plotted along the y-axis."
293 | ]
294 | },
295 | {
296 | "cell_type": "code",
297 | "execution_count": null,
298 | "metadata": {},
299 | "outputs": [],
300 | "source": [
301 | "fig = figure(width=800, height=400)\n",
302 | "\n",
303 | "# initialize theplot with the ecoregions data\n",
304 | "ecoregions.bokeh.plot_by_features(\n",
305 | " type=\"scatter\",\n",
306 | " featureId=\"label\",\n",
307 | " properties=[\"01_ppt\", \"06_ppt\", \"09_ppt\"],\n",
308 | " labels=[\"jan\", \"jun\", \"sep\"],\n",
309 | " figure=fig,\n",
310 | ")\n",
311 | "\n",
312 | "# once created the figure can be modified as needed using pure bokeh members\n",
313 | "fig.title.text = \"Average Monthly Precipitation by Ecoregion\"\n",
314 | "fig.xaxis.axis_label = \"Ecoregion\"\n",
315 | "fig.yaxis.axis_label = \"Precipitation (mm)\"\n",
316 | "\n",
317 | "show(fig)"
318 | ]
319 | },
320 | {
321 | "cell_type": "markdown",
322 | "metadata": {},
323 | "source": [
324 | "### Pie chart\n",
325 | "\n",
326 | "The pie is a property, each slice is the share from each feature whose value is cast as a percentage of the sum of all values of features composing the pie."
327 | ]
328 | },
329 | {
330 | "cell_type": "code",
331 | "execution_count": null,
332 | "metadata": {},
333 | "outputs": [],
334 | "source": [
335 | "fig = figure(match_aspect=True)\n",
336 | "\n",
337 | "# initialize theplot with the ecoregions data\n",
338 | "ecoregions.bokeh.plot_by_features(\n",
339 | " type=\"pie\",\n",
340 | " featureId=\"label\",\n",
341 | " properties=[\"06_ppt\"],\n",
342 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
343 | " figure=fig,\n",
344 | ")\n",
345 | "\n",
346 | "# once created the figure can be modified as needed using pure bokeh members\n",
347 | "fig.title.text = \"Share of precipitation in June by Ecoregion\"\n",
348 | "\n",
349 | "show(fig)"
350 | ]
351 | },
352 | {
353 | "cell_type": "markdown",
354 | "metadata": {},
355 | "source": [
356 | "### Donut chart\n",
357 | "\n",
358 | "The donut is a property, each slice is the share from each feature whose value is cast as a percentage of the sum of all values of features composing the donut."
359 | ]
360 | },
361 | {
362 | "cell_type": "code",
363 | "execution_count": null,
364 | "metadata": {},
365 | "outputs": [],
366 | "source": [
367 | "fig = figure(match_aspect=True)\n",
368 | "\n",
369 | "# initialize theplot with the ecoregions data\n",
370 | "ecoregions.bokeh.plot_by_features(\n",
371 | " type=\"donut\",\n",
372 | " featureId=\"label\",\n",
373 | " properties=[\"07_ppt\"],\n",
374 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
375 | " figure=fig,\n",
376 | ")\n",
377 | "\n",
378 | "# once created the figure can be modified as needed using pure bokeh members\n",
379 | "fig.title.text = \"Share of precipitation in July by Ecoregion\"\n",
380 | "\n",
381 | "show(fig)"
382 | ]
383 | },
384 | {
385 | "cell_type": "markdown",
386 | "metadata": {},
387 | "source": [
388 | "## Plot by properties\n",
389 | "\n",
390 | "Feature properties are plotted along the x-axis by name; values of the given properties are plotted along the y-axis. Series are features labeled by values of a selected property. The type of produced chart can be controlled by the `type` parameter as shown in the following examples."
391 | ]
392 | },
393 | {
394 | "cell_type": "code",
395 | "execution_count": null,
396 | "metadata": {
397 | "tags": [
398 | "remove-input"
399 | ]
400 | },
401 | "outputs": [],
402 | "source": [
403 | "import numpy as np\n",
404 | "from bokeh.plotting import figure, show\n",
405 | "\n",
406 | "# Data for the chart\n",
407 | "features = [\"p1\", \"p2\", \"p3\"]\n",
408 | "p1_values = [0.5, 2.5, 4.5]\n",
409 | "p2_values = [1.5, 3.5, 5.5]\n",
410 | "p3_values = [2.5, 4.0, 6.5]\n",
411 | "\n",
412 | "# Set the width of the bars\n",
413 | "bar_width = 0.25\n",
414 | "index = np.arange(len(features))\n",
415 | "offset = 0.02\n",
416 | "\n",
417 | "# Create the plot\n",
418 | "fig = figure(width=800, height=400)\n",
419 | "\n",
420 | "# Plotting the bars\n",
421 | "rects1 = fig.vbar(x=index, top=p1_values, width=bar_width, legend_label=\"f1\", color=\"#1d6b99\")\n",
422 | "rects2 = fig.vbar(\n",
423 | " x=index + (bar_width + offset), top=p2_values, width=bar_width, legend_label=\"f2\", color=\"#cf513e\"\n",
424 | ")\n",
425 | "rects3 = fig.vbar(\n",
426 | " x=index + 2 * (bar_width + offset),\n",
427 | " top=p3_values,\n",
428 | " width=bar_width,\n",
429 | " legend_label=\"f3\",\n",
430 | " color=\"#f0af07\",\n",
431 | ")\n",
432 | "\n",
433 | "# Add labels, title, and custom x-axis tick labels\n",
434 | "fig.yaxis.axis_label = \"Series property value\"\n",
435 | "fig.xaxis.axis_label = \"Property names\"\n",
436 | "fig.outline_line_color = None\n",
437 | "fig.legend.title = \"Features by property value\"\n",
438 | "fig.legend.location = \"top_left\"\n",
439 | "fig.xaxis.ticker = index + (bar_width + offset)\n",
440 | "fig.xaxis.major_label_overrides = dict(zip(index + (bar_width + offset), features))\n",
441 | "fig.xgrid.grid_line_color = None\n",
442 | "fig.legend.orientation = \"horizontal\"\n",
443 | "\n",
444 | "# Show the plot\n",
445 | "show(fig)"
446 | ]
447 | },
448 | {
449 | "cell_type": "markdown",
450 | "metadata": {},
451 | "source": [
452 | "## Column chart\n",
453 | "\n",
454 | "Feature properties are plotted along the x-axis, labeled and sorted by a dictionary input; the values of the given properties are plotted along the y-axis. Series are features, represented by columns, labeled by values of a selected property."
455 | ]
456 | },
457 | {
458 | "cell_type": "code",
459 | "execution_count": null,
460 | "metadata": {},
461 | "outputs": [],
462 | "source": [
463 | "fig = figure(width=800, height=400)\n",
464 | "\n",
465 | "\n",
466 | "# initialize theplot with the ecoregions data\n",
467 | "ecoregions.bokeh.plot_by_properties(\n",
468 | " type=\"bar\",\n",
469 | " properties=[\n",
470 | " \"01_ppt\",\n",
471 | " \"02_ppt\",\n",
472 | " \"03_ppt\",\n",
473 | " \"04_ppt\",\n",
474 | " \"05_ppt\",\n",
475 | " \"06_ppt\",\n",
476 | " \"07_ppt\",\n",
477 | " \"08_ppt\",\n",
478 | " \"09_ppt\",\n",
479 | " \"10_ppt\",\n",
480 | " \"11_ppt\",\n",
481 | " \"12_ppt\",\n",
482 | " ],\n",
483 | " labels=[\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"],\n",
484 | " featureId=\"label\",\n",
485 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
486 | " figure=fig,\n",
487 | ")\n",
488 | "\n",
489 | "# once created the figure can be modified as needed using pure bokeh members\n",
490 | "fig.title.text = \"Average Monthly Precipitation by Ecoregion\"\n",
491 | "fig.xaxis.axis_label = \"Month\"\n",
492 | "fig.yaxis.axis_label = \"Precipitation (mm)\"\n",
493 | "\n",
494 | "show(fig)"
495 | ]
496 | },
497 | {
498 | "cell_type": "markdown",
499 | "metadata": {},
500 | "source": [
501 | "## Line chart\n",
502 | "\n",
503 | "Feature properties are plotted along the x-axis, labeled and sorted by a dictionary input; the values of the given properties are plotted along the y-axis. Series are features, represented by columns, labeled by values of a selected property."
504 | ]
505 | },
506 | {
507 | "cell_type": "code",
508 | "execution_count": null,
509 | "metadata": {},
510 | "outputs": [],
511 | "source": [
512 | "fig = figure(width=800, height=400)\n",
513 | "\n",
514 | "# initialize theplot with the ecoregions data\n",
515 | "ecoregions.bokeh.plot_by_properties(\n",
516 | " type=\"plot\",\n",
517 | " properties=[\n",
518 | " \"01_ppt\",\n",
519 | " \"02_ppt\",\n",
520 | " \"03_ppt\",\n",
521 | " \"04_ppt\",\n",
522 | " \"05_ppt\",\n",
523 | " \"06_ppt\",\n",
524 | " \"07_ppt\",\n",
525 | " \"08_ppt\",\n",
526 | " \"09_ppt\",\n",
527 | " \"10_ppt\",\n",
528 | " \"11_ppt\",\n",
529 | " \"12_ppt\",\n",
530 | " ],\n",
531 | " featureId=\"label\",\n",
532 | " labels=[\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"],\n",
533 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
534 | " figure=fig,\n",
535 | ")\n",
536 | "\n",
537 | "# once created the figure can be modified as needed using pure bokeh members\n",
538 | "fig.title.text = \"Average Monthly Precipitation by Ecoregion\"\n",
539 | "fig.xaxis.axis_label = \"Month\"\n",
540 | "fig.yaxis.axis_label = \"Precipitation (mm)\"\n",
541 | "\n",
542 | "show(fig)"
543 | ]
544 | },
545 | {
546 | "cell_type": "markdown",
547 | "metadata": {},
548 | "source": [
549 | "### Area chart \n",
550 | "\n",
551 | "Feature properties are plotted along the x-axis, labeled and sorted by a dictionary input; the values of the given properties are plotted along the y-axis. Series are features, represented by lines and shaded areas, labeled by values of a selected property."
552 | ]
553 | },
554 | {
555 | "cell_type": "code",
556 | "execution_count": null,
557 | "metadata": {},
558 | "outputs": [],
559 | "source": [
560 | "fig = figure(width=800, height=400)\n",
561 | "\n",
562 | "# initialize the plot with the ecoregions data\n",
563 | "ecoregions.bokeh.plot_by_properties(\n",
564 | " type=\"fill_between\",\n",
565 | " properties=[\n",
566 | " \"01_ppt\",\n",
567 | " \"02_ppt\",\n",
568 | " \"03_ppt\",\n",
569 | " \"04_ppt\",\n",
570 | " \"05_ppt\",\n",
571 | " \"06_ppt\",\n",
572 | " \"07_ppt\",\n",
573 | " \"08_ppt\",\n",
574 | " \"09_ppt\",\n",
575 | " \"10_ppt\",\n",
576 | " \"11_ppt\",\n",
577 | " \"12_ppt\",\n",
578 | " ],\n",
579 | " labels=[\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"],\n",
580 | " featureId=\"label\",\n",
581 | " colors=[\"#f0af07\", \"#0f8755\", \"#76b349\"],\n",
582 | " figure=fig,\n",
583 | ")\n",
584 | "\n",
585 | "# once created the figure can be modified as needed using pure bokeh members\n",
586 | "fig.title.text = \"Average Monthly Precipitation by Ecoregion\"\n",
587 | "fig.xaxis.axis_label = \"Month\"\n",
588 | "fig.yaxis.axis_label = \"Precipitation (mm)\"\n",
589 | "\n",
590 | "show(fig)"
591 | ]
592 | },
593 | {
594 | "cell_type": "markdown",
595 | "metadata": {},
596 | "source": [
597 | "## Plot hist\n",
598 | "\n",
599 | "```{api}\n",
600 | "{docstring}`ee.FeatureCollection.geetools.plot_hist`\n",
601 | "```\n",
602 | "\n",
603 | "The x-axis is defined by value bins for the range of values of a selected property; the y-axis is the number of elements in the given bin."
604 | ]
605 | },
606 | {
607 | "cell_type": "code",
608 | "execution_count": null,
609 | "metadata": {},
610 | "outputs": [],
611 | "source": [
612 | "fig = figure(width=800, height=400)\n",
613 | "\n",
614 | "# load some data\n",
615 | "normClim = ee.ImageCollection(\"OREGONSTATE/PRISM/Norm91m\").toBands()\n",
616 | "\n",
617 | "# Make a point sample of climate variables for a region in western USA.\n",
618 | "region = ee.Geometry.Rectangle(-123.41, 40.43, -116.38, 45.14)\n",
619 | "climSamp = normClim.sample(region, 5000)\n",
620 | "\n",
621 | "\n",
622 | "# initialize the plot with the ecoregions data\n",
623 | "climSamp.bokeh.plot_hist(\n",
624 | " property=\"07_ppt\", label=\"July Precipitation (mm)\", color=\"#1d6b99\", figure=fig, bins=30\n",
625 | ")\n",
626 | "\n",
627 | "# once created the figure can be modified as needed using pure bokeh members\n",
628 | "fig.title.text = \"July Precipitation Distribution for NW USA\"\n",
629 | "\n",
630 | "show(fig)"
631 | ]
632 | },
633 | {
634 | "cell_type": "code",
635 | "execution_count": null,
636 | "metadata": {},
637 | "outputs": [],
638 | "source": []
639 | }
640 | ],
641 | "metadata": {
642 | "kernelspec": {
643 | "display_name": "ipygee",
644 | "language": "python",
645 | "name": "python3"
646 | },
647 | "language_info": {
648 | "codemirror_mode": {
649 | "name": "ipython",
650 | "version": 3
651 | },
652 | "file_extension": ".py",
653 | "mimetype": "text/x-python",
654 | "name": "python",
655 | "nbconvert_exporter": "python",
656 | "pygments_lexer": "ipython3",
657 | "version": "3.10.16"
658 | }
659 | },
660 | "nbformat": 4,
661 | "nbformat_minor": 2
662 | }
663 |
--------------------------------------------------------------------------------
/ipygee/asset.py:
--------------------------------------------------------------------------------
1 | """The asset manager widget code and functionalities."""
2 |
3 | from __future__ import annotations
4 |
5 | import json
6 | from pathlib import Path
7 | from typing import List, Optional
8 |
9 | import ee
10 | import geetools # noqa
11 | import ipyvuetify as v
12 | import requests
13 | import traitlets as t
14 | from natsort import humansorted
15 |
16 | from .decorator import switch
17 | from .sidecar import HasSideCar
18 |
19 | ICON_STYLE = {
20 | "PARENT": {"color": "black", "icon": "mdi-folder-open"},
21 | "PROJECT": {"color": "red", "icon": "mdi-google-cloud"},
22 | "FOLDER": {"color": "grey", "icon": "mdi-folder"},
23 | "IMAGE": {"color": "purple", "icon": "mdi-image-outline"},
24 | "IMAGE_COLLECTION": {"color": "purple", "icon": "mdi-image-multiple-outline"},
25 | "TABLE": {"color": "green", "icon": "mdi-table"},
26 | "FEATURE_COLLECTION": {"color": "green", "icon": "mdi-tabe"},
27 | }
28 | "The style to apply to each object"
29 |
30 |
31 | class AssetManager(v.Flex, HasSideCar):
32 | """A asset manager widget."""
33 |
34 | # -- Variables -------------------------------------------------------------
35 |
36 | folder: t.Unicode = t.Unicode(".").tag(sync=True)
37 | "The current folder that the user see"
38 |
39 | selected_item: t.Unicode = t.Unicode("").tag(sync=True)
40 | "The selected item of the asset manager"
41 |
42 | sidecar_title = "Assets"
43 | "The title of the sidecar"
44 |
45 | # -- Widgets ---------------------------------------------------------------
46 |
47 | w_new: v.Btn
48 | "The new btn on the top of the asset manager"
49 |
50 | w_reload: v.Btn
51 | "The reload btn at the top of the asset manager"
52 |
53 | w_search: v.Btn
54 | "The search button to crowl into the existing items"
55 |
56 | w_selected: v.TextField
57 | "The field where the user can see the asset Id of the selected item"
58 |
59 | w_list: v.List
60 | "The list of items displayed in the asset manager"
61 |
62 | w_card: v.Card
63 | "The card hosting the list of items"
64 |
65 | w_delete_dialog: v.Dialog
66 | "The dialog to confirm the deletion of an asset"
67 |
68 | w_move_dialog: v.Dialog
69 | "The dialog to confirm the move of an asset"
70 |
71 | w_asset_dialog: v.Dialog
72 | "The dialog to view an asset"
73 |
74 | w_create_dialog: v.Dialog
75 | "The dialog to create a new folder"
76 |
77 | def __init__(self):
78 | """Initialize the class."""
79 | # start by defining al the widgets
80 | # We deactivated the formatting to define each one of them on 1 single line
81 | # fmt: off
82 |
83 | # add a line of buttons to reload and add new projects
84 | self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ml-1", disabled=True, small=True)
85 | self.w_reload = v.Btn(children=[v.Icon(color="primary", children="mdi-reload", small=True)], elevation=2, class_="ma-1", small=True)
86 | self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify", small=True)], elevation=2, class_="mr-1", disabled=True, small=True)
87 | w_main_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search])
88 |
89 | # generate the asset selector and the CRUD buttons
90 | self.w_selected = v.TextField(readonly=True, label="Selected item", v_model="", clearable=True, outlined=True, class_="mt-1")
91 | self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye", small=True)], disabled=True, small=True)
92 | self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy", small=True)], disabled=True, small=True)
93 | self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move", small=True)], disabled=True, small=True)
94 | self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can", small=True)], disabled=True, small=True)
95 | w_btn_list = v.ItemGroup(class_="ma-1 v-btn-toggle",children=[self.w_view, self.w_copy, self.w_move, self.w_delete])
96 |
97 | # generate the initial list
98 | w_group = v.ListItemGroup(children=self.get_items(), v_model="")
99 | self.w_list = v.List(dense=True, v_model=True, children=[w_group], outlined=True)
100 | self.w_card = v.Card(children=[self.w_list], outlined=True, class_="ma-1")
101 |
102 | # create the hidden dialogs
103 | self.w_delete_dialog = DeleteAssetDialog()
104 | self.w_move_dialog = MoveAssetDialog()
105 | self.w_asset_dialog = AssetDialog()
106 | self.w_create_dialog = CreateFolderDialog()
107 |
108 | super().__init__(children=[
109 | self.w_delete_dialog, self.w_move_dialog, self.w_asset_dialog, self.w_create_dialog,
110 | w_main_line, w_btn_list, self.w_selected, self.w_card
111 | ], v_model="", class_="ma-1")
112 | # fmt: on
113 |
114 | # update the template of the DOM object to add a js method to copy to clipboard
115 | # template with js behaviour
116 | js_dir = Path(__file__).parent / "js"
117 | clip = (js_dir / "jupyter_clip.js").read_text()
118 | self.template = "" "" % clip
119 |
120 | # add JS behaviour
121 | t.link((self, "selected_item"), (self, "v_model"))
122 | self.w_list.children[0].observe(self.on_item_select, "v_model")
123 | self.w_reload.on_event("click", self.on_reload)
124 | self.w_delete_dialog.observe(self.on_reload, "value")
125 | self.w_copy.on_event("click", self.on_copy)
126 | self.w_delete.on_event("click", self.on_delete)
127 | self.w_selected.observe(self.activate_buttons, "v_model")
128 | self.w_move.on_event("click", self.on_move)
129 | self.w_view.on_event("click", self.on_view)
130 | self.w_new.on_event("click", self.on_new)
131 |
132 | def get_projects(self) -> List:
133 | """Get the list of project accessible from the authenticated user."""
134 | # recover the saved credentials of the user from the file system
135 | credential_path = Path.home() / ".config" / "earthengine" / "credentials"
136 | creds = json.loads(credential_path.read_text())
137 |
138 | # get an authentication token for this very account and make requests to the Google
139 | # REST API
140 | url = "https://cloudresourcemanager.googleapis.com/v1/projects"
141 | token_url = "https://oauth2.googleapis.com/token"
142 | creds["grant_type"] = "refresh_token"
143 | response = requests.post(token_url, data=creds)
144 |
145 | if response.status_code == 200:
146 | access_token = response.json().get("access_token")
147 | else:
148 | raise ValueError(f"Failed to retrieve access token: {response.text}")
149 |
150 | # Define the API endpoint and headers and list all the projects available
151 | cloud_resource_manager_url = "https://cloudresourcemanager.googleapis.com/v1/projects"
152 | headers = {"Authorization": f"Bearer {access_token}"}
153 | response = requests.get(cloud_resource_manager_url, headers=headers)
154 |
155 | # Handle the response
156 | if response.status_code == 200:
157 | projects = [p["projectId"] for p in response.json()["projects"]]
158 | else:
159 | raise ValueError(f"API request failed: {response.text}")
160 |
161 | # filter out the projects that are not compatible with GEE
162 | url = "https://serviceusage.googleapis.com/v1/projects/{}/services/earthengine.googleapis.com"
163 | gee_projects = []
164 | for p in projects:
165 | response = requests.get(url.format(p), headers=headers)
166 | if response.status_code == 200:
167 | gee_projects.append(p)
168 |
169 | return gee_projects
170 |
171 | def get_items(self) -> List[v.ListItem]:
172 | """Create the list of items inside a folder."""
173 | # special case when we are at the root of everything
174 | # because of the specific display of cloud projects we will store both the name and the id of everything as a dict
175 | # for all other item types it will simply be the Name
176 | if self.folder == ".":
177 | list_items = [{"id": f"projects/{i}/assets", "name": i} for i in self.get_projects()]
178 | else:
179 | list_items = [{"id": str(i), "name": i.name} for i in ee.Asset(self.folder).iterdir()]
180 |
181 | # split the folders and the files to display the folders first
182 | # cloud bucket will be considered as folders
183 | folder_list, file_list = [], [] # type: ignore[var-annotated]
184 |
185 | # walk the list of items and generate a list of ListItem using all the appropriate features
186 | # first we extract the type to deduce the icon and color, then we build the Items with the
187 | # ID and the display name and finally we split them in 2 groups the folders and the files
188 | for i in list_items:
189 | asset = ee.Asset(i["id"])
190 | type = "PROJECT" if asset.is_project() else asset.type
191 | icon = ICON_STYLE[type]["icon"]
192 | color = ICON_STYLE[type]["color"]
193 |
194 | action = v.ListItemAction(
195 | children=[v.Icon(color=color, small=True, children=[icon])], class_="mr-1"
196 | )
197 | content = v.ListItemContent(children=[v.ListItemTitle(children=[i["name"]])])
198 | dst_list = folder_list if type in ["FOLDER", "PROJECT"] else file_list
199 | dst_list.append(v.ListItem(value=i["id"], children=[action, content]))
200 |
201 | # humanly sort the 2 lists so that number are treated nicely
202 | folder_list = humansorted(folder_list, key=lambda x: x.value)
203 | file_list = humansorted(file_list, key=lambda x: x.value)
204 |
205 | # add a parent items if necessary. We follow the same mechanism with specific verifications
206 | # if the parent is a project folder or the root
207 | if self.folder != ".":
208 | icon = ICON_STYLE["PARENT"]["icon"]
209 | color = ICON_STYLE["PARENT"]["color"]
210 |
211 | asset = ee.Asset(self.folder)
212 | parent = ee.Asset("") if asset.is_project() else asset.parent
213 | name = parent.parts[1] if parent.is_project() else parent.name
214 | name = name or "." # special case for the root
215 |
216 | action = v.ListItemAction(
217 | children=[v.Icon(color=color, small=True, children=[icon])], class_="mr-1"
218 | )
219 | content = v.ListItemContent(children=[v.ListItemTitle(children=[name])])
220 | item = v.ListItem(value=str(parent), children=[action, content])
221 |
222 | folder_list.insert(0, item)
223 |
224 | # return the concatenation of the 2 lists
225 | return folder_list + file_list
226 |
227 | @switch("loading", "disabled", member="w_card")
228 | def on_item_select(self, change: dict):
229 | """Act when an item is clicked by the user."""
230 | # exit if nothing is changed to avoid infinite loop upon loading
231 | selected = change["new"]
232 | if not selected:
233 | return
234 |
235 | # select the item in the item TextField so user can interact with it
236 | self.w_selected.v_model = change["new"]
237 |
238 | # reset files. This is resetting the scroll to top without using js scripts
239 | # set the new content files and folders
240 | ee.Asset(change["new"])
241 | if selected == "." or ee.Asset(selected).is_project() or ee.Asset(selected).is_folder():
242 | self.folder = selected
243 | items = self.get_items()
244 | self.w_list.children[0].children = [] # trick to scroll up
245 | self.w_list.children[0].children = items
246 |
247 | def on_reload(self, *args):
248 | """Reload the current folder."""
249 | try:
250 | self.on_item_select(change={"new": self.folder})
251 | except ValueError:
252 | self.on_item_select(change={"new": ee.Asset(self.folder).parent.as_posix()})
253 |
254 | def on_copy(self, *args):
255 | """Copy the selected item to clipboard."""
256 | self.send({"method": "clip", "args": [self.w_selected.v_model]})
257 | self.w_copy.children[0].children = ["mdi-check"]
258 |
259 | @switch("loading", "disabled", member="w_card")
260 | def on_delete(self, *args):
261 | """Delete the selected item.
262 |
263 | Ask for confirmation before deleting via a dialog window.
264 | """
265 | # make sure the current item is deletable. We can only delete assets i.e.
266 | # files and folders. Projects and buckets are not deletable.
267 | selected = self.w_selected.v_model
268 | if selected in [".", ""] or ee.Asset(selected).is_project():
269 | return
270 |
271 | # open the delete dialog with the current file
272 | self.w_delete_dialog.reload(ee.Asset(selected))
273 | self.w_delete_dialog.value = True
274 |
275 | @switch("loading", "disabled", member="w_card")
276 | def on_move(self, *args):
277 | """Copy the selected item.
278 |
279 | Ask for confirmation before moving via a dialog window.
280 | """
281 | # make sure the current item is moveable. We can only move assets i.e.
282 | # files and folders. Projects and buckets are not deletable.
283 | selected = self.w_selected.v_model
284 | if selected in [".", ""] or ee.Asset(selected).is_project():
285 | return
286 |
287 | # open the delete dialog with the current file
288 | self.w_move_dialog.reload(ee.Asset(selected))
289 | self.w_move_dialog.value = True
290 |
291 | @switch("loading", "disabled", member="w_card")
292 | def on_view(self, *args):
293 | """Open the view dialog."""
294 | # make sure the current item is moveable. We can only move assets i.e.
295 | # files and folders. Projects and buckets are not deletable.
296 | selected = ee.Asset(self.w_selected.v_model)
297 | if self.w_selected.v_model in [".", ""] or selected.is_project() or selected.is_folder():
298 | return
299 |
300 | # open the delete dialog with the current file
301 | self.w_asset_dialog.reload(ee.Asset(selected))
302 | self.w_asset_dialog.value = True
303 |
304 | @switch("loading", "disabled", member="w_card")
305 | def on_new(self, *args):
306 | """Create a new folder cia the dialog."""
307 | # We need to be at least in a project to be able to create a new folder
308 | selected = self.w_selected.v_model
309 | if selected in [".", ""]:
310 | return
311 |
312 | self.w_create_dialog.reload(ee.Asset(self.folder))
313 | self.w_create_dialog.value = True
314 |
315 | def activate_buttons(self, change: dict):
316 | """Activate the appropriate buttons whenever the selected item changes."""
317 | # reset everything
318 | self.w_new.disabled = True
319 | self.w_view.disabled = True
320 | self.w_move.disabled = True
321 | self.w_delete.disabled = True
322 | self.w_copy.disabled = True
323 | self.w_copy.children[0].children = ["mdi-content-copy"]
324 |
325 | # We can activate the new button for projects
326 | asset = ee.Asset(change["new"])
327 | if asset.is_absolute():
328 | self.w_new.disabled = False
329 |
330 | # we need to exit if the selected item is a project or a root
331 | if change["new"] in [".", ""] or asset.is_project():
332 | return
333 |
334 | # reactivate delete move and copy for assets
335 | if asset.exists():
336 | self.w_delete.disabled = False
337 | self.w_move.disabled = False
338 | self.w_copy.disabled = False
339 |
340 | # we can only view files
341 | if not asset.is_folder():
342 | self.w_view.disabled = False
343 |
344 |
345 | class DeleteAssetDialog(v.Dialog):
346 | """A dialog to confirm the deletion of an asset."""
347 |
348 | # -- Variables -------------------------------------------------------------
349 |
350 | asset: ee.Asset
351 | "The asset to delete"
352 |
353 | # -- Widgets ---------------------------------------------------------------
354 | w_confirm: v.Btn
355 | "The confirm button"
356 |
357 | w_cancel: v.Btn
358 | "The cancel button"
359 |
360 | def __init__(self, asset: Optional[ee.Asset] = None):
361 | """Initialize the class."""
362 | # start by defining all the widgets
363 | self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
364 | self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
365 | w_title = v.CardTitle(children=["Delete the assets"])
366 | disclaimer = 'Clicking on "confirm" will definitively delete all the following asset. This action is definitive.' # fmt: skip
367 | option = 'Click on "cancel" to abort the deletion.'
368 |
369 | self.ul = v.Html(tag="ul", children=[])
370 | w_content = v.CardText(children=[disclaimer, option, self.ul])
371 |
372 | w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
373 |
374 | self.w_card = v.Card(children=[w_title, w_content, w_actions])
375 |
376 | super().__init__(children=[self.w_card], max_width="50%", persistent=True)
377 |
378 | # js interaction with the btns
379 | self.w_confirm.on_event("click", self.on_confirm)
380 | self.w_cancel.on_event("click", self.on_cancel)
381 |
382 | def reload(self, asset: ee.Asset):
383 | """Reload the dialog with a new asset."""
384 | # We should never arrive here with a non asset
385 | # but to avoid catastrophic destruction we will empty the list first
386 | if asset is None or str(asset) == ".":
387 | self.ul.children = []
388 |
389 | # save the asset as a member and read it
390 | self.asset = asset
391 | assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset]
392 | self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets]
393 |
394 | @switch("loading", "disabled", member="w_card")
395 | def on_confirm(self, *args):
396 | """Confirm the deletion."""
397 | # delete the asset and close the dialog
398 | if self.asset.is_folder():
399 | self.asset.rmdir(recursive=True, dry_run=False)
400 | else:
401 | self.asset.delete()
402 | self.value = False
403 |
404 | @switch("loading", "disabled", member="w_card")
405 | def on_cancel(self, *args):
406 | """Exit without doing anything."""
407 | self.value = False
408 |
409 |
410 | class MoveAssetDialog(v.Dialog):
411 | """A dialog to confirm the move of an asset."""
412 |
413 | # -- Variables -------------------------------------------------------------
414 |
415 | asset: ee.Asset
416 | "The asset to delete"
417 |
418 | # -- Widgets ---------------------------------------------------------------
419 | w_asset: v.TextField
420 | "The destination to move"
421 |
422 | w_confirm: v.Btn
423 | "The confirm button"
424 |
425 | w_cancel: v.Btn
426 | "The cancel button"
427 |
428 | def __init__(self, asset: Optional[ee.Asset] = None):
429 | """Initialize the class."""
430 | # start by defining all the widgets
431 | # fmt: off
432 | self.w_asset = v.TextField(placeholder="Destination", v_model="", clearable=True, outlined=True, class_="ma-1")
433 | self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
434 | self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
435 | w_title = v.CardTitle(children=["Delete the assets"])
436 | disclaimer = 'Clicking on "confirm" will move the following asset to the destination. This initial asset is not deleted.'
437 | option = 'Click on "cancel" to abort the move.'
438 | self.ul = v.Html(tag="ul", children=[])
439 | w_content = v.CardText(children=[self.w_asset, disclaimer, option, self.ul])
440 | w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
441 | self.w_card = v.Card(children=[w_title, w_content, w_actions])
442 | # fmt: on
443 |
444 | super().__init__(children=[self.w_card], max_width="50%", persistent=True)
445 |
446 | # js interaction with the btns
447 | self.w_confirm.on_event("click", self.on_confirm)
448 | self.w_cancel.on_event("click", self.on_cancel)
449 |
450 | def reload(self, asset: ee.Asset):
451 | """Reload the dialog with a new asset."""
452 | # We should never arrive here with a non asset
453 | # but to avoid catastrophic destruction we will empty the list first
454 | if asset is None or str(asset) == ".":
455 | self.ul.children = []
456 |
457 | # save the asset as a member and read it
458 | self.asset = asset
459 | assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset]
460 | self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets]
461 |
462 | @switch("loading", "disabled", member="w_card")
463 | def on_confirm(self, *args):
464 | """Confirm the deletion."""
465 | # remove the warnings
466 | self.w_asset.error_messages = []
467 |
468 | # delete the asset and close the dialog
469 | try:
470 | self.asset.move(ee.Asset(self.w_asset.v_model))
471 | self.value = False
472 | except Exception as e:
473 | self.w_asset.error_messages = [str(e)]
474 |
475 | @switch("loading", "disabled", member="w_card")
476 | def on_cancel(self, *args):
477 | """Exit without doing anything."""
478 | self.value = False
479 |
480 |
481 | class AssetDialog(v.Dialog):
482 | """A dialog to view an asset."""
483 |
484 | # -- Variables -------------------------------------------------------------
485 |
486 | asset: ee.Asset
487 | "The asset to delete"
488 |
489 | # -- Widgets ---------------------------------------------------------------
490 |
491 | w_exit: v.Btn
492 | "The exit button"
493 |
494 | def __init__(self, asset: Optional[ee.Asset] = None):
495 | """Initialize the class."""
496 | # start by defining all the widgets
497 | # fmt: off
498 | self.w_exit = v.Btn(children=[v.Icon(children="mdi-check"), "Exit"])
499 | self.w_title = v.CardTitle(children=["Delete the assets"])
500 | w_content = v.CardText(children=[""])
501 | w_actions = v.CardActions(children=[v.Spacer(), self.w_exit])
502 | self.w_card = v.Card(children=[self.w_title, w_content, w_actions])
503 | # fmt: on
504 |
505 | super().__init__(children=[self.w_card], max_width="50%", persistent=True)
506 |
507 | # js interaction with the btns
508 | self.w_exit.on_event("click", self.on_exit)
509 |
510 | def reload(self, asset: ee.Asset):
511 | """Reload the dialog with a new asset."""
512 | # We should never arrive here with a non asset
513 | # but to avoid catastrophic destruction we will empty the list first
514 | if asset is None or str(asset) == ".":
515 | self.ul.children = []
516 |
517 | # save the asset as a member and read it
518 | self.asset = asset
519 | self.w_title.children = [f"Viewing {asset.name}"]
520 |
521 | @switch("loading", "disabled", member="w_card")
522 | def on_exit(self, *args):
523 | """Exit without doing anything."""
524 | self.value = False
525 |
526 |
527 | class CreateFolderDialog(v.Dialog):
528 | """A dialog to create a new folder asset."""
529 |
530 | # -- Variables -------------------------------------------------------------
531 |
532 | folder: ee.Asset
533 | "The current folder where to create the new folder."
534 |
535 | # -- Widgets ---------------------------------------------------------------
536 | w_asset: v.TextField
537 | "The destination to move"
538 |
539 | w_confirm: v.Btn
540 | "The confirm button"
541 |
542 | w_cancel: v.Btn
543 | "The cancel button"
544 |
545 | def __init__(self, asset: Optional[ee.Asset] = None):
546 | """Initialize the class."""
547 | # start by defining all the widgets
548 | # fmt: off
549 | self.w_asset = v.TextField(placeholder="Folder name", v_model="", clearable=True, outlined=True, class_="ma-1")
550 | self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
551 | self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
552 | w_title = v.CardTitle(children=["Create a new folder"])
553 | w_content = v.CardText(children=[self.w_asset])
554 | w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
555 | self.w_card = v.Card(children=[w_title, w_content, w_actions])
556 | # fmt: on
557 |
558 | super().__init__(children=[self.w_card], max_width="50%", persistent=True)
559 |
560 | # js interaction with the btns
561 | self.w_confirm.on_event("click", self.on_confirm)
562 | self.w_cancel.on_event("click", self.on_cancel)
563 |
564 | def reload(self, folder: ee.Asset):
565 | """Reload the dialog with a new asset."""
566 | # check the new destination is at least a project
567 | if not ee.Asset(folder).is_absolute():
568 | return
569 |
570 | self.folder = folder
571 | self.w_asset.prefix = f"{folder}/"
572 | self.w_asset.v_model = ""
573 |
574 | @switch("loading", "disabled", member="w_card")
575 | def on_confirm(self, *args):
576 | """Confirm the deletion."""
577 | # remove the warnings
578 | self.w_asset.error_messages = []
579 |
580 | # crezte the folder and close the dialog
581 | try:
582 | (self.folder / self.w_asset.v_model).mkdir(exist_ok=True, parents=True)
583 | self.value = False
584 | except Exception as e:
585 | self.w_asset.error_messages = [str(e)]
586 |
587 | @switch("loading", "disabled", member="w_card")
588 | def on_cancel(self, *args):
589 | """Exit without doing anything."""
590 | self.value = False
591 |
--------------------------------------------------------------------------------