Skip to content

Commit 8f2ff7f

Browse files
mfisher87pre-commit-ci[bot]gjmooneykpdavibatpad
authored
Add explore() function and GISDocument.sidecar() method (#340)
* Create GISDocuments as untitled when no path provided * Remove `save_as()` method from GISDocument API, re-add `export_to_qgis()` We now create untitled documents when no path is passed in, so users can use the JupyterLab facilities to rename the document. * Prefer `const` * Fix unit test * Test untitled document numbering * Update docstring * Stub out geo_debug function with jupyterlab-sidecar * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * Add ESPM-157 notebook as example user of geo-debug workflow WIP * Use correct .jGIS extension * Dunno, rebase me later * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Open JGIS in sidecar * Create an anonymous project * Open in a new JupyterLab window/tab! * Update TODO comments * Rename geo_debug -> explore Also move it up a level in the package * Explore explore function at jupytergis package * Update docstring and parameter type * Open JGIS Document as a full window, not widget * Hack around document opening but not displaying layers as expected Co-authored-by: Greg Mooney <[email protected]> Co-authored-by: Kristin Davis <[email protected]> Co-authored-by: Sanjay Bhangar <[email protected]> Co-authored-by: Aman Ahuja <amanahuja@gmail> Co-authored-by: Jonathan Marokhovsky <jmarokhovsky@clarku> Co-authored-by: Tammy Woodard <[email protected]> * Switch back to sidecar approach, use CSS to handle filling space * Add dedicated explore example * Cleanup OBE comment * Remove OBE test * Extract sidecar-opening to GISDocument API User can open sidecar with one line of code. They can learn how to do it from reading our API docs only. * Dynamically determine type of object passed to explore() Currently only support geodataframes and GeoJSON. * Update example notebook to explore a geodataframe * Remove debugging print statement * Move "smart" layer add into GISDocument method * Add basemap parameter to `explore()` * Autodoc `explore()` * Bugfix: don't pass name=None to methods that don't support it * Fix incorrect docstring raises directive * Install jupytergis metapackage in docs environment Autogenerated docs will document `jupytergis.GISDocument` and `jupytergis.explore` instead of `jupytergis_lab.GISDocument` and `jupytergis_lab.explore`. As `jupytergis` is the package we instruct end-users to install, documenting `jupytergis_lab` is confusing. * Fix CSS to properly use vertical space in linked output & sidecar views We expect this change to be eventually included in JupyterLab: jupyterlab/jupyterlab#17487 Including in JGIS for compatibility with versions of JupyterLab that don't include this CSS. * Fix autodoc typo * Fix layer panel not updating in sidecar view Co-authored-by: Nicolas Brichet <[email protected]> * Copy docstring raises entries from underlying method docstring * Attempt to fix lite integration tests #340 (comment) Co-authored-by: martinRenou <[email protected]> Co-authored-by: Nicolas Brichet <[email protected]> * Add note about CSS eventually added in JupyterLab Co-authored-by: Nicolas Brichet <[email protected]> * Move explore module into notebook subpackage Co-authored-by: Nicolas Brichet <[email protected]> * Make generic add_layer method a private internal function Co-authored-by: martinRenou <[email protected]> Co-authored-by: Nicolas Brichet <[email protected]> * Missing sidecar and export * Missing sidecar in docs --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Greg Mooney <[email protected]> Co-authored-by: Kristin Davis <[email protected]> Co-authored-by: Sanjay Bhangar <[email protected]> Co-authored-by: Aman Ahuja <amanahuja@gmail> Co-authored-by: Jonathan Marokhovsky <jmarokhovsky@clarku> Co-authored-by: Tammy Woodard <[email protected]> Co-authored-by: Nicolas Brichet <[email protected]> Co-authored-by: martinRenou <[email protected]>
1 parent 3b09d6b commit 8f2ff7f

File tree

13 files changed

+295
-45
lines changed

13 files changed

+295
-45
lines changed

docs/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies:
1111
- ypywidgets>=0.9.6,<0.10.0
1212
- comm>=0.1.2,<0.2.0
1313
- pydantic>=2,<3
14+
- sidecar
1415
- pip:
1516
- yjs-widgets>=0.4,<0.5
1617
- my-jupyter-shared-drive<0.2.0

docs/user_guide/python_api.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ doc
3434

3535
Once the document is opened/created, you can start creating GIS layers.
3636

37-
## `GISDocument` API Reference
37+
## `explore`
38+
39+
```{eval-rst}
40+
.. autofunction:: jupytergis_lab.explore
41+
```
42+
43+
## `GISDocument`
3844

3945
```{eval-rst}
4046
.. autoclass:: jupytergis_lab.GISDocument

examples/explore.ipynb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "c3f4096f-cbd3-43e8-a986-520681f03581",
6+
"metadata": {},
7+
"source": [
8+
"# ESPM 157 - Intro to Spatial Data\n",
9+
"\n",
10+
"<https://espm-157.carlboettiger.info/spatial-1>\n",
11+
"\n",
12+
"Install dependencies:\n",
13+
"\n",
14+
"```bash\n",
15+
"micromamba install geopandas ibis-duckdb\n",
16+
"```"
17+
]
18+
},
19+
{
20+
"cell_type": "code",
21+
"execution_count": null,
22+
"id": "b76ae1f0-334e-41c0-9533-407c879b4ad6",
23+
"metadata": {},
24+
"outputs": [],
25+
"source": [
26+
"import ibis\n",
27+
"\n",
28+
"con = ibis.duckdb.connect()"
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": null,
34+
"id": "837aa4e0-5eeb-48c1-97ce-3f8706503911",
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"redlines = con.read_geo(\n",
39+
" \"/vsicurl/https://dsl.richmond.edu/panorama/redlining/static/mappinginequality.gpkg\"\n",
40+
")"
41+
]
42+
},
43+
{
44+
"cell_type": "code",
45+
"execution_count": null,
46+
"id": "4d3319fa-b3b4-4931-b073-98b649e41b65",
47+
"metadata": {},
48+
"outputs": [],
49+
"source": [
50+
"city = redlines.filter(redlines.city == \"New Haven\")"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"id": "34f7f4d6-28d6-4716-8e03-ac32c6ae3bb7",
57+
"metadata": {
58+
"scrolled": true
59+
},
60+
"outputs": [],
61+
"source": [
62+
"city_gdf = city.head().execute()\n",
63+
"city_gdf.plot()"
64+
]
65+
},
66+
{
67+
"cell_type": "markdown",
68+
"id": "d37a610a-1444-43a3-900f-dfe16a890ab9",
69+
"metadata": {},
70+
"source": [
71+
"## OK, but what about spatial context?\n",
72+
"\n",
73+
"I want to explore this data more interactively."
74+
]
75+
},
76+
{
77+
"cell_type": "code",
78+
"execution_count": null,
79+
"id": "6e68aaac-dfc0-42d8-a3b9-05fce59524b2",
80+
"metadata": {},
81+
"outputs": [],
82+
"source": [
83+
"from jupytergis import explore\n",
84+
"\n",
85+
"# Open a new exploration window\n",
86+
"explore(city_gdf, layer_name=\"New Haven\", basemap=\"dark\")"
87+
]
88+
}
89+
],
90+
"metadata": {
91+
"kernelspec": {
92+
"display_name": "Python 3 (ipykernel)",
93+
"language": "python",
94+
"name": "python3"
95+
},
96+
"language_info": {
97+
"codemirror_mode": {
98+
"name": "ipython",
99+
"version": 3
100+
},
101+
"file_extension": ".py",
102+
"mimetype": "text/x-python",
103+
"name": "python",
104+
"nbconvert_exporter": "python",
105+
"pygments_lexer": "ipython3",
106+
"version": "3.12.9"
107+
}
108+
},
109+
"nbformat": 4,
110+
"nbformat_minor": 5
111+
}

lite/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies:
1111
- ypywidgets>=0.9.6,<0.10.0
1212
- comm>=0.1.2,<0.2.0
1313
- pydantic>=2,<3
14+
- sidecar
1415
- pip:
1516
- yjs-widgets>=0.4,<0.5
1617
- my-jupyter-shared-drive<0.2.0

packages/base/src/panelview/components/layers.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element {
171171
model?.sharedModel.layersChanged.connect(updateLayers);
172172
model?.sharedModel.layerTreeChanged.connect(updateLayers);
173173

174+
updateLayers();
174175
return () => {
175176
model?.sharedModel.layersChanged.disconnect(updateLayers);
176177
model?.sharedModel.layerTreeChanged.disconnect(updateLayers);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__version__ = "0.4.4"
22

3-
from jupytergis_lab import GISDocument # noqa
3+
from jupytergis_lab import GISDocument, explore # noqa

python/jupytergis_lab/jupytergis_lab/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
__version__ = "dev"
1010

1111
from .notebook import GISDocument # noqa
12+
from .notebook.explore import explore
1213

1314

1415
def _jupyter_labextension_paths():
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
from typing import Any, Literal, Optional
4+
import re
5+
6+
from jupytergis_lab import GISDocument
7+
8+
9+
@dataclass
10+
class Basemap:
11+
name: str
12+
url: str
13+
14+
15+
BasemapChoice = Literal["light", "dark", "topo"]
16+
_basemaps: dict[BasemapChoice, list[Basemap]] = {
17+
"light": [
18+
Basemap(
19+
name="ArcGIS dark basemap",
20+
url="https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}.pbf",
21+
),
22+
Basemap(
23+
name="ArcGIS dark basemap reference",
24+
url="https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}.pbf",
25+
),
26+
],
27+
"dark": [
28+
Basemap(
29+
name="ArcGIS light basemap",
30+
url="https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}.pbf",
31+
),
32+
Basemap(
33+
name="ArcGIS light basemap reference",
34+
url="https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Reference/MapServer/tile/{z}/{y}/{x}.pbf",
35+
),
36+
],
37+
"topo": [
38+
Basemap(
39+
name="USGS topographic basemap",
40+
url="https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}",
41+
),
42+
],
43+
}
44+
45+
46+
def explore(
47+
data: str | Path | Any,
48+
*,
49+
layer_name: Optional[str] = "Exploration layer",
50+
basemap: BasemapChoice = "topo",
51+
) -> GISDocument:
52+
"""Run a JupyterGIS data interaction interface alongside a Notebook.
53+
54+
:param data: A GeoDataFrame or path to a GeoJSON file.
55+
56+
:raises FileNotFoundError: Received a file path that doesn't exist.
57+
:raises NotImplementedError: Received an input value that isn't supported yet.
58+
:raises TypeError: Received an object type that isn't supported.
59+
:raises ValueError: Received an input value that isn't supported.
60+
"""
61+
doc = GISDocument()
62+
63+
for basemap_obj in _basemaps[basemap]:
64+
doc.add_raster_layer(basemap_obj.url, name=basemap_obj.name)
65+
66+
_add_layer(doc=doc, data=data, name=layer_name)
67+
68+
# TODO: Zoom to layer. Currently not exposed in Python API.
69+
70+
doc.sidecar(title="JupyterGIS explorer")
71+
72+
# TODO: should we return `doc`? It enables the exploration environment more usable,
73+
# but by default, `explore(...)` would display a widget in the notebook _and_ open a
74+
# sidecar for the same widget. The user would need to append a semicolon to disable
75+
# that behavior. We can't disable that behavior from within this function to the
76+
# best of my knowlwedge.
77+
78+
79+
def _add_layer(
80+
*,
81+
doc: GISDocument,
82+
data: Any,
83+
name: str,
84+
) -> str:
85+
"""Add a layer to the document, autodetecting its type.
86+
87+
This method currently supports only GeoDataFrames and GeoJSON files.
88+
89+
:param doc: A GISDocument to add the layer to.
90+
:param data: A data object. Valid data objects include geopandas GeoDataFrames and paths to GeoJSON files.
91+
:param name: The name that will be used for the layer.
92+
93+
:return: A layer ID string.
94+
95+
:raises FileNotFoundError: Received a file path that doesn't exist.
96+
:raises NotImplementedError: Received an input value that isn't supported yet.
97+
:raises TypeError: Received an object type that isn't supported.
98+
:raises ValueError: Received an input value that isn't supported.
99+
"""
100+
if isinstance(data, str):
101+
if re.match(r"^(http|https)://", data) is not None:
102+
raise NotImplementedError("URLs not yet supported.")
103+
else:
104+
data = Path(data)
105+
106+
if isinstance(data, Path):
107+
if not data.exists():
108+
raise FileNotFoundError(f"File not found: {data}")
109+
110+
ext = data.suffix.lower()
111+
112+
if ext in [".geojson", ".json"]:
113+
return doc.add_geojson_layer(path=data, name=name)
114+
elif ext in [".tif", ".tiff"]:
115+
raise NotImplementedError("GeoTIFFs not yet supported.")
116+
else:
117+
raise ValueError(f"Unsupported file type: {data}")
118+
119+
try:
120+
from geopandas import GeoDataFrame
121+
122+
if isinstance(data, GeoDataFrame):
123+
return doc.add_geojson_layer(data=data.to_geo_dict(), name=name)
124+
except ImportError:
125+
pass
126+
127+
raise TypeError(f"Unsupported input type: {type(data)}")

0 commit comments

Comments
 (0)