diff --git a/docs/api/controls.md b/docs/api/controls.md new file mode 100644 index 00000000..0913864a --- /dev/null +++ b/docs/api/controls.md @@ -0,0 +1,3 @@ +# lonboard.controls + +::: lonboard.controls.MultiRangeSlider diff --git a/lonboard/__init__.py b/lonboard/__init__.py index 852bdb67..464f93c0 100644 --- a/lonboard/__init__.py +++ b/lonboard/__init__.py @@ -1,7 +1,7 @@ """Python library for fast, interactive geospatial vector data visualization in Jupyter. """ -from . import colormap, traits +from . import colormap, controls, traits from ._layer import ( BaseArrowLayer, BaseLayer, diff --git a/lonboard/controls.py b/lonboard/controls.py new file mode 100644 index 00000000..ac30b388 --- /dev/null +++ b/lonboard/controls.py @@ -0,0 +1,81 @@ +from functools import partial +from typing import Sequence + +import traitlets +from ipywidgets import FloatRangeSlider +from ipywidgets.widgets.trait_types import TypedTuple + +# Import from source to allow mkdocstrings to link to base class +from ipywidgets.widgets.widget_box import VBox + + +class MultiRangeSlider(VBox): + """A widget for multiple ranged sliders. + + This is designed to be used with the + [DataFilterExtension][lonboard.experimental.DataFilterExtension] when you want to + filter on 2 to 4 columns on the same time. + + If you have only a single filter, use an ipywidgets + [FloatRangeSlider][ipywidgets.widgets.widget_float.FloatRangeSlider] directly. + + # Example + + ```py + from ipywidgets import FloatRangeSlider + + slider1 = FloatRangeSlider( + value=(2, 5), + min=0, + max=10, + step=0.1, + description="First slider: " + ) + slider2 = FloatRangeSlider( + value=(30, 40), + min=0, + max=50, + step=1, + description="Second slider: " + ) + multi_slider = MultiRangeSlider([slider1, slider2]) + multi_slider + ``` + + Then to propagate updates to a rendered layer, call + [jsdlink][ipywidgets.widgets.widget_link.jsdlink] to connect the two widgets. + + ```py + from ipywidgets import jsdlink + + jsdlink( + (multi_slider, "value"), + (layer, "filter_range") + ) + ``` + + As you change the slider, the `filter_range` value on the layer class should be + updated. + """ + + # We use a tuple to force reassignment to update the list + # This is because list mutations do not get propagated as events + # https://github.com/jupyter-widgets/ipywidgets/blob/b2531796d414b0970f18050d6819d932417b9953/python/ipywidgets/ipywidgets/widgets/widget_box.py#L52-L54 + value = TypedTuple(trait=TypedTuple(trait=traitlets.Float())).tag(sync=True) + + def __init__(self, children: Sequence[FloatRangeSlider], **kwargs): + # We manage a list of lists to match what deck.gl expects for the + # DataFilterExtension + def callback(change, *, i: int): + value = list(self.value) + value[i] = change["new"] + self.set_trait("value", value) + self.send_state("value") + + initial_values = [] + for i, child in enumerate(children): + func = partial(callback, i=i) + child.observe(func, "value") + initial_values.append(child.value) + + super().__init__(children, value=initial_values, **kwargs) diff --git a/lonboard/experimental/layer_extension.py b/lonboard/experimental/layer_extension.py index 0a998d50..e5c8af3f 100644 --- a/lonboard/experimental/layer_extension.py +++ b/lonboard/experimental/layer_extension.py @@ -148,6 +148,68 @@ class DataFilterExtension(BaseExtension): ) ``` + The `DataFilterExtension` allows filtering on 1 to 4 attributes at the same time. So + if you have four numeric columns of interest, you can filter on the intersection of + all of them. + + For easy visualization, we suggest connecting the `DataFilterExtension` to an + interactive slider from `ipywidgets`. + + ```py + from ipywidgets import FloatRangeSlider + + slider = FloatRangeSlider( + value=(2, 5), + min=0, + max=10, + step=0.1, + description="Slider: " + ) + slider + + jsdlink( + (slider, "value"), + (layer, "filter_range") + ) + ``` + + If you have 2 to 4 columns, use a + [`MultiRangeSlider`][lonboard.controls.MultiRangeSlider], which combines multiple + `FloatRangeSlider` objects in a form that the `DataFilterExtension` expects. + + ```py + from ipywidgets import FloatRangeSlider, jsdlink + + slider1 = FloatRangeSlider( + value=(2, 5), + min=0, + max=10, + step=0.1, + description="First slider: " + ) + slider2 = FloatRangeSlider( + value=(30, 40), + min=0, + max=50, + step=1, + description="Second slider: " + ) + multi_slider = MultiRangeSlider([slider1, slider2]) + multi_slider + + jsdlink( + (multi_slider, "value"), + (layer, "filter_range") + ) + ``` + + # Important notes + + - The DataFilterExtension only supports float32 data, so integer data will be casted + to float32. + - The DataFilterExtension copies all data referenced by `get_filter_value` to the + GPU, so it will increase memory pressure on the GPU. + # Layer Properties ## `filter_enabled` diff --git a/mkdocs.yml b/mkdocs.yml index 2db84667..9c4ca2fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ nav: - api/layers/base-layer.md - api/basemap.md - api/colormap.md + - api/controls.md - api/traits.md - Experimental: - Layer Extensions: @@ -144,6 +145,7 @@ plugins: - https://geoarrow.github.io/geoarrow-rs/python/latest/objects.inv - https://geopandas.org/en/stable/objects.inv - https://geopy.readthedocs.io/en/stable/objects.inv + - https://ipywidgets.readthedocs.io/en/stable/objects.inv - https://matplotlib.org/stable/objects.inv - https://numpy.org/doc/stable/objects.inv - https://pandas.pydata.org/pandas-docs/stable/objects.inv diff --git a/pyproject.toml b/pyproject.toml index 7da2e830..70abd96b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "lonboard" -version = "0.5.0" +version = "0.6.0-beta.1" description = "Python library for fast, interactive geospatial vector data visualization in Jupyter." authors = ["Kyle Barron "] license = "MIT"