Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
02d6e13
View model definitions
kylebarron Oct 8, 2025
ab8c55d
Allow none
kylebarron Oct 8, 2025
71b0b4e
Pass in views to Map constructor
kylebarron Oct 8, 2025
4bfd2ba
move html export to separate file
kylebarron Oct 8, 2025
f14cdba
move view_state down after view
kylebarron Oct 8, 2025
c490176
Set up maplibre basemap widet
kylebarron Oct 8, 2025
aaf66a3
Define split renderers
kylebarron Oct 14, 2025
ca7bfd4
Implement split renderers
kylebarron Oct 14, 2025
a043703
alphabetize
kylebarron Oct 14, 2025
fa0940c
Merge branch 'main' into kyle/view-basemap-refactor
kylebarron Oct 14, 2025
879bff0
Merge branch 'main' into kyle/split-renderers2
kylebarron Oct 14, 2025
08ce7f8
Merge branch 'kyle/split-renderers2' into kyle/view-basemap-refactor
kylebarron Oct 14, 2025
18aacb4
Merge branch 'main' into kyle/view-basemap-refactor
kylebarron Oct 14, 2025
e185723
Merge branch 'main' into kyle/view-basemap-refactor
kylebarron Oct 15, 2025
05b1ec2
Merge branch 'main' into kyle/view-basemap-refactor
kylebarron Oct 16, 2025
3571acf
reduce diff
kylebarron Oct 16, 2025
2e59606
Support deck views
kylebarron Oct 16, 2025
6ff3540
Apply linear gradient background
kylebarron Oct 16, 2025
e846164
Add dark background when in globe view
kylebarron Oct 16, 2025
8c88487
pass undefined when no views passed
kylebarron Oct 16, 2025
188164c
Remove multi-view support for now
kylebarron Oct 16, 2025
89d70eb
fix basemap when constructing `Map` without any parameters
kylebarron Oct 16, 2025
582d606
Let `views` be None
kylebarron Oct 16, 2025
9b8ebcc
remove accidental render_mode
kylebarron Oct 16, 2025
d72e7b7
reduce diff
kylebarron Oct 16, 2025
e7e6a56
wip: PydanticModelTrait
kylebarron Oct 17, 2025
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
6 changes: 3 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import eslintConfigPrettier from "eslint-config-prettier";
import pluginImport from "eslint-plugin-import";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import tseslint from "typescript-eslint";

export default [
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
Expand Down
16 changes: 16 additions & 0 deletions lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
VariableLengthTuple,
ViewStateTrait,
)
from lonboard.view import BaseView

if TYPE_CHECKING:
import sys
Expand Down Expand Up @@ -151,6 +152,8 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
_esm = bundler_output_dir / "index.js"
_css = bundler_output_dir / "index.css"

# TODO: change this view state to allow non-map view states if we have non-map views
# Also allow a list/tuple of view states for multiple views
view_state = ViewStateTrait()
"""
The view state of the map.
Expand All @@ -174,6 +177,7 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
once it's been initially rendered.

"""

_has_click_handlers = t.Bool(default_value=False, allow_none=False).tag(sync=True)
"""
Indicates if a click handler has been registered.
Expand All @@ -192,6 +196,15 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
"""One or more `Layer` objects to display on this map.
"""

views: t.Instance[BaseView | None] = t.Instance(BaseView, allow_none=True).tag(
sync=True,
**ipywidgets.widget_serialization,
)
"""A View instance.

Views represent the "camera(s)" (essentially viewport dimensions and projection matrices) that you look at your data with. deck.gl offers multiple view types for both geospatial and non-geospatial use cases. Read the [Views and Projections](https://deck.gl/docs/developer-guide/views) guide for the concept and examples.
"""

show_tooltip = t.Bool(default_value=False).tag(sync=True)
"""
Whether to render a tooltip on hover on the map.
Expand Down Expand Up @@ -221,6 +234,9 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:

basemap: t.Instance[MaplibreBasemap | None] = t.Instance(
MaplibreBasemap,
# If both `args` and `kw` are None, then the default value is None.
# Set empty kw so that the default is MaplibreBasemap() with default params
kw={},
allow_none=True,
).tag(
sync=True,
Expand Down
8 changes: 7 additions & 1 deletion lonboard/_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from lonboard._utils import timestamp_start_offset

if TYPE_CHECKING:
from pydantic import BaseModel

from lonboard._layer import BaseArrowLayer
from lonboard.experimental._layer import TripsLayer
from lonboard.models import ViewState
Expand Down Expand Up @@ -159,13 +161,17 @@ def validate_accessor_length_matches_table(
raise TraitError("accessor must have same length as table")


def serialize_view_state(data: ViewState | None, obj: Any) -> None | dict[str, Any]: # noqa: ARG001
def serialize_view_state(data: ViewState | None, _obj: Any) -> None | dict[str, Any]:
if data is None:
return None

return data._asdict()


def serialize_pydantic_model(data: BaseModel, _obj: Any) -> None | dict[str, Any]:
return data.model_dump(exclude_unset=True, exclude_none=True)


def serialize_timestamp_accessor(
timestamps: ChunkedArray,
obj: TripsLayer,
Expand Down
55 changes: 55 additions & 0 deletions lonboard/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import NamedTuple

from anywidget.experimental import MimeBundleDescriptor
from pydantic import BaseModel, ConfigDict


class ViewState(NamedTuple):
"""State of a view position of a map."""
Expand All @@ -18,3 +21,55 @@ class ViewState(NamedTuple):

bearing: float
"""Bearing angle in degrees. `0` is north."""


def _to_camel(string: str) -> str:
parts = string.split("_")
return parts[0] + "".join(word.capitalize() for word in parts[1:])


class MapViewState(BaseModel, frozen=True):
"""State of a map view."""

_repr_mimebundle_ = MimeBundleDescriptor(no_view=True)

longitude: float
"""Longitude of the map center"""

latitude: float
"""Latitude of the map center."""

zoom: float
"""Zoom level."""

pitch: float | None = None
"""Pitch (tilt) of the map, in degrees. `0` looks top down"""

bearing: float | None = None
"""Bearing (rotation) of the map, in degrees. `0` is north up"""

min_zoom: float | None = None
"""Min zoom, default `0`"""

max_zoom: float | None = None
"""Max zoom, default `20`"""

min_pitch: float | None = None
"""Min pitch, default `0`"""

max_pitch: float | None = None
"""Max pitch, default `60`"""

position: list[float] | None = None
"""Viewport center offsets from lng, lat in meters"""

near_z: float | None = None
"""The near plane position"""

far_z: float | None = None
"""The far plane position"""

model_config = ConfigDict(
alias_generator=_to_camel,
populate_by_name=True,
)
35 changes: 33 additions & 2 deletions lonboard/traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import sys
import warnings
from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
from typing import TYPE_CHECKING, Any, Generic, NoReturn, TypeVar
from typing import cast as type_cast
from urllib.parse import urlparse

Expand All @@ -24,6 +24,7 @@
Table,
fixed_size_list_array,
)
from pydantic import BaseModel, ValidationError
from traitlets import TraitError, Undefined
from traitlets.utils.descriptions import class_of, describe

Expand All @@ -34,6 +35,7 @@
from lonboard._serialization import (
ACCESSOR_SERIALIZATION,
TABLE_SERIALIZATION,
serialize_pydantic_model,
serialize_view_state,
)
from lonboard._utils import get_geometry_column_index
Expand All @@ -47,6 +49,7 @@
from traitlets.utils.sentinel import Sentinel

from lonboard._layer import BaseArrowLayer
from lonboard._map import Map

DEFAULT_INITIAL_VIEW_STATE = {
"latitude": 10,
Expand Down Expand Up @@ -444,6 +447,34 @@ def validate(self, obj: BaseArrowLayer, value: Any) -> float | ChunkedArray:
return value.rechunk(max_chunksize=obj._rows_per_chunk)


PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel)


class PydanticModelTrait(FixedErrorTraitType, Generic[PydanticModelT]):
"""A trait to validate input for a pydantic model.

The pydantic model must be a subclass of `pydantic.BaseModel`.
"""

klass: type[PydanticModelT]

def __init__(
self: TraitType,
klass: type[PydanticModelT],
*args: Any,
**kwargs: Any,
) -> None:
self.klass = klass # type: ignore[assignment]
super().__init__(*args, **kwargs)
self.tag(sync=True, to_json=serialize_pydantic_model)

def validate(self, obj: HasTraits, value: dict[str, Any]) -> BaseModel:
try:
return self.klass(**value)
except ValidationError as e:
self.error(obj, value, error=e)


class TextAccessor(FixedErrorTraitType):
"""A trait to validate input for a deck.gl text accessor.

Expand Down Expand Up @@ -943,7 +974,7 @@ def __init__(

self.tag(sync=True, to_json=serialize_view_state)

def validate(self, obj: Any, value: Any) -> None | ViewState:
def validate(self, obj: Map, value: Any) -> None | ViewState:
if value is None:
return None

Expand Down
2 changes: 2 additions & 0 deletions lonboard/types/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

if TYPE_CHECKING:
from lonboard.basemap import MaplibreBasemap
from lonboard.view import BaseView


class MapKwargs(TypedDict, total=False):
Expand All @@ -22,4 +23,5 @@ class MapKwargs(TypedDict, total=False):
show_tooltip: bool
show_side_panel: bool
use_device_pixels: int | float | bool
views: BaseView | list[BaseView] | tuple[BaseView, ...]
view_state: dict[str, Any]
Loading
Loading