├── 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 | 6 | 7 | 8 | 15 | 16 | 17 |
9 | 10 | 12rambau
11 | Pierrick Rambaud 12 |
13 | 💻 14 |
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 | "[![github](https://img.shields.io/badge/-see%20sources-white?logo=github&labelColor=555)](https://github.com/gee-community/geetools/blob/main/docs/usage/plot/map-featurecollection.ipynb)\n", 33 | "[![colab](https://img.shields.io/badge/-open%20in%20colab-blue?logo=googlecolab&labelColor=555)](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 | "[![github](https://img.shields.io/badge/-see%20sources-white?logo=github&labelColor=555)](https://github.com/gee-community/geetools/blob/main/docs/usage/plot/map-image.ipynb)\n", 33 | "[![colab](https://img.shields.io/badge/-open%20in%20colab-blue?logo=googlecolab&labelColor=555)](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 | "[![github](https://img.shields.io/badge/-see%20sources-white?logo=github&labelColor=555)](https://github.com/gee-community/ipygee/blob/main/docs/usage/plot/plot-imagecollection.ipynb)\n", 33 | "[![colab](https://img.shields.io/badge/-open%20in%20colab-blue?logo=googlecolab&labelColor=555)](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 | "[![github](https://img.shields.io/badge/-see%20sources-white?logo=github&labelColor=555)](https://github.com/gee-community/ipygee/blob/main/docs/usage/plot/plot-featurecollection.ipynb)\n", 33 | "[![colab](https://img.shields.io/badge/-open%20in%20colab-blue?logo=googlecolab&labelColor=555)](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 | --------------------------------------------------------------------------------