Skip to content

Commit 50dc472

Browse files
HaudinFlorencedavidbrochartmartinRenougithub-actions[bot]
authored
Add subitems support to layers (#1011)
* Add subitems to a layer (either layers or controls). * Update the example notebook Subitems.ipynb. * Fix flake8 formatting issue. * Remove Choropleth.ipynb notebook from commited files. * Restore Choropleth notebook. * Take Choropleth notebook version from master. * Update the example notebook Subitems.ipynb. * Simplify importation from ipyleaflet. * Update ipyleaflet/leaflet.py Co-authored-by: David Brochart <[email protected]> * Update Map.js according to the review comments. * Update js/src/Map.js Co-authored-by: martinRenou <[email protected]> * Update js/src/Map.js Co-authored-by: martinRenou <[email protected]> * Update Map.js and update example notebook Subitems.ipynb. * Update add_subitem_model and remove_subitem_view using instanceof checks. * Remove files that should not be tracked. * Update js/src/Map.js Co-authored-by: martinRenou <[email protected]> * Update js/src/Map.js Co-authored-by: martinRenou <[email protected]> * Update ipyleaflet/leaflet.py Co-authored-by: martinRenou <[email protected]> * Add a Subitems.ipynb notebooks in ui-tests/notebooks and update the example notebooks. * Retry the CI tests. * Update Subitems.ipynb in ui-tests/notebooks. * Add pandas in install_requires in setup.py. * Try to remove the MagnifyingGlass item in ui-tests/notebooks/Subitems.ipynb. * Re try CI tests. * Update Playwright Snapshots * Fix the failing ui-test by setting spin to false for the icon1 AwesomeIcon subitem, in ui-tests/notebooks/Subitems.ipynb. * Remove the marker with an icon and replace it by a simple one. * Restore the marker with the icon. * Update Playwright Snapshots * Retry CI tests after the update of the galata references. * Update Layer.js and Map.js to deal with the model and view of subitems at the scale of the layer. * Update Layer.js and the example notebook Subitems.ipynb. * Update setup.py Co-authored-by: martinRenou <[email protected]> * Add pandas dependency in tests and fix linting in Layers.js. * Remove pandas version in tests dependencies. * Change layer2 basemap in ui-tests/notebooks/Subitems, to try to fix the failing rendering widgets test. * Suppress an empty cell in ui-tests/notebooks/Subitems.ipynb. * Remove ui-tests/tests/ipyleaflet.test.ts-snapshots/Subitems-linux.png. * Update Playwright Snapshots Co-authored-by: David Brochart <[email protected]> Co-authored-by: martinRenou <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 0c95ee0 commit 50dc472

File tree

9 files changed

+349
-9
lines changed

9 files changed

+349
-9
lines changed

.github/workflows/ui-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
channels: conda-forge
2727

2828
- name: Mamba install dependencies
29-
run: mamba install python=${{ matrix.python-version }} pip nodejs=16 flake8 jupyterlab ipywidgets>=8.0.1 jupyter-packaging~=0.7.9
29+
run: mamba install python=${{ matrix.python-version }} pip nodejs=16 flake8 jupyterlab ipywidgets>=8.0.1 jupyter-packaging~=0.7.9 pandas
3030

3131
- name: Install ipyleaflet
3232
run: pip install .

examples/Subitems.ipynb

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"# Set up for JupyterLite\n",
10+
"try:\n",
11+
" import piplite\n",
12+
" await piplite.install('ipyleaflet')\n",
13+
"except ImportError:\n",
14+
" pass"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"from ipyleaflet import Map, Marker, Choropleth, MagnifyingGlass, ColormapControl, AwesomeIcon, basemaps, basemap_to_tiles\n",
24+
"import json\n",
25+
"import pandas as pd\n",
26+
"from ipywidgets import link, FloatSlider\n",
27+
"from branca.colormap import linear\n",
28+
"center = (43, -100)\n",
29+
"zoom = 4"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"geo_json_data = json.load(open(\"us-states.json\"))\n",
39+
"m1 = Map(center=center, zoom=zoom)"
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"unemployment = pd.read_csv(\"US_Unemployment_Oct2012.csv\")\n",
49+
"unemployment = dict(\n",
50+
" zip(unemployment[\"State\"].tolist(), unemployment[\"Unemployment\"].tolist())\n",
51+
")\n",
52+
"\n",
53+
"marker1 = Marker(location=(center))\n",
54+
"\n",
55+
"layer1 = Choropleth(\n",
56+
" geo_data=geo_json_data,\n",
57+
" choro_data=unemployment,\n",
58+
" colormap=linear.YlOrRd_04,\n",
59+
" style={\"fillOpacity\": 0.8, \"dashArray\": \"5, 5\"},\n",
60+
" subitems= (marker1,)\n",
61+
")"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": null,
67+
"metadata": {},
68+
"outputs": [],
69+
"source": [
70+
"m1.add(layer1)\n",
71+
"m1"
72+
]
73+
},
74+
{
75+
"cell_type": "code",
76+
"execution_count": null,
77+
"metadata": {},
78+
"outputs": [],
79+
"source": [
80+
"m1.remove(layer1)\n",
81+
"m1"
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"metadata": {},
88+
"outputs": [],
89+
"source": [
90+
"m1.add(layer1)\n",
91+
"colormap_control1 = ColormapControl(\n",
92+
" caption='Unemployment rate',\n",
93+
" colormap=layer1.colormap,\n",
94+
" value_min=layer1.value_min,\n",
95+
" value_max=layer1.value_max,\n",
96+
" position='topright',\n",
97+
" transparent_bg=True\n",
98+
")"
99+
]
100+
},
101+
{
102+
"cell_type": "code",
103+
"execution_count": null,
104+
"metadata": {},
105+
"outputs": [],
106+
"source": [
107+
"layer1.subitems = layer1.subitems+(colormap_control1,)\n",
108+
"m1"
109+
]
110+
},
111+
{
112+
"cell_type": "code",
113+
"execution_count": null,
114+
"metadata": {},
115+
"outputs": [],
116+
"source": [
117+
"marker2 = Marker(location=(center[0]-4, center[1] - 4))\n",
118+
"marker3 = Marker(location=(center[0]-8, center[1] - 8))\n",
119+
"layer2 = basemap_to_tiles(basemaps.Strava.Water, subitems= (marker2,))\n",
120+
"icon1 = AwesomeIcon(\n",
121+
" name='gear',\n",
122+
" marker_color='blue',\n",
123+
" icon_color='darkblue',\n",
124+
" spin=True\n",
125+
" \n",
126+
")\n",
127+
"marker4 = Marker(icon=icon1, location=(center[0], center[1] - 4))"
128+
]
129+
},
130+
{
131+
"cell_type": "code",
132+
"execution_count": null,
133+
"metadata": {},
134+
"outputs": [],
135+
"source": [
136+
"layer2.subitems = layer2.subitems+(marker3, marker4)\n",
137+
"m1.add(layer2)\n",
138+
"m1"
139+
]
140+
},
141+
{
142+
"cell_type": "code",
143+
"execution_count": null,
144+
"metadata": {},
145+
"outputs": [],
146+
"source": [
147+
"m1.remove(layer1)\n",
148+
"m1"
149+
]
150+
},
151+
{
152+
"cell_type": "code",
153+
"execution_count": null,
154+
"metadata": {},
155+
"outputs": [],
156+
"source": [
157+
"m1.remove(layer2)"
158+
]
159+
}
160+
],
161+
"metadata": {
162+
"kernelspec": {
163+
"display_name": "Python 3 (ipykernel)",
164+
"language": "python",
165+
"name": "python3"
166+
},
167+
"language_info": {
168+
"codemirror_mode": {
169+
"name": "ipython",
170+
"version": 3
171+
},
172+
"file_extension": ".py",
173+
"mimetype": "text/x-python",
174+
"name": "python",
175+
"nbconvert_exporter": "python",
176+
"pygments_lexer": "ipython3",
177+
"version": "3.9.12"
178+
}
179+
},
180+
"nbformat": 4,
181+
"nbformat_minor": 4
182+
}

ipyleaflet/leaflet.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ class Layer(Widget, InteractMixin):
140140
pane = Unicode('').tag(sync=True)
141141

142142
options = List(trait=Unicode()).tag(sync=True)
143+
subitems = Tuple().tag(trait=Instance(Widget), sync=True, **widget_serialization)
144+
145+
@validate('subitems')
146+
def _validate_subitems(self, proposal):
147+
'''Validate subitems list.
148+
149+
Makes sure only one instance of any given subitem can exist in the
150+
subitem list.
151+
'''
152+
subitem_ids = [subitem.model_id for subitem in proposal.value]
153+
if len(set(subitem_ids)) != len(subitem_ids):
154+
raise Exception('duplicate subitem detected, only use each subitem once')
155+
return proposal.value
143156

144157
def __init__(self, **kwargs):
145158
super(Layer, self).__init__(**kwargs)
@@ -2537,6 +2550,7 @@ def add(self, item):
25372550
if item.model_id in self._control_ids:
25382551
raise ControlException('control already on map: %r' % item)
25392552
self.controls = tuple([control for control in self.controls] + [item])
2553+
25402554
return self
25412555

25422556
def remove(self, item):
@@ -2635,4 +2649,5 @@ async def _fit_bounds(self, bounds):
26352649
else:
26362650
self.zoom -= 1
26372651
await wait_for_change(self, 'bounds')
2652+
26382653
break

js/src/Map.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
54
const widgets = require('@jupyter-widgets/base');
65
const L = require('./leaflet.js');
76
const utils = require('./utils.js');
@@ -158,6 +157,7 @@ LeafletMapModel.serializers = {
158157
dragging_style: { deserialize: widgets.unpack_models }
159158
};
160159

160+
161161
export class LeafletMapView extends utils.LeafletDOMWidgetView {
162162
initialize(options) {
163163
super.initialize(options);
@@ -188,12 +188,11 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
188188
}).then(view => {
189189
this.obj.addLayer(view.obj);
190190

191-
// Trigger the displayed event of the child view.
192191
this.displayed.then(() => {
193192
view.trigger('displayed', this);
194193
});
195-
196194
return view;
195+
197196
});
198197
}
199198

@@ -208,11 +207,11 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
208207
}).then(view => {
209208
this.obj.addControl(view.obj);
210209

210+
211211
// Trigger the displayed event of the child view.
212212
this.displayed.then(() => {
213213
view.trigger('displayed', this);
214214
});
215-
216215
return view;
217216
});
218217
}
@@ -483,4 +482,4 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
483482
break;
484483
}
485484
}
486-
}
485+
}

js/src/layers/Layer.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ export class LeafletLayerModel extends widgets.WidgetModel {
2424
popup_min_width: 50,
2525
popup_max_width: 300,
2626
popup_max_height: null,
27-
pane: ''
27+
pane: '',
28+
subitems: []
2829
};
2930
}
3031
}
3132

3233
LeafletLayerModel.serializers = {
3334
...widgets.WidgetModel.serializers,
34-
popup: { deserialize: widgets.unpack_models }
35+
popup: { deserialize: widgets.unpack_models },
36+
subitems: { deserialize: widgets.unpack_models }
3537
};
3638

39+
40+
3741
export class LeafletUILayerModel extends LeafletLayerModel {
3842
defaults() {
3943
return {
@@ -51,6 +55,33 @@ export class LeafletLayerView extends utils.LeafletWidgetView {
5155
this.popup_content_promise = Promise.resolve();
5256
}
5357

58+
remove_subitem_view(child_view) {
59+
if(child_view instanceof LeafletLayerView) {
60+
this.map_view.obj.removeLayer(child_view.obj);
61+
} else {
62+
this.map_view.obj.removeControl(child_view.obj);
63+
}
64+
child_view.remove();
65+
}
66+
67+
add_subitem_model(child_model) {
68+
return this.create_child_view(child_model, {
69+
map_view: this
70+
}).then(view => {
71+
if (child_model instanceof LeafletLayerModel) {
72+
this.map_view.obj.addLayer(view.obj);
73+
} else {
74+
this.map_view.obj.addControl(view.obj);
75+
}
76+
77+
//Trigger the displayed event of the child view.
78+
this.displayed.then(() => {
79+
view.trigger('displayed', this);
80+
});
81+
return view;
82+
});
83+
}
84+
5485
render() {
5586
return Promise.resolve(this.create_obj()).then(() => {
5687
this.leaflet_events();
@@ -60,6 +91,12 @@ export class LeafletLayerView extends utils.LeafletWidgetView {
6091
this.bind_popup(value);
6192
});
6293
this.update_pane();
94+
this.subitem_views = new widgets.ViewList(
95+
this.add_subitem_model,
96+
this.remove_subitem_view,
97+
this
98+
);
99+
this.subitem_views.update(this.model.get('subitems'));
63100
});
64101
}
65102

@@ -128,10 +165,19 @@ export class LeafletLayerView extends utils.LeafletWidgetView {
128165
},
129166
this
130167
);
168+
this.listenTo(
169+
this.model,
170+
'change:subitems',
171+
function () {
172+
this.subitem_views.update(this.subitems);
173+
},
174+
this
175+
);
131176
}
132177

133178
remove() {
134179
super.remove();
180+
this.subitem_views.remove();
135181
this.popup_content_promise.then(() => {
136182
if (this.popup_content) {
137183
this.popup_content.remove();

js/src/layers/LayerGroup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class LeafletLayerGroupModel extends layer.LeafletLayerModel {
1717
}
1818

1919
LeafletLayerGroupModel.serializers = {
20-
...widgets.WidgetModel.serializers,
20+
...layer.LeafletLayerModel.serializers,
2121
layers: { deserialize: widgets.unpack_models }
2222
};
2323

0 commit comments

Comments
 (0)