Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions quickstarts/lonboard.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b90cec6c",
"metadata": {},
"source": [
"# Visualizing Planetary Computer data with Lonboard\n",
"\n",
"This notebook walks through interactive geospatial visualization with [Lonboard](https://developmentseed.org/lonboard/). Lonboard renders large vector datasets on a GPU-accelerated WebGL map directly in Jupyter. Key benefits:\n",
"\n",
"1. **GPU rendering**: pan and zoom through *millions* of features without breaking interactivity.\n",
"2. **No tile server**: geometry streams to the browser as [Apache Arrow](https://arrow.apache.org/); there's no intermediate vector-tile service to stand up.\n",
"3. **Cloud-native vector**: read a STAC GeoParquet partition straight off Azure Blob into a `GeoDataFrame`.\n",
"4. **Composable**: stack multiple vector layers in one `Map`.\n",
"5. **Data-driven styling**: color features by an attribute and mutate the layer in place.\n",
"\n",
"We'll render [Microsoft Building Footprints](https://planetarycomputer.microsoft.com/dataset/ms-buildings) over Portland, Oregon: hundreds of thousands of polygons in a single layer.\n",
"\n",
"The companion [Lonboard tutorial](../overview/lonboard.md) has the full narrative."
]
},
{
"cell_type": "markdown",
"id": "a75e986e",
"metadata": {},
"source": [
"## Install"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2719b6d3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:15.849722Z",
"iopub.status.busy": "2026-06-01T23:09:15.849298Z",
"iopub.status.idle": "2026-06-01T23:09:16.915041Z",
"shell.execute_reply": "2026-06-01T23:09:16.913769Z"
}
},
"outputs": [],
"source": "%pip install --quiet lonboard pystac-client planetary-computer geopandas deltalake adlfs mercantile"
},
{
"cell_type": "markdown",
"id": "dd178afe",
"metadata": {},
"source": [
"## Open the Planetary Computer STAC catalog\n",
"\n",
"`modifier=planetary_computer.sign_inplace` signs every asset as the search returns, so the GeoParquet partition can be read directly.\n",
"\n",
"**Expected result:** working `catalog` client, no output printed."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "6db7f010",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:16.917910Z",
"iopub.status.busy": "2026-06-01T23:09:16.917592Z",
"iopub.status.idle": "2026-06-01T23:09:17.905144Z",
"shell.execute_reply": "2026-06-01T23:09:17.903296Z"
}
},
"outputs": [],
"source": [
"import pystac_client\n",
"import planetary_computer\n",
"\n",
"catalog = pystac_client.Client.open(\n",
" \"https://planetarycomputer.microsoft.com/api/stac/v1\",\n",
" modifier=planetary_computer.sign_inplace,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "626369d5",
"metadata": {},
"source": "## Find the building-footprints partition for Portland\n\nThe `ms-buildings` collection is partitioned by [quadkey](https://learn.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system). Use `mercantile` to convert a Portland coordinate to a zoom-9 quadkey, then fetch the STAC item whose partition covers it.\n\n**Expected result:** one matching item and its Delta Table `data` asset."
},
{
"cell_type": "code",
"execution_count": null,
"id": "2e668d74",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:17.908410Z",
"iopub.status.busy": "2026-06-01T23:09:17.908088Z",
"iopub.status.idle": "2026-06-01T23:09:19.140611Z",
"shell.execute_reply": "2026-06-01T23:09:19.138497Z"
}
},
"outputs": [],
"source": "import mercantile\n\ntile = mercantile.tile(-122.66, 45.52, 9)\nquadkey = mercantile.quadkey(*tile)\n\nitem = next(catalog.search(\n collections=[\"ms-buildings\"],\n query={\n \"msbuildings:region\": {\"eq\": \"UnitedStates\"},\n \"msbuildings:quadkey\": {\"eq\": quadkey},\n },\n).items())\nasset = item.assets[\"data\"]\nquadkey, item.id"
},
{
"cell_type": "markdown",
"id": "1bc86af0",
"metadata": {},
"source": "## Load the footprints into a GeoDataFrame\n\nThe asset is a Delta Table partition on Azure Blob. Open it with `deltalake`, enumerate the parquet files in the partition, then read each one with `geopandas`. Clip to the Portland metro for a focused view.\n\n**Expected result:** a few hundred thousand building polygons."
},
{
"cell_type": "code",
"execution_count": null,
"id": "8078f767",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:19.143991Z",
"iopub.status.busy": "2026-06-01T23:09:19.143563Z",
"iopub.status.idle": "2026-06-01T23:09:34.562416Z",
"shell.execute_reply": "2026-06-01T23:09:34.561197Z"
}
},
"outputs": [],
"source": "import geopandas as gpd\nimport pandas as pd\nfrom deltalake import DeltaTable\n\nstorage_options = {\n \"account_name\": asset.extra_fields[\"table:storage_options\"][\"account_name\"],\n \"sas_token\": asset.extra_fields[\"table:storage_options\"][\"credential\"],\n}\ntable = DeltaTable(asset.href, storage_options=storage_options)\ngdf = pd.concat([\n gpd.read_parquet(uri, storage_options=storage_options)\n for uri in table.file_uris()\n])\ngdf = gdf.cx[-122.85:-122.45, 45.42:45.62]\n\nlen(gdf)"
},
{
"cell_type": "markdown",
"id": "54ac8f51",
"metadata": {},
"source": [
"## Render the footprints\n",
"\n",
"`PolygonLayer.from_geopandas()` uploads the geometry to the GPU as Arrow. The map below is fully interactive. Pan and zoom through every building with no tile server in the loop.\n",
"\n",
"**Expected result:** an interactive map of Portland's building footprints."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "fc44a442",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:34.564879Z",
"iopub.status.busy": "2026-06-01T23:09:34.564469Z",
"iopub.status.idle": "2026-06-01T23:09:35.658328Z",
"shell.execute_reply": "2026-06-01T23:09:35.657485Z"
}
},
"outputs": [],
"source": [
"from lonboard import Map, PolygonLayer\n",
"\n",
"layer = PolygonLayer.from_geopandas(\n",
" gdf,\n",
" get_fill_color=[255, 140, 0, 170],\n",
" get_line_color=[90, 40, 0],\n",
" line_width_min_pixels=0.5,\n",
")\n",
"m = Map(layer, view_state={\"longitude\": -122.66, \"latitude\": 45.52, \"zoom\": 12})\n",
"m"
]
},
{
"cell_type": "markdown",
"id": "90f0fe81",
"metadata": {},
"source": [
"## Color by building height\n",
"\n",
"Each footprint carries a `meanHeight`. Map it through a continuous colormap to shade every polygon: data-driven styling across the whole layer, evaluated on the GPU.\n",
"\n",
"**Expected result:** the same footprints, now colored by height (`plasma`: purple = low, yellow = tall)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "08ea2dc5",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:35.752527Z",
"iopub.status.busy": "2026-06-01T23:09:35.752195Z",
"iopub.status.idle": "2026-06-01T23:09:35.932090Z",
"shell.execute_reply": "2026-06-01T23:09:35.930838Z"
}
},
"outputs": [],
"source": [
"import matplotlib as mpl\n",
"from lonboard.colormap import apply_continuous_cmap\n",
"\n",
"heights = gdf[\"meanHeight\"].clip(0, 30)\n",
"normalized = (heights - heights.min()) / (heights.max() - heights.min())\n",
"\n",
"layer.get_fill_color = apply_continuous_cmap(\n",
" normalized.to_numpy(), mpl.colormaps[\"plasma\"], alpha=0.8\n",
")\n",
"m"
]
},
{
"cell_type": "markdown",
"id": "49ee64a4",
"metadata": {},
"source": [
"## Mutate in place\n",
"\n",
"Changing a layer property updates the existing map without re-uploading geometry.\n",
"\n",
"**Expected result:** the rendered footprints redraw at 50% opacity."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "dc9d1f2a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T23:09:35.934563Z",
"iopub.status.busy": "2026-06-01T23:09:35.934163Z",
"iopub.status.idle": "2026-06-01T23:09:35.938547Z",
"shell.execute_reply": "2026-06-01T23:09:35.937594Z"
}
},
"outputs": [],
"source": [
"layer.opacity = 0.5"
]
},
{
"cell_type": "markdown",
"id": "7c643f03",
"metadata": {},
"source": [
"## You're done\n",
"\n",
"If every cell above rendered a map, the stack is wired up end-to-end: STAC search → cloud GeoParquet → Arrow upload → GPU-rendered vector → data-driven styling, all with no tile server.\n",
"\n",
"Swap in your own bbox, collection, or `GeoDataFrame` and the same pattern applies. For pixel-level *raster* analysis (window reads, overview traversal), see the [async-geotiff tutorial](../overview/async-geotiff.md). For a standalone web app rather than a notebook, the [deck.gl-raster tutorial](../overview/deckgl-raster.md) builds a raster renderer in TypeScript."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}