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
350 changes: 350 additions & 0 deletions quickstarts/lonboard.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
{
"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": 1,
"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": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\r\n",
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.1.2\u001b[0m\r\n",
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpython -m pip install --upgrade pip\u001b[0m\r\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install --quiet lonboard pystac-client planetary-computer dask-geopandas adlfs"
]
},
{
"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",
"\n",
"The `ms-buildings` collection is partitioned by [quadkey](https://learn.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system). Compute the zoom-9 quadkey for a Portland coordinate and fetch the STAC item whose partition covers it.\n",
"\n",
"**Expected result:** one matching item and its GeoParquet `data` asset."
]
},
{
"cell_type": "code",
"execution_count": 3,
"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": [
{
"data": {
"text/plain": [
"('021230223', 'UnitedStates_21230223_2023-04-25')"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import math\n",
"\n",
"\n",
"def quadkey(lat, lon, zoom):\n",
" n = 2 ** zoom\n",
" x = int((lon + 180.0) / 360.0 * n)\n",
" y = int((1.0 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2.0 * n)\n",
" digits = []\n",
" for i in range(zoom, 0, -1):\n",
" bit = 1 << (i - 1)\n",
" digits.append(str((1 if x & bit else 0) + (2 if y & bit else 0)))\n",
" return \"\".join(digits)\n",
"\n",
"\n",
"qk = quadkey(45.52, -122.66, 9)\n",
"item = next(catalog.search(\n",
" collections=[\"ms-buildings\"],\n",
" query={\n",
" \"msbuildings:region\": {\"eq\": \"UnitedStates\"},\n",
" \"msbuildings:quadkey\": {\"eq\": int(qk)},\n",
" },\n",
").items())\n",
"asset = item.assets[\"data\"]\n",
"qk, item.id"
]
},
{
"cell_type": "markdown",
"id": "1bc86af0",
"metadata": {},
"source": [
"## Load the footprints into a GeoDataFrame\n",
"\n",
"The asset is a Delta/Parquet partition on Azure Blob. `dask_geopandas.read_parquet` reads it with the asset's `table:storage_options` (account + SAS), then `.compute()` materializes a GeoDataFrame. Clip to the Portland metro for a focused view.\n",
"\n",
"**Expected result:** a few hundred thousand building polygons."
]
},
{
"cell_type": "code",
"execution_count": 4,
"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": [
{
"data": {
"text/plain": [
"324600"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import dask_geopandas\n",
"\n",
"gdf = dask_geopandas.read_parquet(\n",
" asset.href,\n",
" storage_options=asset.extra_fields[\"table:storage_options\"],\n",
").compute()\n",
"gdf = gdf.cx[-122.85:-122.45, 45.42:45.62]\n",
"\n",
"len(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
}