Skip to content

Commit 90148e7

Browse files
committed
Add visible_callback
1 parent 15b63ca commit 90148e7

File tree

2 files changed

+151
-4
lines changed

2 files changed

+151
-4
lines changed

examples/markers_or_density.ipynb

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "15eefe2f-7d1b-4814-9a5b-3b57df451fd7",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"from ipyleaflet import GeoData, Map, basemaps, LayersControl, WidgetControl\n",
11+
"from ipywidgets import FloatSlider\n",
12+
"import xarray_leaflet\n",
13+
"import geopandas as gpd\n",
14+
"import matplotlib.pyplot as plt"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"id": "30114fbe-e6ef-4547-98ab-28686f8980b1",
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"!wget https://raw.githubusercontent.com/drei01/geojson-world-cities/master/cities.geojson"
25+
]
26+
},
27+
{
28+
"cell_type": "code",
29+
"execution_count": null,
30+
"id": "ab8cd637-be46-4953-96a8-edd918aeccf8",
31+
"metadata": {},
32+
"outputs": [],
33+
"source": [
34+
"df = gpd.read_file(\"cities.geojson\")\n",
35+
"df.geometry = df.geometry.centroid\n",
36+
"measurement = \"mask\"\n",
37+
"df[measurement] = 1"
38+
]
39+
},
40+
{
41+
"cell_type": "code",
42+
"execution_count": null,
43+
"id": "55a78adf",
44+
"metadata": {},
45+
"outputs": [],
46+
"source": [
47+
"m = Map(center=(34.36519854648025, -47.3743535773436), zoom=3, basemap=basemaps.CartoDB.DarkMatter, interpolation='nearest')\n",
48+
"m"
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": null,
54+
"id": "6180d7f5-05e5-4f56-b179-78cc8f581f1a",
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"class LayerManager:\n",
59+
"\n",
60+
" def __init__(self, df, measurement):\n",
61+
" self.df = df\n",
62+
" self.measurement = measurement\n",
63+
" self.marker_layer = None\n",
64+
"\n",
65+
" def visible_callback(self, m, da, bbox):\n",
66+
" # show density if there are too many points\n",
67+
" # otherwise show individual markers\n",
68+
" density_visible = da.sum() > 500\n",
69+
" if self.marker_layer:\n",
70+
" # remove markers\n",
71+
" m.remove(self.marker_layer)\n",
72+
" self.marker_layer = None\n",
73+
" if not density_visible:\n",
74+
" # only show markers that are on the map view\n",
75+
" self.marker_layer = GeoData(geo_dataframe=self.df.clip(bbox))\n",
76+
" m.add(self.marker_layer)\n",
77+
" return density_visible\n",
78+
"\n",
79+
"layer_manager = LayerManager(df, measurement)"
80+
]
81+
},
82+
{
83+
"cell_type": "code",
84+
"execution_count": null,
85+
"id": "2ab54311-d960-4b51-9623-e8d91b97ad75",
86+
"metadata": {},
87+
"outputs": [],
88+
"source": [
89+
"l = df.leaflet.plot(m, measurement=\"mask\", colormap=plt.cm.inferno, visible_callback=layer_manager.visible_callback)"
90+
]
91+
},
92+
{
93+
"cell_type": "code",
94+
"execution_count": null,
95+
"id": "fe1ea3ac-7c1b-4443-8f82-2bffe819b27c",
96+
"metadata": {},
97+
"outputs": [],
98+
"source": [
99+
"layers_control = LayersControl(position='topright')\n",
100+
"m.add_control(layers_control)\n",
101+
"\n",
102+
"opacity_slider = FloatSlider(description='Opacity:', min=0, max=1, value=1)\n",
103+
"\n",
104+
"def set_opacity(change):\n",
105+
" l.opacity = change['new']\n",
106+
"\n",
107+
"opacity_slider.observe(set_opacity, names='value')\n",
108+
"slider_control = WidgetControl(widget=opacity_slider, position='bottomleft')\n",
109+
"m.add_control(slider_control)"
110+
]
111+
}
112+
],
113+
"metadata": {
114+
"kernelspec": {
115+
"display_name": "Python 3 (ipykernel)",
116+
"language": "python",
117+
"name": "python3"
118+
},
119+
"language_info": {
120+
"codemirror_mode": {
121+
"name": "ipython",
122+
"version": 3
123+
},
124+
"file_extension": ".py",
125+
"mimetype": "text/x-python",
126+
"name": "python",
127+
"nbconvert_exporter": "python",
128+
"pygments_lexer": "ipython3",
129+
"version": "3.10.5"
130+
}
131+
},
132+
"nbformat": 4,
133+
"nbformat_minor": 5
134+
}

xarray_leaflet/xarray_leaflet.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import asyncio
22
import os
33
import tempfile
4-
from typing import Optional
4+
from typing import Callable, Optional
55

66
import geopandas as gpd
77
import matplotlib as mpl
88
import mercantile
99
import numpy as np
1010
import pandas as pd
1111
import xarray as xr
12-
from ipyleaflet import DrawControl, LocalTileLayer, WidgetControl
12+
from ipyleaflet import DrawControl, Layer, LocalTileLayer, WidgetControl
1313
from ipyspin import Spinner
1414
from IPython.display import Image, display
1515
from ipyurl import Url
@@ -62,6 +62,7 @@ def plot(
6262
resampling=Resampling.nearest,
6363
get_base_url=None,
6464
measurement: Optional[str] = None,
65+
visible_callback: Optional[Callable] = None,
6566
):
6667
"""Display an array as an interactive map.
6768
@@ -114,6 +115,13 @@ def plot(
114115
A function taking the window URL and returning the base URL to use.
115116
measurement: str, optional
116117
The geocube measurement.
118+
visible_callback: callable, optional
119+
A callable taking the following arguments:
120+
- the ipyleaflet.Map
121+
- the xarray.DataArray of the visible region
122+
- the mercantile.LngLatBbox of the visible region
123+
124+
and returning True if the layer should be shown, False otherwise.
117125
118126
Returns
119127
-------
@@ -125,6 +133,7 @@ def plot(
125133

126134
if self.is_vector:
127135
# source is a GeoDataFrame (vector)
136+
self.visible_callback = visible_callback
128137
if measurement is None:
129138
raise RuntimeError("You must provide a 'measurement'.")
130139
self.measurement = measurement
@@ -365,6 +374,10 @@ def _get_vector_tiles(self, change=None):
365374
if self.dynamic:
366375
llbbox = mercantile.LngLatBbox(west, south, east, north)
367376
da_visible = self.zvect.get_da_llbbox(llbbox, z)
377+
# check if we must show the layer
378+
if self.visible_callback and not self.visible_callback(self.m, da_visible, llbbox):
379+
self.m.remove_control(self.spinner_control)
380+
return
368381
if da_visible is None:
369382
self.max_value = 0
370383
else:
@@ -600,7 +613,7 @@ async def async_fit_bounds(self):
600613
class DataArrayLeaflet(Leaflet):
601614
"""A DataArraye extension for tiled map plotting, based on (ipy)leaflet."""
602615

603-
def __init__(self, da: xr.DataArray = None):
616+
def __init__(self, da: xr.DataArray):
604617
self._da = da
605618
self._da_selected = None
606619
self.is_vector = False
@@ -610,6 +623,6 @@ def __init__(self, da: xr.DataArray = None):
610623
class GeoDataFrameLeaflet(Leaflet):
611624
"""A GeoDataFrame extension for tiled map plotting, based on (ipy)leaflet."""
612625

613-
def __init__(self, df: gpd.GeoDataFrame = None):
626+
def __init__(self, df: gpd.GeoDataFrame):
614627
self._df = df
615628
self.is_vector = True

0 commit comments

Comments
 (0)