Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 62 additions & 21 deletions lonboard/_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
ColorAccessor,
FloatAccessor,
NormalAccessor,
VariableLengthTuple,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -105,24 +106,14 @@ def __init__(self, *, extensions: Sequence[BaseExtension] = (), **kwargs):

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

# TODO: the extensions list is not observed; separately, the list object itself does
# not propagate events, so an append wouldn't work.

# @traitlets.observe("extensions")
# def _observe_extensions(self, change):
# """When a new extension is assigned, add its layer props to this layer."""
# new_extensions: List[BaseExtension] = change["new"]
# for extension in new_extensions:
# self.add_traits(**extension._layer_traits)

def _add_extension_traits(self, extensions: Sequence[BaseExtension]):
"""Assign selected traits from the extension onto this Layer."""
for extension in extensions:
Expand All @@ -146,6 +137,56 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]):
if trait.get_metadata("sync"):
self.keys.append(name)

# This doesn't currently work due to I think some race conditions around syncing
# traits vs the other parameters.

# def add_extension(self, extension: BaseExtension, **props):
# """Add a new layer extension to an existing layer instance.

# Any properties for the added extension should also be passed as keyword
# arguments to this function.

# Examples:

# ```py
# from lonboard import ScatterplotLayer
# from lonboard.layer_extension import DataFilterExtension

# gdf = geopandas.GeoDataFrame(...)
# layer = ScatterplotLayer.from_geopandas(gdf)

# extension = DataFilterExtension(filter_size=1)
# filter_values = gdf["filter_column"]

# layer.add_extension(
# extension,
# get_filter_value=filter_values,
# filter_range=[0, 1]
# )
# ```

# Args:
# extension: The new extension to add.

# Raises:
# ValueError: if another extension of the same type already exists on the
# layer.
# """
# if any(isinstance(extension, type(ext)) for ext in self.extensions):
# raise ValueError("Only one extension of each type permitted")

# with self.hold_trait_notifications():
# self._add_extension_traits([extension])
# self.extensions += (extension,)

# # Assign any extension properties
# added_names: List[str] = []
# for prop_name, prop_value in props.items():
# self.set_trait(prop_name, prop_value)
# added_names.append(prop_name)

# self.send_state(added_names + ["extensions"])

pickable = traitlets.Bool(True).tag(sync=True)
"""
Whether the layer responds to mouse pointer picking events.
Expand Down Expand Up @@ -423,9 +464,9 @@ def __init__(self, **kwargs: BitmapLayerKwargs):

bounds = traitlets.Union(
[
traitlets.List(traitlets.Float(), minlen=4, maxlen=4),
traitlets.List(
traitlets.List(traitlets.Float(), minlen=2, maxlen=2),
VariableLengthTuple(traitlets.Float(), minlen=4, maxlen=4),
VariableLengthTuple(
VariableLengthTuple(traitlets.Float(), minlen=2, maxlen=2),
minlen=4,
maxlen=4,
),
Expand All @@ -447,7 +488,7 @@ def __init__(self, **kwargs: BitmapLayerKwargs):
- Default: `0`
"""

transparent_color = traitlets.List(
transparent_color = VariableLengthTuple(
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
)
"""The color to use for transparent pixels, in `[r, g, b, a]`.
Expand All @@ -456,7 +497,7 @@ def __init__(self, **kwargs: BitmapLayerKwargs):
- Default: `[0, 0, 0, 0]`
"""

tint_color = traitlets.List(
tint_color = VariableLengthTuple(
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
)
"""The color to tint the bitmap by, in `[r, g, b]`.
Expand Down Expand Up @@ -519,7 +560,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
_layer_type = traitlets.Unicode("bitmap-tile").tag(sync=True)

data = traitlets.Union(
[traitlets.Unicode(), traitlets.List(traitlets.Unicode(), minlen=1)]
[traitlets.Unicode(), VariableLengthTuple(traitlets.Unicode(), minlen=1)]
).tag(sync=True)
"""
Either a URL template or an array of URL templates from which the tile data should
Expand Down Expand Up @@ -574,7 +615,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
- Default: `None`
"""

extent = traitlets.List(
extent = VariableLengthTuple(
traitlets.Float(), minlen=4, maxlen=4, allow_none=True, default_value=None
).tag(sync=True)
"""
Expand Down Expand Up @@ -657,7 +698,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
- Default: `0`
"""

transparent_color = traitlets.List(
transparent_color = VariableLengthTuple(
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
)
"""The color to use for transparent pixels, in `[r, g, b, a]`.
Expand All @@ -666,7 +707,7 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs):
- Default: `[0, 0, 0, 0]`
"""

tint_color = traitlets.List(
tint_color = VariableLengthTuple(
traitlets.Float(), default_value=None, allow_none=True, minlen=3, maxlen=4
)
"""The color to tint the bitmap by, in `[r, g, b]`.
Expand Down Expand Up @@ -2016,7 +2057,7 @@ def from_duckdb(
- Default: `0.05`
"""

color_domain = traitlets.List(
color_domain = VariableLengthTuple(
traitlets.Float(), default_value=None, allow_none=True, minlen=2, maxlen=2
).tag(sync=True)
# """
Expand Down
73 changes: 69 additions & 4 deletions lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
from lonboard._layer import BaseLayer
from lonboard._viewport import compute_view
from lonboard.basemap import CartoBasemap
from lonboard.traits import DEFAULT_INITIAL_VIEW_STATE, BasemapUrl, ViewStateTrait
from lonboard.traits import (
DEFAULT_INITIAL_VIEW_STATE,
BasemapUrl,
VariableLengthTuple,
ViewStateTrait,
)
from lonboard.types.map import MapKwargs

if TYPE_CHECKING:
Expand Down Expand Up @@ -131,7 +136,7 @@ def __init__(
This API is not yet stabilized and may change in the future.
"""

layers = traitlets.List(trait=traitlets.Instance(BaseLayer)).tag(
layers = VariableLengthTuple(traitlets.Instance(BaseLayer)).tag(
sync=True, **ipywidgets.widget_serialization
)
"""One or more `Layer` objects to display on this map.
Expand Down Expand Up @@ -170,7 +175,7 @@ def __init__(
custom_attribution = traitlets.Union(
[
traitlets.Unicode(allow_none=True),
traitlets.List(traitlets.Unicode(allow_none=False)),
VariableLengthTuple(traitlets.Unicode(allow_none=False)),
]
).tag(sync=True)
"""
Expand Down Expand Up @@ -306,6 +311,66 @@ def __init__(
global `parameters` when that layer is rendered.
"""

def add_layer(
self,
layers: BaseLayer | Sequence[BaseLayer] | Map,
*,
focus: bool = False,
reset_zoom: bool = False,
):
"""Add one or more new layers to the map.

Examples:

```py
from lonboard import viz

m = viz(some_data)
m.add_layer(viz(more_data), focus=True)
```

Args:
layers: New layers to add to the map. This can be:
- a layer instance
- a list or tuple of layer instances
- another `Map` instance, in which case its layers will be added to this
map. This lets you pass the result of `viz` into this method.

focus: If True, set the view state of the map based on the _newly-added_
layers. Defaults to False.
reset_zoom: If True, set the view state of the map based on _all_ layers.
Defaults to False.

Raises:
ValueError: _description_
"""

if focus and reset_zoom:
raise ValueError("focus and reset_zoom may not both be set.")

if isinstance(layers, Map):
new_layers = layers.layers
self.layers += layers.layers
# self.layers =x
# layers = layers.layers
elif isinstance(layers, BaseLayer):
new_layers = (layers,)
layers = [layers]
self.layers += (layers,)
else:
new_layers = tuple(layers)
self.layers += tuple(layers)

self.layers += new_layers

# self.layers += tuple(layers)

if focus:
self.view_state = compute_view(new_layers) # type: ignore

elif reset_zoom:
self.view_state = compute_view(self.layers) # type: ignore

def set_view_state(
self,
*,
Expand Down Expand Up @@ -482,4 +547,4 @@ def as_html(self) -> HTML:

@traitlets.default("view_state")
def _default_initial_view_state(self):
return compute_view(self.layers)
return compute_view(self.layers) # type: ignore
6 changes: 3 additions & 3 deletions lonboard/_viewport.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
from __future__ import annotations

import math
from typing import List, Tuple
from typing import Sequence, Tuple

from lonboard._geoarrow.ops.bbox import Bbox
from lonboard._geoarrow.ops.centroid import WeightedCentroid
from lonboard._layer import BaseLayer


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

Expand Down Expand Up @@ -55,7 +55,7 @@ def bbox_to_zoom_level(bbox: Bbox) -> int:
return zoom_level


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

Expand Down
3 changes: 3 additions & 0 deletions lonboard/_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ def viz(

Alternatively, you can pass a `list` or `tuple` of any of the above inputs.

If you want to easily add more data, to an existing map, you can pass the output of
`viz` into [`Map.add_layer`][lonboard.Map.add_layer].

Args:
data: a data object of any supported type.

Expand Down
21 changes: 11 additions & 10 deletions lonboard/layer_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
FilterValueAccessor,
FloatAccessor,
PointAccessor,
VariableLengthTuple,
)


Expand Down Expand Up @@ -318,9 +319,9 @@ class DataFilterExtension(BaseExtension):
_layer_traits = {
"filter_categories": traitlets.Union(
[
traitlets.List(traitlets.Any()),
traitlets.List(
traitlets.List(traitlets.Any()),
VariableLengthTuple(traitlets.Any()),
VariableLengthTuple(
VariableLengthTuple(traitlets.Any()),
minlen=2,
maxlen=4,
),
Expand All @@ -331,9 +332,9 @@ class DataFilterExtension(BaseExtension):
"filter_enabled": traitlets.Bool(True).tag(sync=True),
"filter_range": traitlets.Union(
[
traitlets.List(traitlets.Float(), minlen=2, maxlen=2),
traitlets.List(
traitlets.List(traitlets.Float(), minlen=2, maxlen=2),
VariableLengthTuple(traitlets.Float(), minlen=2, maxlen=2),
VariableLengthTuple(
VariableLengthTuple(traitlets.Float(), minlen=2, maxlen=2),
minlen=2,
maxlen=4,
),
Expand All @@ -350,21 +351,21 @@ class DataFilterExtension(BaseExtension):
"get_filter_category": FilterValueAccessor(default_value=None, allow_none=True),
}

filter_size = traitlets.Int(1, min=1, max=4).tag(sync=True)
filter_size = traitlets.Int(None, min=1, max=4, allow_none=True).tag(sync=True)
"""The size of the filter (number of columns to filter by).

The data filter can show/hide data based on 1-4 numeric properties of each object.

- Type: `int`, optional
- Type: `int`. This is required if using range-based filtering.
- Default 1.
"""

category_size = traitlets.Int(1, min=1, max=4).tag(sync=True)
category_size = traitlets.Int(None, min=1, max=4, allow_none=True).tag(sync=True)
"""The size of the category filter (number of columns to filter by).

The category filter can show/hide data based on 1-4 properties of each object.

- Type: `int`, optional
- Type: `int`. This is required if using category-based filtering.
- Default 0.
"""

Expand Down
Loading