Skip to content

Commit 00c3c16

Browse files
committed
Merge branch 'main' into kyle/quak
2 parents 58e9c4d + 09c0005 commit 00c3c16

16 files changed

+1189
-685
lines changed

lonboard/_layer.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
ColorAccessor,
4646
FloatAccessor,
4747
NormalAccessor,
48+
VariableLengthTuple,
4849
)
4950

5051
if TYPE_CHECKING:
@@ -111,24 +112,14 @@ def __init__(self, *, extensions: Sequence[BaseExtension] = (), **kwargs):
111112

112113
# TODO: validate that only one extension per type is included. E.g. you can't have
113114
# two data filter extensions.
114-
extensions = traitlets.List(trait=traitlets.Instance(BaseExtension)).tag(
115+
extensions = VariableLengthTuple(traitlets.Instance(BaseExtension)).tag(
115116
sync=True, **ipywidgets.widget_serialization
116117
)
117118
"""
118119
A list of [layer extension](https://developmentseed.org/lonboard/latest/api/layer-extensions/)
119120
objects to add additional features to a layer.
120121
"""
121122

122-
# TODO: the extensions list is not observed; separately, the list object itself does
123-
# not propagate events, so an append wouldn't work.
124-
125-
# @traitlets.observe("extensions")
126-
# def _observe_extensions(self, change):
127-
# """When a new extension is assigned, add its layer props to this layer."""
128-
# new_extensions: List[BaseExtension] = change["new"]
129-
# for extension in new_extensions:
130-
# self.add_traits(**extension._layer_traits)
131-
132123
def _add_extension_traits(self, extensions: Sequence[BaseExtension]):
133124
"""Assign selected traits from the extension onto this Layer."""
134125
for extension in extensions:
@@ -154,14 +145,52 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]):
154145
if trait.get_metadata("sync"):
155146
self.keys.append(name)
156147

157-
def add_extension(self, extension: BaseExtension):
158-
if any(isinstance(ext, extension.__class__) for ext in self.extensions):
159-
raise ValueError("Cannot handle multiple of the same extension")
148+
def add_extension(self, extension: BaseExtension, **props):
149+
"""Add a new layer extension to an existing layer instance.
150+
151+
Any properties for the added extension should also be passed as keyword
152+
arguments to this function.
153+
154+
Examples:
155+
156+
```py
157+
from lonboard import ScatterplotLayer
158+
from lonboard.layer_extension import DataFilterExtension
159+
160+
gdf = geopandas.GeoDataFrame(...)
161+
layer = ScatterplotLayer.from_geopandas(gdf)
162+
163+
extension = DataFilterExtension(filter_size=1)
164+
filter_values = gdf["filter_column"]
165+
166+
layer.add_extension(
167+
extension,
168+
get_filter_value=filter_values,
169+
filter_range=[0, 1]
170+
)
171+
```
172+
173+
Args:
174+
extension: The new extension to add.
175+
176+
Raises:
177+
ValueError: if another extension of the same type already exists on the
178+
layer.
179+
"""
180+
if any(isinstance(extension, type(ext)) for ext in self.extensions):
181+
raise ValueError("Only one extension of each type permitted")
182+
183+
with self.hold_trait_notifications():
184+
self._add_extension_traits([extension])
185+
self.extensions += (extension,)
160186

161-
# Maybe keep a registry of which extensions have already been added?
162-
self._add_extension_traits([extension])
187+
# Assign any extension properties
188+
added_names: List[str] = []
189+
for prop_name, prop_value in props.items():
190+
self.set_trait(prop_name, prop_value)
191+
added_names.append(prop_name)
163192

164-
self.extensions.append(extension)
193+
self.send_state(added_names + ["extensions"])
165194

166195
pickable = traitlets.Bool(True).tag(sync=True)
167196
"""
@@ -492,9 +521,9 @@ def __init__(self, **kwargs: BitmapLayerKwargs):
492521

493522
bounds = traitlets.Union(
494523
[
495-
traitlets.List(traitlets.Float(), minlen=4, maxlen=4),
496-
traitlets.List(
497-
traitlets.List(traitlets.Float(), minlen=2, maxlen=2),
524+
VariableLengthTuple(traitlets.Float(), minlen=4, maxlen=4),
525+
VariableLengthTuple(
526+
VariableLengthTuple(traitlets.Float(), minlen=2, maxlen=2),
498527
minlen=4,
499528
maxlen=4,
500529
),
@@ -516,7 +545,7 @@ def __init__(self, **kwargs: BitmapLayerKwargs):
516545
- Default: `0`
517546
"""
518547

519-
transparent_color = traitlets.List(
548+
transparent_color = VariableLengthTuple(
520549
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
521550
)
522551
"""The color to use for transparent pixels, in `[r, g, b, a]`.
@@ -525,7 +554,7 @@ def __init__(self, **kwargs: BitmapLayerKwargs):
525554
- Default: `[0, 0, 0, 0]`
526555
"""
527556

528-
tint_color = traitlets.List(
557+
tint_color = VariableLengthTuple(
529558
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
530559
)
531560
"""The color to tint the bitmap by, in `[r, g, b]`.
@@ -588,7 +617,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
588617
_layer_type = traitlets.Unicode("bitmap-tile").tag(sync=True)
589618

590619
data = traitlets.Union(
591-
[traitlets.Unicode(), traitlets.List(traitlets.Unicode(), minlen=1)]
620+
[traitlets.Unicode(), VariableLengthTuple(traitlets.Unicode(), minlen=1)]
592621
).tag(sync=True)
593622
"""
594623
Either a URL template or an array of URL templates from which the tile data should
@@ -643,7 +672,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
643672
- Default: `None`
644673
"""
645674

646-
extent = traitlets.List(
675+
extent = VariableLengthTuple(
647676
traitlets.Float(), minlen=4, maxlen=4, allow_none=True, default_value=None
648677
).tag(sync=True)
649678
"""
@@ -726,7 +755,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
726755
- Default: `0`
727756
"""
728757

729-
transparent_color = traitlets.List(
758+
transparent_color = VariableLengthTuple(
730759
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
731760
)
732761
"""The color to use for transparent pixels, in `[r, g, b, a]`.
@@ -735,7 +764,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
735764
- Default: `[0, 0, 0, 0]`
736765
"""
737766

738-
tint_color = traitlets.List(
767+
tint_color = VariableLengthTuple(
739768
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
740769
)
741770
"""The color to tint the bitmap by, in `[r, g, b]`.
@@ -1999,7 +2028,7 @@ class HeatmapLayer(BaseArrowLayer):
19992028
def __init__(
20002029
self, *, table: ArrowStreamExportable, **kwargs: Unpack[HeatmapLayerKwargs]
20012030
):
2002-
err_msg = """\
2031+
err_msg = """
20032032
The `HeatmapLayer` is not currently working.
20042033
20052034
As of Lonboard v0.10, Lonboard upgraded to version 9.0 of the underlying
@@ -2085,7 +2114,7 @@ def from_duckdb(
20852114
- Default: `0.05`
20862115
"""
20872116

2088-
color_domain = traitlets.List(
2117+
color_domain = VariableLengthTuple(
20892118
traitlets.Float(), default_value=None, allow_none=True, minlen=2, maxlen=2
20902119
).tag(sync=True)
20912120
# """

lonboard/_map.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
from lonboard._layer import BaseLayer
1515
from lonboard._viewport import compute_view
1616
from lonboard.basemap import CartoBasemap
17-
from lonboard.traits import DEFAULT_INITIAL_VIEW_STATE, BasemapUrl, ViewStateTrait
17+
from lonboard.traits import (
18+
DEFAULT_INITIAL_VIEW_STATE,
19+
BasemapUrl,
20+
VariableLengthTuple,
21+
ViewStateTrait,
22+
)
1823
from lonboard.types.map import MapKwargs
1924

2025
if TYPE_CHECKING:
@@ -131,7 +136,7 @@ def __init__(
131136
This API is not yet stabilized and may change in the future.
132137
"""
133138

134-
layers = traitlets.List(trait=traitlets.Instance(BaseLayer)).tag(
139+
layers = VariableLengthTuple(traitlets.Instance(BaseLayer)).tag(
135140
sync=True, **ipywidgets.widget_serialization
136141
)
137142
"""One or more `Layer` objects to display on this map.
@@ -170,7 +175,7 @@ def __init__(
170175
custom_attribution = traitlets.Union(
171176
[
172177
traitlets.Unicode(allow_none=True),
173-
traitlets.List(traitlets.Unicode(allow_none=False)),
178+
VariableLengthTuple(traitlets.Unicode(allow_none=False)),
174179
]
175180
).tag(sync=True)
176181
"""
@@ -306,6 +311,66 @@ def __init__(
306311
global `parameters` when that layer is rendered.
307312
"""
308313

314+
def add_layer(
315+
self,
316+
layers: BaseLayer | Sequence[BaseLayer] | Map,
317+
*,
318+
focus: bool = False,
319+
reset_zoom: bool = False,
320+
):
321+
"""Add one or more new layers to the map.
322+
323+
Examples:
324+
325+
```py
326+
from lonboard import viz
327+
328+
m = viz(some_data)
329+
m.add_layer(viz(more_data), focus=True)
330+
```
331+
332+
Args:
333+
layers: New layers to add to the map. This can be:
334+
- a layer instance
335+
- a list or tuple of layer instances
336+
- another `Map` instance, in which case its layers will be added to this
337+
map. This lets you pass the result of `viz` into this method.
338+
339+
focus: If True, set the view state of the map based on the _newly-added_
340+
layers. Defaults to False.
341+
reset_zoom: If True, set the view state of the map based on _all_ layers.
342+
Defaults to False.
343+
344+
Raises:
345+
ValueError: _description_
346+
"""
347+
348+
if focus and reset_zoom:
349+
raise ValueError("focus and reset_zoom may not both be set.")
350+
351+
if isinstance(layers, Map):
352+
new_layers = layers.layers
353+
self.layers += layers.layers
354+
# self.layers =x
355+
# layers = layers.layers
356+
elif isinstance(layers, BaseLayer):
357+
new_layers = (layers,)
358+
layers = [layers]
359+
self.layers += (layers,)
360+
else:
361+
new_layers = tuple(layers)
362+
self.layers += tuple(layers)
363+
364+
self.layers += new_layers
365+
366+
# self.layers += tuple(layers)
367+
368+
if focus:
369+
self.view_state = compute_view(new_layers) # type: ignore
370+
371+
elif reset_zoom:
372+
self.view_state = compute_view(self.layers) # type: ignore
373+
309374
def set_view_state(
310375
self,
311376
*,
@@ -482,4 +547,4 @@ def as_html(self) -> HTML:
482547

483548
@traitlets.default("view_state")
484549
def _default_initial_view_state(self):
485-
return compute_view(self.layers)
550+
return compute_view(self.layers) # type: ignore

lonboard/_viewport.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from __future__ import annotations
99

1010
import math
11-
from typing import List, Tuple
11+
from typing import Sequence, Tuple
1212

1313
from lonboard._geoarrow.ops.bbox import Bbox
1414
from lonboard._geoarrow.ops.centroid import WeightedCentroid
1515
from lonboard._layer import BaseLayer
1616

1717

18-
def get_bbox_center(layers: List[BaseLayer]) -> Tuple[Bbox, WeightedCentroid]:
18+
def get_bbox_center(layers: Sequence[BaseLayer]) -> Tuple[Bbox, WeightedCentroid]:
1919
"""Get the bounding box and geometric (weighted) center of the geometries in the
2020
table."""
2121

@@ -55,7 +55,7 @@ def bbox_to_zoom_level(bbox: Bbox) -> int:
5555
return zoom_level
5656

5757

58-
def compute_view(layers: List[BaseLayer]):
58+
def compute_view(layers: Sequence[BaseLayer]):
5959
"""Automatically computes a view state for the data passed in."""
6060
bbox, center = get_bbox_center(layers)
6161

lonboard/_viz.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import json
6-
from random import shuffle
76
from textwrap import dedent
87
from typing import (
98
TYPE_CHECKING,
@@ -84,6 +83,7 @@ def __geo_interface__(self) -> dict: ...
8483
"#FFFF66", # yellow
8584
"#00FFFF", # turquoise
8685
]
86+
COLOR_COUNTER = 0
8787
DEFAULT_POLYGON_LINE_COLOR = [0, 0, 0, 200]
8888

8989

@@ -166,6 +166,9 @@ def viz(
166166
167167
Alternatively, you can pass a `list` or `tuple` of any of the above inputs.
168168
169+
If you want to easily add more data, to an existing map, you can pass the output of
170+
`viz` into [`Map.add_layer`][lonboard.Map.add_layer].
171+
169172
Args:
170173
data: a data object of any supported type.
171174
@@ -188,30 +191,32 @@ def viz(
188191
Returns:
189192
widget visualizing the provided data.
190193
"""
191-
color_ordering = COLORS.copy()
192-
shuffle(color_ordering)
194+
global COLOR_COUNTER
193195

194196
if isinstance(data, (list, tuple)):
195197
layers: List[Union[ScatterplotLayer, PathLayer, PolygonLayer]] = []
196198
for i, item in enumerate(data):
197199
ls = create_layers_from_data_input(
198200
item,
199-
_viz_color=color_ordering[i % len(color_ordering)],
201+
_viz_color=COLORS[(COLOR_COUNTER + i) % len(COLORS)],
200202
scatterplot_kwargs=scatterplot_kwargs,
201203
path_kwargs=path_kwargs,
202204
polygon_kwargs=polygon_kwargs,
203205
con=con,
204206
)
205207
layers.extend(ls)
208+
209+
COLOR_COUNTER += len(layers)
206210
else:
207211
layers = create_layers_from_data_input(
208212
data,
209-
_viz_color=color_ordering[0],
213+
_viz_color=COLORS[COLOR_COUNTER % len(COLORS)],
210214
scatterplot_kwargs=scatterplot_kwargs,
211215
path_kwargs=path_kwargs,
212216
polygon_kwargs=polygon_kwargs,
213217
con=con,
214218
)
219+
COLOR_COUNTER += 1
215220

216221
map_kwargs = {} if not map_kwargs else map_kwargs
217222

0 commit comments

Comments
 (0)