├── geoviews ├── bokeh.ext.json ├── tests │ ├── data │ │ └── __init__.py │ ├── ui │ │ ├── __init__.py │ │ └── test_plot.py │ ├── plotting │ │ ├── __init__.py │ │ ├── bokeh │ │ │ ├── __init__.py │ │ │ ├── test_models.py │ │ │ ├── test_tiles.py │ │ │ └── test_bokeh_plot.py │ │ └── mpl │ │ │ └── test_plot.py │ ├── __init__.py │ ├── test_import.py │ ├── test_conversions.py │ ├── test_util.py │ ├── conftest.py │ └── test_operation.py ├── icons │ ├── PolyBreak.png │ ├── DenoteBackground.png │ └── DenoteForeground.png ├── index.ts ├── data │ └── __init__.py ├── models │ ├── index.ts │ ├── __init__.py │ ├── clear_tool.ts │ ├── restore_tool.ts │ ├── checkpoint_tool.ts │ ├── custom_tools.py │ ├── wind_barb.py │ └── wind_barb.ts ├── __main__.py ├── feature.py ├── package.json ├── element │ ├── comparison.py │ └── __init__.py ├── plotting │ ├── __init__.py │ ├── plot.py │ └── mpl │ │ └── chart.py ├── tsconfig.json ├── streams.py ├── __version.py ├── operation │ ├── __init__.py │ └── regrid.py ├── _warnings.py ├── annotators.py ├── __init__.py └── .eslintrc.js ├── HOWTORELEASE.md ├── doc ├── _static │ ├── logo.png │ ├── favicon.ico │ ├── logo_stacked.png │ ├── images │ │ ├── census.png │ │ ├── landsat.png │ │ ├── glaciers.png │ │ ├── bay_trimesh.png │ │ └── gerrymandering.png │ ├── logo_horizontal.png │ └── custom.css ├── governance │ └── project-docs │ │ ├── LICENSE.md │ │ ├── CONTRIBUTING.md │ │ ├── GOVERNANCE.md │ │ └── MEMBERS.md ├── releases.rst ├── about.rst ├── user_guide │ ├── index.rst │ ├── Using_Features_Offline.md │ └── Using_WMTS_Offline.md ├── conf.py ├── reference_manual │ └── index.rst └── index.rst ├── .github ├── FUNDING.yml └── workflows │ ├── downstream_tests.yaml │ ├── nightly_lock.yaml │ └── docs.yaml ├── examples ├── assets │ ├── cities.csv │ └── boundaries │ │ ├── boundaries.dbf │ │ ├── boundaries.shp │ │ └── boundaries.shx ├── datasets.yml ├── conftest.py ├── gallery │ ├── matplotlib │ │ ├── tile_sources.ipynb │ │ ├── xarray_quadmesh.ipynb │ │ ├── brexit_choropleth.ipynb │ │ ├── xarray_image.ipynb │ │ ├── new_york_boroughs.ipynb │ │ ├── world_population.ipynb │ │ ├── city_population.ipynb │ │ ├── filled_contours.ipynb │ │ ├── vectorfield_example.ipynb │ │ ├── orthographic_vectorfield.ipynb │ │ ├── airport_graph.ipynb │ │ ├── great_circle.ipynb │ │ ├── katrina_track.ipynb │ │ ├── trimesh_uk.ipynb │ │ └── wind_barbs_example.ipynb │ └── bokeh │ │ ├── tile_sources.ipynb │ │ ├── xarray_quadmesh.ipynb │ │ ├── brexit_choropleth.ipynb │ │ ├── new_york_boroughs.ipynb │ │ ├── xarray_image.ipynb │ │ ├── world_population.ipynb │ │ ├── city_populations_2050.ipynb │ │ ├── vectorfield_example.ipynb │ │ ├── filled_contours.ipynb │ │ ├── orthographic_vectorfield.ipynb │ │ ├── great_circle.ipynb │ │ ├── airport_graph.ipynb │ │ ├── katrina_track.ipynb │ │ ├── trimesh_uk.ipynb │ │ └── wind_barbs_example.ipynb └── Homepage.ipynb ├── CODE_OF_CONDUCT.md ├── scripts ├── conda │ ├── build.sh │ ├── recipe-recommended │ │ └── meta.yaml │ └── recipe-core │ │ └── meta.yaml ├── download_data.py └── sync_git_tags.py ├── .pre-commit-config.yaml ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── hatch_build.py ├── pixi.toml └── README.md /geoviews/bokeh.ext.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /geoviews/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /geoviews/tests/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/bokeh/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /HOWTORELEASE.md: -------------------------------------------------------------------------------- 1 | Please see: https://holoviz.org/contributing.html#releasing 2 | -------------------------------------------------------------------------------- /doc/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/logo.png -------------------------------------------------------------------------------- /doc/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: holoviz 4 | -------------------------------------------------------------------------------- /doc/_static/logo_stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/logo_stacked.png -------------------------------------------------------------------------------- /examples/assets/cities.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/examples/assets/cities.csv -------------------------------------------------------------------------------- /geoviews/icons/PolyBreak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/geoviews/icons/PolyBreak.png -------------------------------------------------------------------------------- /doc/_static/images/census.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/images/census.png -------------------------------------------------------------------------------- /doc/_static/images/landsat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/images/landsat.png -------------------------------------------------------------------------------- /doc/_static/images/glaciers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/images/glaciers.png -------------------------------------------------------------------------------- /doc/_static/logo_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/logo_horizontal.png -------------------------------------------------------------------------------- /doc/_static/images/bay_trimesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/images/bay_trimesh.png -------------------------------------------------------------------------------- /geoviews/icons/DenoteBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/geoviews/icons/DenoteBackground.png -------------------------------------------------------------------------------- /geoviews/icons/DenoteForeground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/geoviews/icons/DenoteForeground.png -------------------------------------------------------------------------------- /doc/_static/images/gerrymandering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/doc/_static/images/gerrymandering.png -------------------------------------------------------------------------------- /examples/assets/boundaries/boundaries.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/examples/assets/boundaries/boundaries.dbf -------------------------------------------------------------------------------- /examples/assets/boundaries/boundaries.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/examples/assets/boundaries/boundaries.shp -------------------------------------------------------------------------------- /examples/assets/boundaries/boundaries.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holoviz/geoviews/HEAD/examples/assets/boundaries/boundaries.shx -------------------------------------------------------------------------------- /doc/governance/project-docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | For the license, see [HoloViz/GeoViews - LICENSE.txt](https://github.com/holoviz/geoviews/blob/main/LICENSE). 4 | -------------------------------------------------------------------------------- /geoviews/index.ts: -------------------------------------------------------------------------------- 1 | import * as GeoViews from "./models" 2 | export {GeoViews} 3 | 4 | import {register_models} from "@bokehjs/base" 5 | register_models(GeoViews as any) 6 | -------------------------------------------------------------------------------- /doc/releases.rst: -------------------------------------------------------------------------------- 1 | Releases 2 | ======== 3 | 4 | View the `Changelog on GitHub `_ for a list of all releases and changes. 5 | -------------------------------------------------------------------------------- /geoviews/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .geom_dict import GeomDictInterface 2 | from .geopandas import GeoPandasInterface 3 | from .iris import CubeInterface 4 | 5 | __all__ = ("CubeInterface", "GeoPandasInterface", "GeomDictInterface") 6 | -------------------------------------------------------------------------------- /examples/datasets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | data: 4 | 5 | - url: 'http://assets.holoviews.org/geoviews-sample-data.zip' 6 | title: 'GeoViews sample data' 7 | files: 8 | - ensemble.nc 9 | - ensembles.nc 10 | - pre-industrial.nc 11 | -------------------------------------------------------------------------------- /geoviews/models/index.ts: -------------------------------------------------------------------------------- 1 | export {CheckpointTool} from "./checkpoint_tool" 2 | export {ClearTool} from "./clear_tool" 3 | export {PolyVertexDrawTool} from "./poly_draw" 4 | export {PolyVertexEditTool} from "./poly_edit" 5 | export {RestoreTool} from "./restore_tool" 6 | export {WindBarb} from "./wind_barb" 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | For the code of conduct, see [HoloViz/HoloViz - CODE_OF_CONDUCT.md](https://github.com/holoviz/holoviz/blob/geoviews-gov/CODE_OF_CONDUCT.md). 4 | 5 | The GeoViews Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. 6 | -------------------------------------------------------------------------------- /geoviews/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from holoviews.testing import add_comparison 2 | 3 | from ..element.geo import ( 4 | FilledContours, 5 | Image, 6 | ImageStack, 7 | LineContours, 8 | Points, 9 | WindBarbs, 10 | ) 11 | 12 | add_comparison(FilledContours, Image, ImageStack, LineContours, Points, WindBarbs) 13 | -------------------------------------------------------------------------------- /geoviews/__main__.py: -------------------------------------------------------------------------------- 1 | def main(args=None): 2 | try: 3 | import pyct.cmd 4 | except ImportError: 5 | import sys 6 | 7 | from . import _missing_cmd 8 | print(_missing_cmd()) 9 | sys.exit(1) 10 | return pyct.cmd.substitute_main('geoviews',args=args) 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /doc/governance/project-docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For the contributing policy, see [HoloViz/HoloViz - CONTRIBUTING.md](https://github.com/holoviz/holoviz/blob/geoviews-gov/doc/governance/project-docs/CONTRIBUTING.md). 4 | 5 | The GeoViews Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. 6 | -------------------------------------------------------------------------------- /geoviews/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .custom_tools import ( 2 | CheckpointTool, 3 | ClearTool, 4 | PolyVertexDrawTool, 5 | PolyVertexEditTool, 6 | RestoreTool, 7 | ) 8 | from .wind_barb import WindBarb 9 | 10 | __all__ = ( 11 | "CheckpointTool", 12 | "ClearTool", 13 | "PolyVertexDrawTool", 14 | "PolyVertexEditTool", 15 | "RestoreTool", 16 | "WindBarb" 17 | ) 18 | -------------------------------------------------------------------------------- /geoviews/tests/test_import.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from subprocess import check_output 3 | from textwrap import dedent 4 | 5 | 6 | def test_no_blocklist_imports(): 7 | check = """\ 8 | import sys 9 | import geoviews as gv 10 | 11 | blocklist = {"panel", "IPython", "datashader", "iris", "dask", "bokeh.models"} 12 | mods = blocklist & set(sys.modules) 13 | 14 | if mods: 15 | print(", ".join(mods), end="") 16 | """ 17 | 18 | output = check_output([sys.executable, '-c', dedent(check)]) 19 | 20 | assert output == b"" 21 | -------------------------------------------------------------------------------- /doc/governance/project-docs/GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance Policy 2 | 3 | 4 | The "Project" is herein defined as the activities related to this specific GitHub repository [`GeoViews`](https://github.com/holoviz/geoviews), within the `HoloViz` GitHub Organization. 5 | 6 | 7 | This Project adopts the governance specified by all of the numbered sections of [HoloViz/HoloViz - GOVERNANCE.md](https://github.com/holoviz/holoviz/blob/geoviews-gov/doc/governance/project-docs/GOVERNANCE.md). 8 | 9 | 10 | The GeoViews Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. 11 | -------------------------------------------------------------------------------- /geoviews/feature.py: -------------------------------------------------------------------------------- 1 | from cartopy import feature as cf 2 | 3 | from .element import Feature 4 | 5 | borders = Feature(cf.BORDERS, group='Borders') 6 | coastline = Feature(cf.COASTLINE, group='Coastline') 7 | land = Feature(cf.LAND, group='Land') 8 | lakes = Feature(cf.LAKES, group='Lakes') 9 | ocean = Feature(cf.OCEAN, group='Ocean') 10 | rivers = Feature(cf.RIVERS, group='Rivers') 11 | states = Feature(cf.STATES, group='States') 12 | grid = Feature(cf.NaturalEarthFeature(category='physical', 13 | name='graticules_30', 14 | scale='110m'), group='Grid') 15 | -------------------------------------------------------------------------------- /doc/governance/project-docs/MEMBERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | 4 | For member policy, see the description at the top of [HoloViz/HoloViz - MEMBERS.md](https://github.com/holoviz/holoviz/blob/geoviews-gov/doc/governance/project-docs/MEMBERS.md). 5 | 6 | 7 | The GeoViews Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. 8 | 9 | 10 | | **NAME** | **Role** | **GitHub Handle** | 11 | | --- | --- | --- | 12 | | Philipp Rudiger | Project Director | [philippjfr](https://github.com/philippjfr) | 13 | | Simon Høxbro Hansen | Maintainer | [hoxbro](https://github.com/hoxbro) | 14 | | Andrew Huang | Maintainer | [ahuang11](https://github.com/ahuang11) | 15 | -------------------------------------------------------------------------------- /.github/workflows/downstream_tests.yaml: -------------------------------------------------------------------------------- 1 | name: downstream_tests 2 | 3 | on: 4 | # Run this workflow after the build workflow has completed. 5 | workflow_run: 6 | workflows: [packages] 7 | types: [completed] 8 | # Or by triggering it manually via Github's UI 9 | workflow_dispatch: 10 | inputs: 11 | manual: 12 | description: don't change me! 13 | type: boolean 14 | required: true 15 | default: true 16 | 17 | jobs: 18 | downstream_tests: 19 | uses: holoviz-dev/holoviz_tasks/.github/workflows/run_downstream_tests.yaml@main 20 | with: 21 | downstream_repos_as_json: "{\"downstream_repo\":[\"hvplot\"]}" 22 | secrets: 23 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 24 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/bokeh/test_models.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from geoviews.models.custom_tools import _BokehCheck 6 | 7 | 8 | def test_bokeh_version(): 9 | # Can be removed when minimum Python version is 3.11 10 | tomllib = pytest.importorskip("tomllib") 11 | 12 | pyproject = Path(__file__).parents[4] / "pyproject.toml" 13 | pyproject.resolve(strict=True) 14 | requires = tomllib.loads(pyproject.read_text())["build-system"]["requires"] 15 | 16 | for req in requires: 17 | if req.startswith("bokeh"): 18 | break 19 | else: 20 | raise ValueError("Bokeh not found in build-system requires") 21 | 22 | assert req == _BokehCheck._bokeh_require_version 23 | -------------------------------------------------------------------------------- /geoviews/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holoviz/geoviews", 3 | "version": "1.15.0-a.2", 4 | "description": "Simple, concise geographical visualization in Python", 5 | "license": "BSD-3-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/holoviz/geoviews.git" 9 | }, 10 | "dependencies": { 11 | "@bokeh/bokehjs": "^3.8.0", 12 | "@stylistic/eslint-plugin": "^1.6.3", 13 | "@typescript-eslint/eslint-plugin": "^7.2.0", 14 | "@typescript-eslint/parser": "^7.2.0", 15 | "eslint": "^8.57.0" 16 | }, 17 | "files": [ 18 | "dist/**/*.{js,js.map,d.ts,json,css}" 19 | ], 20 | "main": "dist/geoviews.min.js", 21 | "scripts": { 22 | "lint": "npx eslint './*.ts' './models/**/*.ts'" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/nightly_lock.yaml: -------------------------------------------------------------------------------- 1 | name: nightly_lock 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | env: 8 | PACKAGE: "geoviews" 9 | 10 | jobs: 11 | pixi_lock: 12 | if: ${{ !github.event.repository.fork }} 13 | name: Pixi lock 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | steps: 17 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 18 | - name: Upload lock-file to S3 19 | env: 20 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 21 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 22 | AWS_DEFAULT_REGION: "eu-west-1" 23 | run: | 24 | zip $(date +%Y-%m-%d).zip pixi.lock pixi.toml 25 | aws s3 cp ./$(date +%Y-%m-%d).zip s3://assets.holoviz.org/lock/$PACKAGE/ 26 | -------------------------------------------------------------------------------- /examples/conftest.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec 2 | 3 | # depend on optional iris, xesmf, etc 4 | collect_ignore_glob = [ 5 | "Homepage.ipynb", 6 | "user_guide/Resampling_Grids.ipynb", 7 | "user_guide/Gridded_Datasets_*.ipynb", 8 | "gallery/bokeh/xarray_gridded.ipynb", 9 | "gallery/*/xarray_image.ipynb", 10 | "gallery/*/xarray_quadmesh.ipynb", 11 | "gallery/*/katrina_track.ipynb", 12 | ] 13 | 14 | 15 | # Needed for gpd.read_file 16 | if find_spec("fiona") is None: 17 | collect_ignore_glob += [ 18 | "gallery/bokeh/brexit_choropleth.ipynb", 19 | "gallery/bokeh/new_york_boroughs.ipynb", 20 | "gallery/matplotlib/brexit_choropleth.ipynb", 21 | "gallery/matplotlib/new_york_boroughs.ipynb", 22 | "user_guide/Geometries.ipynb", 23 | "user_guide/Working_with_Bokeh.ipynb", 24 | ] 25 | -------------------------------------------------------------------------------- /doc/_static/custom.css: -------------------------------------------------------------------------------- 1 | :root[data-theme="light"], 2 | :root[data-theme="dark"] { 3 | --pst-color-inline-code: var(--holoviz-main-color); 4 | --pst-color-link-hover: var(--holoviz-main-color); 5 | --pst-color-link: var(--holoviz-main-color); 6 | --pst-color-primary: var(--holoviz-main-color); 7 | --pst-color-secondary-highlight: var(--holoviz-main-color); 8 | --pst-color-secondary: var(--holoviz-main-color); 9 | --pst-violet-600: var(--holoviz-main-color); /* back to top hover */ 10 | --sd-color-card-border-hover: var(--holoviz-main-color); 11 | } 12 | 13 | :root[data-theme="light"] { 14 | --holoviz-main-color: #387bb2; 15 | } 16 | 17 | :root[data-theme="dark"] { 18 | --holoviz-main-color: #387bb2; 19 | } 20 | 21 | #binder-link { 22 | display: inline-block; 23 | font-size: 0.9rem; 24 | padding-left: 1.5rem; 25 | padding-top: 1rem; 26 | padding-bottom: 1rem; 27 | } 28 | -------------------------------------------------------------------------------- /scripts/conda/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | PACKAGE="geoviews" 6 | 7 | python -m build --sdist . 8 | 9 | VERSION=$(python -c "import $PACKAGE; print($PACKAGE._version.__version__)") 10 | export VERSION 11 | 12 | BK_CHANNEL=$(python -c " 13 | import bokeh 14 | from packaging.version import Version 15 | 16 | if Version(bokeh.__version__).is_prerelease: 17 | print('bokeh/label/rc') 18 | else: 19 | print('bokeh') 20 | ") 21 | 22 | conda build scripts/conda/recipe-core --no-anaconda-upload --no-verify -c pyviz -c "$BK_CHANNEL" -c conda-forge --package-format 2 23 | conda build scripts/conda/recipe-recommended --no-anaconda-upload --no-verify -c pyviz -c "$BK_CHANNEL" -c conda-forge --package-format 2 24 | 25 | mv "$CONDA_PREFIX/conda-bld/noarch/$PACKAGE-core-$VERSION-py_0.conda" dist 26 | mv "$CONDA_PREFIX/conda-bld/noarch/$PACKAGE-$VERSION-py_0.conda" dist 27 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/bokeh/test_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from geoviews.element import WMTS 5 | from geoviews.plotting.bokeh import TilePlot 6 | from geoviews.tile_sources import OSM 7 | 8 | from .test_bokeh_plot import TestBokehPlot, bokeh_renderer 9 | 10 | 11 | class TestWMTSPlot(TestBokehPlot): 12 | 13 | def test_xyzservices_tileprovider(self): 14 | xyzservices = pytest.importorskip("xyzservices") 15 | osm = xyzservices.providers.OpenStreetMap.Mapnik 16 | 17 | tiles = WMTS(osm) 18 | plot = bokeh_renderer.get_plot(tiles) 19 | glyph = plot.handles["glyph"] 20 | assert glyph.attribution == osm.html_attribution 21 | assert glyph.url == osm.build_url(scale_factor="@2x") 22 | assert glyph.max_zoom == osm.max_zoom 23 | 24 | def test_max_zoom_default(self): 25 | tile_source = TilePlot(OSM).get_data(OSM, (0, 0, 0, 0), {})[1]["tile_source"] 26 | assert tile_source.max_zoom == 19 27 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/bokeh/test_bokeh_plot.py: -------------------------------------------------------------------------------- 1 | import pyviz_comms as comms 2 | from holoviews.plotting.bokeh.element import ElementPlot 3 | from param import concrete_descendents 4 | 5 | from geoviews import Store 6 | 7 | bokeh_renderer = Store.renderers['bokeh'] 8 | 9 | 10 | class TestBokehPlot: 11 | 12 | def setup_method(self): 13 | self.previous_backend = Store.current_backend 14 | self.comm_manager = bokeh_renderer.comm_manager 15 | bokeh_renderer.comm_manager = comms.CommManager 16 | Store.set_current_backend('bokeh') 17 | self._padding = {} 18 | for plot in concrete_descendents(ElementPlot).values(): 19 | self._padding[plot] = plot.padding 20 | plot.padding = 0 21 | 22 | def teardown_method(self): 23 | Store.current_backend = self.previous_backend 24 | bokeh_renderer.comm_manager = self.comm_manager 25 | for plot, padding in self._padding.items(): 26 | plot.padding = padding 27 | -------------------------------------------------------------------------------- /scripts/conda/recipe-recommended/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set pyproject = load_file_data('../../../pyproject.toml', from_recipe_dir=True) %} 2 | {% set project = pyproject['project'] %} 3 | 4 | package: 5 | name: {{ project["name"] }} 6 | version: {{ VERSION }} 7 | 8 | source: 9 | url: ../../../dist/{{ project["name"] }}-{{ VERSION }}.tar.gz 10 | 11 | build: 12 | noarch: python 13 | 14 | requirements: 15 | host: 16 | - python {{ project['requires-python'] }} 17 | {% for dep in pyproject['build-system']['requires'] %} 18 | - {{ dep }} 19 | {% endfor %} 20 | run: 21 | - python {{ project['requires-python'] }} 22 | - geoviews-core =={{ VERSION }} 23 | {% for dep in project['optional-dependencies']['recommended'] %} 24 | - {{ dep.replace("geopandas", "geopandas-base").replace("matplotlib", "matplotlib-base") }} 25 | {% endfor %} 26 | 27 | about: 28 | home: {{ project['urls']['Homepage'] }} 29 | summary: {{ project['description'] }} 30 | license: {{ project['license'] }} 31 | -------------------------------------------------------------------------------- /scripts/download_data.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from pathlib import Path 3 | 4 | BASE_PATH = Path(__file__).resolve().parents[1] 5 | 6 | 7 | with suppress(ImportError): 8 | import pyct.cmd 9 | 10 | pyct.cmd.fetch_data( 11 | name="data", 12 | path=str(BASE_PATH / "examples"), 13 | datasets="datasets.yml", 14 | ) 15 | 16 | 17 | with suppress(ImportError): 18 | import geodatasets as gds 19 | 20 | gds.get_path("geoda airbnb") 21 | gds.get_path("nybb") 22 | 23 | 24 | with suppress(ImportError): 25 | import cftime # noqa: F401 26 | import pooch # noqa: F401 27 | import scipy # noqa: F401 28 | import xarray as xr 29 | 30 | xr.tutorial.open_dataset("air_temperature") 31 | xr.tutorial.open_dataset("rasm") 32 | 33 | with suppress(ImportError): 34 | from cartopy.feature import shapereader 35 | 36 | shapereader.natural_earth(name="coastline") 37 | shapereader.natural_earth(name="land") 38 | shapereader.natural_earth(name="ocean") 39 | shapereader.natural_earth(category="cultural", name="admin_0_boundary_lines_land") 40 | -------------------------------------------------------------------------------- /doc/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | GeoViews is completely open source, available under a BSD license freely for both commercial and non-commercial use. GeoViews was originally developed with the support of `Anaconda Inc. `_, and is now maintained by Anaconda developers and community contributors. 5 | 6 | GeoViews is part of the `HoloViz `_ family of tools. The `holoviz.org `_ website shows how to use GeoViews together with other libraries to solve complex problems, with detailed tutorials and examples. You can see a variety of projects using GeoViews at `examples.holoviz.org `_, and you can compare GeoViews to other available tools at `pyviz.org `_. 7 | 8 | If you have any questions or usage issues visit the `GeoViews Discourse `_ site. 9 | 10 | If you like GeoViews and have built something you want to share, tweet a link or screenshot of your latest creation at @HoloViews, along with any other library you used (@HoloViz_org, @Panel_org, @Datashader, @Bokeh, @Matplotlib, etc.). Thanks! 11 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/tile_sources.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "\n", 11 | "from geoviews import opts, tile_sources as gvts\n", 12 | "\n", 13 | "gv.extension('matplotlib')" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Plot" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "opts.defaults(\n", 30 | " opts.Layout(sublabel_format='', vspace=0.1, hspace=0.1, fig_size=200),\n", 31 | " opts.WMTS(zoom=0))\n", 32 | "\n", 33 | "(gvts.StamenToner + gvts.EsriNatGeo + gvts.EsriImagery +\n", 34 | " gvts.EsriUSATopo + gvts.EsriTerrain + gvts.EsriReference + gvts.StamenTerrain).cols(4)" 35 | ] 36 | } 37 | ], 38 | "metadata": { 39 | "language_info": { 40 | "name": "python", 41 | "pygments_lexer": "ipython3" 42 | } 43 | }, 44 | "nbformat": 4, 45 | "nbformat_minor": 2 46 | } 47 | -------------------------------------------------------------------------------- /geoviews/tests/test_conversions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | try: 4 | from iris.tests.stock import lat_lon_cube 5 | except ImportError: 6 | raise unittest.SkipTest("iris not available") from None 7 | 8 | from holoviews.core import HoloMap 9 | from holoviews.element import Curve 10 | from holoviews.testing import assert_element_equal 11 | 12 | from geoviews.element import Dataset, Image, is_geographic 13 | 14 | 15 | class TestConversions: 16 | 17 | def setup_method(self): 18 | self.cube = lat_lon_cube() 19 | 20 | def test_is_geographic_2d(self): 21 | assert is_geographic(Dataset(self.cube, kdims=['longitude', 'latitude']), ['longitude', 'latitude']) 22 | 23 | def test_geographic_conversion(self): 24 | assert_element_equal(Dataset(self.cube, kdims=['longitude', 'latitude']).to.image(), Image(self.cube, kdims=['longitude', 'latitude'])) 25 | 26 | def test_nongeographic_conversion(self): 27 | converted = Dataset(self.cube, kdims=['longitude', 'latitude']).to.curve(['longitude']) 28 | assert isinstance(converted, HoloMap) 29 | assert converted.kdims == ['latitude'] 30 | assert isinstance(converted.last, Curve) 31 | -------------------------------------------------------------------------------- /geoviews/element/comparison.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from holoviews.element.comparison import Comparison as HvComparison 4 | 5 | from .geo import FilledContours, Image, ImageStack, LineContours, Points, WindBarbs 6 | 7 | 8 | class Comparison(HvComparison): 9 | 10 | @classmethod 11 | def register(cls): 12 | super().register() 13 | cls.equality_type_funcs[Image] = cls.compare_dataset 14 | cls.equality_type_funcs[ImageStack] = cls.compare_dataset 15 | cls.equality_type_funcs[Points] = cls.compare_dataset 16 | cls.equality_type_funcs[LineContours] = cls.compare_dataset 17 | cls.equality_type_funcs[FilledContours] = cls.compare_dataset 18 | cls.equality_type_funcs[WindBarbs] = cls.compare_dataset 19 | return cls.equality_type_funcs 20 | 21 | 22 | class ComparisonTestCase(Comparison, TestCase): 23 | """Class to integrate the Comparison class with unittest.TestCase.""" 24 | 25 | def __init__(self, *args, **kwargs): 26 | TestCase.__init__(self, *args, **kwargs) 27 | registry = Comparison.register() 28 | for k, v in registry.items(): 29 | self.addTypeEqualityFunc(k, v) 30 | -------------------------------------------------------------------------------- /scripts/sync_git_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to sync tags from upstream repository to forked repository 3 | """ 4 | 5 | import sys 6 | from subprocess import run 7 | 8 | 9 | def main(package: str) -> None: 10 | origin = run( 11 | ["git", "remote", "get-url", "origin"], check=True, capture_output=True 12 | ) 13 | upstream = run( 14 | ["git", "remote", "get-url", "upstream"], check=False, capture_output=True 15 | ) 16 | url = ( 17 | f"https://github.com/holoviz/{package}.git" 18 | if origin.stdout.startswith(b"http") 19 | else f"git@github.com:holoviz/{package}.git" 20 | ) 21 | 22 | if url == origin.stdout.strip().decode(): 23 | print("Not a forked repository, exiting.") 24 | return 25 | elif upstream.returncode: 26 | print(f"Adding {url!r} as remote upstream") 27 | run(["git", "remote", "add", "upstream", url], check=True, capture_output=True) 28 | 29 | print(f"Syncing tags from {package} repository with your forked repository") 30 | run(["git", "fetch", "--tags", "upstream"], check=True, capture_output=True) 31 | run(["git", "push", "--tags"], check=True, capture_output=True) 32 | 33 | 34 | if __name__ == "__main__": 35 | main(sys.argv[1]) 36 | -------------------------------------------------------------------------------- /scripts/conda/recipe-core/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set pyproject = load_file_data('../../../pyproject.toml', from_recipe_dir=True) %} 2 | {% set project = pyproject['project'] %} 3 | 4 | package: 5 | name: {{ project["name"] }}-core 6 | version: {{ VERSION }} 7 | 8 | source: 9 | url: ../../../dist/{{ project["name"] }}-{{ VERSION }}.tar.gz 10 | 11 | build: 12 | noarch: python 13 | script: {{ PYTHON }} -m pip install --no-deps -vv . 14 | entry_points: 15 | {% for group,epoints in project.get("entry_points",{}).items() %} 16 | {% for entry_point in epoints %} 17 | - {{ entry_point }} 18 | {% endfor %} 19 | {% endfor %} 20 | 21 | requirements: 22 | build: 23 | - python {{ project['requires-python'] }} 24 | {% for dep in pyproject['build-system']['requires'] %} 25 | - {{ dep }} 26 | {% endfor %} 27 | run: 28 | - python {{ project['requires-python'] }} 29 | {% for dep in project.get('dependencies', []) %} 30 | - {{ dep }} 31 | {% endfor %} 32 | 33 | test: 34 | imports: 35 | - {{ project["name"] }} 36 | commands: 37 | - pip check 38 | requires: 39 | - pip 40 | 41 | about: 42 | home: {{ project['urls']['Homepage'] }} 43 | summary: {{ project['description'] }} 44 | license: {{ project['license'] }} 45 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/tile_sources.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "\n", 11 | "from geoviews import opts, tile_sources as gvts\n", 12 | "\n", 13 | "gv.extension('bokeh')" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## List" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "gvts.tile_sources\n" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "## Plot" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "opts.defaults(\n", 46 | " opts.WMTS(width=200, height=200, xaxis=None, yaxis=None, level='annotation'))\n", 47 | "\n", 48 | "gv.NdLayout(gvts.tile_sources.items())" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "language_info": { 54 | "name": "python", 55 | "pygments_lexer": "ipython3" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 2 60 | } 61 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/xarray_quadmesh.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import xarray as xr\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "gv.extension('bokeh')" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "## Define data" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "tiles = gv.tile_sources.EsriImagery\n", 29 | "rasm = xr.tutorial.open_dataset('rasm').load()\n", 30 | "qmeshes = gv.Dataset(rasm.Tair[::4, ::3, ::3]).to(gv.QuadMesh, groupby='time')" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## Plot" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "tiles * qmeshes.opts(width=700, height=350)" 47 | ] 48 | } 49 | ], 50 | "metadata": { 51 | "language_info": { 52 | "name": "python", 53 | "pygments_lexer": "ipython3" 54 | } 55 | }, 56 | "nbformat": 4, 57 | "nbformat_minor": 2 58 | } 59 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: (\.min\.js$|\.svg$|\.html$) 2 | default_stages: [pre-commit] 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v6.0.0 6 | hooks: 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: mixed-line-ending 10 | - id: check-builtin-literals 11 | - id: check-case-conflict 12 | - id: check-docstring-first 13 | - id: check-executables-have-shebangs 14 | - id: check-toml 15 | - id: check-yaml 16 | exclude: conda.recipe 17 | - id: check-json 18 | - id: detect-private-key 19 | - repo: https://github.com/astral-sh/ruff-pre-commit 20 | rev: v0.14.3 21 | hooks: 22 | - id: ruff-check 23 | files: geoviews/ 24 | - repo: https://github.com/hoxbro/clean_notebook 25 | rev: v0.1.17 26 | hooks: 27 | - id: clean-notebook 28 | - repo: https://github.com/crate-ci/typos 29 | rev: v1.39.0 30 | hooks: 31 | - id: typos 32 | - repo: https://github.com/pre-commit/pygrep-hooks 33 | rev: v1.10.0 34 | hooks: 35 | - id: rst-backticks 36 | - id: rst-directive-colons 37 | - id: rst-inline-touching-normal 38 | - repo: https://github.com/shellcheck-py/shellcheck-py 39 | rev: v0.11.0.1 40 | hooks: 41 | - id: shellcheck 42 | -------------------------------------------------------------------------------- /geoviews/plotting/__init__.py: -------------------------------------------------------------------------------- 1 | from holoviews import Store, extension 2 | from holoviews.core.options import Compositor 3 | from holoviews.operation.element import contours 4 | 5 | from ..element import Contours, Polygons 6 | 7 | 8 | def _load_bokeh(): 9 | from geoviews.plotting import bokeh # noqa: F401 10 | 11 | extension.register_backend_callback('bokeh', _load_bokeh) 12 | 13 | def _load_mpl(): 14 | from geoviews.plotting import mpl # noqa: F401 15 | extension.register_backend_callback('matplotlib', _load_mpl) 16 | 17 | backends = Store.loaded_backends() 18 | if 'bokeh' in backends: 19 | _load_bokeh() 20 | if 'matplotlib' in backends: 21 | _load_mpl() 22 | 23 | Compositor.register(Compositor("LineContours", contours, None, 24 | 'data', transfer_options=True, 25 | transfer_parameters=True, 26 | output_type=Contours, 27 | backends=['bokeh', 'matplotlib'])) 28 | Compositor.register(Compositor("FilledContours", contours.instance(filled=True), 29 | None, 'data', transfer_options=True, 30 | transfer_parameters=True, 31 | output_type=Polygons, 32 | backends=['bokeh', 'matplotlib'])) 33 | -------------------------------------------------------------------------------- /geoviews/models/clear_tool.ts: -------------------------------------------------------------------------------- 1 | import type * as p from "@bokehjs/core/properties" 2 | import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" 3 | import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" 4 | import {tool_icon_reset} from "@bokehjs/styles/icons.css" 5 | 6 | export class ClearToolView extends ActionToolView { 7 | declare model: ClearTool 8 | 9 | doit(): void { 10 | for (const source of this.model.sources) { 11 | source.clear() 12 | } 13 | } 14 | } 15 | 16 | export namespace ClearTool { 17 | export type Attrs = p.AttrsOf 18 | export type Props = ActionTool.Props & { 19 | sources: p.Property 20 | } 21 | } 22 | 23 | export interface ClearTool extends ClearTool.Attrs {} 24 | 25 | export class ClearTool extends ActionTool { 26 | declare properties: ClearTool.Props 27 | 28 | constructor(attrs?: Partial) { 29 | super(attrs) 30 | } 31 | 32 | static override __module__ = "geoviews.models.custom_tools" 33 | 34 | static { 35 | this.prototype.default_view = ClearToolView 36 | 37 | this.define(({List, Ref}) => ({ 38 | sources: [ List(Ref(ColumnDataSource)), [] ], 39 | })) 40 | } 41 | 42 | override tool_name = "Clear data" 43 | override tool_icon = tool_icon_reset 44 | } 45 | -------------------------------------------------------------------------------- /geoviews/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitThis": true, 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "noImplicitOverride": true, 9 | "strictNullChecks": true, 10 | "strictBindCallApply": true, 11 | "strictFunctionTypes": true, 12 | "strictPropertyInitialization": false, 13 | "useUnknownInCatchVariables": true, 14 | "alwaysStrict": true, 15 | "noErrorTruncation": true, 16 | "noEmitOnError": false, 17 | "stripInternal": true, 18 | "declaration": true, 19 | "sourceMap": true, 20 | "declarationMap": true, 21 | "importHelpers": false, 22 | "experimentalDecorators": true, 23 | "module": "ES2022", 24 | "moduleResolution": "node", 25 | "isolatedModules": true, 26 | "verbatimModuleSyntax": true, 27 | "resolveJsonModule": true, 28 | "esModuleInterop": true, 29 | "skipLibCheck": true, 30 | "target": "ES2022", 31 | "lib": ["es2022", "dom", "dom.iterable"], 32 | "baseUrl": ".", 33 | "outDir": "./dist/lib", 34 | "rootDirs": [".", "./dist/dts/"], 35 | "paths": { 36 | "@bokehjs/*": [ 37 | "node_modules/@bokeh/bokehjs/build/js/lib/*" 38 | ] 39 | } 40 | }, 41 | "include": ["./*.ts", "./models/", "./dist/dts/"] 42 | } 43 | -------------------------------------------------------------------------------- /geoviews/tests/test_util.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import pytest 3 | 4 | import geoviews as gv 5 | from geoviews.util import from_xarray, process_crs 6 | 7 | try: 8 | import rioxarray as rxr 9 | except ImportError: 10 | rxr = None 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "raw_crs", 15 | [ 16 | "+init=epsg:26911", 17 | "4326", 18 | 4326, 19 | "epsg:4326", 20 | "EPSG: 4326", 21 | ccrs.PlateCarree(), 22 | ], 23 | ) 24 | def test_process_crs(raw_crs) -> None: 25 | crs = process_crs(raw_crs) 26 | assert isinstance(crs, ccrs.CRS) 27 | 28 | 29 | # To avoid '+init=:' syntax is deprecated. 30 | @pytest.mark.filterwarnings("ignore::FutureWarning") 31 | def test_process_crs_raises_error(): 32 | with pytest.raises( 33 | ValueError, match="must be defined as a EPSG code, proj4 string" 34 | ): 35 | process_crs(43823) 36 | 37 | 38 | @pytest.mark.skipif(rxr is None, reason="Needs rioxarray to be installed") 39 | def test_from_xarray(): 40 | file = ( 41 | "https://github.com/holoviz/hvplot/raw/main/hvplot/tests/data/RGB-red.byte.tif" 42 | ) 43 | output = from_xarray(rxr.open_rasterio(file)) 44 | 45 | assert isinstance(output, gv.RGB) 46 | assert sorted(map(str, output.kdims)) == ["x", "y"] 47 | assert isinstance(output.crs, ccrs.CRS) 48 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/xarray_quadmesh.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import xarray as xr\n", 10 | "import cartopy.crs as ccrs\n", 11 | "import geoviews as gv\n", 12 | "\n", 13 | "gv.extension('matplotlib')\n", 14 | "gv.output(fig='svg', size=300)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "tiles = gv.tile_sources.EsriImagery\n", 31 | "rasm = xr.tutorial.open_dataset('rasm').load()\n", 32 | "qmeshes = gv.Dataset(rasm.Tair[::4, ::3, ::3]).to(gv.QuadMesh, groupby='time')" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Plot" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "tiles.opts(zoom=1) * qmeshes.opts(projection=ccrs.GOOGLE_MERCATOR)" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "language_info": { 54 | "name": "python", 55 | "pygments_lexer": "ipython3" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 2 60 | } 61 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | /doc/reference_Manual/* 62 | !/doc/reference_Manual/index.rst 63 | jupyter_execute/ 64 | builtdocs/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | .DS_Store 76 | # Website 77 | doc/_build 78 | 79 | geoviews/.version 80 | *~ 81 | 82 | .doit.db 83 | .pytest_cache 84 | 85 | node_modules/ 86 | .bokeh 87 | examples/data 88 | 89 | # Pixi + hatch 90 | pixi.lock 91 | .pixi 92 | geoviews/_version.py 93 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/brexit_choropleth.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geopandas as gpd\n", 11 | "import geoviews as gv\n", 12 | "\n", 13 | "gv.extension('matplotlib')\n", 14 | "gv.output(fig='svg', size=200)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Declaring data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "geometries = gpd.read_file('../../assets/boundaries/boundaries.shp')\n", 31 | "referendum = pd.read_csv('../../assets/referendum.csv')\n", 32 | "gdf = gpd.GeoDataFrame(pd.merge(geometries, referendum))" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Plot" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "gv.Polygons(gdf, vdims=['name', 'leaveVoteshare'], label='Brexit Referendum Vote').opts(\n", 49 | " color='leaveVoteshare', colorbar=True, padding=0.1)" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "language_info": { 55 | "name": "python", 56 | "pygments_lexer": "ipython3" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 2 61 | } 62 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/brexit_choropleth.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geopandas as gpd\n", 11 | "import geoviews as gv\n", 12 | "\n", 13 | "gv.extension('bokeh')" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Declaring data" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "geometries = gpd.read_file('../../assets/boundaries/boundaries.shp')\n", 30 | "referendum = pd.read_csv('../../assets/referendum.csv')\n", 31 | "gdf = gpd.GeoDataFrame(pd.merge(geometries, referendum))" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Plot" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "gv.Polygons(gdf, vdims=['name', 'leaveVoteshare'], label='Brexit Referendum Vote').opts(\n", 48 | " tools=['hover'], width=550, height=700, color='leaveVoteshare',\n", 49 | " colorbar=True, toolbar='above', xaxis=None, yaxis=None)" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "language_info": { 55 | "name": "python", 56 | "pygments_lexer": "ipython3" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 2 61 | } 62 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/new_york_boroughs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geodatasets as gds\n", 10 | "import geopandas as gpd\n", 11 | "import geoviews as gv\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "tiles = gv.tile_sources.OSM\n", 31 | "\n", 32 | "# Project data to Web Mercator\n", 33 | "nybb = gpd.read_file(gds.get_path('nybb'))\n", 34 | "poly_data = nybb.to_crs(ccrs.GOOGLE_MERCATOR.proj4_init)\n", 35 | "polys = gv.Polygons(poly_data, vdims=['BoroName'], crs=ccrs.GOOGLE_MERCATOR)" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Plot" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "tiles * polys.opts(color='BoroName', cmap='Category20', width=800, height=600, tools=['hover', 'tap'])" 52 | ] 53 | } 54 | ], 55 | "metadata": { 56 | "language_info": { 57 | "name": "python", 58 | "pygments_lexer": "ipython3" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 2 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, HoloViz Developers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of cube-explorer nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/xarray_image.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "import geoviews.feature as gf\n", 11 | "import xarray as xr\n", 12 | "from cartopy import crs\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "ensemble = xr.open_dataset('../../data/ensemble.nc')\n", 31 | "dataset = gv.Dataset(ensemble, ['longitude', 'latitude', 'time'], 'surface_temperature', crs=crs.PlateCarree())\n", 32 | "images = dataset.to(gv.Image)\n", 33 | "# Equivalent full definition:\n", 34 | "# images = dataset.to(gv.Image, ['longitude', 'latitude'], 'surface_temperature', 'time')" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Plot" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "images.opts(cmap='viridis', colorbar=True, width=600, height=500) * gf.coastline" 51 | ] 52 | } 53 | ], 54 | "metadata": { 55 | "language_info": { 56 | "name": "python", 57 | "pygments_lexer": "ipython3" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 2 62 | } 63 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/xarray_image.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "import geoviews.feature as gf\n", 11 | "import xarray as xr\n", 12 | "from cartopy import crs\n", 13 | "\n", 14 | "gv.extension('matplotlib')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "ensemble = xr.open_dataset('../../data/ensemble.nc')\n", 31 | "dataset = gv.Dataset(ensemble, ['longitude', 'latitude', 'time'], 'surface_temperature', crs=crs.PlateCarree())\n", 32 | "images = dataset.to(gv.Image)\n", 33 | "# Equivalent full definition:\n", 34 | "# images = dataset.to(gv.Image, ['longitude', 'latitude'], 'surface_temperature', 'time')" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Plot" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "images.opts(cmap='viridis', colorbar=True, fig_size=250) * gf.coastline" 51 | ] 52 | } 53 | ], 54 | "metadata": { 55 | "language_info": { 56 | "name": "python", 57 | "pygments_lexer": "ipython3" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 2 62 | } 63 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/new_york_boroughs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geodatasets as gds\n", 10 | "import geopandas as gpd\n", 11 | "import geoviews as gv\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "gv.output(dpi=120, fig='svg')" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## Define data" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "tiles = gv.tile_sources.OSM \n", 32 | "\n", 33 | "# Project data to Web Mercator\n", 34 | "nybb = gpd.read_file(gds.get_path('nybb'))\n", 35 | "poly_data = nybb.to_crs(ccrs.GOOGLE_MERCATOR.proj4_init)\n", 36 | "polys = gv.Polygons(poly_data, vdims='BoroName', crs=ccrs.GOOGLE_MERCATOR)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Plot" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "tiles.opts(zoom=10) * polys.opts(color='BoroName', cmap='tab20', fig_size=200, padding=0.1)" 53 | ] 54 | } 55 | ], 56 | "metadata": { 57 | "language_info": { 58 | "name": "python", 59 | "pygments_lexer": "ipython3" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 2 64 | } 65 | -------------------------------------------------------------------------------- /geoviews/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | 3 | from holoviews.tests.conftest import ( # noqa: F401 4 | bokeh_backend, 5 | port, 6 | serve_hv, 7 | server_cleanup, 8 | ) 9 | 10 | import geoviews as gv 11 | 12 | CUSTOM_MARKS = ("ui",) 13 | 14 | 15 | def pytest_addoption(parser): 16 | for marker in CUSTOM_MARKS: 17 | parser.addoption( 18 | f"--{marker}", 19 | action="store_true", 20 | default=False, 21 | help=f"Run {marker} related tests", 22 | ) 23 | 24 | 25 | def pytest_configure(config): 26 | for marker in CUSTOM_MARKS: 27 | config.addinivalue_line("markers", f"{marker}: {marker} test marker") 28 | 29 | 30 | def pytest_collection_modifyitems(config, items): 31 | skipped, selected = [], [] 32 | markers = [m for m in CUSTOM_MARKS if config.getoption(f"--{m}")] 33 | empty = not markers 34 | for item in items: 35 | if empty and any(m in item.keywords for m in CUSTOM_MARKS): 36 | skipped.append(item) 37 | elif empty: 38 | selected.append(item) 39 | elif not empty and any(m in item.keywords for m in markers): 40 | selected.append(item) 41 | else: 42 | skipped.append(item) 43 | 44 | config.hook.pytest_deselected(items=skipped) 45 | items[:] = selected 46 | 47 | 48 | with suppress(Exception): 49 | gv.extension("bokeh") 50 | 51 | with suppress(Exception): 52 | gv.extension("matplotlib") 53 | 54 | with suppress(ImportError): 55 | import matplotlib.pyplot as plt 56 | 57 | plt.switch_backend("agg") 58 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/world_population.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "from geoviews import dim\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "cities = pd.read_csv('../../assets/cities.csv', encoding=\"ISO-8859-1\")\n", 31 | "population = gv.Dataset(cities, kdims=['City', 'Country', 'Year'])\n", 32 | "points = population.to(gv.Points, ['Longitude', 'Latitude'], ['Population', 'City', 'Country'])\n", 33 | "\n", 34 | "tiles = gv.tile_sources.OSM" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Plot" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "tiles * points.opts(\n", 51 | " color='Population', cmap='viridis', size=dim('Population')*0.000001,\n", 52 | " tools=['hover'], global_extent=True, width=600, height=600)" 53 | ] 54 | } 55 | ], 56 | "metadata": { 57 | "language_info": { 58 | "name": "python", 59 | "pygments_lexer": "ipython3" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 2 64 | } 65 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/world_population.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "from geoviews import dim\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "gv.output(fig='svg', size=200)" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## Define data" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "cities = pd.read_csv('../../assets/cities.csv', encoding=\"ISO-8859-1\")\n", 32 | "population = gv.Dataset(cities, kdims=['City', 'Country', 'Year'])\n", 33 | "points = population.to(gv.Points, ['Longitude', 'Latitude'], ['Population', 'City', 'Country'])\n", 34 | "\n", 35 | "tiles = gv.tile_sources.OSM" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Plot" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "tiles.opts(zoom=0) * points.opts(\n", 52 | " s=dim('Population')*0.00001, color='Population', cmap='viridis', global_extent=True)" 53 | ] 54 | } 55 | ], 56 | "metadata": { 57 | "language_info": { 58 | "name": "python", 59 | "pygments_lexer": "ipython3" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 2 64 | } 65 | -------------------------------------------------------------------------------- /geoviews/streams.py: -------------------------------------------------------------------------------- 1 | from holoviews.streams import PolyDraw, PolyEdit 2 | 3 | 4 | class PolyVertexEdit(PolyEdit): 5 | """Attaches a PolyVertexEditTool and syncs the datasource. 6 | 7 | shared : boolean 8 | Whether PolyEditTools should be shared between multiple elements 9 | 10 | node_style : dict 11 | A dictionary specifying the style options for the intermediate nodes. 12 | 13 | feature_style : dict 14 | A dictionary specifying the style options for the intermediate nodes. 15 | """ 16 | 17 | def __init__(self, node_style=None, feature_style=None, **params): 18 | if feature_style is None: 19 | feature_style = {} 20 | if node_style is None: 21 | node_style = {} 22 | self.node_style = node_style 23 | self.feature_style = feature_style 24 | super().__init__(**params) 25 | 26 | 27 | class PolyVertexDraw(PolyDraw): 28 | """Attaches a PolyVertexDrawTool and syncs the datasource. 29 | 30 | shared : boolean 31 | Whether PolyEditTools should be shared between multiple elements 32 | 33 | node_style : dict 34 | A dictionary specifying the style options for the intermediate nodes. 35 | 36 | feature_style : dict 37 | A dictionary specifying the style options for the intermediate nodes. 38 | """ 39 | 40 | def __init__(self, node_style=None, feature_style=None, **params): 41 | if feature_style is None: 42 | feature_style = {} 43 | if node_style is None: 44 | node_style = {} 45 | self.node_style = node_style 46 | self.feature_style = feature_style 47 | super().__init__(**params) 48 | -------------------------------------------------------------------------------- /doc/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | User Guide 3 | ********** 4 | 5 | Contents: 6 | 7 | * `Projections `_ 8 | Plotting data on various geographic projections. 9 | 10 | * `Geometries `_ 11 | Working with geometries and shapes. 12 | 13 | * `Gridded Datasets I `_ 14 | Make full use of multidimensional gridded datasets stored in netCDF and other common formats. 15 | 16 | * `Gridded Datasets II `_ 17 | Explore complex datasets quickly with lower dimensional views. 18 | 19 | * `Working with Bokeh `_ 20 | Using the Bokeh extension for more interactivity. 21 | 22 | * `Resampling large grids `_ 23 | Using the datashader and xESMF libraries to regrid large meshes. 24 | 25 | * `Annotating Geographic Elements `_ 26 | Demonstrates the use of the annotate operation to annotate geographic elements. 27 | 28 | * `Using Features Offline `_ 29 | Demonstrates how to use the features module offline. 30 | 31 | * `Using WMTS Offline `_ 32 | Demonstrates how to use WMTS (web-map-tile-services) offline. 33 | 34 | .. toctree:: 35 | :titlesonly: 36 | :hidden: 37 | :maxdepth: 2 38 | 39 | Projections 40 | Geometries 41 | Gridded Datasets I 42 | Gridded Datasets II 43 | Working with Bokeh 44 | Resampling Grids 45 | Annotating Geographic Elements 46 | Using Features Offline 47 | Using WMTS Offline 48 | -------------------------------------------------------------------------------- /geoviews/models/restore_tool.ts: -------------------------------------------------------------------------------- 1 | import type * as p from "@bokehjs/core/properties" 2 | import type {Data} from "@bokehjs/core/types" 3 | import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" 4 | import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" 5 | import {tool_icon_undo} from "@bokehjs/styles/icons.css" 6 | 7 | type BufferedColumnDataSource = ColumnDataSource & {buffer?: Data[]} 8 | 9 | export class RestoreToolView extends ActionToolView { 10 | declare model: RestoreTool 11 | 12 | doit(): void { 13 | const sources = this.model.sources as BufferedColumnDataSource[] 14 | for (const source of sources) { 15 | const new_data = source.buffer?.pop() 16 | if (new_data == null) { 17 | continue 18 | } 19 | source.data = new_data 20 | source.change.emit() 21 | source.properties.data.change.emit() 22 | } 23 | } 24 | } 25 | 26 | export namespace RestoreTool { 27 | export type Attrs = p.AttrsOf 28 | export type Props = ActionTool.Props & { 29 | sources: p.Property 30 | } 31 | } 32 | 33 | export interface RestoreTool extends RestoreTool.Attrs {} 34 | 35 | export class RestoreTool extends ActionTool { 36 | declare properties: RestoreTool.Props 37 | 38 | constructor(attrs?: Partial) { 39 | super(attrs) 40 | } 41 | 42 | static override __module__ = "geoviews.models.custom_tools" 43 | 44 | static { 45 | this.prototype.default_view = RestoreToolView 46 | 47 | this.define(({List, Ref}) => ({ 48 | sources: [ List(Ref(ColumnDataSource)), [] ], 49 | })) 50 | } 51 | 52 | override tool_name = "Restore" 53 | override tool_icon = tool_icon_undo 54 | } 55 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/city_population.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "from geoviews import dim, tile_sources as gvts\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "\n", 16 | "gv.output(dpi=120)" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Declare data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "cities = pd.read_csv('../../assets/cities.csv', encoding=\"ISO-8859-1\")\n", 33 | "\n", 34 | "points = gv.Points(cities, ['Longitude', 'Latitude'], ['City', 'Population', 'Country', 'Year'],\n", 35 | " group='Top 20 Cities by population in 2050').select(Year=2050).sort('Population').iloc[-20:]" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Plot" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "(gvts.CartoMidnight().opts(zoom=1) *\n", 52 | " points.opts(\n", 53 | " s=dim('Population')*0.00002, color='Population', cmap='viridis',\n", 54 | " fig_size=300, global_extent=True) *\n", 55 | " gv.Labels(points).opts(color='white'))" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "language_info": { 61 | "name": "python", 62 | "pygments_lexer": "ipython3" 63 | } 64 | }, 65 | "nbformat": 4, 66 | "nbformat_minor": 2 67 | } 68 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/city_populations_2050.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.tile_sources as gvts\n", 12 | "\n", 13 | "gv.extension('bokeh')" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Declare data" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "cities = pd.read_csv('../../assets/cities.csv', encoding=\"ISO-8859-1\")\n", 30 | "\n", 31 | "points = gv.Points(cities, ['Longitude', 'Latitude'], ['City', 'Population', 'Country', 'Year'],\n", 32 | " label='Top 20 Cities by population in 2050').select(Year=2050).sort('Population').iloc[-20:]" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Plot" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "(gvts.CartoMidnight *\n", 49 | " points.opts(\n", 50 | " size=gv.dim('Population')*0.000002, fill_color='Population', line_color='gray', cmap='viridis',\n", 51 | " width=700, height=600, global_extent=True, tools=['hover'], show_legend=False) *\n", 52 | " gv.Labels(points).opts(\n", 53 | " text_font_size='8pt', text_color='white'))" 54 | ] 55 | } 56 | ], 57 | "metadata": { 58 | "language_info": { 59 | "name": "python", 60 | "pygments_lexer": "ipython3" 61 | } 62 | }, 63 | "nbformat": 4, 64 | "nbformat_minor": 2 65 | } 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to geoviews 2 | 3 | In proposing contributions to this project, you are agreeing to the following contribution terms. 4 | 5 | Grant of Copyright Licence. Subject to the terms and conditions of this Agreement, You hereby grant to the Met Office and to recipients of software distributed by the Met Office a perpetual, worldwide, non-exclusive, royalty-free, irrevocable copyright licence to reproduce, prepare derivative works of, publicly display, publicly perform, sublicence, and distribute Your Contributions and such derivative works under the terms of the: 6 | http://opensource.org/licenses/BSD-3-Clause 7 | 8 | Intellectual Property Infringement. If any third party makes any claim against You or any other entity, alleging that your Contribution, or the Work to which you have contributed, infringes the intellectual property rights of that third party , then You shall inform the Met Office within 5 Working Days of such claim in order for the Met Office to take all appropriate action it deems necessary in relation to the claim. 9 | 10 | You represent that you are legally entitled to grant the above licence. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to the Met Office. 11 | 12 | You represent that each of Your Contributions is Your original creation and that you have not assigned or otherwise given up your interest in the Contribution to any third party You represent that Your Contribution submissions include complete details of any third-party licence or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 13 | -------------------------------------------------------------------------------- /geoviews/__version.py: -------------------------------------------------------------------------------- 1 | """Define the package version. 2 | 3 | Called __version.py as setuptools_scm will create a _version.py 4 | """ 5 | 6 | import os.path 7 | 8 | PACKAGE = "geoviews" 9 | 10 | try: 11 | # For performance reasons on imports, avoid importing setuptools_scm 12 | # if not in a .git folder 13 | if os.path.exists(os.path.join(os.path.dirname(__file__), "..", ".git")): 14 | # If setuptools_scm is installed (e.g. in a development environment with 15 | # an editable install), then use it to determine the version dynamically. 16 | from setuptools_scm import get_version 17 | 18 | # This will fail with LookupError if the package is not installed in 19 | # editable mode or if Git is not installed. 20 | __version__ = get_version(root="..", relative_to=__file__) 21 | else: 22 | raise FileNotFoundError 23 | except (ImportError, LookupError, FileNotFoundError): 24 | # As a fallback, use the version that is hard-coded in the file. 25 | try: 26 | # __version__ was added in _version in setuptools-scm 7.0.0, we rely on 27 | # the hopefully stable version variable. 28 | from ._version import version as __version__ 29 | except (ModuleNotFoundError, ImportError): 30 | # Either _version doesn't exist (ModuleNotFoundError) or version isn't 31 | # in _version (ImportError). ModuleNotFoundError is a subclass of 32 | # ImportError, let's be explicit anyway. 33 | 34 | # Try something else: 35 | from importlib.metadata import PackageNotFoundError, version 36 | 37 | try: 38 | __version__ = version(PACKAGE) 39 | except PackageNotFoundError: 40 | # The user is probably trying to run this without having installed 41 | # the package. 42 | __version__ = "0.0.0+unknown" 43 | 44 | __all__ = ("__version__",) 45 | -------------------------------------------------------------------------------- /geoviews/tests/test_operation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from holoviews.operation import contours 3 | 4 | import geoviews as gv 5 | 6 | xr = pytest.importorskip("xarray") 7 | 8 | 9 | @pytest.mark.filterwarnings( 10 | "ignore:numpy.ndarray size changed, may indicate binary incompatibility" # https://github.com/pydata/xarray/issues/7259 11 | ) 12 | def test_quadmesh_contoures_filled(): 13 | # Regression test for: https://github.com/holoviz/holoviews/pull/5925 14 | ds = xr.tutorial.open_dataset("air_temperature").isel(time=0) 15 | p1 = gv.QuadMesh(ds, kdims=["lon", "lat"]) 16 | p2 = contours(p1, filled=True) 17 | gv.renderer("bokeh").get_plot(p2) 18 | 19 | 20 | def test_unwrap_lons(): 21 | pytest.importorskip("datashader") 22 | # Regression test for: https://github.com/holoviz/geoviews/pull/722 23 | from holoviews.operation.datashader import rasterize 24 | ds = xr.tutorial.open_dataset("air_temperature").isel(time=0) 25 | p1 = gv.Image(ds) 26 | p2 = rasterize(p1, filled=True) 27 | gv.renderer("bokeh").get_plot(p2) 28 | for x in p2.range(0): 29 | assert x >= 0 30 | assert x <= 360 31 | p2.callback() 32 | for x in p2.range(0): 33 | assert x >= 0 34 | assert x <= 360 35 | 36 | 37 | def test_no_unwrap_lons(): 38 | pytest.importorskip("datashader") 39 | # Regression test for: https://github.com/holoviz/geoviews/pull/722 40 | from holoviews.operation.datashader import rasterize 41 | ds = xr.tutorial.open_dataset("air_temperature").isel(time=0) 42 | # to -180, 180 43 | ds["lon"] = (ds["lon"] + 180) % 360 - 180 44 | 45 | p1 = gv.Image(ds) 46 | p2 = rasterize(p1, filled=True) 47 | gv.renderer("bokeh").get_plot(p2) 48 | for x in p2.range(0): 49 | assert x >= -180 50 | assert x <= 180 51 | p2.callback() 52 | for x in p2.range(0): 53 | assert x >= -180 54 | assert x <= 180 55 | -------------------------------------------------------------------------------- /geoviews/models/checkpoint_tool.ts: -------------------------------------------------------------------------------- 1 | import type * as p from "@bokehjs/core/properties" 2 | import {entries} from "@bokehjs/core/util/object" 3 | import type {Data} from "@bokehjs/core/types" 4 | import {copy} from "@bokehjs/core/util/array" 5 | import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" 6 | import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" 7 | import {tool_icon_save} from "@bokehjs/styles/icons.css" 8 | 9 | type BufferedColumnDataSource = ColumnDataSource & {buffer?: Data[]} 10 | 11 | export class CheckpointToolView extends ActionToolView { 12 | declare model: CheckpointTool 13 | 14 | doit(): void { 15 | const sources = this.model.sources as BufferedColumnDataSource[] 16 | for (const source of sources) { 17 | if (source.buffer == null) { 18 | source.buffer = [] 19 | } 20 | const data_copy: Data = {} 21 | for (const [key, column] of entries(source.data)) { 22 | const new_column = [] 23 | for (const arr of column) { 24 | if (Array.isArray(arr) || ArrayBuffer.isView(arr)) { 25 | new_column.push(copy(arr as any)) 26 | } else { 27 | new_column.push(arr) 28 | } 29 | } 30 | data_copy[key] = new_column 31 | } 32 | source.buffer.push(data_copy) 33 | } 34 | } 35 | } 36 | 37 | export namespace CheckpointTool { 38 | export type Attrs = p.AttrsOf 39 | export type Props = ActionTool.Props & { 40 | sources: p.Property 41 | } 42 | } 43 | 44 | export interface CheckpointTool extends CheckpointTool.Attrs {} 45 | 46 | export class CheckpointTool extends ActionTool { 47 | declare properties: CheckpointTool.Props 48 | 49 | constructor(attrs?: Partial) { 50 | super(attrs) 51 | } 52 | 53 | static override __module__ = "geoviews.models.custom_tools" 54 | 55 | static { 56 | this.prototype.default_view = CheckpointToolView 57 | 58 | this.define(({List, Ref}) => ({ 59 | sources: [ List(Ref(ColumnDataSource)), [] ], 60 | })) 61 | } 62 | 63 | override tool_name = "Checkpoint" 64 | override tool_icon = tool_icon_save 65 | } 66 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/filled_contours.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "\n", 16 | "gv.output(fig='svg', size=200)" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Define data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "def sample_data(shape=(73, 145)):\n", 33 | " \"\"\"Returns ``lons``, ``lats`` and ``data`` of some fake data.\"\"\"\n", 34 | " nlats, nlons = shape\n", 35 | " ys = np.linspace(-np.pi / 2, np.pi / 2, nlats)\n", 36 | " xs = np.linspace(0, 2 * np.pi, nlons)\n", 37 | " lons, lats = np.meshgrid(xs, ys)\n", 38 | " wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)\n", 39 | " mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)\n", 40 | "\n", 41 | " lats = np.rad2deg(ys)\n", 42 | " lons = np.rad2deg(xs)\n", 43 | " data = wave + mean\n", 44 | "\n", 45 | " return lons, lats, data\n", 46 | "\n", 47 | "lons, lats, data = sample_data()\n", 48 | "\n", 49 | "# Make sure we declare the central longitude\n", 50 | "contours = gv.FilledContours((lons, lats, data), crs=ccrs.PlateCarree(central_longitude=180))" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Plot" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "contours.opts(colorbar=True, levels=8, color_levels=10, cmap='nipy_spectral', projection=ccrs.Mollweide()) * gf.coastline" 67 | ] 68 | } 69 | ], 70 | "metadata": { 71 | "language_info": { 72 | "name": "python", 73 | "pygments_lexer": "ipython3" 74 | } 75 | }, 76 | "nbformat": 4, 77 | "nbformat_minor": 2 78 | } 79 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/vectorfield_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import cartopy.crs as ccrs\n", 12 | "\n", 13 | "gv.extension('bokeh')" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Define data" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "def sample_data(shape=(20, 30)):\n", 30 | " \"\"\"\n", 31 | " Return ``(x, y, u, v, crs)`` of some vector data\n", 32 | " computed mathematically. The returned crs will be a rotated\n", 33 | " pole CRS, meaning that the vectors will be unevenly spaced in\n", 34 | " regular PlateCarree space.\n", 35 | "\n", 36 | " \"\"\"\n", 37 | " crs = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)\n", 38 | "\n", 39 | " x = np.linspace(311.9, 391.1, shape[1])\n", 40 | " y = np.linspace(-23.6, 24.8, shape[0])\n", 41 | "\n", 42 | " x2d, y2d = np.meshgrid(x, y)\n", 43 | " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", 44 | " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", 45 | "\n", 46 | " return x, y, u, v, crs\n", 47 | "\n", 48 | "xs, ys, U, V, crs = sample_data()\n", 49 | "mag = np.sqrt(U**2 + V**2)\n", 50 | "angle = np.pi / 2 - np.arctan2(-V, -U)\n", 51 | "tiles = gv.tile_sources.OSM\n", 52 | "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## Plot" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "tiles * vectorfield.opts(magnitude='Magnitude', width=600, height=500, padding=0.1)" 69 | ] 70 | } 71 | ], 72 | "metadata": { 73 | "language_info": { 74 | "name": "python", 75 | "pygments_lexer": "ipython3" 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 2 80 | } 81 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/filled_contours.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "\n", 13 | "from cartopy import crs\n", 14 | "from geoviews import opts\n", 15 | "\n", 16 | "gv.extension('bokeh')" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Define data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "def sample_data(shape=(73, 145)):\n", 33 | " \"\"\"Returns ``lons``, ``lats`` and ``data`` of some fake data.\"\"\"\n", 34 | " nlats, nlons = shape\n", 35 | " ys = np.linspace(-np.pi / 2, np.pi / 2, nlats)\n", 36 | " xs = np.linspace(0, 2*np.pi, nlons)\n", 37 | " lons, lats = np.meshgrid(xs, ys)\n", 38 | " wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)\n", 39 | " mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)\n", 40 | "\n", 41 | " lats = np.rad2deg(ys)\n", 42 | " lons = np.rad2deg(xs)\n", 43 | " data = wave + mean\n", 44 | "\n", 45 | " return lons, lats, data\n", 46 | "\n", 47 | "lons, lats, data = sample_data()\n", 48 | "\n", 49 | "# Make sure we declare the central longitude\n", 50 | "contours = gv.FilledContours((lons, lats, data), crs=crs.PlateCarree(central_longitude=180))" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Plot" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "(contours * gf.coastline).opts(\n", 67 | " opts.FilledContours(levels=8, color_levels=10, cmap='nipy_spectral',\n", 68 | " colorbar=True, width=600, height=300, projection=crs.Mollweide()))" 69 | ] 70 | } 71 | ], 72 | "metadata": { 73 | "language_info": { 74 | "name": "python", 75 | "pygments_lexer": "ipython3" 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 2 80 | } 81 | -------------------------------------------------------------------------------- /geoviews/models/custom_tools.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | from bokeh import __version__ as bokeh_version 4 | from bokeh.core.properties import Any, Dict, Instance, List, String 5 | from bokeh.models import ColumnDataSource, PolyDrawTool, PolyEditTool, Tool 6 | from packaging.requirements import Requirement 7 | 8 | from .._warnings import warn 9 | 10 | 11 | class _BokehCheck(Tool): 12 | _bokeh_require_version = "bokeh ==3.8.*" 13 | def __init__(self, *args, **kwargs): 14 | self._check_bokeh_version() 15 | super().__init__(*args, **kwargs) 16 | 17 | @classmethod 18 | @cache 19 | def _check_bokeh_version(cls) -> None: 20 | bokeh_req = Requirement(cls._bokeh_require_version) 21 | if bokeh_req.specifier.contains(bokeh_version): 22 | return 23 | msg = f"{cls.__name__} only official supported with {cls._bokeh_require_version}, you have {bokeh_version} installed." 24 | warn(msg, RuntimeWarning) 25 | 26 | 27 | class CheckpointTool(_BokehCheck, Tool): 28 | """Checkpoints the data on the supplied ColumnDataSources, allowing 29 | the RestoreTool to restore the data to a previous state. 30 | """ 31 | 32 | sources = List(Instance(ColumnDataSource)) 33 | 34 | 35 | class RestoreTool(_BokehCheck, Tool): 36 | """Restores the data on the supplied ColumnDataSources to a previous 37 | checkpoint created by the CheckpointTool 38 | """ 39 | 40 | sources = List(Instance(ColumnDataSource)) 41 | 42 | 43 | class ClearTool(_BokehCheck, Tool): 44 | """Clears the data on the supplied ColumnDataSources.""" 45 | 46 | sources = List(Instance(ColumnDataSource)) 47 | 48 | 49 | class PolyVertexEditTool(_BokehCheck, PolyEditTool): 50 | 51 | node_style = Dict(String, Any, help=""" 52 | Custom styling to apply to the intermediate nodes of a patch or line glyph.""") 53 | 54 | end_style = Dict(String, Any, help=""" 55 | Custom styling to apply to the start and nodes of a patch or line glyph.""") 56 | 57 | 58 | class PolyVertexDrawTool(_BokehCheck, PolyDrawTool): 59 | 60 | node_style = Dict(String, Any, help=""" 61 | Custom styling to apply to the intermediate nodes of a patch or line glyph.""") 62 | 63 | end_style = Dict(String, Any, help=""" 64 | Custom styling to apply to the start and nodes of a patch or line glyph.""") 65 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/vectorfield_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import cartopy.crs as ccrs\n", 12 | "\n", 13 | "gv.extension('matplotlib')\n", 14 | "\n", 15 | "gv.output(fig='svg', size=200)" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## Define data" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def sample_data(shape=(20, 30)):\n", 32 | " \"\"\"\n", 33 | " Return ``(x, y, u, v, crs)`` of some vector data\n", 34 | " computed mathematically. The returned crs will be a rotated\n", 35 | " pole CRS, meaning that the vectors will be unevenly spaced in\n", 36 | " regular PlateCarree space.\n", 37 | "\n", 38 | " \"\"\"\n", 39 | " crs = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)\n", 40 | "\n", 41 | " x = np.linspace(311.9, 391.1, shape[1])\n", 42 | " y = np.linspace(-23.6, 24.8, shape[0])\n", 43 | "\n", 44 | " x2d, y2d = np.meshgrid(x, y)\n", 45 | " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", 46 | " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", 47 | "\n", 48 | " return x, y, u, v, crs\n", 49 | "\n", 50 | "xs, ys, U, V, crs = sample_data()\n", 51 | "mag = np.sqrt(U**2 + V**2)\n", 52 | "angle = np.pi / 2 - np.arctan2(-V, -U)\n", 53 | "tiles = gv.tile_sources.OSM\n", 54 | "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Plot" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "tiles.opts(zoom=4) * vectorfield.opts(magnitude='Magnitude', padding=0.1)" 71 | ] 72 | } 73 | ], 74 | "metadata": { 75 | "language_info": { 76 | "name": "python", 77 | "pygments_lexer": "ipython3" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /geoviews/operation/__init__.py: -------------------------------------------------------------------------------- 1 | from holoviews.core import Element 2 | from holoviews.operation.element import contours 3 | from holoviews.operation.stats import bivariate_kde 4 | 5 | from .. import element as gv_element 6 | from ..element import _Element 7 | from .projection import ( # noqa: F401 8 | project, 9 | project_geom, 10 | project_graph, 11 | project_image, 12 | project_path, 13 | project_points, 14 | project_quadmesh, 15 | project_shape, 16 | project_vectorfield, 17 | project_windbarbs, 18 | ) 19 | from .resample import resample_geometry # noqa: F401 20 | 21 | geo_ops = [contours, bivariate_kde] 22 | try: 23 | from holoviews.operation.datashader import dynspread, shade, stack 24 | from holoviews.operation.resample import ResampleOperation2D 25 | geo_ops += [ResampleOperation2D, shade, stack, dynspread] 26 | except ImportError: 27 | pass 28 | 29 | def convert_to_geotype(element, crs=None): 30 | """Converts a HoloViews element type to the equivalent GeoViews 31 | element if given a coordinate reference system. 32 | """ 33 | geotype = getattr(gv_element, type(element).__name__, None) 34 | if crs is None or geotype is None or isinstance(element, _Element): 35 | return element 36 | return element.clone(new_type=geotype, crs=crs) 37 | 38 | 39 | def find_crs(op, element): 40 | """Traverses the supplied object looking for coordinate reference 41 | systems (crs). If multiple clashing reference systems are found 42 | it will throw an error. 43 | """ 44 | crss = [crs for crs in element.traverse(lambda x: x.crs, [_Element]) 45 | if crs is not None] 46 | if not crss: 47 | return {} 48 | crs = crss[0] 49 | if any(crs != ocrs for ocrs in crss[1:]): 50 | raise ValueError(f'Cannot {type(op).__name__} Elements in different ' 51 | 'coordinate reference systems.') 52 | return {'crs': crs} 53 | 54 | 55 | def add_crs(op, element, **kwargs): 56 | """Converts any elements in the input to their equivalent geotypes 57 | if given a coordinate reference system. 58 | """ 59 | return element.map(lambda x: convert_to_geotype(x, kwargs.get('crs')), Element) 60 | 61 | 62 | for op in geo_ops: 63 | op._preprocess_hooks = op._preprocess_hooks + [find_crs] 64 | op._postprocess_hooks = op._postprocess_hooks + [add_crs] 65 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/orthographic_vectorfield.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Declare data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def sample_data(shape=(20, 30)):\n", 31 | " \"\"\"\n", 32 | " Returns ``(x, y, u, v, crs)`` of some vector data\n", 33 | " computed mathematically. The returned crs will be a rotated\n", 34 | " pole CRS, meaning that the vectors will be unevenly spaced in\n", 35 | " regular PlateCarree space.\n", 36 | "\n", 37 | " \"\"\"\n", 38 | " crs = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)\n", 39 | "\n", 40 | " x = np.linspace(311.9, 391.1, shape[1])\n", 41 | " y = np.linspace(-23.6, 24.8, shape[0])\n", 42 | "\n", 43 | " x2d, y2d = np.meshgrid(x, y)\n", 44 | " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", 45 | " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", 46 | "\n", 47 | " return x, y, u, v, crs\n", 48 | "\n", 49 | "xs, ys, U, V, crs = sample_data()\n", 50 | "mag = np.sqrt(U**2 + V**2)\n", 51 | "angle = np.pi / 2 - np.arctan2(-V, -U)\n", 52 | "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## Plot" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "gf.ocean * gf.land * gf.coastline * vectorfield.opts(\n", 69 | " magnitude='Magnitude', width=500, height=500,\n", 70 | " projection=ccrs.Orthographic(-10, 45), global_extent=True)" 71 | ] 72 | } 73 | ], 74 | "metadata": { 75 | "language_info": { 76 | "name": "python", 77 | "pygments_lexer": "ipython3" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/orthographic_vectorfield.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "\n", 16 | "gv.output(fig='svg', size=200)" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Define data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "def sample_data(shape=(20, 30)):\n", 33 | " \"\"\"\n", 34 | " Returns ``(x, y, u, v, crs)`` of some vector data\n", 35 | " computed mathematically. The returned crs will be a rotated\n", 36 | " pole CRS, meaning that the vectors will be unevenly spaced in\n", 37 | " regular PlateCarree space.\n", 38 | "\n", 39 | " \"\"\"\n", 40 | " crs = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)\n", 41 | "\n", 42 | " x = np.linspace(311.9, 391.1, shape[1])\n", 43 | " y = np.linspace(-23.6, 24.8, shape[0])\n", 44 | "\n", 45 | " x2d, y2d = np.meshgrid(x, y)\n", 46 | " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", 47 | " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", 48 | "\n", 49 | " return x, y, u, v, crs\n", 50 | "\n", 51 | "xs, ys, U, V, crs = sample_data()\n", 52 | "mag = np.sqrt(U**2 + V**2)\n", 53 | "angle = np.pi / 2 - np.arctan2(-V, -U)\n", 54 | "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Plot" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "gf.ocean * gf.coastline * gf.land * vectorfield.opts(\n", 71 | " magnitude='Magnitude', show_grid=True, projection=ccrs.Orthographic(-10, 45), global_extent=True)" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "language_info": { 77 | "name": "python", 78 | "pygments_lexer": "ipython3" 79 | } 80 | }, 81 | "nbformat": 4, 82 | "nbformat_minor": 2 83 | } 84 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/great_circle.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pyproj\n", 10 | "import numpy as np\n", 11 | "import geoviews as gv\n", 12 | "from bokeh.sampledata.airport_routes import airports, routes\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def get_circle_path(start, end, samples=200):\n", 31 | " sx, sy = start\n", 32 | " ex, ey = end\n", 33 | " g = pyproj.Geod(ellps='WGS84')\n", 34 | " (az12, az21, dist) = g.inv(sx, sy, ex, ey)\n", 35 | " lonlats = g.npts(sx, sy, ex, ey, samples)\n", 36 | " return np.array([(sx, sy)]+lonlats+[(ex, ey)])\n", 37 | "\n", 38 | "# Compute great-circle paths for all US flight routes from Honolulu\n", 39 | "paths = []\n", 40 | "honolulu = (-157.9219970703125, 21.318700790405273)\n", 41 | "routes = routes[routes.SourceID==3728]\n", 42 | "airports = airports[airports.AirportID.isin(list(routes.DestinationID)+[3728])]\n", 43 | "for i, route in routes.iterrows():\n", 44 | " airport = airports[airports.AirportID==route.DestinationID].iloc[0]\n", 45 | " paths.append(get_circle_path(honolulu, (airport.Longitude, airport.Latitude)))\n", 46 | "tiles = gv.tile_sources.OSM\n", 47 | "\n", 48 | "# Define Graph from Nodes and EdgePaths\n", 49 | "path = gv.EdgePaths(paths)\n", 50 | "points = gv.Nodes(airports, ['Longitude', 'Latitude', 'AirportID'], ['Name', 'City'])\n", 51 | "graph = gv.Graph((routes, points, path), ['SourceID', 'DestinationID'])" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## Plot" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "tiles * graph.opts(width=600, height=400, tools=['hover', 'tap'], node_color='black')" 68 | ] 69 | } 70 | ], 71 | "metadata": { 72 | "language_info": { 73 | "name": "python", 74 | "pygments_lexer": "ipython3" 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 2 79 | } 80 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/airport_graph.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "from bokeh.sampledata.airport_routes import airports, routes\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "\n", 16 | "gv.output(fig='svg')" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Define data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "# Count the number of connections from each airport\n", 33 | "counts = routes.groupby('SourceID')[['Stops']].count().reset_index().rename(columns={'Stops': 'Connections'})\n", 34 | "airports_df = pd.merge(airports, counts, left_on='AirportID', right_on='SourceID', how='left')\n", 35 | "\n", 36 | "# Select only US mainland airports & convert from Mercator to Latitudes/Longitudes\n", 37 | "airport_points = gv.Points(airports_df, ['Longitude', 'Latitude'])[-170:-50, 0: 50]\n", 38 | "\n", 39 | "# Declare nodes, graph and tiles\n", 40 | "nodes = gv.Nodes(airport_points, ['Longitude', 'Latitude', 'AirportID'],\n", 41 | " ['Name', 'City', 'Connections'])\n", 42 | "graph = gv.Graph((routes, nodes), ['SourceID', 'DestinationID'], ['Source', 'Destination'])\n", 43 | "tiles = gv.tile_sources.OSM\n", 44 | "\n", 45 | "# Select 50 busiest airports\n", 46 | "busiest = list(routes.groupby('SourceID').count().sort_values('Stops').iloc[-50:].index.values)\n", 47 | "busiest_airports = graph.select(AirportID=busiest, selection_mode='nodes')" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "## Plot" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "gf.ocean * gf.land * gf.coastline * gf.borders * busiest_airports.opts(\n", 64 | " node_size=8, edge_linewidth=1, edge_alpha=0.05, fig_size=300, padding=0.1)" 65 | ] 66 | } 67 | ], 68 | "metadata": { 69 | "language_info": { 70 | "name": "python", 71 | "pygments_lexer": "ipython3" 72 | } 73 | }, 74 | "nbformat": 4, 75 | "nbformat_minor": 2 76 | } 77 | -------------------------------------------------------------------------------- /hatch_build.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import os 5 | import sys 6 | import typing as t 7 | from pathlib import Path 8 | 9 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 10 | 11 | BASE_DIR = Path(__file__).parent 12 | GREEN, RED, RESET = "\033[0;32m", "\033[0;31m", "\033[0m" 13 | 14 | 15 | def build_models(): 16 | from bokeh.ext import build 17 | 18 | print(f"{GREEN}[GEOVIEWS]{RESET} Starting building custom models", flush=True) 19 | geoviews_dir = BASE_DIR / "geoviews" 20 | success = build(geoviews_dir) 21 | if sys.platform != "win32": 22 | # npm can cause non-blocking stdout; so reset it just in case 23 | import fcntl 24 | 25 | flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) 26 | fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) 27 | 28 | if success: 29 | print(f"{GREEN}[GEOVIEWS]{RESET} Finished building custom models", flush=True) 30 | else: 31 | print(f"{RED}[GEOVIEWS]{RESET} Failed building custom models", flush=True) 32 | sys.exit(1) 33 | 34 | 35 | def clean_js_version(version): 36 | version = version.replace("-", "") 37 | for dev in ("a", "b", "rc"): 38 | version = version.replace(dev + ".", dev) 39 | return version 40 | 41 | 42 | def validate_js_version(version): 43 | # TODO: Double check the logic in this function 44 | version = version.split(".post")[0] 45 | with open("./geoviews/package.json") as f: 46 | package_json = json.load(f) 47 | js_version = package_json["version"] 48 | version = version.split("+")[0] 49 | if any(dev in version for dev in ("a", "b", "rc")) and "-" not in js_version: 50 | raise ValueError( 51 | f"geoviews.js dev versions ({js_version}) must separate dev suffix with a dash, e.g. v1.0.0rc1 should be v1.0.0-rc.1." 52 | ) 53 | if version != "None" and version != clean_js_version(js_version): 54 | raise ValueError( 55 | f"geoviews.js version ({js_version}) does not match geoviews version ({version}). Cannot build release." 56 | ) 57 | 58 | 59 | class BuildHook(BuildHookInterface): 60 | """The hatch build hook.""" 61 | 62 | PLUGIN_NAME = "install" 63 | 64 | def initialize(self, version: str, build_data: dict[str, t.Any]) -> None: 65 | """Initialize the plugin.""" 66 | if self.target_name not in ["wheel", "sdist"]: 67 | return 68 | 69 | validate_js_version(self.metadata.version) 70 | build_models() 71 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/great_circle.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pyproj\n", 10 | "import numpy as np\n", 11 | "import geoviews as gv\n", 12 | "import geoviews.feature as gf\n", 13 | "from bokeh.sampledata.airport_routes import airports, routes\n", 14 | "\n", 15 | "gv.extension('matplotlib')\n", 16 | "\n", 17 | "gv.output(fig='svg', size=250)" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "## Define data" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "def get_circle_path(start, end, samples=200):\n", 34 | " sx, sy = start\n", 35 | " ex, ey = end\n", 36 | " g = pyproj.Geod(ellps='WGS84')\n", 37 | " (az12, az21, dist) = g.inv(sx, sy, ex, ey)\n", 38 | " lonlats = g.npts(sx, sy, ex, ey, samples)\n", 39 | " return np.array([(sx, sy)]+lonlats+[(ex, ey)])\n", 40 | "\n", 41 | "# Compute great-circle paths for all US flight routes from Honolulu\n", 42 | "paths = []\n", 43 | "honolulu = (-157.9219970703125, 21.318700790405273)\n", 44 | "routes = routes[routes.SourceID==3728]\n", 45 | "airports = airports[airports.AirportID.isin(list(routes.DestinationID)+[3728])]\n", 46 | "for i, route in routes.iterrows():\n", 47 | " airport = airports[airports.AirportID==route.DestinationID].iloc[0]\n", 48 | " paths.append(get_circle_path(honolulu, (airport.Longitude, airport.Latitude)))\n", 49 | "\n", 50 | "# Define Graph from Nodes and EdgePaths\n", 51 | "path = gv.EdgePaths(paths)\n", 52 | "points = gv.Nodes(airports, ['Longitude', 'Latitude', 'AirportID'], ['Name', 'City'])\n", 53 | "graph = gv.Graph((routes, points, path), ['SourceID', 'DestinationID'])" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "## Plot" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "gf.ocean * gf.land * gf.lakes * gf.coastline * graph.opts(\n", 70 | " node_color='black', node_size=8)" 71 | ] 72 | } 73 | ], 74 | "metadata": { 75 | "language_info": { 76 | "name": "python", 77 | "pygments_lexer": "ipython3" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/airport_graph.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "from geoviews import opts\n", 13 | "from bokeh.sampledata.airport_routes import airports, routes\n", 14 | "\n", 15 | "gv.extension('bokeh')" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## Define data" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "# Count the number of connections from each airport\n", 32 | "counts = routes.groupby('SourceID')[['Stops']].count().reset_index().rename(columns={'Stops': 'Connections'})\n", 33 | "airports_df = pd.merge(airports, counts, left_on='AirportID', right_on='SourceID', how='left')\n", 34 | "\n", 35 | "# Select only US mainland airports & convert from Mercator to Latitudes/Longitudes\n", 36 | "airport_points = gv.Points(airports_df, ['Longitude', 'Latitude']).select(Longitude=(-170, -50), Latitude=(0, 50))\n", 37 | "\n", 38 | "# Declare nodes, graph and tiles\n", 39 | "nodes = gv.Nodes(airport_points, ['Longitude', 'Latitude', 'AirportID'],\n", 40 | " ['Name', 'City', 'Connections'])\n", 41 | "graph = gv.Graph((routes, nodes), ['SourceID', 'DestinationID'], ['Source', 'Destination'])\n", 42 | "tiles = gv.tile_sources.OSM\n", 43 | "\n", 44 | "# Select 50 busiest airports\n", 45 | "busiest = list(routes.groupby('SourceID').count().sort_values('Stops').iloc[-50:].index.values)\n", 46 | "busiest_airports = graph.select(AirportID=busiest, selection_mode='nodes')" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Plot" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "(tiles * busiest_airports).opts(\n", 63 | " opts.Graph(edge_selection_line_color='black', edge_hover_line_color='red',\n", 64 | " edge_line_width=1, edge_line_alpha=0.01, edge_nonselection_line_alpha=0.01,\n", 65 | " width=800, height=600))" 66 | ] 67 | } 68 | ], 69 | "metadata": { 70 | "language_info": { 71 | "name": "python", 72 | "pygments_lexer": "ipython3" 73 | } 74 | }, 75 | "nbformat": 4, 76 | "nbformat_minor": 2 77 | } 78 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nbsite.shared_conf import * 4 | 5 | project = 'GeoViews' 6 | authors = 'HoloViz Developers' 7 | copyright = '2018 ' + authors 8 | description = 'Geographic visualizations for HoloViews.' 9 | 10 | import geoviews 11 | version = release = base_version(geoviews.__version__) 12 | 13 | html_static_path += ['_static'] 14 | 15 | html_css_files += [ 16 | 'custom.css' 17 | ] 18 | 19 | html_theme = 'pydata_sphinx_theme' 20 | html_logo = "_static/logo_horizontal.png" 21 | html_favicon = "_static/favicon.ico" 22 | 23 | html_theme_options = { 24 | "github_url": "https://github.com/holoviz/geoviews", 25 | "icon_links": [ 26 | { 27 | "name": "Twitter", 28 | "url": "https://twitter.com/HoloViews", 29 | "icon": "fa-brands fa-twitter-square", 30 | }, 31 | { 32 | "name": "Discourse", 33 | "url": "https://discourse.holoviz.org/c/geoviews", 34 | "icon": "fa-brands fa-discourse", 35 | }, 36 | { 37 | "name": "Discord", 38 | "url": "https://discord.gg/AXRHnJU6sP", 39 | "icon": "fa-brands fa-discord", 40 | }, 41 | ], 42 | "pygments_dark_style": "material" 43 | } 44 | 45 | extensions += [ 46 | 'nbsite.gallery', 47 | 'nbsite.analytics', 48 | 'numpydoc', 49 | 'sphinx_reredirects', 50 | ] 51 | 52 | intersphinx_mapping = { 53 | 'panel': ('https://panel.holoviz.org/', None), 54 | 'holoviews':('https://holoviews.org/', None), 55 | } 56 | 57 | numpydoc_xref_param_type = True 58 | numpydoc_xref_type = True 59 | 60 | myst_enable_extensions = ["colon_fence", "deflist"] 61 | numpydoc_show_inherited_class_members = False 62 | numpydoc_class_members_toctree = False 63 | 64 | napoleon_numpy_docstring = True 65 | 66 | nbsite_analytics = { 67 | 'goatcounter_holoviz': True, 68 | } 69 | 70 | redirects = { 71 | 'topics/index': 'https://examples.holoviz.org', 72 | } 73 | 74 | nbsite_gallery_conf = { 75 | 'github_org': 'holoviz', 76 | 'github_project': 'geoviews', 77 | 'backends': ['bokeh', 'matplotlib'], 78 | 'galleries': { 79 | 'gallery': { 80 | 'title': 'Gallery' 81 | } 82 | }, 83 | 'thumbnail_url': 'https://assets.holoviz.org/geoviews/thumbnails', 84 | } 85 | 86 | html_context.update({ 87 | "last_release": f"v{release}", 88 | "github_user": "holoviz", 89 | "github_repo": "geoviews", 90 | "default_mode": "light" 91 | }) 92 | 93 | # Override the Sphinx default title that appends `documentation` 94 | html_title = f'{project} v{version}' 95 | -------------------------------------------------------------------------------- /geoviews/_warnings.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import warnings 4 | 5 | import holoviews as hv 6 | import param 7 | from packaging.version import Version 8 | 9 | __all__ = ( 10 | "GeoviewsDeprecationWarning", 11 | "GeoviewsUserWarning", 12 | "deprecated", 13 | "find_stack_level", 14 | "warn", 15 | ) 16 | 17 | 18 | def warn(message, category=None, stacklevel=None): 19 | if stacklevel is None: 20 | stacklevel = find_stack_level() 21 | 22 | warnings.warn(message, category, stacklevel=stacklevel) 23 | 24 | 25 | def find_stack_level(): 26 | """Find the first place in the stack that is not inside 27 | Geoviews, Holoviews, or Param. 28 | 29 | Inspired by: pandas.util._exceptions.find_stack_level 30 | """ 31 | import geoviews as gv 32 | 33 | pkg_dir = os.path.dirname(gv.__file__) 34 | test_dir = os.path.join(pkg_dir, "tests") 35 | param_dir = os.path.dirname(param.__file__) 36 | hv_dir = os.path.dirname(hv.__file__) 37 | 38 | frame = inspect.currentframe() 39 | stacklevel = 0 40 | while frame: 41 | fname = inspect.getfile(frame) 42 | if ( 43 | fname.startswith((pkg_dir, param_dir, hv_dir)) 44 | ) and not fname.startswith(test_dir): 45 | frame = frame.f_back 46 | stacklevel += 1 47 | else: 48 | break 49 | 50 | return stacklevel 51 | 52 | 53 | def deprecated(remove_version, old, new=None, extra=None): 54 | import geoviews as gv 55 | 56 | current_version = Version(Version(gv.__version__).base_version) 57 | 58 | if isinstance(remove_version, str): 59 | remove_version = Version(remove_version) 60 | 61 | if remove_version <= current_version: 62 | # This error is mainly for developers to remove the deprecated. 63 | raise ValueError( 64 | f"{old!r} should have been removed in {remove_version}" 65 | f", current version {current_version}." 66 | ) 67 | 68 | message = f"{old!r} is deprecated and will be removed in version {remove_version}." 69 | 70 | if new: 71 | message = f"{message[:-1]}, use {new!r} instead." 72 | 73 | if extra: 74 | message += " " + extra.strip() 75 | 76 | warn(message, GeoviewsDeprecationWarning) 77 | 78 | 79 | class GeoviewsDeprecationWarning(DeprecationWarning): 80 | """A Geoviews-specific ``DeprecationWarning`` subclass. 81 | 82 | Used to selectively filter Geoviews deprecations for unconditional display. 83 | """ 84 | 85 | 86 | class GeoviewsUserWarning(UserWarning): 87 | """A Geoviews-specific ``UserWarning`` subclass. 88 | 89 | Used to selectively filter Geoviews warnings for unconditional display. 90 | """ 91 | 92 | 93 | warnings.simplefilter("always", GeoviewsDeprecationWarning) 94 | warnings.simplefilter("always", GeoviewsUserWarning) 95 | -------------------------------------------------------------------------------- /geoviews/models/wind_barb.py: -------------------------------------------------------------------------------- 1 | """WindBarb glyph for rendering wind barbs in Bokeh.""" 2 | 3 | from bokeh.core.properties import AngleSpec, Float, Include, NumberSpec, field 4 | from bokeh.core.property_mixins import LineProps 5 | from bokeh.models import XYGlyph 6 | 7 | 8 | class WindBarb(XYGlyph): 9 | """ 10 | A glyph for rendering wind barbs at (x, y) locations. 11 | 12 | Wind barbs are traditionally used in meteorology to plot wind speed 13 | and direction. The barb consists of a staff with flags and barbs 14 | representing wind speed increments. 15 | """ 16 | 17 | # Explicit args 18 | _args = ("x", "y", "angle", "magnitude") 19 | 20 | # The x-coordinates 21 | x = NumberSpec( 22 | default=field("x"), 23 | help=""" 24 | The x-coordinates of the wind barbs. 25 | """, 26 | ) 27 | 28 | # The y-coordinates 29 | y = NumberSpec( 30 | default=field("y"), 31 | help=""" 32 | The y-coordinates of the wind barbs. 33 | """, 34 | ) 35 | 36 | # The angle in radians (meteorological convention: direction FROM which wind blows) 37 | angle = AngleSpec( 38 | default=field("angle"), 39 | help=""" 40 | The angles to rotate the wind barbs, in radians. 41 | """, 42 | ) 43 | 44 | # The magnitude (wind speed) 45 | magnitude = NumberSpec( 46 | default=field("magnitude"), 47 | help=""" 48 | The magnitudes (wind speeds) for the wind barbs. 49 | """, 50 | ) 51 | 52 | # Scale factor for the barb size 53 | scale = Float( 54 | default=1.0, 55 | help=""" 56 | A scaling factor to apply to the wind barb size. 57 | """, 58 | ) 59 | 60 | # Barb dimensions 61 | barb_length = Float( 62 | default=30.0, 63 | help=""" 64 | Length of the main barb staff. 65 | """, 66 | ) 67 | 68 | barb_width = Float( 69 | default=15.0, 70 | help=""" 71 | Width of the barb lines. 72 | """, 73 | ) 74 | 75 | flag_width = Float( 76 | default=15.0, 77 | help=""" 78 | Width of the 50-knot flag triangles. 79 | """, 80 | ) 81 | 82 | spacing = Float( 83 | default=6.0, 84 | help=""" 85 | Vertical spacing between barbs and flags. 86 | """, 87 | ) 88 | 89 | calm_circle_radius = Float( 90 | default=3.0, 91 | help=""" 92 | Radius of the circle drawn for calm winds (< 5 knots). 93 | """, 94 | ) 95 | 96 | # Line properties 97 | line_props = Include( 98 | LineProps, 99 | help=""" 100 | The {prop} values for the wind barbs. 101 | """, 102 | ) 103 | -------------------------------------------------------------------------------- /doc/reference_manual/index.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | API Reference Manual 3 | ******************** 4 | 5 | To learn how to use GeoViews, we recommend starting with the 6 | `User guide`_, and then utilizing the online help features within 7 | IPython or Jupyter Notebook to explore further. Each component should support 8 | tab-completion and provide inline help, which is typically sufficient to 9 | understand how to use GeoViews effectively. 10 | 11 | For a comprehensive reference to every object in GeoViews, this API documentation 12 | is generated directly from the source code's documentation and declarations. 13 | While it may be verbose due to the inclusion of many inherited or less commonly 14 | used methods, it offers a complete listing of all attributes and methods for each 15 | class, which can be challenging to extract directly from the source code. 16 | 17 | -------- 18 | 19 | Module Structure 20 | ________________ 21 | 22 | GeoViews subpackages 23 | --------------------- 24 | 25 | `annotators`_ 26 | Helper functions and classes to annotate visual elements. 27 | 28 | `data`_ 29 | Data interface classes enabling GeoViews to work with various data types. 30 | 31 | `element`_ 32 | Core elements that form the basis of geographic visualizations. 33 | 34 | `links`_ 35 | Tools for linking different elements and streams. 36 | 37 | `models`_ 38 | Custom models extending GeoViews' capabilities. 39 | 40 | `operation`_ 41 | Operations applied to transform existing elements or data structures. 42 | 43 | `plotting`_ 44 | Base plotting classes and utilities. 45 | 46 | `plotting.bokeh`_ 47 | Bokeh-specific plotting classes and utilities. 48 | 49 | `plotting.matplotlib`_ 50 | Matplotlib-specific plotting classes and utilities. 51 | 52 | `streams`_ 53 | Stream classes providing interactivity for dynamic maps. 54 | 55 | `util`_ 56 | High-level utilities supporting GeoViews functionality. 57 | 58 | .. toctree:: 59 | :maxdepth: 2 60 | :hidden: 61 | 62 | annotators 63 | data 64 | element 65 | links 66 | models 67 | operation 68 | plotting 69 | plotting.bokeh 70 | plotting.matplotlib 71 | streams 72 | util 73 | 74 | .. _User guide: ../user_guide/index.html 75 | 76 | .. _annotators: geoviews.annotators.html 77 | .. _data: geoviews.data.html 78 | .. _element: geoviews.element.html 79 | .. _links: geoviews.links.html 80 | .. _models: geoviews.models.html 81 | .. _operation: geoviews.operation.html 82 | .. _plotting: geoviews.plotting.html 83 | .. _plotting.bokeh: geoviews.plotting.bokeh.html 84 | .. _plotting.matplotlib: geoviews.plotting.mpl.html 85 | .. _streams: geoviews.streams.html 86 | .. _util: geoviews.util.html 87 | -------------------------------------------------------------------------------- /geoviews/tests/ui/test_plot.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import holoviews as hv 3 | import numpy as np 4 | import pytest 5 | from holoviews.tests.ui import expect, wait_until 6 | 7 | import geoviews as gv 8 | 9 | pytestmark = pytest.mark.ui 10 | 11 | xr = pytest.importorskip("xarray") 12 | 13 | 14 | @pytest.mark.usefixtures("bokeh_backend") 15 | def test_range_correct_longitude(serve_hv): 16 | """ 17 | Regression test for https://github.com/holoviz/geoviews/issues/753 18 | """ 19 | coastline = gv.feature.coastline().opts(active_tools=["box_zoom"]) 20 | xy_range = hv.streams.RangeXY(source=coastline) 21 | 22 | page = serve_hv(coastline) 23 | hv_plot = page.locator(".bk-events") 24 | 25 | expect(hv_plot).to_have_count(1) 26 | 27 | bbox = hv_plot.bounding_box() 28 | hv_plot.click() 29 | 30 | page.mouse.move(bbox["x"] + 100, bbox["y"] + 100) 31 | page.mouse.down() 32 | page.mouse.move(bbox["x"] + 150, bbox["y"] + 150, steps=5) 33 | page.mouse.up() 34 | 35 | wait_until(lambda: np.isclose(xy_range.x_range[0], -105.68691588784145), page) 36 | wait_until(lambda: np.isclose(xy_range.x_range[1], -21.80841121496224), page) 37 | 38 | 39 | @pytest.mark.usefixtures("bokeh_backend") 40 | @pytest.mark.parametrize(("lon_start", "lon_end"), [(-180, 180), (0, 360)]) 41 | @pytest.mark.parametrize("bbox_x", [100, 250]) 42 | def test_rasterize_with_coastline_not_blank_on_zoom(serve_hv, lon_start, lon_end, bbox_x): 43 | """ 44 | Regression test for https://github.com/holoviz/geoviews/issues/726 45 | """ 46 | from holoviews.operation.datashader import rasterize 47 | 48 | lon = np.linspace(lon_start, lon_end, 360) 49 | lat = np.linspace(-90, 90, 180) 50 | data = np.random.rand(180, 360) 51 | ds = xr.Dataset({"data": (["lat", "lon"], data)}, coords={"lon": lon, "lat": lat}) 52 | 53 | overlay = rasterize( 54 | gv.Image(ds, ["lon", "lat"], ["data"], crs=ccrs.PlateCarree()).opts( 55 | tools=["hover"], active_tools=["box_zoom"] 56 | ) 57 | ) * gv.feature.coastline() 58 | 59 | page = serve_hv(overlay) 60 | 61 | hv_plot = page.locator(".bk-events") 62 | 63 | expect(hv_plot).to_have_count(1) 64 | 65 | bbox = hv_plot.bounding_box() 66 | hv_plot.click() 67 | 68 | page.mouse.move(bbox["x"] + bbox_x, bbox["y"] + 100) 69 | page.mouse.down() 70 | page.mouse.move(bbox["x"] + bbox_x + 50, bbox["y"] + 150, steps=5) 71 | page.mouse.up() 72 | 73 | # get hover tooltip 74 | page.mouse.move(bbox["x"] + 100, bbox["y"] + 150) 75 | 76 | wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) 77 | 78 | expect(page.locator(".bk-Tooltip")).to_contain_text("lon:") 79 | expect(page.locator(".bk-Tooltip")).to_contain_text("lat:") 80 | expect(page.locator(".bk-Tooltip")).to_contain_text("data:") 81 | expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") 82 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | - "v[0-9]+.[0-9]+.[0-9]+a[0-9]+" 8 | - "v[0-9]+.[0-9]+.[0-9]+b[0-9]+" 9 | - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+" 10 | workflow_dispatch: 11 | inputs: 12 | target: 13 | description: "Site to build and deploy" 14 | type: choice 15 | options: 16 | - dev 17 | - main 18 | - dryrun 19 | required: true 20 | default: dryrun 21 | schedule: 22 | - cron: "0 18 * * SUN" 23 | 24 | defaults: 25 | run: 26 | shell: bash -e {0} 27 | 28 | env: 29 | DISPLAY: ":99.0" 30 | 31 | jobs: 32 | pixi_lock: 33 | name: Pixi lock 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 37 | 38 | docs_build: 39 | name: Build Documentation 40 | needs: [pixi_lock] 41 | runs-on: "macos-latest" 42 | timeout-minutes: 180 43 | outputs: 44 | tag: ${{ steps.vars.outputs.tag }} 45 | steps: 46 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 47 | with: 48 | environments: docs 49 | - name: Build documentation 50 | run: pixi run -e docs docs-build 51 | - uses: actions/upload-artifact@v4 52 | if: always() 53 | with: 54 | name: docs 55 | if-no-files-found: error 56 | path: builtdocs 57 | - name: Set output 58 | id: vars 59 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 60 | 61 | docs_publish: 62 | name: Publish Documentation 63 | runs-on: "ubuntu-latest" 64 | needs: [docs_build] 65 | steps: 66 | - uses: actions/download-artifact@v4 67 | with: 68 | name: docs 69 | path: builtdocs/ 70 | - name: Set output 71 | id: vars 72 | run: echo "tag=${{ needs.docs_build.outputs.tag }}" >> $GITHUB_OUTPUT 73 | - name: Deploy dev 74 | uses: peaceiris/actions-gh-pages@v4 75 | if: | 76 | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || 77 | (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) 78 | with: 79 | personal_token: ${{ secrets.ACCESS_TOKEN }} 80 | external_repository: holoviz-dev/geoviews 81 | publish_dir: ./builtdocs 82 | force_orphan: true 83 | - name: Deploy main 84 | if: | 85 | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') || 86 | (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) 87 | uses: peaceiris/actions-gh-pages@v4 88 | with: 89 | github_token: ${{ secrets.GITHUB_TOKEN }} 90 | publish_dir: ./builtdocs 91 | cname: geoviews.org 92 | force_orphan: true 93 | -------------------------------------------------------------------------------- /geoviews/plotting/plot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import param 3 | from holoviews.core import CompositeOverlay, Element 4 | 5 | from ..util import project_extents 6 | 7 | 8 | def _get_projection(el): 9 | """Get coordinate reference system from non-auxiliary elements. 10 | 11 | Return value is a tuple of a precedence integer and the projection, 12 | to allow non-auxiliary components to take precedence. 13 | """ 14 | result = None 15 | if hasattr(el, 'crs'): 16 | result = (int(el._auxiliary_component), el.crs) 17 | return result 18 | 19 | 20 | class ProjectionPlot(param.Parameterized): 21 | """Implements custom _get_projection method to make the coordinate 22 | reference system available to HoloViews plots as a projection. 23 | """ 24 | 25 | infer_projection = param.Boolean(default=True, doc=""" 26 | Whether the projection should be inferred from the element crs.""") 27 | 28 | def _get_projection(self, obj): 29 | # Look up custom projection in options 30 | isoverlay = lambda x: isinstance(x, CompositeOverlay) 31 | opts = self._traverse_options(obj, 'plot', ['projection', 'infer_projection'], 32 | [CompositeOverlay, Element], 33 | keyfn=isoverlay, defaults=False) 34 | from_overlay = not all(p is None for p in opts.get(True, {}).get('projection', [])) 35 | projections = opts.get(from_overlay, {}).get('projection', []) 36 | infer = any(opts.get(from_overlay, {}).get('infer_projection', [])) or self.infer_projection 37 | custom_projs = [p for p in projections if p is not None] 38 | 39 | if len(set([type(p) for p in custom_projs])) > 1: 40 | raise Exception("An axis may only be assigned one projection type") 41 | elif custom_projs: 42 | return custom_projs[0] 43 | if not infer: 44 | return self.projection 45 | 46 | # If no custom projection is supplied traverse object to get 47 | # the custom projections and sort by precedence 48 | projections = sorted([p for p in obj.traverse(_get_projection, [Element]) 49 | if p is not None and p[1] is not None]) 50 | if projections: 51 | return projections[0][1] 52 | else: 53 | return None 54 | 55 | def get_extents(self, element, ranges, range_type='combined', **kwargs): 56 | """Subclasses the get_extents method using the GeoAxes 57 | set_extent method to project the extents to the 58 | Elements coordinate reference system. 59 | """ 60 | proj = self.projection 61 | if self.global_extent and range_type in ('combined', 'data'): 62 | (x0, x1), (y0, y1) = proj.x_limits, proj.y_limits 63 | return (x0, y0, x1, y1) 64 | extents = super().get_extents(element, ranges, range_type) 65 | if not getattr(element, 'crs', None) or not self.geographic: 66 | return extents 67 | elif any(e is None or not np.isfinite(e) for e in extents): 68 | extents = None 69 | else: 70 | extents = project_extents(extents, element.crs, proj) 71 | return (np.nan,)*4 if not extents else extents 72 | -------------------------------------------------------------------------------- /geoviews/annotators.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import param 3 | from holoviews.annotators import ( 4 | Annotator, 5 | PathAnnotator, 6 | PointAnnotator, 7 | PolyAnnotator, 8 | RectangleAnnotator, 9 | annotate, 10 | ) 11 | from holoviews.plotting.links import DataLink, VertexTableLink as hvVertexTableLink 12 | from panel.util import param_name 13 | 14 | from .element import Path 15 | from .links import ( 16 | HvRectanglesTableLink, 17 | PointTableLink, 18 | RectanglesTableLink, 19 | VertexTableLink, 20 | ) 21 | from .models.custom_tools import CheckpointTool, ClearTool, RestoreTool 22 | from .operation import project 23 | from .streams import PolyVertexDraw, PolyVertexEdit 24 | 25 | Annotator._tools = [CheckpointTool, RestoreTool, ClearTool] 26 | Annotator.table_transforms.append(project.instance(projection=ccrs.PlateCarree())) 27 | 28 | def get_point_table_link(self, source, target): 29 | if hasattr(source.callback.inputs[0], 'crs'): 30 | return PointTableLink(source, target) 31 | else: 32 | return DataLink(source, target) 33 | 34 | PointAnnotator._link_type = get_point_table_link 35 | 36 | def get_rectangles_table_link(self, source, target): 37 | if hasattr(source.callback.inputs[0], 'crs'): 38 | return RectanglesTableLink(source, target) 39 | else: 40 | return HvRectanglesTableLink(source, target) 41 | 42 | RectangleAnnotator._link_type = get_rectangles_table_link 43 | 44 | def get_vertex_table_link(self, source, target): 45 | if hasattr(source.callback.inputs[0], 'crs'): 46 | return VertexTableLink(source, target) 47 | else: 48 | return hvVertexTableLink(source, target) 49 | 50 | PathAnnotator._vertex_table_link = get_vertex_table_link 51 | PolyAnnotator._vertex_table_link = get_vertex_table_link 52 | 53 | def initialize_tools(plot, element): 54 | """Initializes the Checkpoint and Restore tools.""" 55 | cds = plot.handles['source'] 56 | checkpoint = plot.state.select(type=CheckpointTool) 57 | restore = plot.state.select(type=RestoreTool) 58 | clear = plot.state.select(type=ClearTool) 59 | if checkpoint: 60 | checkpoint[0].sources.append(cds) 61 | if restore: 62 | restore[0].sources.append(cds) 63 | if clear: 64 | clear[0].sources.append(cds) 65 | 66 | Annotator._extra_opts['hooks'] = [initialize_tools] 67 | 68 | 69 | class PathBreakingAnnotator(PathAnnotator): 70 | 71 | feature_style = param.Dict(default={'fill_color': 'blue', 'size': 10}, doc=""" 72 | Styling to apply to the feature vertices.""") 73 | 74 | node_style = param.Dict(default={'fill_color': 'indianred', 'size': 6}, doc=""" 75 | Styling to apply to the node vertices.""") 76 | 77 | def _init_stream(self): 78 | name = param_name(self.name) 79 | style_kwargs = dict(node_style=self.node_style, feature_style=self.feature_style) 80 | self._stream = PolyVertexDraw( 81 | source=self.plot, data={}, num_objects=self.num_objects, 82 | show_vertices=self.show_vertices, tooltip=f'{name} Tool', 83 | **style_kwargs 84 | ) 85 | if self.edit_vertices: 86 | self._vertex_stream = PolyVertexEdit( 87 | source=self.plot, tooltip=f'{name} Edit Tool', 88 | **style_kwargs 89 | ) 90 | 91 | annotate._annotator_types[Path] = PathBreakingAnnotator 92 | -------------------------------------------------------------------------------- /geoviews/tests/plotting/mpl/test_plot.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import pytest 4 | import pyviz_comms as comms 5 | from param import concrete_descendents 6 | from shapely.geometry import MultiPolygon, Polygon 7 | 8 | from geoviews import Polygons, Store 9 | from geoviews.plotting.mpl import ElementPlot 10 | 11 | mpl_renderer = Store.renderers['matplotlib'] 12 | 13 | 14 | class TestMPLPlot: 15 | 16 | def setup_method(self): 17 | self.previous_backend = Store.current_backend 18 | self.comm_manager = mpl_renderer.comm_manager 19 | mpl_renderer.comm_manager = comms.CommManager 20 | Store.set_current_backend('matplotlib') 21 | self._padding = {} 22 | for plot in concrete_descendents(ElementPlot).values(): 23 | self._padding[plot] = plot.padding 24 | plot.padding = 0 25 | 26 | def teardown_method(self): 27 | Store.current_backend = self.previous_backend 28 | mpl_renderer.comm_manager = self.comm_manager 29 | plt.close(plt.gcf()) 30 | for plot, padding in self._padding.items(): 31 | plot.padding = padding 32 | 33 | def test_polygons_categorical_color_with_geopandas(self): 34 | # Test for https://github.com/holoviz/holoviews/pull/6762 35 | 36 | pytest.importorskip("holoviews", minversion="1.23.0a1") 37 | gpd = pytest.importorskip("geopandas") 38 | 39 | data = { 40 | 'state': ['Texas', 'Hawaii', 'Michigan', 'Florida'], 41 | 'bea_region': ['Southwest', 'Far West', 'Great Lakes', 'Southeast'], 42 | 'geometry': [ 43 | Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]), 44 | MultiPolygon([ 45 | Polygon([(3, 0), (3.5, 0), (3.5, 0.5), (3, 0.5)]), 46 | Polygon([(4, 0), (4.5, 0), (4.5, 0.5), (4, 0.5)]), 47 | Polygon([(5, 0), (5.5, 0), (5.5, 0.5), (5, 0.5)]), 48 | ]), 49 | MultiPolygon([ 50 | Polygon([(0, 3), (1.5, 3), (1.5, 4.5), (0, 4.5)]), 51 | Polygon([(2, 3), (3, 3), (3, 4), (2, 4)]), 52 | ]), 53 | Polygon([(6, 0), (8, 0), (8, 2), (6, 2)]), 54 | ] 55 | } 56 | 57 | gdf = gpd.GeoDataFrame(data) 58 | 59 | # Verify test data is structured correctly 60 | expected_geom_types = {'Texas': 'Polygon', 'Hawaii': 'MultiPolygon', 61 | 'Michigan': 'MultiPolygon', 'Florida': 'Polygon'} 62 | 63 | for state, expected_type in expected_geom_types.items(): 64 | actual_type = gdf.loc[gdf['state'] == state, 'geometry'].iloc[0].geom_type 65 | assert actual_type == expected_type 66 | 67 | polygons = Polygons(gdf, vdims=["bea_region"]).opts(c="bea_region") 68 | 69 | plot = mpl_renderer.get_plot(polygons) 70 | array = plot.handles["artist"].get_array() 71 | 72 | assert array.dtype.kind == 'i' 73 | assert len(np.unique(array)) == len(gdf['bea_region'].values) 74 | 75 | # CRITICAL TEST: Verify multi-polygon handling 76 | # Without the fix: len(array) = len(gdf) (4, one color value per state) 77 | # With the fix: len(array) = total_subpolygons (7, one color value per sub-polygon) 78 | total_subpolygons = sum( 79 | len(geom.geoms) if geom.geom_type == 'MultiPolygon' else 1 80 | for geom in gdf.geometry 81 | ) 82 | 83 | assert len(array) == total_subpolygons 84 | -------------------------------------------------------------------------------- /doc/user_guide/Using_Features_Offline.md: -------------------------------------------------------------------------------- 1 | # Using Features Offline 2 | 3 | ## Creating Environment 4 | 5 | Under the hood, GeoViews features simply wrap ``cartopy`` features, so it's a matter of properly 6 | configuring ``cartopy`` ahead of time. 7 | 8 | 1. Create a new cartopy environment (or use an existing one): 9 | 10 | ```bash 11 | conda create -n cartopy_env python=3.10 12 | ``` 13 | 14 | 2. Install the required packages (note that `cartopy_offlinedata` is about 200MBs): 15 | 16 | ```bash 17 | conda install -c conda-forge geoviews cartopy cartopy_offlinedata 18 | ``` 19 | 20 | Or if you have an environment already, you may just need [`cartopy_offlinedata`](https://anaconda.org/conda-forge/cartopy_offlinedata): 21 | 22 | ```bash 23 | conda install -c conda-forge cartopy_offlinedata 24 | ``` 25 | 26 | ## Verifying Setup 27 | 28 | Now, we will verify that the shapefiles are available offline. 29 | 30 | 1. Ensure offline shapefiles were downloaded: 31 | 32 | ```python 33 | from pathlib import Path 34 | import cartopy 35 | 36 | data_dir = Path(cartopy.config["pre_existing_data_dir"]) 37 | shapefiles = data_dir / "shapefiles" / "natural_earth" / "cultural" 38 | list(shapefiles.glob("*")) 39 | ``` 40 | 41 | 2. Test GeoViews offline (toggle internet off): 42 | 43 | ```python 44 | import geoviews as gv 45 | from bokeh.resources import INLINE 46 | 47 | gv.extension("bokeh") 48 | 49 | coastline = gv.feature.coastline() 50 | borders = gv.feature.borders() 51 | world = (coastline * borders).opts(global_extent=True) 52 | 53 | gv.save(world, "world.html", resources=INLINE) 54 | ``` 55 | 56 | Please ensure to set [`resources=INLINE`](https://docs.bokeh.org/en/latest/docs/reference/resources.html#bokeh.resources.INLINE) if the machine you're using is completely 57 | offline and you intend to view the output on that machine. 58 | Failure to do so will result in the HTML file appearing empty when opened. 59 | 60 | ## Changing Directory 61 | 62 | If you wish to change the default data directory, follow these steps. 63 | 64 | 1. Create a new directory and move the data: 65 | 66 | ```python 67 | from pathlib import Path 68 | import cartopy 69 | 70 | new_data_dir = Path("~/.cartopy").expanduser() 71 | new_data_dir.mkdir(exist_ok=True) 72 | 73 | data_dir = Path(cartopy.config["pre_existing_data_dir"]) 74 | data_dir.rename(new_data_dir / "cartopy") 75 | ``` 76 | 77 | 2. Point to the new data directory within the script: 78 | 79 | ```python 80 | from pathlib import Path 81 | 82 | import cartopy 83 | import geoviews as gv 84 | from bokeh.resources import INLINE 85 | 86 | cartopy.config["pre_existing_data_dir"] = str(Path("~/.cartopy/cartopy").expanduser()) 87 | 88 | gv.extension("bokeh") 89 | 90 | coastline = gv.feature.coastline() 91 | borders = gv.feature.borders() 92 | world = (coastline * borders).opts(global_extent=True) 93 | 94 | gv.save(world, "world.html", resources=INLINE) 95 | ``` 96 | 97 | 3. Or set an environment variable ``CARTOPY_DATA_DIR``: 98 | 99 | For sh: 100 | ```bash 101 | export CARTOPY_DATA_DIR="$HOME/.cartopy/cartopy" 102 | ``` 103 | 104 | For powershell: 105 | ```powershell 106 | $env:CARTOPY_DATA_DIR = "$HOME/.cartopy/cartopy" 107 | ``` 108 | 109 | For cmd: 110 | ```cmd 111 | set CARTOPY_DATA_DIR=%USERPROFILE%\.cartopy\cartopy 112 | ``` 113 | 114 | Please note using tilde (``~``) in the environment variable will not work. 115 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/katrina_track.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "import cartopy.io.shapereader as shpreader\n", 11 | "import shapely.geometry as sgeom\n", 12 | "\n", 13 | "gv.extension('matplotlib')\n", 14 | "\n", 15 | "gv.output(fig='svg', size=250)" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## Define data" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def sample_data():\n", 32 | " \"\"\"\n", 33 | " Returns a list of latitudes and a list of longitudes (lons, lats)\n", 34 | " for Hurricane Katrina (2005).\n", 35 | "\n", 36 | " The data was originally sourced from the HURDAT2 dataset from AOML/NOAA:\n", 37 | " http://www.aoml.noaa.gov/hrd/hurdat/newhurdat-all.html on 14th Dec 2012.\n", 38 | "\n", 39 | " \"\"\"\n", 40 | " lons = [-75.1, -75.7, -76.2, -76.5, -76.9, -77.7, -78.4, -79.0,\n", 41 | " -79.6, -80.1, -80.3, -81.3, -82.0, -82.6, -83.3, -84.0,\n", 42 | " -84.7, -85.3, -85.9, -86.7, -87.7, -88.6, -89.2, -89.6,\n", 43 | " -89.6, -89.6, -89.6, -89.6, -89.1, -88.6, -88.0, -87.0,\n", 44 | " -85.3, -82.9]\n", 45 | "\n", 46 | " lats = [23.1, 23.4, 23.8, 24.5, 25.4, 26.0, 26.1, 26.2, 26.2, 26.0,\n", 47 | " 25.9, 25.4, 25.1, 24.9, 24.6, 24.4, 24.4, 24.5, 24.8, 25.2,\n", 48 | " 25.7, 26.3, 27.2, 28.2, 29.3, 29.5, 30.2, 31.1, 32.6, 34.1,\n", 49 | " 35.6, 37.0, 38.6, 40.1]\n", 50 | "\n", 51 | " return lons, lats\n", 52 | "\n", 53 | "shapename = 'admin_1_states_provinces_lakes'\n", 54 | "states_shp = shpreader.natural_earth(resolution='110m',\n", 55 | " category='cultural', name=shapename)\n", 56 | "\n", 57 | "lons, lats = sample_data()\n", 58 | "track = sgeom.LineString(zip(lons, lats))\n", 59 | "\n", 60 | "title = 'US States which intersect the track of Hurricane Katrina (2005)'\n", 61 | "\n", 62 | "track_buffer = track.buffer(2)\n", 63 | "intersects, buffers, other = [], [], []\n", 64 | "for state in shpreader.Reader(states_shp).geometries():\n", 65 | " if state.intersects(track):\n", 66 | " intersects.append(state)\n", 67 | " elif state.intersects(track_buffer):\n", 68 | " buffers.append(state)\n", 69 | " else:\n", 70 | " other.append(state)\n", 71 | "\n", 72 | "intersects = gv.Polygons(intersects, label='State directly intersects with track').opts(facecolor='#FF0000')\n", 73 | "buffers = gv.Polygons(buffers, label='State is within 2 degrees of track').opts(facecolor='#FF7E00')\n", 74 | "other = gv.Polygons(other).opts(facecolor=(0.9375, 0.9375, 0.859375))\n", 75 | "track_buffer = gv.Shape(track_buffer).opts(alpha=0.5)\n", 76 | "track = gv.Shape(track)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## Plot" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "(intersects * buffers * other * track_buffer * track).opts(title=title)" 93 | ] 94 | } 95 | ], 96 | "metadata": { 97 | "language_info": { 98 | "name": "python", 99 | "pygments_lexer": "ipython3" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 2 104 | } 105 | -------------------------------------------------------------------------------- /examples/Homepage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "GeoViews is a [Python](http://python.org) library that makes it easy to explore and visualize geographical, meteorological, and oceanographic datasets, such as those used in weather, climate, and remote sensing research. \n", 8 | "\n", 9 | "GeoViews is built on the [HoloViews](http://holoviews.org) library for building flexible visualizations of multidimensional data. GeoViews adds a family of geographic plot types based on the [Cartopy](http://scitools.org.uk/cartopy) library, plotted using either the [Matplotlib](http://matplotlib.org) or [Bokeh](http://bokeh.pydata.org) packages. \n", 10 | "With GeoViews, you can now work easily and naturally with large, multidimensional geographic datasets, instantly visualizing any subset or combination of them, while always being able to access the raw data underlying any plot. Here's a simple example:" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import geoviews as gv\n", 20 | "import geoviews.feature as gf\n", 21 | "import xarray as xr\n", 22 | "from cartopy import crs\n", 23 | "\n", 24 | "gv.extension('bokeh', 'matplotlib')" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "(gf.ocean + gf.land + gf.ocean * gf.land * gf.coastline * gf.borders).opts(\n", 34 | " 'Feature', projection=crs.Geostationary(), global_extent=True, height=325)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "GeoViews is designed to work well with the [Iris](http://scitools.org.uk/iris) and [xarray](http://xarray.pydata.org) libraries for working with multidimensional arrays, such as those stored in netCDF files. GeoViews also accepts data as NumPy arrays and Pandas data frames. In each case, the data can be left stored in its original, native format, wrapped in a HoloViews or GeoViews object that provides instant interactive visualizations.\n", 42 | "\n", 43 | "The following example loads a dataset originally taken from [iris-sample-data](https://github.com/SciTools/iris-sample-data) and quickly builds an interactive tool for exploring how the data changes over time:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "dataset = gv.Dataset(xr.open_dataset('./data/ensemble.nc'))\n", 53 | "ensemble = dataset.to(gv.Image, ['longitude', 'latitude'], 'surface_temperature')\n", 54 | "\n", 55 | "gv.output(ensemble.opts(cmap='viridis', colorbar=True, fig_size=120, backend='matplotlib') * gf.coastline(),\n", 56 | " backend='matplotlib')" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "GeoViews also natively supports geopandas datastructures allowing us to easily plot shapefiles and choropleths:" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "import geopandas as gpd\n", 73 | "import geodatasets as gds\n", 74 | "\n", 75 | "nybb = gpd.read_file(gds.get_path('nybb'))\n", 76 | "poly_data = nybb.to_crs(crs.GOOGLE_MERCATOR.proj4_init)\n", 77 | "gv.tile_sources.OSM * gv.Polygons(poly_data, vdims='BoroName', crs=crs.GOOGLE_MERCATOR).opts(height=500, width=500, tools=[\"hover\"])" 78 | ] 79 | } 80 | ], 81 | "metadata": { 82 | "language_info": { 83 | "name": "python", 84 | "pygments_lexer": "ipython3" 85 | } 86 | }, 87 | "nbformat": 4, 88 | "nbformat_minor": 4 89 | } 90 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/katrina_track.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import geoviews as gv\n", 10 | "import cartopy.crs as ccrs\n", 11 | "import cartopy.io.shapereader as shpreader\n", 12 | "import shapely.geometry as sgeom\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def sample_data():\n", 31 | " \"\"\"\n", 32 | " Returns a list of latitudes and a list of longitudes (lons, lats)\n", 33 | " for Hurricane Katrina (2005).\n", 34 | "\n", 35 | " The data was originally sourced from the HURDAT2 dataset from AOML/NOAA:\n", 36 | " http://www.aoml.noaa.gov/hrd/hurdat/newhurdat-all.html on 14th Dec 2012.\n", 37 | "\n", 38 | " \"\"\"\n", 39 | " lons = [-75.1, -75.7, -76.2, -76.5, -76.9, -77.7, -78.4, -79.0,\n", 40 | " -79.6, -80.1, -80.3, -81.3, -82.0, -82.6, -83.3, -84.0,\n", 41 | " -84.7, -85.3, -85.9, -86.7, -87.7, -88.6, -89.2, -89.6,\n", 42 | " -89.6, -89.6, -89.6, -89.6, -89.1, -88.6, -88.0, -87.0,\n", 43 | " -85.3, -82.9]\n", 44 | "\n", 45 | " lats = [23.1, 23.4, 23.8, 24.5, 25.4, 26.0, 26.1, 26.2, 26.2, 26.0,\n", 46 | " 25.9, 25.4, 25.1, 24.9, 24.6, 24.4, 24.4, 24.5, 24.8, 25.2,\n", 47 | " 25.7, 26.3, 27.2, 28.2, 29.3, 29.5, 30.2, 31.1, 32.6, 34.1,\n", 48 | " 35.6, 37.0, 38.6, 40.1]\n", 49 | "\n", 50 | " return lons, lats\n", 51 | "\n", 52 | "shapename = 'admin_1_states_provinces_lakes'\n", 53 | "states_shp = shpreader.natural_earth(resolution='110m',\n", 54 | " category='cultural', name=shapename)\n", 55 | "\n", 56 | "lons, lats = sample_data()\n", 57 | "track = sgeom.LineString(zip(lons, lats))\n", 58 | "\n", 59 | "title = 'US States which intersect the track of Hurricane Katrina (2005)'\n", 60 | "\n", 61 | "track_buffer = track.buffer(2)\n", 62 | "intersects, buffers, other = [], [], []\n", 63 | "for state in shpreader.Reader(states_shp).geometries():\n", 64 | " if state.intersects(track):\n", 65 | " intersects.append(state)\n", 66 | " elif state.intersects(track_buffer):\n", 67 | " buffers.append(state)\n", 68 | " else:\n", 69 | " other.append(state)\n", 70 | "\n", 71 | "intersects = gv.Polygons(intersects, label='State directly intersects with track').opts(show_legend=True, fill_color='#FF0000')\n", 72 | "buffers = gv.Polygons(buffers, label='State is within 2 degrees of track').opts(show_legend=True, fill_color='#FF7E00')\n", 73 | "other = gv.Polygons(other).opts(fill_color=(0.9375, 0.9375, 0.859375))\n", 74 | "track_buffer = gv.Shape(track_buffer).opts(alpha=0.5)\n", 75 | "track = gv.Shape(track)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "## Plot" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "(intersects * buffers * other * track_buffer * track).opts(\n", 92 | " width=700, height=500, projection=ccrs.PlateCarree(), title=title, show_legend=True)" 93 | ] 94 | } 95 | ], 96 | "metadata": { 97 | "language_info": { 98 | "name": "python", 99 | "pygments_lexer": "ipython3" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 2 104 | } 105 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. raw:: html 2 | 3 |

4 | 5 | 6 | **Geographic visualizations for HoloViews** 7 | 8 | 9 | .. notebook:: geoviews ../examples/Homepage.ipynb 10 | 11 | 12 | Please see the `User Guide `_ for further documentation. 13 | 14 | If you have any `issues `_ or wish 15 | to `contribute code `_., you can visit 16 | our `GitHub site `_ or file a topic on 17 | the `HoloViz Discourse `_. 18 | 19 | Installation 20 | ============ 21 | 22 | You can install GeoViews and its dependencies using pip:: 23 | 24 | pip install geoviews 25 | 26 | Alternatively you can use conda to install geoviews: 27 | 28 | conda install -c pyviz geoviews 29 | 30 | Or, the geoviews-core package, which only installs the minimal 31 | dependencies required to run geoviews:: 32 | 33 | conda install -c pyviz geoviews-core 34 | 35 | Once installed you can copy the examples into the current directory 36 | using the ``geoviews`` command and run them using the Jupyter 37 | notebook:: 38 | 39 | geoviews examples 40 | cd geoviews-examples 41 | jupyter notebook 42 | 43 | (Here ``geoviews examples`` is a shorthand for ``geoviews copy-examples 44 | --path geoviews-examples && geoviews fetch-data --path 45 | geoviews-examples``.) 46 | 47 | In the classic Jupyter notebook environment and JupyterLab, first make 48 | sure to load the ``gv.extension()``. For versions of 49 | ``jupyterlab>=3.0`` the necessary extension is automatically bundled 50 | in the ``pyviz_comms`` package, which must be >=2.0. However note that 51 | for version of ``jupyterlab<3.0`` you must also manually install the 52 | JupyterLab extension with:: 53 | 54 | conda install -c conda-forge jupyterlab 55 | jupyter labextension install @pyviz/jupyterlab_pyviz 56 | 57 | Once you have installed JupyterLab and the extension launch it with:: 58 | 59 | jupyter-lab 60 | 61 | If you want to try out the latest features between releases, you can 62 | get the latest dev release by specifying ``-c pyviz/label/dev`` in place 63 | of ``-c pyviz``. 64 | 65 | Additional dependencies 66 | ======================= 67 | 68 | If you need to install libraries only available from conda-forge, such 69 | as Iris (to use data stored in Iris cubes) or xesmf, you should 70 | install from conda-forge:: 71 | 72 | conda create -n env-name -c pyviz -c conda-forge geoviews iris xesmf 73 | conda activate env-name 74 | 75 | **Note -- Do not mix conda-forge and defaults.** I.e., do not install 76 | packages from conda-forge into a GeoViews environment created with 77 | defaults. If you are using the base environment of mini/anaconda, or 78 | an environment created without specifying conda-forge before defaults, 79 | and you then install from conda-forge, you will very likely have 80 | incompatibilities in underlying, low-level dependencies. These binary 81 | (ABI) incompatibilities can lead to segfaults because of differences 82 | in how non-Python packages are built between conda-forge and defaults. 83 | 84 | ----- 85 | 86 | GeoViews itself is installable using ``pip``, as long as you're using 87 | cartopy>=0.22.0; otherwise you will first need to have installed the 88 | `dependencies of cartopy `_, 89 | or have set up your system to be able to build them. 90 | 91 | .. toctree:: 92 | :hidden: 93 | :maxdepth: 2 94 | 95 | Home 96 | User Guide 97 | Gallery 98 | Examples 99 | API 100 | Developer Guide 101 | Releases 102 | About 103 | -------------------------------------------------------------------------------- /geoviews/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from holoviews import ( 4 | Cycle, 5 | Dimension, 6 | DynamicMap, 7 | GridSpace, 8 | HoloMap, 9 | Layout, 10 | NdLayout, 11 | NdOverlay, 12 | Overlay, 13 | Palette, 14 | Store, 15 | dim, 16 | extension, 17 | help, 18 | opts, 19 | output, 20 | render, 21 | renderer, 22 | save, 23 | ) 24 | 25 | from . import data, feature, plotting, tile_sources 26 | from .__version import __version__ 27 | from ._warnings import GeoviewsDeprecationWarning, GeoviewsUserWarning 28 | from .element import ( 29 | RGB, 30 | WMTS, 31 | Contours, 32 | Dataset, 33 | EdgePaths, 34 | Feature, 35 | FilledContours, 36 | Graph, 37 | HexTiles, 38 | Image, 39 | ImageStack, 40 | Labels, 41 | LineContours, 42 | Nodes, 43 | Path, 44 | Points, 45 | Polygons, 46 | QuadMesh, 47 | Rectangles, 48 | Segments, 49 | Shape, 50 | Text, 51 | Tiles, 52 | TriMesh, 53 | VectorField, 54 | WindBarbs, 55 | ) 56 | from .util import from_xarray 57 | 58 | __all__ = ( 59 | "RGB", 60 | "WMTS", 61 | "Contours", 62 | "Cycle", 63 | "Dataset", 64 | "Dimension", 65 | "DynamicMap", 66 | "EdgePaths", 67 | "Feature", 68 | "FilledContours", 69 | "GeoviewsDeprecationWarning", 70 | "GeoviewsUserWarning", 71 | "Graph", 72 | "GridSpace", 73 | "HexTiles", 74 | "HoloMap", 75 | "Image", 76 | "ImageStack", 77 | "Labels", 78 | "Layout", 79 | "LineContours", 80 | "NdLayout", 81 | "NdOverlay", 82 | "Nodes", 83 | "Overlay", 84 | "Palette", 85 | "Path", 86 | "Points", 87 | "Polygons", 88 | "QuadMesh", 89 | "Rectangles", 90 | "Segments", 91 | "Shape", 92 | "Store", 93 | "Text", 94 | "Tiles", 95 | "TriMesh", 96 | "VectorField", 97 | "WindBarbs", 98 | "__version__", 99 | "annotate", # Lazy modules 100 | "data", 101 | "dim", 102 | "extension", 103 | "feature", 104 | "from_xarray", 105 | "help", 106 | "operation", # Lazy modules 107 | "opts", 108 | "output", 109 | "plotting", 110 | "project", # Lazy modules 111 | "render", 112 | "renderer", 113 | "save", 114 | "tile_sources", 115 | ) 116 | 117 | # Ensure opts utility is initialized with GeoViews elements 118 | if Store._options: 119 | Store.set_current_backend(Store.current_backend) 120 | 121 | # make pyct's example/data commands available if possible 122 | try: 123 | from pyct.cmd import ( 124 | copy_examples as _copy, 125 | examples as _examples, 126 | fetch_data as _fetch, 127 | ) 128 | copy_examples = partial(_copy, 'geoviews') 129 | fetch_data = partial(_fetch, 'geoviews') 130 | examples = partial(_examples, 'geoviews') 131 | except ImportError: 132 | def _missing_cmd(*args,**kw): return("install pyct to enable this command (e.g. `conda install -c pyviz pyct`)") 133 | _copy = _fetch = _examples = _missing_cmd 134 | def _err(): raise ValueError(_missing_cmd()) 135 | fetch_data = copy_examples = examples = _err 136 | del partial, _examples, _copy, _fetch 137 | 138 | 139 | def __getattr__(attr): 140 | # Lazy loading heavy modules 141 | if attr == 'annotate': 142 | from .annotators import annotate 143 | return annotate 144 | elif attr == 'project': 145 | from .operation import project 146 | return project 147 | elif attr == 'operation': 148 | from . import operation 149 | return operation 150 | raise AttributeError(f"module {__name__} has no attribute {attr!r}") 151 | 152 | 153 | def __dir__(): 154 | return __all__ 155 | 156 | from typing import TYPE_CHECKING 157 | 158 | if TYPE_CHECKING: 159 | from . import operation 160 | from .annotators import annotate 161 | from .operation import project 162 | -------------------------------------------------------------------------------- /geoviews/element/__init__.py: -------------------------------------------------------------------------------- 1 | from holoviews.element import ( 2 | ElementConversion, 3 | Path as HvPath, 4 | Points as HvPoints, 5 | Polygons as HvPolygons, 6 | ) 7 | 8 | from .geo import ( 9 | RGB, 10 | WMTS, 11 | Contours, 12 | Dataset, 13 | EdgePaths, 14 | Feature, 15 | FilledContours, 16 | Graph, 17 | HexTiles, 18 | Image, 19 | ImageStack, 20 | Labels, 21 | LineContours, 22 | Nodes, 23 | Path, 24 | Points, 25 | Polygons, 26 | QuadMesh, 27 | Rectangles, 28 | Segments, 29 | Shape, 30 | Text, 31 | Tiles, 32 | TriMesh, 33 | VectorField, 34 | WindBarbs, 35 | _Element, 36 | is_geographic, 37 | ) 38 | 39 | __all__ = ( 40 | "RGB", 41 | "WMTS", 42 | "Contours", 43 | "Dataset", 44 | "EdgePaths", 45 | "Feature", 46 | "FilledContours", 47 | "GeoConversion", 48 | "Graph", 49 | "HexTiles", 50 | "Image", 51 | "ImageStack", 52 | "Labels", 53 | "LineContours", 54 | "Nodes", 55 | "Path", 56 | "Points", 57 | "Polygons", 58 | "QuadMesh", 59 | "Rectangles", 60 | "Segments", 61 | "Shape", 62 | "Text", 63 | "Tiles", 64 | "TriMesh", 65 | "VectorField", 66 | "WindBarbs", 67 | "is_geographic", 68 | ) 69 | 70 | 71 | class GeoConversion(ElementConversion): 72 | """GeoConversion is a very simple container object which can 73 | be given an existing Dataset and provides methods to convert 74 | the Dataset into most other Element types. If the requested 75 | key dimensions correspond to geographical coordinates the 76 | conversion interface will automatically use a geographical 77 | Element type while all other plot will use regular HoloViews 78 | Elements. 79 | """ 80 | 81 | def __init__(self, cube): 82 | self._element = cube 83 | 84 | def __call__(self, *args, **kwargs): 85 | group_type = args[0] 86 | if 'crs' not in kwargs and issubclass(group_type, _Element): 87 | kwargs['crs'] = self._element.crs 88 | is_gpd = self._element.interface.datatype == 'geodataframe' 89 | if is_gpd: 90 | kdims = args[1] if len(args) > 1 else kwargs.get('kdims', None) 91 | if len(args) > 1: 92 | args = (Dataset, [])+args[2:] 93 | else: 94 | args = (Dataset,) 95 | kwargs['kdims'] = [] 96 | converted = super().__call__(*args, **kwargs) 97 | if is_gpd: 98 | if kdims is None: kdims = group_type.kdims 99 | converted = converted.map(lambda x: x.clone(kdims=kdims, new_type=group_type), Dataset) 100 | return converted 101 | 102 | def linecontours(self, kdims=None, vdims=None, mdims=None, **kwargs): 103 | return self(LineContours, kdims, vdims, mdims, **kwargs) 104 | 105 | def filledcontours(self, kdims=None, vdims=None, mdims=None, **kwargs): 106 | return self(FilledContours, kdims, vdims, mdims, **kwargs) 107 | 108 | def image(self, kdims=None, vdims=None, mdims=None, **kwargs): 109 | return self(Image, kdims, vdims, mdims, **kwargs) 110 | 111 | def image_stack(self, kdims=None, vdims=None, mdims=None, **kwargs): 112 | return self(ImageStack, kdims, vdims, mdims, **kwargs) 113 | 114 | def points(self, kdims=None, vdims=None, mdims=None, **kwargs): 115 | if kdims is None: kdims = self._element.kdims 116 | el_type = Points if is_geographic(self._element, kdims) else HvPoints 117 | return self(el_type, kdims, vdims, mdims, **kwargs) 118 | 119 | def polygons(self, kdims=None, vdims=None, mdims=None, **kwargs): 120 | if kdims is None: kdims = self._element.kdims 121 | el_type = Polygons if is_geographic(self._element, kdims) else HvPolygons 122 | return self(el_type, kdims, vdims, mdims, **kwargs) 123 | 124 | def path(self, kdims=None, vdims=None, mdims=None, **kwargs): 125 | if kdims is None: kdims = self._element.kdims 126 | el_type = Path if is_geographic(self._element, kdims) else HvPath 127 | return self(el_type, kdims, vdims, mdims, **kwargs) 128 | 129 | 130 | Dataset._conversion_interface = GeoConversion 131 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/trimesh_uk.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('bokeh')" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "# Some points defining a triangulation over (roughly) Britain.\n", 31 | "xy = np.asarray([\n", 32 | " [-0.101, 0.872], [-0.080, 0.883], [-0.069, 0.888], [-0.054, 0.890],\n", 33 | " [-0.045, 0.897], [-0.057, 0.895], [-0.073, 0.900], [-0.087, 0.898],\n", 34 | " [-0.090, 0.904], [-0.069, 0.907], [-0.069, 0.921], [-0.080, 0.919],\n", 35 | " [-0.073, 0.928], [-0.052, 0.930], [-0.048, 0.942], [-0.062, 0.949],\n", 36 | " [-0.054, 0.958], [-0.069, 0.954], [-0.087, 0.952], [-0.087, 0.959],\n", 37 | " [-0.080, 0.966], [-0.085, 0.973], [-0.087, 0.965], [-0.097, 0.965],\n", 38 | " [-0.097, 0.975], [-0.092, 0.984], [-0.101, 0.980], [-0.108, 0.980],\n", 39 | " [-0.104, 0.987], [-0.102, 0.993], [-0.115, 1.001], [-0.099, 0.996],\n", 40 | " [-0.101, 1.007], [-0.090, 1.010], [-0.087, 1.021], [-0.069, 1.021],\n", 41 | " [-0.052, 1.022], [-0.052, 1.017], [-0.069, 1.010], [-0.064, 1.005],\n", 42 | " [-0.048, 1.005], [-0.031, 1.005], [-0.031, 0.996], [-0.040, 0.987],\n", 43 | " [-0.045, 0.980], [-0.052, 0.975], [-0.040, 0.973], [-0.026, 0.968],\n", 44 | " [-0.020, 0.954], [-0.006, 0.947], [ 0.003, 0.935], [ 0.006, 0.926],\n", 45 | " [ 0.005, 0.921], [ 0.022, 0.923], [ 0.033, 0.912], [ 0.029, 0.905],\n", 46 | " [ 0.017, 0.900], [ 0.012, 0.895], [ 0.027, 0.893], [ 0.019, 0.886],\n", 47 | " [ 0.001, 0.883], [-0.012, 0.884], [-0.029, 0.883], [-0.038, 0.879],\n", 48 | " [-0.057, 0.881], [-0.062, 0.876], [-0.078, 0.876], [-0.087, 0.872],\n", 49 | " [-0.030, 0.907], [-0.007, 0.905], [-0.057, 0.916], [-0.025, 0.933],\n", 50 | " [-0.077, 0.990], [-0.059, 0.993]])\n", 51 | "# Make lats + lons\n", 52 | "x = xy[:, 0]*180/3.14159\n", 53 | "y = xy[:, 1]*180/3.14159\n", 54 | "\n", 55 | "# A selected triangulation of the points.\n", 56 | "triangles = np.asarray([\n", 57 | " [67, 66, 1], [65, 2, 66], [ 1, 66, 2], [64, 2, 65], [63, 3, 64],\n", 58 | " [60, 59, 57], [ 2, 64, 3], [ 3, 63, 4], [ 0, 67, 1], [62, 4, 63],\n", 59 | " [57, 59, 56], [59, 58, 56], [61, 60, 69], [57, 69, 60], [ 4, 62, 68],\n", 60 | " [ 6, 5, 9], [61, 68, 62], [69, 68, 61], [ 9, 5, 70], [ 6, 8, 7],\n", 61 | " [ 4, 70, 5], [ 8, 6, 9], [56, 69, 57], [69, 56, 52], [70, 10, 9],\n", 62 | " [54, 53, 55], [56, 55, 53], [68, 70, 4], [52, 56, 53], [11, 10, 12],\n", 63 | " [69, 71, 68], [68, 13, 70], [10, 70, 13], [51, 50, 52], [13, 68, 71],\n", 64 | " [52, 71, 69], [12, 10, 13], [71, 52, 50], [71, 14, 13], [50, 49, 71],\n", 65 | " [49, 48, 71], [14, 16, 15], [14, 71, 48], [17, 19, 18], [17, 20, 19],\n", 66 | " [48, 16, 14], [48, 47, 16], [47, 46, 16], [16, 46, 45], [23, 22, 24],\n", 67 | " [21, 24, 22], [17, 16, 45], [20, 17, 45], [21, 25, 24], [27, 26, 28],\n", 68 | " [20, 72, 21], [25, 21, 72], [45, 72, 20], [25, 28, 26], [44, 73, 45],\n", 69 | " [72, 45, 73], [28, 25, 29], [29, 25, 31], [43, 73, 44], [73, 43, 40],\n", 70 | " [72, 73, 39], [72, 31, 25], [42, 40, 43], [31, 30, 29], [39, 73, 40],\n", 71 | " [42, 41, 40], [72, 33, 31], [32, 31, 33], [39, 38, 72], [33, 72, 38],\n", 72 | " [33, 38, 34], [37, 35, 38], [34, 38, 35], [35, 37, 36]])\n", 73 | "\n", 74 | "trimesh = gv.TriMesh((triangles, (x, y)), crs=ccrs.PlateCarree())" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Plot" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "gf.coastline.opts(line_color='red', scale='50m', width=600, height=500) * trimesh" 91 | ] 92 | } 93 | ], 94 | "metadata": { 95 | "language_info": { 96 | "name": "python", 97 | "pygments_lexer": "ipython3" 98 | } 99 | }, 100 | "nbformat": 4, 101 | "nbformat_minor": 2 102 | } 103 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/trimesh_uk.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "import geoviews.feature as gf\n", 12 | "import cartopy.crs as ccrs\n", 13 | "\n", 14 | "gv.extension('matplotlib')\n", 15 | "\n", 16 | "gv.output(fig='svg', size=200)" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Define data" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "# Some points defining a triangulation over (roughly) Britain.\n", 33 | "xy = np.asarray([\n", 34 | " [-0.101, 0.872], [-0.080, 0.883], [-0.069, 0.888], [-0.054, 0.890],\n", 35 | " [-0.045, 0.897], [-0.057, 0.895], [-0.073, 0.900], [-0.087, 0.898],\n", 36 | " [-0.090, 0.904], [-0.069, 0.907], [-0.069, 0.921], [-0.080, 0.919],\n", 37 | " [-0.073, 0.928], [-0.052, 0.930], [-0.048, 0.942], [-0.062, 0.949],\n", 38 | " [-0.054, 0.958], [-0.069, 0.954], [-0.087, 0.952], [-0.087, 0.959],\n", 39 | " [-0.080, 0.966], [-0.085, 0.973], [-0.087, 0.965], [-0.097, 0.965],\n", 40 | " [-0.097, 0.975], [-0.092, 0.984], [-0.101, 0.980], [-0.108, 0.980],\n", 41 | " [-0.104, 0.987], [-0.102, 0.993], [-0.115, 1.001], [-0.099, 0.996],\n", 42 | " [-0.101, 1.007], [-0.090, 1.010], [-0.087, 1.021], [-0.069, 1.021],\n", 43 | " [-0.052, 1.022], [-0.052, 1.017], [-0.069, 1.010], [-0.064, 1.005],\n", 44 | " [-0.048, 1.005], [-0.031, 1.005], [-0.031, 0.996], [-0.040, 0.987],\n", 45 | " [-0.045, 0.980], [-0.052, 0.975], [-0.040, 0.973], [-0.026, 0.968],\n", 46 | " [-0.020, 0.954], [-0.006, 0.947], [ 0.003, 0.935], [ 0.006, 0.926],\n", 47 | " [ 0.005, 0.921], [ 0.022, 0.923], [ 0.033, 0.912], [ 0.029, 0.905],\n", 48 | " [ 0.017, 0.900], [ 0.012, 0.895], [ 0.027, 0.893], [ 0.019, 0.886],\n", 49 | " [ 0.001, 0.883], [-0.012, 0.884], [-0.029, 0.883], [-0.038, 0.879],\n", 50 | " [-0.057, 0.881], [-0.062, 0.876], [-0.078, 0.876], [-0.087, 0.872],\n", 51 | " [-0.030, 0.907], [-0.007, 0.905], [-0.057, 0.916], [-0.025, 0.933],\n", 52 | " [-0.077, 0.990], [-0.059, 0.993]])\n", 53 | "# Make lats + lons\n", 54 | "x = xy[:, 0]*180/3.14159\n", 55 | "y = xy[:, 1]*180/3.14159\n", 56 | "\n", 57 | "# A selected triangulation of the points.\n", 58 | "triangles = np.asarray([\n", 59 | " [67, 66, 1], [65, 2, 66], [ 1, 66, 2], [64, 2, 65], [63, 3, 64],\n", 60 | " [60, 59, 57], [ 2, 64, 3], [ 3, 63, 4], [ 0, 67, 1], [62, 4, 63],\n", 61 | " [57, 59, 56], [59, 58, 56], [61, 60, 69], [57, 69, 60], [ 4, 62, 68],\n", 62 | " [ 6, 5, 9], [61, 68, 62], [69, 68, 61], [ 9, 5, 70], [ 6, 8, 7],\n", 63 | " [ 4, 70, 5], [ 8, 6, 9], [56, 69, 57], [69, 56, 52], [70, 10, 9],\n", 64 | " [54, 53, 55], [56, 55, 53], [68, 70, 4], [52, 56, 53], [11, 10, 12],\n", 65 | " [69, 71, 68], [68, 13, 70], [10, 70, 13], [51, 50, 52], [13, 68, 71],\n", 66 | " [52, 71, 69], [12, 10, 13], [71, 52, 50], [71, 14, 13], [50, 49, 71],\n", 67 | " [49, 48, 71], [14, 16, 15], [14, 71, 48], [17, 19, 18], [17, 20, 19],\n", 68 | " [48, 16, 14], [48, 47, 16], [47, 46, 16], [16, 46, 45], [23, 22, 24],\n", 69 | " [21, 24, 22], [17, 16, 45], [20, 17, 45], [21, 25, 24], [27, 26, 28],\n", 70 | " [20, 72, 21], [25, 21, 72], [45, 72, 20], [25, 28, 26], [44, 73, 45],\n", 71 | " [72, 45, 73], [28, 25, 29], [29, 25, 31], [43, 73, 44], [73, 43, 40],\n", 72 | " [72, 73, 39], [72, 31, 25], [42, 40, 43], [31, 30, 29], [39, 73, 40],\n", 73 | " [42, 41, 40], [72, 33, 31], [32, 31, 33], [39, 38, 72], [33, 72, 38],\n", 74 | " [33, 38, 34], [37, 35, 38], [34, 38, 35], [35, 37, 36]])\n", 75 | "\n", 76 | "trimesh = gv.TriMesh((triangles, (x, y)), crs=ccrs.PlateCarree())" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## Plot" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "gf.coastline().opts(edgecolor='red', scale='50m') * trimesh.opts(padding=0.1)" 93 | ] 94 | } 95 | ], 96 | "metadata": { 97 | "language_info": { 98 | "name": "python", 99 | "pygments_lexer": "ipython3" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 2 104 | } 105 | -------------------------------------------------------------------------------- /geoviews/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: ["./tsconfig.json"], 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [], 10 | rules: { 11 | "@typescript-eslint/ban-types": ["error", { 12 | types: { 13 | Function: false, 14 | object: false, 15 | "{}": false, 16 | }, 17 | }], 18 | "@typescript-eslint/consistent-type-assertions": "error", 19 | "@typescript-eslint/consistent-type-imports": ["error", { 20 | prefer: "type-imports", 21 | fixStyle: "separate-type-imports", 22 | }], 23 | "@typescript-eslint/member-delimiter-style": ["error", { 24 | multiline: { 25 | delimiter: "none", 26 | requireLast: true, 27 | }, 28 | singleline: { 29 | delimiter: "comma", 30 | requireLast: false, 31 | }, 32 | }], 33 | "@typescript-eslint/semi": ["error", "never"], 34 | "@typescript-eslint/type-annotation-spacing": ["error"], 35 | "@typescript-eslint/no-unnecessary-condition": ["error", {allowConstantLoopConditions: true}], 36 | "@typescript-eslint/strict-boolean-expressions": ["error", { 37 | allowAny: true, 38 | allowString: false, 39 | allowNumber: false, 40 | allowNullableObject: false, 41 | allowNullableBoolean: false, 42 | allowNullableString: false, 43 | allowNullableNumber: false, 44 | }], 45 | "@typescript-eslint/no-unnecessary-type-assertion": ["error"], 46 | "@typescript-eslint/no-unnecessary-type-constraint": ["error"], 47 | "@typescript-eslint/switch-exhaustiveness-check": ["error"], 48 | "no-self-assign": ["error", { 49 | props: false, 50 | }], 51 | "comma-dangle": ["off"], 52 | "@typescript-eslint/comma-dangle": ["error", { 53 | arrays: "always-multiline", 54 | objects: "always-multiline", 55 | imports: "always-multiline", 56 | exports: "always-multiline", 57 | functions: "always-multiline", 58 | enums: "always-multiline", 59 | generics: "always-multiline", 60 | tuples: "always-multiline", 61 | }], 62 | "comma-spacing": ["error", {before: false, after: true}], 63 | "dot-notation": "error", 64 | "eol-last": ["error", "always"], 65 | indent: "off", 66 | "@typescript-eslint/indent": ["error", 2, { 67 | SwitchCase: 1, 68 | outerIIFEBody: 1, 69 | ArrayExpression: "first", 70 | ObjectExpression: "first", 71 | ImportDeclaration: "first", 72 | VariableDeclarator: "first", 73 | CallExpression: {arguments: 1}, 74 | FunctionDeclaration: {body: 1, parameters: "off"}, 75 | FunctionExpression: {body: 1, parameters: "off"}, 76 | ignoredNodes: ["ConditionalExpression"], 77 | }], 78 | "@typescript-eslint/no-floating-promises": ["error", {ignoreVoid: true}], 79 | "no-debugger": "error", 80 | "no-floating-decimal": ["error"], 81 | "no-multiple-empty-lines": ["error", {max: 1, maxBOF: 0, maxEOF: 0}], 82 | "no-new-wrappers": "error", 83 | "no-template-curly-in-string": "error", 84 | "no-throw-literal": "error", 85 | "no-trailing-spaces": ["error"], 86 | "no-var": "error", 87 | "object-shorthand": "error", 88 | "prefer-const": ["error", {destructuring: "all"}], 89 | "prefer-exponentiation-operator": "error", 90 | "quote-props": ["error", "as-needed"], 91 | "object-curly-spacing": ["error", "never"], 92 | "space-before-blocks": ["error", "always"], 93 | "space-before-function-paren": ["error", { 94 | anonymous: "never", 95 | named: "never", 96 | asyncArrow: "always", 97 | }], 98 | "space-in-parens": ["error", "never"], 99 | "keyword-spacing": ["error", { 100 | before: true, 101 | after: true, 102 | }], 103 | "func-call-spacing": ["error", "never"], 104 | "no-whitespace-before-property": ["error"], 105 | "block-spacing": ["error", "always"], 106 | "key-spacing": ["error", { 107 | beforeColon: false, 108 | afterColon: true, 109 | mode: "minimum", 110 | }], 111 | "space-unary-ops": ["error", { 112 | words: true, 113 | nonwords: false, 114 | overrides: {}, 115 | }], 116 | "guard-for-in": ["error"], 117 | quotes: ["error", "double", { 118 | avoidEscape: true, 119 | allowTemplateLiterals: false, 120 | }], 121 | "brace-style": ["error", "1tbs", {allowSingleLine: true}], 122 | curly: ["error", "all"], 123 | "prefer-template": ["error"], 124 | "generator-star-spacing": ["error", { 125 | before: false, 126 | after: true, 127 | anonymous: {before: false, after: true}, 128 | method: {before: true, after: false}, 129 | }], 130 | "yield-star-spacing": ["error", {before: false, after: true}], 131 | }, 132 | overrides: [ 133 | { 134 | extends: ["plugin:@typescript-eslint/disable-type-checked"], 135 | files: ["./**/*.js"], 136 | }, 137 | ], 138 | } 139 | -------------------------------------------------------------------------------- /examples/gallery/bokeh/wind_barbs_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "gv.extension('bokeh')" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "## Define data\n", 20 | "\n", 21 | "IMPORTANT:\n", 22 | "\n", 23 | "Unlike the `VectorField` direction, which follows the mathematical convention:\n", 24 | "\n", 25 | "Reference: 0° = East (positive x-axis)\n", 26 | "Direction: Counterclockwise\n", 27 | "Meaning: Direction the vector points to\n", 28 | "Formula: θ = arctan2(v, u)\n", 29 | "\n", 30 | "```\n", 31 | " N (90°)\n", 32 | " |\n", 33 | "W (180°)-+-E (0°)\n", 34 | " |\n", 35 | " S (270°)\n", 36 | "```\n", 37 | "\n", 38 | "The `WindBarbs` direction follows the meteorological convention:\n", 39 | "\n", 40 | "Reference: 0° = North\n", 41 | "Direction: Clockwise\n", 42 | "Meaning: Direction the wind is coming from\n", 43 | "Formula: θ = 90° - arctan2(-v, -u)\n", 44 | "\n", 45 | "```\n", 46 | " N (0°/360°)\n", 47 | " |\n", 48 | "W (270°)-+-E (90°)\n", 49 | " |\n", 50 | " S (180°)\n", 51 | "```\n", 52 | "\n", 53 | "For convenience, `from_uv` can be used to create a `WindBarbs` directly from U/V components to use the meteorological conventions." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "lat = np.arange(60, 37.5, -2.5)\n", 63 | "lon = np.arange(270, 292.5, 2.5)\n", 64 | "uwnd = np.array(\n", 65 | " [\n", 66 | " [2, 0, -2, -2, -3, -3, -3, -2, -1],\n", 67 | " [2, 0, -2, -2, -2, -2, -2, -1, 1],\n", 68 | " [2, -1, -2, -2, -2, -1, 0, 1, 3],\n", 69 | " [3, 0, -3, -5, -5, -4, -4, -2, 0],\n", 70 | " [8, 4, 0, -3, -5, -6, -6, -6, -5],\n", 71 | " [12, 10, 8, 5, 3, 0, -2, -2, -2],\n", 72 | " [13, 14, 16, 16, 14, 12, 10, 9, 10],\n", 73 | " [13, 18, 22, 24, 25, 24, 23, 22, 23],\n", 74 | " [20, 25, 29, 32, 33, 32, 32, 33, 34],\n", 75 | " ]\n", 76 | ")\n", 77 | "vwwnd = np.array(\n", 78 | " [\n", 79 | " [3, 1, 0, -1, -1, 0, 1, 3, 4],\n", 80 | " [-2, -3, -3, -2, 0, 2, 4, 6, 8],\n", 81 | " [-6, -6, -4, -1, 2, 5, 7, 10, 12],\n", 82 | " [-12, -10, -6, -1, 4, 7, 10, 12, 14],\n", 83 | " [-17, -15, -10, -4, 2, 6, 9, 12, 16],\n", 84 | " [-20, -18, -14, -8, -2, 2, 5, 10, 16],\n", 85 | " [-17, -16, -13, -9, -6, -3, 1, 7, 15],\n", 86 | " [-11, -10, -8, -6, -6, -5, -2, 6, 15],\n", 87 | " [-5, -3, -2, -2, -4, -5, -2, 6, 15],\n", 88 | " ]\n", 89 | ")\n", 90 | "\n", 91 | "# equivalent to metpy.calc.wind_direction(uwnd, vwwnd).to(\"radian\")\n", 92 | "wdir = np.pi / 2 - np.arctan2(-vwwnd, -uwnd)\n", 93 | "wspd = np.sqrt(uwnd**2 + vwwnd**2)\n", 94 | "\n", 95 | "wind_barbs = gv.WindBarbs((lon, lat, wdir, wspd)).opts(\n", 96 | " width=500, height=500, padding=0.5, title=\"Wind Barbs from direction/speed\"\n", 97 | ")\n", 98 | "wind_barbs_from_uv = gv.WindBarbs.from_uv((lon, lat, uwnd, vwwnd)).opts(\n", 99 | " width=500, height=500, padding=0.5, title=\"Wind Barbs from U/V components\"\n", 100 | ")\n", 101 | "coastline = gv.feature.coastline()" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "## Plot" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "wind_barbs * coastline + wind_barbs_from_uv * coastline" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "Alternatively, `WindBarbs` can be adapted to oceanography convention:\n", 125 | "\n", 126 | "- Takes the direction the fluid is moving\n", 127 | "\n", 128 | "- But uses North = 0° and clockwise (like meteorology)\n", 129 | "\n", 130 | "- It's 180° opposite from meteorological convention" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "wind_barbs_ocean = gv.WindBarbs((lon, lat, wdir, wspd)).opts(\n", 140 | " width=500, height=500, padding=0.5, title=\"Wind Barbs from direction/speed\", convention=\"to\"\n", 141 | ")\n", 142 | "wind_barbs_from_uv_ocean = gv.WindBarbs.from_uv((lon, lat, uwnd, vwwnd)).opts(\n", 143 | " width=500, height=500, padding=0.5, title=\"Wind Barbs from U/V components\", convention=\"to\"\n", 144 | ")\n", 145 | "wind_barbs_from_uv_ocean * coastline + wind_barbs_ocean * coastline" 146 | ] 147 | } 148 | ], 149 | "metadata": { 150 | "language_info": { 151 | "name": "python", 152 | "pygments_lexer": "ipython3" 153 | } 154 | }, 155 | "nbformat": 4, 156 | "nbformat_minor": 2 157 | } 158 | -------------------------------------------------------------------------------- /examples/gallery/matplotlib/wind_barbs_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import geoviews as gv\n", 11 | "\n", 12 | "gv.extension('matplotlib')\n", 13 | "\n", 14 | "gv.output(fig='svg', size=200)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Define data\n", 22 | "\n", 23 | "IMPORTANT:\n", 24 | "\n", 25 | "Unlike the `VectorField` direction, which follows the mathematical convention:\n", 26 | "\n", 27 | "- Reference: 0° = East (positive x-axis)\n", 28 | "\n", 29 | "- Direction: Counterclockwise\n", 30 | "\n", 31 | "- Meaning: Direction the vector points to\n", 32 | "\n", 33 | "- Formula: θ = arctan2(v, u)\n", 34 | "\n", 35 | "```\n", 36 | " N (90°)\n", 37 | " |\n", 38 | "W (180°)-+-E (0°)\n", 39 | " |\n", 40 | " S (270°)\n", 41 | "```\n", 42 | "\n", 43 | "The `WindBarbs` direction follows the meteorological convention:\n", 44 | "\n", 45 | "- Reference: 0° = North\n", 46 | "\n", 47 | "- Direction: Clockwise\n", 48 | "\n", 49 | "- Meaning: Direction the wind is coming **from**\n", 50 | "\n", 51 | "- Formula: θ = 90° - arctan2(-v, -u)\n", 52 | "\n", 53 | "```\n", 54 | " N (0°/360°)\n", 55 | " |\n", 56 | "W (270°)-+-E (90°)\n", 57 | " |\n", 58 | " S (180°)\n", 59 | "```\n", 60 | "\n", 61 | "For convenience, `from_uv` can be used to create a `WindBarbs` directly from U/V components to use the meteorological conventions." 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "lat = np.arange(60, 37.5, -2.5)\n", 71 | "lon = np.arange(270, 292.5, 2.5)\n", 72 | "uwnd = np.array(\n", 73 | " [\n", 74 | " [2, 0, -2, -2, -3, -3, -3, -2, -1],\n", 75 | " [2, 0, -2, -2, -2, -2, -2, -1, 1],\n", 76 | " [2, -1, -2, -2, -2, -1, 0, 1, 3],\n", 77 | " [3, 0, -3, -5, -5, -4, -4, -2, 0],\n", 78 | " [8, 4, 0, -3, -5, -6, -6, -6, -5],\n", 79 | " [12, 10, 8, 5, 3, 0, -2, -2, -2],\n", 80 | " [13, 14, 16, 16, 14, 12, 10, 9, 10],\n", 81 | " [13, 18, 22, 24, 25, 24, 23, 22, 23],\n", 82 | " [20, 25, 29, 32, 33, 32, 32, 33, 34],\n", 83 | " ]\n", 84 | ")\n", 85 | "vwwnd = np.array(\n", 86 | " [\n", 87 | " [3, 1, 0, -1, -1, 0, 1, 3, 4],\n", 88 | " [-2, -3, -3, -2, 0, 2, 4, 6, 8],\n", 89 | " [-6, -6, -4, -1, 2, 5, 7, 10, 12],\n", 90 | " [-12, -10, -6, -1, 4, 7, 10, 12, 14],\n", 91 | " [-17, -15, -10, -4, 2, 6, 9, 12, 16],\n", 92 | " [-20, -18, -14, -8, -2, 2, 5, 10, 16],\n", 93 | " [-17, -16, -13, -9, -6, -3, 1, 7, 15],\n", 94 | " [-11, -10, -8, -6, -6, -5, -2, 6, 15],\n", 95 | " [-5, -3, -2, -2, -4, -5, -2, 6, 15],\n", 96 | " ]\n", 97 | ")\n", 98 | "\n", 99 | "# equivalent to metpy.calc.wind_direction(uwnd, vwwnd).to(\"radian\")\n", 100 | "wdir = np.pi / 2 - np.arctan2(-vwwnd, -uwnd)\n", 101 | "wspd = np.sqrt(uwnd**2 + vwwnd**2)\n", 102 | "\n", 103 | "wind_barbs = gv.WindBarbs((lon, lat, wdir, wspd)).opts(\n", 104 | " fig_size=250, length=6.5, padding=0.5, title=\"Wind Barbs from direction/speed\"\n", 105 | ")\n", 106 | "wind_barbs_from_uv = gv.WindBarbs.from_uv((lon, lat, uwnd, vwwnd)).opts(\n", 107 | " fig_size=250, length=6.5, padding=0.5, title=\"Wind Barbs from U/V components\"\n", 108 | ")\n", 109 | "coastline = gv.feature.coastline()" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "## Plot" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "wind_barbs * coastline + wind_barbs_from_uv * coastline" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "Alternatively, `WindBarbs` can be adapted to oceanography convention:\n", 133 | "\n", 134 | "- Takes the direction the fluid is moving\n", 135 | "\n", 136 | "- But uses North = 0° and clockwise (like meteorology)\n", 137 | "\n", 138 | "- It's 180° opposite from meteorological convention" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "wind_barbs_ocean = gv.WindBarbs((lon, lat, wdir, wspd)).opts(\n", 148 | " fig_size=250, length=6.5, padding=0.5, title=\"Wind Barbs from direction/speed\", convention=\"to\"\n", 149 | ")\n", 150 | "wind_barbs_from_uv_ocean = gv.WindBarbs.from_uv((lon, lat, uwnd, vwwnd)).opts(\n", 151 | " fig_size=250, length=6.5, padding=0.5, title=\"Wind Barbs from U/V components\", convention=\"to\"\n", 152 | ")\n", 153 | "\n", 154 | "wind_barbs_ocean * coastline + wind_barbs_from_uv_ocean * coastline" 155 | ] 156 | } 157 | ], 158 | "metadata": { 159 | "language_info": { 160 | "name": "python", 161 | "pygments_lexer": "ipython3" 162 | } 163 | }, 164 | "nbformat": 4, 165 | "nbformat_minor": 2 166 | } 167 | -------------------------------------------------------------------------------- /geoviews/plotting/mpl/chart.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import param 3 | from holoviews.core.options import abbreviated_exception 4 | from holoviews.plotting.mpl.element import ColorbarPlot 5 | from holoviews.util.transform import dim 6 | 7 | 8 | class WindBarbsPlot(ColorbarPlot): 9 | """Barbs are traditionally used in meteorology as a way to plot the speed and 10 | direction of wind observations, but can technically be used to plot any two 11 | dimensional vector quantity. As opposed to arrows, which give vector 12 | magnitude by the length of the arrow, the barbs give more quantitative 13 | information about the vector magnitude by putting slanted lines or a 14 | triangle for various increments in magnitude. 15 | 16 | The largest increment is given by a triangle (or "flag"). After those come full lines (barbs). 17 | The smallest increment is a half line. There is only, of course, ever at most 1 half line. 18 | If the magnitude is small and only needs a single half-line and no full lines or 19 | triangles, the half-line is offset from the end of the barb so that it can be 20 | easily distinguished from barbs with a single full line. The magnitude for the barb 21 | shown above would nominally be 65, using the standard increments of 50, 10, and 5. 22 | """ 23 | 24 | padding = param.ClassSelector(default=0.05, class_=(int, float, tuple)) 25 | 26 | convention = param.Selector(default="from", objects=["from", "to"], doc=""" 27 | Convention to return direction; 'from' returns the direction the wind is coming from 28 | (meteorological convention), 'to' returns the direction the wind is going towards 29 | (oceanographic convention).""") 30 | 31 | style_opts = [ 32 | "alpha", 33 | "color", 34 | "edgecolors", 35 | "facecolors", 36 | "linewidth", 37 | "marker", 38 | "visible", 39 | "cmap", 40 | "norm", 41 | # barb specific 42 | "length", 43 | "barbcolor", 44 | "flagcolor", 45 | "fill_empty", 46 | "rounding", 47 | "barb_increments", 48 | "flip_barb", 49 | "pivot", 50 | "sizes", 51 | "width", 52 | ] 53 | 54 | _nonvectorized_styles = [ 55 | "alpha", 56 | "marker", 57 | "cmap", 58 | "visible", 59 | "norm", 60 | # TODO: clarify whether these are vectorized or not 61 | "length", 62 | "barbcolor", 63 | "flagcolor", 64 | "fill_empty", 65 | "rounding", 66 | "barb_increments", 67 | "flip_barb", 68 | "pivot", 69 | "sizes", 70 | "width", 71 | ] 72 | 73 | _plot_methods = dict(single="barbs") 74 | 75 | def _get_us_vs(self, element): 76 | radians = element.dimension_values(2) if len(element.data) else [] 77 | 78 | mag_dim = element.get_dimension(3) 79 | if isinstance(mag_dim, dim): 80 | magnitudes = mag_dim.apply(element, flat=True) 81 | else: 82 | magnitudes = element.dimension_values(mag_dim) 83 | 84 | if self.convention == "to": 85 | radians -= np.pi 86 | 87 | if self.invert_axes: 88 | radians -= 0.5 * np.pi 89 | 90 | us = -magnitudes * np.sin(radians.flatten()) 91 | vs = -magnitudes * np.cos(radians.flatten()) 92 | 93 | return us, vs 94 | 95 | def get_data(self, element, ranges, style): 96 | # Compute coordinates 97 | xidx, yidx = (1, 0) if self.invert_axes else (0, 1) 98 | xs = element.dimension_values(xidx) if len(element.data) else [] 99 | ys = element.dimension_values(yidx) if len(element.data) else [] 100 | 101 | # Compute U and V components as required by matplotlib plt.barbs 102 | us, vs = self._get_us_vs(element) 103 | args = (xs, ys, us, vs) 104 | 105 | color = style.get('color', None) # must do before apply transform 106 | flagcolor = style.get('flagcolor', None) 107 | barbcolor = style.get('barbcolor', None) 108 | 109 | # Process style 110 | with abbreviated_exception(): 111 | style = self._apply_transforms(element, ranges, style) 112 | uses_color = ((isinstance(color, str) and color in element) or isinstance(color, dim)) 113 | if uses_color and (flagcolor is not None or barbcolor is not None): 114 | self.param.warning( 115 | "Cannot declare style mapping for 'color' option and either " 116 | "'flagcolor' and 'barbcolor'; ignoring 'flagcolor' and 'barbcolor'.") 117 | style.pop('flagcolor', None) 118 | style.pop('barbcolor', None) 119 | if "vmin" in style: 120 | style["clim"] = (style.pop("vmin"), style.pop("vmax")) 121 | if "c" in style: 122 | style["array"] = style.pop("c") 123 | if "pivot" not in style: 124 | style["pivot"] = "tip" 125 | return args, style, {} 126 | 127 | def update_handles(self, key, axis, element, ranges, style): 128 | args, style, axis_kwargs = self.get_data(element, ranges, style) 129 | 130 | barbs = self.handles["artist"] 131 | barbs.set_offsets(np.column_stack(args[:2])) 132 | if "color" in style: 133 | if "flagcolor" not in style: 134 | barbs.set_facecolors(style["color"]) 135 | if "barbcolor" not in style: 136 | barbs.set_edgecolors(style["color"]) 137 | if "array" in style: 138 | barbs.set_array(style["array"]) 139 | if "clim" in style: 140 | barbs.set_clim(style["clim"]) 141 | if "linewidth" in style: 142 | barbs.set_linewidths(style["linewidth"]) 143 | return axis_kwargs 144 | -------------------------------------------------------------------------------- /pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | name = "geoviews" 3 | channels = ["pyviz/label/dev", "conda-forge"] 4 | platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"] 5 | 6 | [tasks] 7 | install = 'python -m pip install --no-deps --disable-pip-version-check -e .' 8 | sync-git-tags = 'python scripts/sync_git_tags.py geoviews' 9 | 10 | [activation.env] 11 | PYTHONIOENCODING = "utf-8" 12 | USE_PYGEOS = "0" 13 | 14 | [environments] 15 | test-310 = ["py310", "test-core", "test-unit-task", "optional", "test-example", "download-data"] 16 | test-311 = ["py311", "test-core", "test-unit-task", "optional", "test-example", "download-data"] 17 | test-312 = ["py312", "test-core", "test-unit-task", "optional", "test-example", "download-data"] 18 | test-313 = ["py313", "test-core", "test-unit-task", "optional", "test-example", "download-data"] 19 | test-314 = ["py314", "test-core", "test-unit-task", "optional-314", "test-example", "download-data"] 20 | test-core = ["py314", "test-unit-task", "test-core"] 21 | test-ui = ["py313", "test-core", "optional", "test-ui", "download-data"] 22 | docs = ["py311", "optional", "doc", "download-data"] 23 | build = ["py311", "build"] 24 | lint = ["py311", "lint"] 25 | download-data = ["download-data"] 26 | 27 | [dependencies] 28 | bokeh = ">=3.8.0" 29 | cartopy = ">=0.18.0" 30 | holoviews = ">=1.23.0a0" # Pinning because of tests 31 | nodejs = ">=20" 32 | numpy = "*" 33 | packaging = "*" 34 | panel = ">=1.0.0" 35 | param = "*" 36 | pip = "*" 37 | pyproj = "*" 38 | shapely = ">=1.8.0" 39 | xyzservices = "*" 40 | 41 | [feature.py310.dependencies] 42 | python = "3.10.*" 43 | 44 | [feature.py311.dependencies] 45 | python = "3.11.*" 46 | 47 | [feature.py312.dependencies] 48 | python = "3.12.*" 49 | 50 | [feature.py312.activation.env] 51 | COVERAGE_CORE = "sysmon" 52 | 53 | [feature.py313.dependencies] 54 | python = "3.13.*" 55 | 56 | [feature.py313.activation.env] 57 | COVERAGE_CORE = "sysmon" 58 | 59 | [feature.py314.dependencies] 60 | python = "3.14.*" 61 | 62 | [feature.py314.activation.env] 63 | COVERAGE_CORE = "sysmon" 64 | 65 | [feature.download-data.tasks] 66 | download-data = 'python scripts/download_data.py' 67 | 68 | [feature.download-data.dependencies] 69 | cftime = "*" 70 | geodatasets = "*" 71 | pooch = "*" 72 | pyct = "*" 73 | scipy = "*" 74 | setuptools = "*" # Because of pyct 75 | xarray = "*" 76 | 77 | # ============================================= 78 | # =================== TESTS =================== 79 | # ============================================= 80 | [feature.test-core.dependencies] 81 | psutil = "*" 82 | pytest = "*" 83 | pytest-cov = "*" 84 | pytest-github-actions-annotate-failures = "*" 85 | pytest-xdist = "*" 86 | 87 | [feature.test-unit-task.tasks] # So it is not showing up in the test-ui environment 88 | test-unit = 'pytest geoviews/tests -n logical --dist loadgroup' 89 | 90 | [feature.optional.dependencies] 91 | bokeh_sampledata = "*" 92 | cftime = "*" 93 | datashader = "*" 94 | filelock = "*" 95 | fiona = "*" 96 | geopandas-base = "*" 97 | iris = ">=3.5" 98 | matplotlib-base = ">2.2" 99 | nbval = "*" 100 | netcdf4 = "*" 101 | pandas = "*" 102 | pyviz_comms = "*" 103 | rioxarray = "*" 104 | scipy = "*" 105 | shapely = "*" 106 | xarray = "*" 107 | xesmf = "*" 108 | 109 | [feature.optional-314.dependencies] 110 | bokeh_sampledata = "*" 111 | cftime = "*" 112 | datashader = "*" 113 | filelock = "*" 114 | fiona = "*" 115 | geopandas-base = "*" 116 | iris = ">=3.5" 117 | matplotlib-base = ">2.2" 118 | nbval = "*" 119 | netcdf4 = "*" 120 | pandas = "*" 121 | pyviz_comms = "*" 122 | rioxarray = "*" 123 | scipy = "*" 124 | shapely = "*" 125 | xarray = "*" 126 | # xesmf = "*" 127 | 128 | [feature.test-example.tasks] 129 | test-example = 'pytest -n logical --dist loadscope --nbval-lax examples' 130 | 131 | [feature.test-example.dependencies] 132 | nbval = "*" 133 | 134 | [feature.test-ui.dependencies] 135 | playwright = "*" 136 | pytest-playwright = "*" 137 | 138 | [feature.test-ui.tasks] 139 | _install-ui = 'playwright install chromium' 140 | 141 | [feature.test-ui.tasks.test-ui] 142 | cmd = 'pytest geoviews/tests/ui --ui --browser chromium' 143 | depends-on = ["_install-ui"] 144 | 145 | # ============================================= 146 | # =================== DOCS ==================== 147 | # ============================================= 148 | [feature.doc.dependencies] 149 | graphviz = "*" 150 | lxml = "*" 151 | nbsite = ">=0.8.4,<0.9.0" 152 | numpydoc = "*" 153 | selenium = "*" 154 | sphinx-reredirects = "*" 155 | 156 | [feature.doc.activation.env] 157 | MOZ_HEADLESS = "1" 158 | MPLBACKEND = "Agg" 159 | SPHINX_APIDOC_OPTIONS = "members,show-inheritance" 160 | 161 | [feature.doc.tasks] 162 | _docs-generate-rst = 'nbsite generate-rst --org holoviz --project-name geoviews' 163 | _docs-generate = 'nbsite build --what=html --output=builtdocs --org holoviz --project-name geoviews' 164 | _docs-refmanual = 'sphinx-apidoc -e -o doc/reference_manual/ geoviews/ geoviews/tests --ext-autodoc --ext-intersphinx' 165 | 166 | [feature.doc.tasks.docs-build] 167 | depends-on = ['_docs-generate-rst', '_docs-refmanual', '_docs-generate'] 168 | 169 | # ============================================= 170 | # ================== BUILD ==================== 171 | # ============================================= 172 | [feature.build.dependencies] 173 | python-build = "*" 174 | conda-build = "*" 175 | 176 | [feature.build.tasks] 177 | build-conda = 'bash scripts/conda/build.sh' 178 | build-pip = 'python -m build .' 179 | build-npm = { cmd = "npm pack .", cwd = "geoviews" } 180 | 181 | # ============================================= 182 | # =================== LINT ==================== 183 | # ============================================= 184 | [feature.lint.dependencies] 185 | pre-commit = "*" 186 | 187 | [feature.lint.tasks] 188 | lint = 'pre-commit run --all-files' 189 | lint-install = 'pre-commit install' 190 | -------------------------------------------------------------------------------- /doc/user_guide/Using_WMTS_Offline.md: -------------------------------------------------------------------------------- 1 | # Using WMTS Offline 2 | 3 | ## Caching the Tiles 4 | 5 | Web map tile services simply provide tiled images for a given target domain request. So to use them offline, you simply need to copy these images from their server to a preferred local mirror and point to that local mirror. 6 | 7 | However, attempting to determine the corresponding tiles to specific target domains can be a daunting task--thankfully, Cartopy provides utilities that can assist you with this task. 8 | 9 | When Cartopy is invoked for a given target domain, it retrieves and stores the relevant map tiles in a NumPy, binary file format `.npy`. 10 | 11 | ```python 12 | from pathlib import Path 13 | 14 | import cartopy.crs as ccrs 15 | import cartopy.io.img_tiles as cimgt 16 | import numpy as np 17 | from PIL import Image 18 | from shapely import box 19 | 20 | 21 | def cache_tiles( 22 | tile_source, 23 | max_target_z=1, 24 | x_bounds=(-180, 180), 25 | y_bounds=(-90, 90), 26 | cache_dir="tiles", 27 | ): 28 | """ 29 | Caches map tiles within specified bounds from a given tile source. 30 | 31 | Args: 32 | tile_source (str or cartopy.io.img_tiles.Tiles): The tile source to use for caching. 33 | It can be a string specifying a built-in tile source, or an instance of a custom tile source class. 34 | max_target_z (int, optional): The maximum zoom level to cache. Defaults to 1. 35 | x_bounds (tuple, optional): The longitudinal bounds of the tiles to cache. Defaults to (-180, 180). 36 | y_bounds (tuple, optional): The latitudinal bounds of the tiles to cache. Defaults to (-90, 90). 37 | cache_dir (str, optional): The directory to store the cached tiles. Defaults to "tiles". 38 | 39 | Returns: 40 | pathlib.Path: The path to the cache directory. 41 | """ 42 | if not isinstance(tile_source, cimgt.GoogleWTS): 43 | tile_source = getattr(cimgt, tile_source) 44 | tiles = tile_source(cache=cache_dir) 45 | 46 | bbox = ccrs.GOOGLE_MERCATOR.transform_points( 47 | ccrs.PlateCarree(), x=np.array(x_bounds), y=np.array(y_bounds) 48 | )[:, :-1].flatten() # drop Z, then convert to x0, y0, x1, y1 49 | target_domain = box(*bbox) 50 | 51 | for target_z in range(max_target_z): 52 | tiles.image_for_domain(target_domain, target_z) 53 | return Path(cache_dir) / tile_source.__name__ 54 | ``` 55 | 56 | As an example, to cache OpenStreetMaps tiles, you can simply call the provided function, ensuring that you specify a maximum zoom level (`max_target_z`). 57 | 58 | ```python 59 | cache_dir = cache_tiles("OSM", max_target_z=6) 60 | ``` 61 | 62 | WARNING: When working with higher zoom levels, it is **highly recommended** to specify the `x_bounds` and `y_bounds` parameters to your region of interest. This is crucial to prevent potential issues such as rate limiting or, in extreme cases, *being banned*. 63 | 64 | As the zoom level increases, the time required for downloading and caching the tiles grows exponentially due to the increasing number of fine-grained tiles that need to be retrieved. By setting appropriate boundaries, you can effectively manage the download process and mitigate the risk of encountering problems related to excessive requests. 65 | 66 | Here is a table illustrating the number of tiles for *global extents* at different zoom levels: 67 | 68 | ``` 69 | z=0: 1 tile (entire world) 70 | z=1: 4 tiles 71 | z=2: 16 tiles 72 | z=3: 64 tiles 73 | z=4: 256 tiles 74 | z=5: 1,024 tiles 75 | z=6: 4,096 tiles 76 | z=7: 16,384 tiles 77 | z=8: 65,536 tiles 78 | z=9: 262,144 tiles 79 | z=10: 1,048,576 tiles 80 | z=11: 4,194,304 tiles 81 | z=12: 16,777,216 tiles 82 | z=13: 67,108,864 tiles 83 | z=14: 268,435,456 tiles 84 | z=15: 1,073,741,824 tiles 85 | z=16: 4,294,967,296 tiles 86 | z=17: 17,179,869,184 tiles 87 | z=18: 68,719,476,736 tiles 88 | z=19: 274,877,906,944 tiles 89 | z=20: 1,099,511,627,776 tiles 90 | ``` 91 | 92 | ## Converting to PNG 93 | 94 | Since GeoViews lacks support for reading cached NumPy binary files, an additional step is required to: 95 | 96 | 1. convert them to PNG format 97 | 2. update their directories 98 | 3. build a format string containing "{X}/{Y}/{Z}" (or similar variations) 99 | 100 | Fortunately, this process only involves a straightforward loop that performs minimal processing on each file. 101 | 102 | ```python 103 | def convert_tiles_cache(cache_dir): 104 | """ 105 | Converts cached tiles from numpy format to PNG format. 106 | 107 | Args: 108 | cache_dir (str): The directory containing the cached tiles in numpy format. 109 | 110 | Returns: 111 | str: The format string representing the converted PNG tiles. 112 | """ 113 | for np_path in Path(cache_dir).rglob("*.npy"): 114 | img = Image.fromarray(np.load(np_path)) 115 | img_path = Path(str(np_path.with_suffix(".png")).replace("_", "/")) 116 | img_path.parent.mkdir(parents=True, exist_ok=True) 117 | img.save(img_path) 118 | 119 | tiles_fmt = str(cache_dir / "{X}" / "{Y}" / "{Z}.png") 120 | return tiles_fmt 121 | ``` 122 | 123 | ```python 124 | tiles_fmt = convert_tiles_cache(cache_dir) 125 | ``` 126 | 127 | ## Testing Locally 128 | 129 | Now, all that's left is passing that generated tiles format string into `gv.WMTS`. 130 | 131 | ```python 132 | import geoviews as gv 133 | 134 | gv.extension("bokeh") 135 | 136 | gv.WMTS(tiles_dir).opts(global_extent=True) 137 | ``` 138 | 139 | Please keep in mind that when reaching higher zoom levels beyond the cached max_target_z, you might encounter a blank map. 140 | 141 | To avoid this issue, it is essential to set the `max_zoom` option to the same value as `max_target_z`. 142 | 143 | ```python 144 | import geoviews as gv 145 | 146 | gv.extension("bokeh") 147 | 148 | gv.WMTS(tiles_dir).opts(global_extent=True, max_zoom=6) 149 | ``` 150 | -------------------------------------------------------------------------------- /geoviews/operation/regrid.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import param 5 | import xarray as xr 6 | from holoviews.core.data import XArrayInterface 7 | from holoviews.core.util import get_param_values 8 | from holoviews.element import Image as HvImage, QuadMesh as HvQuadMesh 9 | from holoviews.operation.datashader import regrid 10 | 11 | from ..element import Image, QuadMesh, is_geographic 12 | 13 | 14 | class weighted_regrid(regrid): 15 | """Implements weighted regridding of rectilinear and curvilinear 16 | grids using the xESMF library, supporting all the ESMF regridding 17 | algorithms including bilinear, conservative and nearest neighbour 18 | regridding. The operation will always store the sparse weight 19 | matrix to disk and reuse the weights for later aggregations. To 20 | delete the weight files call the clean_weight_files method on the 21 | operation. 22 | """ 23 | 24 | interpolation = param.Selector(default='bilinear', 25 | objects=['bilinear', 'conservative', 'nearest_s2d', 'nearest_d2s'], doc=""" 26 | Interpolation method""") 27 | 28 | reuse_weights = param.Boolean(default=True, doc=""" 29 | Whether to cache the sparse regridding weights in memory. 30 | Can provide considerable speedups when exploring a larger 31 | dataset.""") 32 | 33 | save_weights = param.Boolean(default=False, doc=""" 34 | Whether to save weight file to speed up future regridding 35 | operations.""") 36 | 37 | file_pattern = param.String(default='{method}_{x_range}_{y_range}_{width}x{height}.nc', 38 | doc=""" 39 | The file pattern used to store the regridding weights when the 40 | reuse_weights parameter is disabled. Note the files are not 41 | cleared automatically so make sure you clean up the cached 42 | files when you are done.""") 43 | 44 | _files = [] 45 | 46 | _weights = {} 47 | 48 | _per_element = True 49 | 50 | def _get_regridder(self, element): 51 | try: 52 | import xesmf as xe 53 | except ImportError: 54 | raise ImportError("xESMF library required for weighted regridding.") from None 55 | x, y = element.kdims 56 | if self.p.target: 57 | tx, ty = self.p.target.kdims[:2] 58 | if issubclass(self.p.target.interface, XArrayInterface): 59 | ds_out = self.p.target.data 60 | ds_out = ds_out.rename({tx.name: 'lon', ty.name: 'lat'}) 61 | height, width = ds_out[tx.name].shape 62 | else: 63 | xs = self.p.target.dimension_values(0, expanded=False) 64 | ys = self.p.target.dimension_values(1, expanded=False) 65 | ds_out = xr.Dataset({'lat': ys, 'lon': xs}) 66 | height, width = len(ys), len(xs) 67 | x_range = ds_out[tx.name].min(), ds_out[tx.name].max() 68 | y_range = ds_out[ty.name].min(), ds_out[ty.name].max() 69 | xtype, ytype = 'numeric', 'numeric' 70 | else: 71 | info = self._get_sampling(element, x, y) 72 | (x_range, y_range), _, (width, height), (xtype, ytype) = info 73 | if x_range[0] > x_range[1]: 74 | x_range = x_range[::-1] 75 | element = element.select(**{x.name: x_range, y.name: y_range}) 76 | ys = np.linspace(y_range[0], y_range[1], height) 77 | xs = np.linspace(x_range[0], x_range[1], width) 78 | ds_out = xr.Dataset({'lat': ys, 'lon': xs}) 79 | 80 | irregular = any(element.interface.irregular(element, d) 81 | for d in [x, y]) 82 | coord_opts = {'flat': False} if irregular else {'expanded': False} 83 | coords = tuple(element.dimension_values(d, **coord_opts) 84 | for d in [x, y]) 85 | arrays = self._get_xarrays(element, coords, xtype, ytype) 86 | ds = xr.Dataset(arrays) 87 | ds = ds.rename({x.name: 'lon', y.name: 'lat'}) 88 | 89 | x_range = str(tuple(f'{r:.3f}' for r in x_range)).replace("'", '') 90 | y_range = str(tuple(f'{r:.3f}' for r in y_range)).replace("'", '') 91 | filename = self.p.file_pattern.format( 92 | method=self.p.interpolation, width=width, height=height, 93 | x_range=x_range, y_range=y_range 94 | ) 95 | reuse_weights = os.path.isfile(os.path.abspath(filename)) 96 | save_filename = filename if self.p.save_weights or reuse_weights else None 97 | regridder = xe.Regridder(ds, ds_out, self.p.interpolation, 98 | reuse_weights=reuse_weights, 99 | weights=self._weights.get(filename), 100 | filename=save_filename) 101 | if save_filename: 102 | self._files.append(os.path.abspath(filename)) 103 | if self.p.reuse_weights: 104 | self._weights[filename] = regridder.weights 105 | return regridder, arrays 106 | 107 | 108 | def _process(self, element, key=None): 109 | regridder, arrays = self._get_regridder(element) 110 | x, y = element.kdims 111 | ds = xr.Dataset({vd: regridder(arr) for vd, arr in arrays.items()}) 112 | ds = ds.rename({'lon': x.name, 'lat': y.name}) 113 | params = get_param_values(element) 114 | if is_geographic(element): 115 | try: 116 | return Image(ds, crs=element.crs, **params) 117 | except Exception: 118 | return QuadMesh(ds, crs=element.crs, **params) 119 | try: 120 | return HvImage(ds, **params) 121 | except Exception: 122 | return HvQuadMesh(ds, **params) 123 | 124 | 125 | @classmethod 126 | def clean_weight_files(cls): 127 | """Cleans existing weight files.""" 128 | deleted = [] 129 | for f in cls._files: 130 | try: 131 | os.remove(f) 132 | deleted.append(f) 133 | except FileNotFoundError: 134 | pass 135 | print(f'Deleted {len(deleted)} weight files') 136 | cls._files = [] 137 | -------------------------------------------------------------------------------- /geoviews/models/wind_barb.ts: -------------------------------------------------------------------------------- 1 | import {XYGlyph, XYGlyphView} from "@bokehjs/models/glyphs/xy_glyph" 2 | import {LineVector} from "@bokehjs/core/property_mixins" 3 | import type {PointGeometry} from "@bokehjs/core/geometry" 4 | import type {Context2d} from "@bokehjs/core/util/canvas" 5 | import type * as visuals from "@bokehjs/core/visuals" 6 | import * as p from "@bokehjs/core/properties" 7 | import {Selection} from "@bokehjs/models/selections/selection" 8 | 9 | export interface WindBarbView extends WindBarb.Data {} 10 | 11 | export class WindBarbView extends XYGlyphView { 12 | declare model: WindBarb 13 | declare visuals: WindBarb.Visuals 14 | 15 | protected override _paint(ctx: Context2d, indices: number[], data?: WindBarb.Data): void { 16 | const {sx, sy, angle, magnitude} = data ?? this 17 | const y = this.y 18 | const scale = this.model.scale 19 | 20 | for (const i of indices) { 21 | const screen_x = sx[i] 22 | const screen_y = sy[i] 23 | const a = angle.get(i) 24 | const mag = magnitude.get(i) 25 | const lat = y[i] 26 | 27 | if (!isFinite(screen_x + screen_y + a + mag + lat)) 28 | continue 29 | 30 | this._draw_wind_barb(ctx, screen_x, screen_y, a, mag, scale, i) 31 | } 32 | } 33 | 34 | protected _draw_wind_barb(ctx: Context2d, cx: number, cy: number, angle: number, magnitude: number, scale: number, idx: number = 0): void { 35 | // Wind barb drawing using meteorological convention 36 | // magnitude is in knots (or appropriate units) 37 | // angle is in meteorological convention (direction wind comes FROM) 38 | // barbs point in the direction the wind is coming FROM 39 | 40 | const barb_length = this.model.barb_length * scale 41 | const barb_width = this.model.barb_width * scale 42 | const flag_width = this.model.flag_width * scale 43 | 44 | ctx.save() 45 | ctx.translate(cx, cy) 46 | ctx.rotate(-angle) 47 | 48 | ctx.beginPath() 49 | 50 | this.visuals.line.apply(ctx, idx) 51 | ctx.strokeStyle = ctx.strokeStyle || "black" 52 | 53 | ctx.lineCap = "round" 54 | ctx.lineJoin = "round" 55 | 56 | // Determine barbs/flags based on magnitude 57 | // Standard increments: 50 knots = flag (triangle), 10 knots = full barb, 5 knots = half barb 58 | const mag_rounded = Math.round(magnitude / 5) * 5 59 | 60 | if (mag_rounded >= 5) { 61 | // Draw the main staff (pointing in direction wind is coming from) 62 | ctx.moveTo(0, 0) 63 | ctx.lineTo(0, -barb_length) 64 | ctx.stroke() 65 | 66 | let remaining = mag_rounded 67 | let y_offset = -barb_length 68 | const spacing = this.model.spacing * scale 69 | 70 | // Draw 50-knot flags (filled triangles) 71 | while (remaining >= 50) { 72 | ctx.fillStyle = ctx.strokeStyle || "black" 73 | ctx.beginPath() 74 | ctx.moveTo(0, y_offset) 75 | ctx.lineTo(flag_width, y_offset + spacing) 76 | ctx.lineTo(0, y_offset + spacing * 2) 77 | ctx.closePath() 78 | ctx.fill() 79 | y_offset += spacing * 2.5 80 | remaining -= 50 81 | } 82 | 83 | // Draw 10-knot barbs (full lines) 84 | while (remaining >= 10) { 85 | ctx.beginPath() 86 | ctx.moveTo(0, y_offset) 87 | ctx.lineTo(barb_width, y_offset + barb_width * 0.2) 88 | ctx.stroke() 89 | y_offset += spacing 90 | remaining -= 10 91 | } 92 | 93 | // Draw 5-knot half-barb 94 | if (remaining >= 5) { 95 | ctx.beginPath() 96 | ctx.moveTo(0, y_offset) 97 | ctx.lineTo(barb_width / 2, y_offset + barb_width * 0.1) 98 | ctx.stroke() 99 | } 100 | } else { 101 | // For calm winds (< 5 knots), draw only a circle (no staff line) 102 | ctx.beginPath() 103 | ctx.arc(0, 0, this.model.calm_circle_radius * scale, 0, 2 * Math.PI) 104 | ctx.stroke() 105 | } 106 | 107 | ctx.restore() 108 | } 109 | 110 | protected override _hit_point(geometry: PointGeometry): Selection { 111 | const {sx, sy} = geometry 112 | 113 | const candidates = [] 114 | 115 | for (let i = 0; i < this.data_size; i++) { 116 | const dx = this.sx[i] - sx 117 | const dy = this.sy[i] - sy 118 | const dist = Math.sqrt(dx * dx + dy * dy) 119 | if (dist < 10 * this.model.scale) { // Hit radius 120 | candidates.push(i) 121 | } 122 | } 123 | 124 | return new Selection({indices: candidates}) 125 | } 126 | 127 | override draw_legend_for_index(ctx: Context2d, {x0, x1, y0, y1}: {x0: number, y0: number, x1: number, y1: number}, _index: number): void { 128 | const cx = (x0 + x1) / 2 129 | const cy = (y0 + y1) / 2 130 | 131 | // Draw a representative wind barb in the legend 132 | this._draw_wind_barb(ctx, cx, cy, Math.PI / 4, 25, 0.5) 133 | } 134 | } 135 | 136 | export namespace WindBarb { 137 | export type Attrs = p.AttrsOf 138 | 139 | export type Props = XYGlyph.Props & { 140 | angle: p.AngleSpec 141 | magnitude: p.NumberSpec 142 | scale: p.Property 143 | barb_length: p.Property 144 | barb_width: p.Property 145 | flag_width: p.Property 146 | spacing: p.Property 147 | calm_circle_radius: p.Property 148 | } 149 | 150 | export type Visuals = XYGlyph.Visuals & {line: visuals.LineVector} 151 | 152 | export type Data = p.GlyphDataOf 153 | } 154 | 155 | export interface WindBarb extends WindBarb.Attrs {} 156 | 157 | export class WindBarb extends XYGlyph { 158 | declare properties: WindBarb.Props 159 | declare __view_type__: WindBarbView 160 | 161 | constructor(attrs?: Partial) { 162 | super(attrs) 163 | } 164 | 165 | static override __module__ = "geoviews.models.wind_barb" 166 | 167 | static { 168 | this.prototype.default_view = WindBarbView 169 | 170 | this.define(({Float}) => ({ 171 | angle: [p.AngleSpec, {value: 0}], 172 | magnitude: [p.NumberSpec, {value: 0}], 173 | scale: [Float, 1.0], 174 | barb_length: [Float, 30.0], 175 | barb_width: [Float, 15.0], 176 | flag_width: [Float, 15.0], 177 | spacing: [Float, 6.0], 178 | calm_circle_radius: [Float, 3.0], 179 | })) 180 | 181 | this.mixins(LineVector) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ----------------- 4 | 5 | **Geographic visualizations for HoloViews.** 6 | 7 | | | | 8 | | --- | --- | 9 | | Downloads | ![https://pypistats.org/packages/geoviews](https://img.shields.io/pypi/dm/geoviews?label=pypi) ![https://anaconda.org/pyviz/geoviews](https://pyviz.org/_static/cache/geoviews_conda_downloads_badge.svg) 10 | | Build Status | [![Linux/MacOS/Windows Build Status](https://github.com/holoviz/geoviews/workflows/tests/badge.svg?query=branch:main)](https://github.com/holoviz/geoviews/actions/workflows/test.yaml?query=branch%3Amain) | 11 | | Coverage | [![codecov](https://codecov.io/gh/holoviz/geoviews/branch/main/graph/badge.svg)](https://codecov.io/gh/holoviz/geoviews) | 12 | | Latest dev release | [![Github tag](https://img.shields.io/github/tag/holoviz/geoviews.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/geoviews/tags) [![dev-site](https://img.shields.io/website-up-down-green-red/https/holoviz-dev.github.io/geoviews.svg?label=dev%20website)](https://holoviz-dev.github.io/geoviews/) | 13 | | Latest release | [![Github release](https://img.shields.io/github/release/holoviz/geoviews.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/geoviews/releases) [![PyPI version](https://img.shields.io/pypi/v/geoviews.svg?colorB=cc77dd)](https://pypi.python.org/pypi/geoviews) [![geoviews version](https://img.shields.io/conda/v/pyviz/geoviews.svg?colorB=4488ff&style=flat)](https://anaconda.org/pyviz/geoviews) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/geoviews.svg?label=conda%7Cconda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/geoviews) [![defaults version](https://img.shields.io/conda/v/anaconda/geoviews.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/anaconda/geoviews) | 14 | | Docs | [![gh-pages](https://img.shields.io/github/last-commit/holoviz/geoviews/gh-pages.svg)](https://github.com/holoviz/geoviews/tree/gh-pages) [![site](https://img.shields.io/website-up-down-green-red/http/geoviews.org.svg)](http://geoviews.org) | 15 | | Support | [![Discourse](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.holoviz.org)](https://discourse.holoviz.org/) | 16 | 17 | ## What is it? 18 | 19 | GeoViews is a Python library that makes it easy to explore and 20 | visualize any data that includes geographic locations. It has 21 | particularly powerful support for multidimensional meteorological 22 | and oceanographic datasets, such as those used in weather, climate, 23 | and remote sensing research, but is useful for almost anything 24 | that you would want to plot on a map! You can see lots of example 25 | notebooks at [geoviews.org](https://geoviews.org), and a good 26 | overview is in our [blog post announcement](https://www.continuum.io/blog/developer-blog/introducing-geoviews). 27 | 28 | GeoViews is built on the [HoloViews](https://holoviews.org) library for 29 | building flexible visualizations of multidimensional data. GeoViews 30 | adds a family of geographic plot types based on the 31 | [Cartopy](http://scitools.org.uk/cartopy) library, plotted using 32 | either the [Matplotlib](http://matplotlib.org) or 33 | [Bokeh](https://bokeh.org) packages. 34 | 35 | Each of the new GeoElement plot types is a new HoloViews Element that 36 | has an associated geographic projection based on ``cartopy.crs``. The 37 | GeoElements currently include ``Feature``, ``WMTS``, ``Tiles``, 38 | ``Points``, ``Contours``, ``Image``, ``QuadMesh``, ``TriMesh``, 39 | ``RGB``, ``HSV``, ``Labels``, ``Graph``, ``HexTiles``, ``VectorField`` 40 | and ``Text`` objects, each of which can easily be overlaid in the same 41 | plots. E.g. an object with temperature data can be overlaid with 42 | coastline data using an expression like ``gv.Image(temperature) * 43 | gv.Feature(cartopy.feature.COASTLINE)``. Each GeoElement can also be 44 | freely combined in layouts with any other HoloViews Element , making 45 | it simple to make even complex multi-figure layouts of overlaid 46 | objects. 47 | 48 | ## Installation 49 | 50 | If you want the latest GeoViews, you will need an up-to-date environment. Updating is never risk-free, but it is a good idea in general and the commands `conda list --revisions` and `conda install --revision N` can usually recover from updates gone awry. 51 | 52 | ``` 53 | conda update --all 54 | ``` 55 | 56 | You can then install GeoViews and all of its dependencies with the following: 57 | 58 | ``` 59 | conda install -c pyviz geoviews 60 | ``` 61 | 62 | Alternatively you can install the geoviews-core package, which 63 | only installs the minimal dependencies required to run geoviews: 64 | 65 | ``` 66 | conda install -c pyviz geoviews-core 67 | ``` 68 | 69 | In certain circumstances proj6 issues may prevent installation or 70 | cause issues (particularly with cartopy<=0.17). If you encounter these 71 | issues ensure you also pin proj4:: 72 | 73 | conda install proj4<6 74 | 75 | Once installed you can copy the examples into the current directory 76 | using the ``geoviews`` command and run them using the Jupyter 77 | notebook: 78 | 79 | ``` 80 | geoviews examples 81 | cd geoviews-examples 82 | jupyter notebook 83 | ``` 84 | 85 | (Here `geoviews examples` is a shorthand for `geoviews copy-examples 86 | --path geoviews-examples && geoviews fetch-data --path 87 | geoviews-examples`.) 88 | 89 | In the classic Jupyter notebook environment and JupyterLab, first make sure to load the `gv.extension()`. GeoViews objects will then render themselves if they are the last item in a notebook cell. For versions of `jupyterlab>=3.0` the necessary extension is automatically bundled in the `pyviz_comms` package, which must be >=2.0. However note that for version of `jupyterlab<3.0` you must also manually install the JupyterLab extension with: 90 | 91 | ```bash 92 | jupyter labextension install @pyviz/jupyterlab_pyviz 93 | ``` 94 | 95 | Once you have installed JupyterLab and the extension launch it with: 96 | 97 | ``` 98 | jupyter-lab 99 | ``` 100 | 101 | If you want to try out the latest features between releases, you can 102 | get the latest dev release by specifying `-c pyviz/label/dev` in place 103 | of `-c pyviz`. 104 | 105 | ### Additional dependencies 106 | 107 | If you need to install libraries only available from conda-forge, such 108 | as Iris (to use data stored in Iris cubes) or xesmf, you should 109 | install from conda-forge: 110 | 111 | ``` 112 | conda create -n env-name -c pyviz -c conda-forge geoviews iris xesmf 113 | conda activate env-name 114 | ``` 115 | 116 | ----- 117 | 118 | GeoViews itself is also installable using `pip`, but to do that you 119 | will first need to have installed the [dependencies of cartopy](http://scitools.org.uk/cartopy/docs/latest/installing.html#requirements), 120 | or else have set up your system to be able to build them. 121 | --------------------------------------------------------------------------------