Skip to content

Commit e1baef5

Browse files
ATL2001kylebarron
andauthored
Add linked maps example (#655)
I'm still mostly new to GitHub, but I think I've got this PR for adding a linked maps example to the documentation as we discussed in [issue 637](#637) Let me know if there's anything that needs tweaked! --------- Co-authored-by: Kyle Barron <[email protected]>
1 parent d991607 commit e1baef5

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

assets/linked-maps.gif

426 KB
Loading

examples/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Motor Vehicle Crashes in NYC ![](../assets/motor-vehicle-crashes-nyc.jpg)](../examples/map_challenge/1-points) using [`ScatterplotLayer`](../api/layers/scatterplot-layer)
1212
- [Rivers in Asia ![](../assets/rivers-asia.jpg)](../examples/map_challenge/6-asia/) using [`PathLayer`](../api/layers/path-layer)
1313
- [Inflation Reduction Act Projects ![](../assets/column-layer.jpg)](../examples/column-layer/) using [`ColumnLayer`](../api/layers/column-layer)
14+
- [Linked Maps ![](../assets/linked-maps.gif)](../examples/linked-maps/)
1415

1516
</div>
1617

examples/linked-maps.ipynb

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"## Linked Maps\n",
8+
"\n",
9+
"This notebook demonstrates how you can link two different Lonboard maps using the [`ipywidgets.observe`](https://ipywidgets.readthedocs.io/en/8.1.5/examples/Widget%20Events.html#traitlet-events) method, so panning/zooming one map will automatically pan/zoom the other map.\n",
10+
"\n",
11+
"Linked maps can be useful in a variety of situations:\n",
12+
"\n",
13+
"- Before/After maps, where one map shows data before something happened and the other after the event\n",
14+
"- To showcase results of different processing methodologies\n",
15+
"- To simply present multiple maps with different data that doesn't easily fit on one map\n"
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": null,
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"from functools import partial\n",
25+
"from typing import List\n",
26+
"\n",
27+
"import ipywidgets as widgets\n",
28+
"import traitlets\n",
29+
"\n",
30+
"import lonboard\n",
31+
"from lonboard import Map\n",
32+
"from lonboard.basemap import CartoBasemap\n",
33+
"from lonboard.models import ViewState"
34+
]
35+
},
36+
{
37+
"cell_type": "markdown",
38+
"metadata": {},
39+
"source": [
40+
"## Create the maps\n",
41+
"\n",
42+
"Because layers don't matter for this example, we are going to create two maps without any layers, one map using the Positron basemap, and another using the Dark Matter basemap.\n",
43+
"\n",
44+
"To start, the view state on the Positron map to be focused on the Gateway Arch in St. Louis Missouri, and the Dark Matter map will be centered on the Statue of Liberty in New York City, New York.\n",
45+
"\n",
46+
"We'll present the two maps side by side in an ipywidgets HBox to keep them tidy. Setting the layout of the maps to \"flex='1'\" will allow the maps to display inside the HBox.\n"
47+
]
48+
},
49+
{
50+
"cell_type": "code",
51+
"execution_count": null,
52+
"metadata": {},
53+
"outputs": [],
54+
"source": [
55+
"## Create postitron map focused on the arch\n",
56+
"positron_map = Map(\n",
57+
" layers=[],\n",
58+
" basemap_style=CartoBasemap.Positron,\n",
59+
" view_state={\n",
60+
" \"longitude\": -90.1849,\n",
61+
" \"latitude\": 38.6245,\n",
62+
" \"zoom\": 16,\n",
63+
" \"pitch\": 0,\n",
64+
" \"bearing\": 0,\n",
65+
" },\n",
66+
" layout=widgets.Layout(flex=\"1\"),\n",
67+
")\n",
68+
"\n",
69+
"## Create postitron map focused on the lady liberty\n",
70+
"darkmatter_map = Map(\n",
71+
" layers=[],\n",
72+
" basemap_style=CartoBasemap.DarkMatter,\n",
73+
" view_state={\n",
74+
" \"longitude\": -74.04454,\n",
75+
" \"latitude\": 40.6892,\n",
76+
" \"zoom\": 16,\n",
77+
" \"pitch\": 0,\n",
78+
" \"bearing\": 0,\n",
79+
" },\n",
80+
" layout=widgets.Layout(flex=\"1\"),\n",
81+
")\n",
82+
"\n",
83+
"maps_box = widgets.HBox([positron_map, darkmatter_map])\n",
84+
"maps_box"
85+
]
86+
},
87+
{
88+
"cell_type": "markdown",
89+
"metadata": {},
90+
"source": [
91+
"## Linking the Maps (the easy way to understand)\n",
92+
"\n",
93+
"If you haven't yet run the cells below, you'll see that you can pan/zoom the two maps independent of one another.  Panning/zooming one map will not affect the other map.  After we run the code below though, the two maps will synchronize with each other, when we pan/zoom one map, the other map will automatically match the map that was modified.\n",
94+
"\n",
95+
"To achieve the view state synchronization, we'll write two simple callback function for each of the maps. The functions will receive events from the interaction with the maps, and if the interaction with the map changed the view_state, we'll set the view_state on the other map to match the view_state of the the map that we interacted with.\n"
96+
]
97+
},
98+
{
99+
"cell_type": "code",
100+
"execution_count": null,
101+
"metadata": {},
102+
"outputs": [],
103+
"source": [
104+
"def sync_positron_to_darkmatter(event: traitlets.utils.bunch.Bunch) -> None:\n",
105+
" if isinstance(event.get(\"new\"), ViewState):\n",
106+
" darkmatter_map.view_state = positron_map.view_state\n",
107+
"\n",
108+
"\n",
109+
"positron_map.observe(sync_positron_to_darkmatter)\n",
110+
"\n",
111+
"\n",
112+
"def sync_darkmatter_to_positron(event: traitlets.utils.bunch.Bunch) -> None:\n",
113+
" if isinstance(event.get(\"new\"), ViewState):\n",
114+
" positron_map.view_state = darkmatter_map.view_state\n",
115+
"\n",
116+
"\n",
117+
"darkmatter_map.observe(sync_darkmatter_to_positron)"
118+
]
119+
},
120+
{
121+
"cell_type": "markdown",
122+
"metadata": {},
123+
"source": [
124+
"## Linking the Maps (the more elegant/robust way)\n",
125+
"\n",
126+
"In the block above we are typing a lot of code, and the two functions are basically the same, just with hard coded maps to target in the functions, and we're explicitly calling the originating map's `view_state` even though the `event[\"new\"]` actually is the view state.  Additionally if we had a lot of maps to sync, this would get out of hand quickly. None of that is idea, but it makes the concept easy to understand. Below is a better way to sync the maps, albeit a bit more abstract.\n",
127+
"\n",
128+
"Luckily `functools.partial` can help us out. Instead of writing a function per map, we can write one function that take the same events from the widget, but also another parameter which is a list of Lonboard maps. Then when we register the callback function with the map's `observe()` method, we pass partial as the function and tell partial to use the `link_maps` function and provide the list of the other maps to sync with this map. This way we have one function that we wrote which can be used to sync any map with any number of other maps.\n"
129+
]
130+
},
131+
{
132+
"cell_type": "code",
133+
"execution_count": null,
134+
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"def link_maps(event: traitlets.utils.bunch.Bunch, other_maps: List[Map] = []):\n",
138+
" if isinstance(event.get(\"new\"), ViewState):\n",
139+
" for lonboard_map in other_maps:\n",
140+
" lonboard_map.view_state = event[\"new\"]\n",
141+
"\n",
142+
"\n",
143+
"positron_map.observe(partial(link_maps, other_maps=[darkmatter_map]))\n",
144+
"darkmatter_map.observe(partial(link_maps, other_maps=[positron_map]))"
145+
]
146+
},
147+
{
148+
"cell_type": "code",
149+
"execution_count": null,
150+
"metadata": {},
151+
"outputs": [],
152+
"source": []
153+
}
154+
],
155+
"metadata": {
156+
"kernelspec": {
157+
"display_name": "Python 3 (ipykernel)",
158+
"language": "python",
159+
"name": "python3"
160+
},
161+
"language_info": {
162+
"codemirror_mode": {
163+
"name": "ipython",
164+
"version": 3
165+
},
166+
"file_extension": ".py",
167+
"mimetype": "text/x-python",
168+
"name": "python",
169+
"nbconvert_exporter": "python",
170+
"pygments_lexer": "ipython3",
171+
"version": "3.11.8"
172+
}
173+
},
174+
"nbformat": 4,
175+
"nbformat_minor": 4
176+
}

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ nav:
4545
- examples/migration.ipynb
4646
- examples/data-filter-extension.ipynb
4747
- examples/column-layer.ipynb
48+
- examples/linked-maps.ipynb
4849
- Integrations:
4950
- examples/duckdb.ipynb
5051
- ColorPicker: examples/integrations/color-picker.ipynb

0 commit comments

Comments
 (0)