Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d37c8ce
add FilterCategoryAccessor modeled after FilterValueAccessor
ATL2001 Sep 21, 2025
53abc41
use FilterCategoryAccessor for get_filter_category, and set the min v…
ATL2001 Sep 21, 2025
8ff247e
pass filterSize and categorySize if they're both defined to the _Data…
ATL2001 Sep 21, 2025
5c7f72e
add temporary example notebook
ATL2001 Sep 21, 2025
a1f519b
lint
ATL2001 Sep 21, 2025
e5281be
more linting...
ATL2001 Sep 21, 2025
732fcf2
Merge branch 'main' into category_filter
kylebarron Oct 14, 2025
2403779
simplify if/else blocks and add defaults if None on python side
ATL2001 Oct 16, 2025
e4625c2
change min values to 1 and set default to 1 for filter_size
ATL2001 Oct 16, 2025
647a111
default category_size to None, filter_size to 1
ATL2001 Oct 16, 2025
b0086ad
added a few more cells to make sure I didnt break anything :)
ATL2001 Oct 16, 2025
daf0a51
lint the trowaway notebook
ATL2001 Oct 16, 2025
fa6d94b
Merge branch 'main' into category_filter
ATL2001 Oct 18, 2025
6f2a8da
Merge branch 'main' into category_filter
ATL2001 Oct 27, 2025
90426c0
update docstring for filter/category_size being optional
ATL2001 Oct 27, 2025
4270606
need to use ravel("C") for multiple categories
ATL2001 Nov 1, 2025
00b0763
add data filter extension tests
ATL2001 Nov 1, 2025
7046fad
Merge remote-tracking branch 'upstream/main' into category_filter
ATL2001 Nov 1, 2025
d733fce
move FilterCategoryAccessor to _extensions.py
ATL2001 Nov 1, 2025
23b8fc7
import TraitError from traitlets
ATL2001 Nov 1, 2025
6eec9d0
remove temp notebook
ATL2001 Nov 9, 2025
95ca5b0
add data-filter-extension-categorical example
ATL2001 Nov 9, 2025
5727578
Merge branch 'main' into category_filter
ATL2001 Nov 9, 2025
a5b1c03
ruff format
ATL2001 Nov 9, 2025
9982c63
lint
ATL2001 Nov 9, 2025
5fde03e
format again
ATL2001 Nov 9, 2025
607c926
typo
ATL2001 Nov 9, 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
257 changes: 257 additions & 0 deletions examples/data-filter-extension-categorical.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "5a203c06-68a6-4335-9037-aef706980245",
"metadata": {},
"outputs": [],
"source": [
"# /// script\n",
"# requires-python = \">=3.12\"\n",
"# dependencies = [\n",
"# \"geodatasets\",\n",
"# \"geopandas\",\n",
"# \"ipywidgets\",\n",
"# \"lonboard\",\n",
"# \"numpy\",\n",
"# \"palettable\",\n",
"# \"pandas\",\n",
"# \"shapely\",\n",
"# ]\n",
"# ///"
]
},
{
"cell_type": "markdown",
"id": "ad4207fc-7b6d-4301-b79e-34ff1d961994",
"metadata": {},
"source": [
"# Categorical Filtering with the DataFilterExtension\n",
"\n",
"The `DataFilterExtension` adds GPU-based data filtering functionalities to layers, allowing the layer to show/hide objects based on user-defined properties."
]
},
{
"cell_type": "markdown",
"id": "0eba4205-2605-4922-be6a-ee71a4d8e389",
"metadata": {},
"source": [
"## Dependencies\n",
"\n",
"Install [`uv`](https://docs.astral.sh/uv) and then launch this notebook with:\n",
"\n",
"```\n",
"uvx juv run examples/data-filter-extension-categorical.ipynb\n",
"```\n",
"\n",
"(The `uvx` command is included when installing `uv`)."
]
},
{
"cell_type": "markdown",
"id": "a4a41490-4c62-4ed1-90f9-5126fa3e532f",
"metadata": {},
"source": [
"## Categorical Filtering\n",
"In this example the `DataFilterExtension` will be used to filter the display the home sales dataset from geodatasets based on the number of bedrooms, bathrooms.\n",
"\n",
"To demonstrate, we'll create:\n",
"\n",
"1. A Geopandas GeoDataFrame of home sales data.\n",
"2. A Lonboard ScatterPlotLayer from the GeoDataFrame that has a DataFilterExtension set up for categorical filtering.\n",
"3. Some IPyWidgets linked to the ScatterPlotLayer's `filter_categories` property to allow us to interactively filter the points on the map."
]
},
{
"cell_type": "markdown",
"id": "799e41b8-e75a-453c-a9ac-59ed6e254cd7",
"metadata": {},
"source": [
"### Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b7965b2a-4e93-438f-81ea-cf39917fb654",
"metadata": {},
"outputs": [],
"source": [
"import geodatasets\n",
"import geopandas\n",
"import traitlets\n",
"from ipywidgets import Button, HBox, SelectMultiple, VBox\n",
"from palettable.colorbrewer.diverging import RdYlGn_11\n",
"\n",
"import lonboard\n",
"from lonboard import Map, ScatterplotLayer\n",
"from lonboard.layer_extension import DataFilterExtension"
]
},
{
"cell_type": "markdown",
"id": "6a703ab0-6b15-4e6e-b45b-ed915fa4d670",
"metadata": {},
"source": [
"### Reading the Home Sales Data\n",
"\n",
"This example makes use the geodatasets python package to access some spatial data easily.\n",
"\n",
"Calling geodatasets.get_path() will download data the specified data to the machine and return the path to the downloaded file. If the file has already been downloaded it will simply return the path to the file. [See downloading and caching](https://geodatasets.readthedocs.io/en/latest/introduction.html#downloading-and-caching) for further details."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "78c0a363-83dd-4dee-8ac6-10ff67e8e817",
"metadata": {},
"outputs": [],
"source": [
"home_sales_df = geopandas.read_file(geodatasets.get_path(\"geoda.home_sales\"))[\n",
" [\"price\", \"bedrooms\", \"bathrooms\", \"geometry\"]\n",
"]\n",
"home_sales_df"
]
},
{
"cell_type": "markdown",
"id": "c295793b-fc34-4e34-a2a0-6f941d7bf8fd",
"metadata": {},
"source": [
"### Create the `ScatterplotLayer` with a `DataFilterExtension` extension for categorical filtering\n",
"\n",
"The `DataFilterExtension` will be cretated with `filter_size=None` to indicate we do not want to use a range filter, and `category_size=2` to indicate we want to use two different categories from the data to filter the data with explicit values.\n",
"\n",
"The points in the layer will be symbolized based on a continuous colormap of the price. Lower priced homes will be red, and higher priced homes will be green. We'll throw out the upper and lower 5% of value from the color map so the upper and lower outliers do not influence the colormap."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14a311f4-7344-496d-a9b3-7ec8eaee5cef",
"metadata": {},
"outputs": [],
"source": [
"min_bound = home_sales_df[\"price\"].quantile(0.05)\n",
"max_bound = home_sales_df[\"price\"].quantile(0.95)\n",
"price = home_sales_df[\"price\"]\n",
"normalized_price = (price - min_bound) / (max_bound - min_bound)\n",
"\n",
"home_sale_layer = ScatterplotLayer.from_geopandas(\n",
" home_sales_df,\n",
" get_fill_color=lonboard.colormap.apply_continuous_cmap(normalized_price, RdYlGn_11),\n",
" radius_min_pixels=5,\n",
" extensions=[\n",
" DataFilterExtension(filter_size=None, category_size=2),\n",
" ],\n",
" get_filter_category=home_sales_df[[\"bedrooms\", \"bathrooms\"]].values,\n",
" filter_categories=[[], []],\n",
")"
]
},
{
"cell_type": "markdown",
"id": "83854bcd-a17c-4d2a-91b7-c6626dd67ede",
"metadata": {},
"source": [
"### Create the iPyWidgets to interact with the `DataFilterExtension`\n",
"\n",
"Since we want to only display the points which are tied to specific number of bedrooms and bathrooms we'll:\n",
"\n",
"1. Create two ipywidgets `SelectMultiple` widgets which will hold the different numbers of bedrooms and bathrooms in the home sales data.\n",
"2. Create two ipywidgets `Button` widgets which will clear the selected values for the bedrooms and bathrooms.\n",
"4. Observe the changes made to the `SelectMultiple` widgets to update the layer's `filter_categories` property.\n",
"\n",
"This will enable us to select one or more number of bedrooms or bathrooms and have the map instantly react to disply only the data that matches the selections. If a select widget does not have a selection, all the values from that selector will be used."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2ea817c2-7a6d-4b43-99f4-ae0f638151e9",
"metadata": {},
"outputs": [],
"source": [
"unique_bedrooms_values = list(home_sales_df[\"bedrooms\"].sort_values().unique())\n",
"unique_bathrooms_values = list(home_sales_df[\"bathrooms\"].sort_values().unique())\n",
"\n",
"bedrooms_select = SelectMultiple(description=\"Bedrooms\", options=unique_bedrooms_values)\n",
"bathrooms_select = SelectMultiple(\n",
" description=\"Bathrooms\",\n",
" options=unique_bathrooms_values,\n",
")\n",
"\n",
"\n",
"def on_select_change(_: traitlets.utils.bunch.Bunch = None) -> None:\n",
" \"\"\"Set the layer's filter_categories property based on widget selections.\"\"\"\n",
" bedrooms = bedrooms_select.value\n",
" bathrooms = bathrooms_select.value\n",
" if len(bedrooms) == 0:\n",
" bedrooms = unique_bedrooms_values\n",
" if len(bathrooms) == 0:\n",
" bathrooms = unique_bathrooms_values\n",
" home_sale_layer.filter_categories = [bedrooms, bathrooms]\n",
"\n",
"\n",
"bedrooms_select.observe(on_select_change, \"value\")\n",
"bathrooms_select.observe(on_select_change, \"value\")\n",
"\n",
"clear_bedrooms_button = Button(description=\"Clear Bedrooms\")\n",
"\n",
"\n",
"def clear_bedrooms(_: Button) -> None:\n",
" bedrooms_select.value = []\n",
"\n",
"\n",
"clear_bedrooms_button.on_click(clear_bedrooms)\n",
"\n",
"clear_bathrooms_button = Button(description=\"Clear Bathrooms\")\n",
"\n",
"\n",
"def clear_bathrooms(_: Button) -> None:\n",
" bathrooms_select.value = []\n",
"\n",
"\n",
"clear_bathrooms_button.on_click(clear_bathrooms)\n",
"\n",
"home_sale_map = Map(\n",
" layers=[home_sale_layer],\n",
" basemap=lonboard.basemap.MaplibreBasemap(),\n",
")\n",
"on_select_change() # fire the function once to initially set the layer's filter_categories, and display all points\n",
"\n",
"display(home_sale_map)\n",
"display(\n",
" HBox(\n",
" [\n",
" VBox([bedrooms_select, clear_bedrooms_button]),\n",
" VBox([bathrooms_select, clear_bathrooms_button]),\n",
" ],\n",
" ),\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "lonboard_category_filter",
"language": "python",
"name": "lonboard_category_filter"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
14 changes: 9 additions & 5 deletions lonboard/layer_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from lonboard._base import BaseExtension
from lonboard.traits import (
DashArrayAccessor,
FilterCategoryAccessor,
FilterValueAccessor,
FloatAccessor,
PointAccessor,
Expand Down Expand Up @@ -353,15 +354,18 @@ class DataFilterExtension(BaseExtension):
"filter_transform_size": t.Bool(default_value=True).tag(sync=True),
"filter_transform_color": t.Bool(default_value=True).tag(sync=True),
"get_filter_value": FilterValueAccessor(default_value=None, allow_none=True),
"get_filter_category": FilterValueAccessor(default_value=None, allow_none=True),
"get_filter_category": FilterCategoryAccessor(
default_value=None,
allow_none=True,
),
}

filter_size = t.Int(None, min=1, max=4, allow_none=True).tag(sync=True)
filter_size = t.Int(1, 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`. This is required if using range-based filtering.
- Type: `int`, optional. This is required if using range-based filtering.
- Default 1.
"""

Expand All @@ -370,8 +374,8 @@ class DataFilterExtension(BaseExtension):

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

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


Expand Down
3 changes: 2 additions & 1 deletion lonboard/traits/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ._a5 import A5Accessor
from ._base import FixedErrorTraitType, VariableLengthTuple
from ._color import ColorAccessor
from ._extensions import DashArrayAccessor, FilterValueAccessor
from ._extensions import DashArrayAccessor, FilterCategoryAccessor, FilterValueAccessor
from ._float import FloatAccessor
from ._h3 import H3Accessor
from ._map import BasemapUrl, MapHeightTrait, ViewStateTrait
Expand All @@ -23,6 +23,7 @@
"BasemapUrl",
"ColorAccessor",
"DashArrayAccessor",
"FilterCategoryAccessor",
"FilterValueAccessor",
"FixedErrorTraitType",
"FloatAccessor",
Expand Down
Loading