Skip to content

Commit

Permalink
Add linked maps example (#655)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
ATL2001 and kylebarron authored Oct 3, 2024
1 parent d991607 commit e1baef5
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
Binary file added assets/linked-maps.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Motor Vehicle Crashes in NYC ![](../assets/motor-vehicle-crashes-nyc.jpg)](../examples/map_challenge/1-points) using [`ScatterplotLayer`](../api/layers/scatterplot-layer)
- [Rivers in Asia ![](../assets/rivers-asia.jpg)](../examples/map_challenge/6-asia/) using [`PathLayer`](../api/layers/path-layer)
- [Inflation Reduction Act Projects ![](../assets/column-layer.jpg)](../examples/column-layer/) using [`ColumnLayer`](../api/layers/column-layer)
- [Linked Maps ![](../assets/linked-maps.gif)](../examples/linked-maps/)

</div>

Expand Down
176 changes: 176 additions & 0 deletions examples/linked-maps.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Linked Maps\n",
"\n",
"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",
"\n",
"Linked maps can be useful in a variety of situations:\n",
"\n",
"- Before/After maps, where one map shows data before something happened and the other after the event\n",
"- To showcase results of different processing methodologies\n",
"- To simply present multiple maps with different data that doesn't easily fit on one map\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from functools import partial\n",
"from typing import List\n",
"\n",
"import ipywidgets as widgets\n",
"import traitlets\n",
"\n",
"import lonboard\n",
"from lonboard import Map\n",
"from lonboard.basemap import CartoBasemap\n",
"from lonboard.models import ViewState"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create the maps\n",
"\n",
"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",
"\n",
"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",
"\n",
"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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"## Create postitron map focused on the arch\n",
"positron_map = Map(\n",
" layers=[],\n",
" basemap_style=CartoBasemap.Positron,\n",
" view_state={\n",
" \"longitude\": -90.1849,\n",
" \"latitude\": 38.6245,\n",
" \"zoom\": 16,\n",
" \"pitch\": 0,\n",
" \"bearing\": 0,\n",
" },\n",
" layout=widgets.Layout(flex=\"1\"),\n",
")\n",
"\n",
"## Create postitron map focused on the lady liberty\n",
"darkmatter_map = Map(\n",
" layers=[],\n",
" basemap_style=CartoBasemap.DarkMatter,\n",
" view_state={\n",
" \"longitude\": -74.04454,\n",
" \"latitude\": 40.6892,\n",
" \"zoom\": 16,\n",
" \"pitch\": 0,\n",
" \"bearing\": 0,\n",
" },\n",
" layout=widgets.Layout(flex=\"1\"),\n",
")\n",
"\n",
"maps_box = widgets.HBox([positron_map, darkmatter_map])\n",
"maps_box"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Linking the Maps (the easy way to understand)\n",
"\n",
"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",
"\n",
"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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sync_positron_to_darkmatter(event: traitlets.utils.bunch.Bunch) -> None:\n",
" if isinstance(event.get(\"new\"), ViewState):\n",
" darkmatter_map.view_state = positron_map.view_state\n",
"\n",
"\n",
"positron_map.observe(sync_positron_to_darkmatter)\n",
"\n",
"\n",
"def sync_darkmatter_to_positron(event: traitlets.utils.bunch.Bunch) -> None:\n",
" if isinstance(event.get(\"new\"), ViewState):\n",
" positron_map.view_state = darkmatter_map.view_state\n",
"\n",
"\n",
"darkmatter_map.observe(sync_darkmatter_to_positron)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Linking the Maps (the more elegant/robust way)\n",
"\n",
"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",
"\n",
"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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def link_maps(event: traitlets.utils.bunch.Bunch, other_maps: List[Map] = []):\n",
" if isinstance(event.get(\"new\"), ViewState):\n",
" for lonboard_map in other_maps:\n",
" lonboard_map.view_state = event[\"new\"]\n",
"\n",
"\n",
"positron_map.observe(partial(link_maps, other_maps=[darkmatter_map]))\n",
"darkmatter_map.observe(partial(link_maps, other_maps=[positron_map]))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.11.8"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- examples/migration.ipynb
- examples/data-filter-extension.ipynb
- examples/column-layer.ipynb
- examples/linked-maps.ipynb
- Integrations:
- examples/duckdb.ipynb
- ColorPicker: examples/integrations/color-picker.ipynb
Expand Down

0 comments on commit e1baef5

Please sign in to comment.