├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── carbonplan_maps ├── __init__.py ├── _widget.py └── widget.js ├── notebooks └── Example.ipynb └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | __pycache__ 3 | dist/ 4 | .venv/ 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-docstring-first 10 | - id: check-json 11 | - id: check-yaml 12 | - id: double-quote-string-fixer 13 | - id: debug-statements 14 | - id: mixed-line-ending 15 | 16 | - repo: https://github.com/charliermarsh/ruff-pre-commit 17 | rev: 'v0.0.292' 18 | hooks: 19 | - id: ruff 20 | args: ['--fix'] 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 23.9.1 24 | hooks: 25 | - id: black 26 | - id: black-jupyter 27 | 28 | - repo: https://github.com/keewis/blackdoc 29 | rev: v0.3.8 30 | hooks: 31 | - id: blackdoc 32 | 33 | - repo: https://github.com/pre-commit/mirrors-prettier 34 | rev: v3.0.3 35 | hooks: 36 | - id: prettier 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manzt/carbonplan-maps/3f8603042522e83fba0e7abddea63b0463a690e0/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "quoteProps": "as-needed", 7 | "jsxSingleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Trevor Manz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Here be dragons** 🐉 2 | 3 | # carbonplan-maps-widget 4 | 5 | A Jupyter Widget for `@carbonplan/maps`. Built with 6 | [`anywidget`](https://github.com/manzt/anywidget). 7 | 8 | # usage 9 | 10 | ```python 11 | import carbonplan_maps 12 | 13 | map_widget = carbonplan_maps.Widget( 14 | source="https://carbonplan-maps.s3.us-west-2.amazonaws.com/v2/demo/2d/tavg", 15 | variable="tavg", 16 | dimensions=("y", "x"), 17 | ) 18 | map_widget 19 | ``` 20 | 21 | ![Screen Recording 2023-07-17 at 7 48 47 AM](https://github.com/manzt/carbonplan/assets/24403730/3bd2256e-9c4b-4b2b-9fc9-a8d5d1197f05) 22 | 23 | combine with other widgets... 24 | 25 | ```python 26 | import ipywidgets 27 | 28 | colormap = ipywidgets.Dropdown(options=["warm", "fire", "water"]) 29 | clim = ipywidgets.FloatRangeSlider(min=-20, max=30) 30 | opacity = ipywidgets.FloatSlider(min=0, max=1, step=0.001) 31 | region = ipywidgets.Checkbox(description="Region") 32 | 33 | ipywidgets.link((map_widget, "colormap"), (colormap, "value")) 34 | ipywidgets.link((map_widget, "clim"), (clim, "value")) 35 | ipywidgets.link((map_widget, "opacity"), (opacity, "value")) 36 | ipywidgets.link((map_widget, "region"), (region, "value")) 37 | 38 | ipywidgets.VBox([ 39 | ipywidgets.HBox([colormap, opacity, clim]), 40 | region, 41 | map_widget, 42 | ]) 43 | ``` 44 | 45 | ![Screen Recording 2023-07-17 at 8 00 16 AM](https://github.com/manzt/carbonplan/assets/24403730/4d35f702-1833-471a-9e11-8c7de2aed289) 46 | 47 | # development 48 | 49 | ```sh 50 | python3 -m venv .venv 51 | source .venv/bin/activate 52 | pip install -e ".[dev]" 53 | ``` 54 | 55 | ```sh 56 | jupyter lab 57 | ``` 58 | -------------------------------------------------------------------------------- /carbonplan_maps/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from importlib.metadata import PackageNotFoundError, version 4 | 5 | try: 6 | __version__ = version('carbonplan') 7 | except PackageNotFoundError: 8 | __version__ = 'unknown' 9 | 10 | from ._widget import Widget 11 | -------------------------------------------------------------------------------- /carbonplan_maps/_widget.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import anywidget 4 | import traitlets 5 | 6 | __all__ = ['Widget'] 7 | 8 | 9 | class Widget(anywidget.AnyWidget): 10 | _esm = pathlib.Path(__file__).parent / 'widget.js' 11 | 12 | # required 13 | source = traitlets.Unicode(allow_none=False).tag(sync=True) 14 | variable = traitlets.Unicode(allow_none=False).tag(sync=True) 15 | dimensions = traitlets.Tuple(allow_none=False).tag(sync=True) 16 | 17 | # optional 18 | opacity = traitlets.Float(1.0).tag(sync=True) 19 | colormap = traitlets.Unicode('warm').tag(sync=True) 20 | clim = traitlets.Tuple(default_value=(-20, 30)).tag(sync=True) 21 | height = traitlets.Unicode('300px').tag(sync=True) 22 | region = traitlets.Bool(False).tag(sync=True) 23 | selector = traitlets.Dict().tag(sync=True) 24 | mode = traitlets.Unicode('texture').tag(sync=True) 25 | 26 | # data 27 | data = traitlets.Any().tag(sync=True) 28 | -------------------------------------------------------------------------------- /carbonplan_maps/widget.js: -------------------------------------------------------------------------------- 1 | import { useColormap } from 'https://esm.sh/@carbonplan/colormaps@4?deps=react@18' 2 | import { 3 | Fill, 4 | Line, 5 | Map, 6 | Raster, 7 | RegionPicker, 8 | } from 'https://esm.sh/@carbonplan/maps@3?deps=react@18' 9 | import ReactDOM from 'https://esm.sh/react-dom@18/client' 10 | import React from 'https://esm.sh/react@18' 11 | 12 | let bucket = 'https://storage.googleapis.com/carbonplan-maps/' 13 | 14 | let WidgetModelContext = React.createContext() 15 | 16 | function useModelState(name) { 17 | let model = React.useContext(WidgetModelContext) 18 | let [state, setState] = React.useState(model.get(name)) 19 | React.useEffect(() => { 20 | let event = `change:${name}` 21 | let cb = () => setState(model.get(name)) 22 | model.on(event, cb) 23 | return () => { 24 | model.off(event, cb) 25 | } 26 | }, [model, name]) 27 | return [ 28 | state, 29 | (val, options) => { 30 | model.set(name, val, options) 31 | model.save_changes() 32 | }, 33 | ] 34 | } 35 | 36 | function App() { 37 | 38 | let [source] = useModelState('source') 39 | let [colormap_str] = useModelState('colormap') 40 | let [clim] = useModelState('clim') 41 | let [variable] = useModelState('variable') 42 | let [dimensions] = useModelState('dimensions') 43 | let [height] = useModelState('height') 44 | let [opacity] = useModelState('opacity') 45 | let [region] = useModelState('region') 46 | let [selector] = useModelState('selector') 47 | let [mode] = useModelState('mode') 48 | let colormap = useColormap(colormap_str, {}) 49 | 50 | // setting this way avoids constant re-rendering 51 | let model = React.useContext(WidgetModelContext) 52 | let regionOptions = { 53 | setData(data) { 54 | model.set('data', data) 55 | model.save_changes() 56 | }, 57 | } 58 | 59 | return React.createElement( 60 | 'div', 61 | { style: { height } }, 62 | React.createElement( 63 | Map, 64 | null, 65 | region && React.createElement(RegionPicker, null), 66 | React.createElement(Fill, { 67 | color: '#1b1e23', 68 | source: bucket + 'basemaps/ocean', 69 | variable: 'ocean', 70 | }), 71 | React.createElement(Line, { 72 | color: 'white', 73 | source: bucket + 'basemaps/land', 74 | variable: 'land', 75 | }), 76 | React.createElement(Raster, { 77 | colormap, 78 | clim, 79 | source, 80 | variable, 81 | dimensions, 82 | opacity, 83 | regionOptions, 84 | selector, 85 | mode, 86 | }) 87 | ) 88 | ) 89 | } 90 | 91 | export function render({ model, el }) { 92 | let root = ReactDOM.createRoot(el) 93 | let app = React.createElement(App) 94 | root.render( 95 | React.createElement( 96 | WidgetModelContext.Provider, 97 | { value: model }, 98 | React.createElement(App) 99 | ) 100 | ) 101 | return () => root.unmount() 102 | } 103 | -------------------------------------------------------------------------------- /notebooks/Example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "60abeb11-680d-4430-a919-b20e7f26dd95", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "data": { 11 | "application/vnd.jupyter.widget-view+json": { 12 | "model_id": "d17d68e34d42447393b804cdb2235b83", 13 | "version_major": 2, 14 | "version_minor": 0 15 | }, 16 | "text/plain": [ 17 | "Widget(clim=(-20, 30), dimensions=('y', 'x'), source='https://carbonplan-maps.s3.us-west-2.amazonaws.com/v2/de…" 18 | ] 19 | }, 20 | "execution_count": 2, 21 | "metadata": {}, 22 | "output_type": "execute_result" 23 | } 24 | ], 25 | "source": [ 26 | "import carbonplan_maps\n", 27 | "\n", 28 | "map_widget = carbonplan_maps.Widget(\n", 29 | " source=\"https://carbonplan-maps.s3.us-west-2.amazonaws.com/v2/demo/2d/tavg\",\n", 30 | " variable=\"tavg\",\n", 31 | " dimensions=(\"y\", \"x\"),\n", 32 | ")\n", 33 | "map_widget" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "id": "375c454e-b044-4631-a7d2-9096ac68d583", 40 | "metadata": {}, 41 | "outputs": [ 42 | { 43 | "data": { 44 | "application/vnd.jupyter.widget-view+json": { 45 | "model_id": "213008e0a24b4535b23f5ee62f36bd85", 46 | "version_major": 2, 47 | "version_minor": 0 48 | }, 49 | "text/plain": [ 50 | "VBox(children=(HBox(children=(Dropdown(index=14, options=('oranges', 'yellows', 'reds', 'greens', 'teals', 'bl…" 51 | ] 52 | }, 53 | "execution_count": 3, 54 | "metadata": {}, 55 | "output_type": "execute_result" 56 | } 57 | ], 58 | "source": [ 59 | "import ipywidgets\n", 60 | "\n", 61 | "colormap = ipywidgets.Dropdown(\n", 62 | " options=[\n", 63 | " \"oranges\",\n", 64 | " \"yellows\",\n", 65 | " \"reds\",\n", 66 | " \"greens\",\n", 67 | " \"teals\",\n", 68 | " \"blues\",\n", 69 | " \"purples\",\n", 70 | " \"pinks\",\n", 71 | " \"greys\",\n", 72 | " \"fire\",\n", 73 | " \"earth\",\n", 74 | " \"water\",\n", 75 | " \"heart\",\n", 76 | " \"wind\",\n", 77 | " \"warm\",\n", 78 | " \"cool\",\n", 79 | " \"pinkgreen\",\n", 80 | " \"redteal\",\n", 81 | " \"orangeblue\",\n", 82 | " \"yellowpurple\",\n", 83 | " \"redgrey\",\n", 84 | " \"orangegrey\",\n", 85 | " \"yellowgrey\",\n", 86 | " \"greengrey\",\n", 87 | " \"tealgrey\",\n", 88 | " \"bluegrey\",\n", 89 | " \"purplegrey\",\n", 90 | " \"pinkgrey\",\n", 91 | " \"rainbow\",\n", 92 | " \"sinebow\",\n", 93 | " ]\n", 94 | ")\n", 95 | "clim = ipywidgets.FloatRangeSlider(min=-20, max=30)\n", 96 | "opacity = ipywidgets.FloatSlider(min=0, max=1, step=0.001)\n", 97 | "region = ipywidgets.Checkbox(description=\"Region\")\n", 98 | "\n", 99 | "ipywidgets.link((map_widget, \"colormap\"), (colormap, \"value\"))\n", 100 | "ipywidgets.link((map_widget, \"clim\"), (clim, \"value\"))\n", 101 | "ipywidgets.link((map_widget, \"opacity\"), (opacity, \"value\"))\n", 102 | "ipywidgets.link((map_widget, \"region\"), (region, \"value\"))\n", 103 | "\n", 104 | "ipywidgets.VBox(\n", 105 | " [\n", 106 | " ipywidgets.HBox([colormap, opacity, clim]),\n", 107 | " region,\n", 108 | " map_widget,\n", 109 | " ]\n", 110 | ")" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 4, 116 | "id": "d0b8426b-c863-4194-9e3a-03441a76b7bf", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "# Select region and then execute this cell\n", 121 | "map_widget.data" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 5, 127 | "id": "273f83c9-45af-4e2a-9593-5bd496a69313", 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "data": { 132 | "application/vnd.jupyter.widget-view+json": { 133 | "model_id": "ffec5bbd071a4514b63204fc394d03fc", 134 | "version_major": 2, 135 | "version_minor": 0 136 | }, 137 | "text/plain": [ 138 | "Widget(clim=(-20, 30), selector={'month': 1, 'band': 'tavg'}, source='https://carbonplan-maps.s3.us-west-2.ama…" 139 | ] 140 | }, 141 | "execution_count": 5, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "carbonplan_maps.Widget(\n", 148 | " source=\"https://carbonplan-maps.s3.us-west-2.amazonaws.com/v2/demo/4d/tavg-prec-month\",\n", 149 | " variable=\"climate\",\n", 150 | " selector={\"month\": 1, \"band\": \"tavg\"},\n", 151 | ")" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 6, 157 | "id": "9b79b047-fd9d-427c-8987-804b8266281b", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "_.clim = (-10, 20)" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "id": "4a3937aa-ead0-4d28-9c1f-ba2402434b60", 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [] 171 | } 172 | ], 173 | "metadata": { 174 | "kernelspec": { 175 | "display_name": "Python 3 (ipykernel)", 176 | "language": "python", 177 | "name": "python3" 178 | }, 179 | "language_info": { 180 | "codemirror_mode": { 181 | "name": "ipython", 182 | "version": 3 183 | }, 184 | "file_extension": ".py", 185 | "mimetype": "text/x-python", 186 | "name": "python", 187 | "nbconvert_exporter": "python", 188 | "pygments_lexer": "ipython3", 189 | "version": "3.11.6" 190 | } 191 | }, 192 | "nbformat": 4, 193 | "nbformat_minor": 5 194 | } 195 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "carbonplan-maps" 7 | version = "0.1.0" 8 | description = "A Jupyter Widget for @carbonplan/maps" 9 | authors = [ 10 | { name = "Trevor Manz", email = "trevor.j.manz@gmail.com" } 11 | ] 12 | readme = "README.md" 13 | requires-python = ">= 3.10" 14 | dependencies = [ 15 | "anywidget>=0.6.1" 16 | ] 17 | license = { text = "MIT" } 18 | 19 | [project.optional-dependencies] 20 | dev = [ 21 | "jupyterlab", 22 | "watchfiles" 23 | ] 24 | 25 | [tool.hatch.envs.default] 26 | features = ["dev"] 27 | 28 | 29 | [tool.black] 30 | line-length = 100 31 | target-version = ['py310'] 32 | skip-string-normalization = true 33 | 34 | 35 | [tool.ruff] 36 | line-length = 100 37 | target-version = "py310" 38 | builtins = ["ellipsis"] 39 | # Exclude a variety of commonly ignored directories. 40 | exclude = [ 41 | ".bzr", 42 | ".direnv", 43 | ".eggs", 44 | ".git", 45 | ".hg", 46 | ".mypy_cache", 47 | ".nox", 48 | ".pants.d", 49 | ".ruff_cache", 50 | ".svn", 51 | ".tox", 52 | ".venv", 53 | "__pypackages__", 54 | "_build", 55 | "buck-out", 56 | "build", 57 | "dist", 58 | "node_modules", 59 | "venv", 60 | ] 61 | per-file-ignores = {} 62 | # E402: module level import not at top of file 63 | # E501: line too long - let black worry about that 64 | # E731: do not assign a lambda expression, use a def 65 | ignore = ["E402", "E501", "E731"] 66 | select = [ 67 | # Pyflakes 68 | "F", 69 | # Pycodestyle 70 | "E", 71 | "W", 72 | # isort 73 | "I", 74 | # Pyupgrade 75 | "UP", 76 | ] 77 | 78 | 79 | [tool.ruff.mccabe] 80 | max-complexity = 18 81 | 82 | [tool.ruff.isort] 83 | known-first-party = ["carbonplan_maps"] 84 | --------------------------------------------------------------------------------