diff --git a/examples/internet-speeds.ipynb b/examples/internet-speeds.ipynb
index 4bc925d2..45d38e02 100644
--- a/examples/internet-speeds.ipynb
+++ b/examples/internet-speeds.ipynb
@@ -30,37 +30,43 @@
"outputs": [],
"source": [
"from pathlib import Path\n",
+ "import pyarrow as pa\n",
"\n",
+ "from arro3.core import Array, Table, DataType, ChunkedArray\n",
"import geopandas as gpd\n",
"import numpy as np\n",
"import pandas as pd\n",
"import shapely\n",
"from palettable.colorbrewer.diverging import BrBG_10\n",
"\n",
+ "import quak\n",
"from lonboard import Map, ScatterplotLayer\n",
- "from lonboard.colormap import apply_continuous_cmap"
+ "from lonboard.colormap import apply_continuous_cmap\n",
+ "from lonboard.layer_extension import DataFilterExtension"
]
},
{
- "cell_type": "markdown",
- "id": "d51ca576",
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "1ae334fe-bb36-4ab2-9b8c-b2fe10bd4038",
"metadata": {},
+ "outputs": [],
"source": [
- "## Fetch data\n",
- "\n"
+ "import sqlglot"
]
},
{
"cell_type": "markdown",
- "id": "c747d8b9-94b9-421a-967a-8350bf72de9a",
+ "id": "d51ca576",
"metadata": {},
"source": [
- "The URL for a single data file for mobile network speeds in the first quarter of 2019:"
+ "## Fetch data\n",
+ "\n"
]
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 3,
"id": "34ac8eae",
"metadata": {},
"outputs": [],
@@ -68,21 +74,9 @@
"url = \"https://ookla-open-data.s3.us-west-2.amazonaws.com/parquet/performance/type=mobile/year=2019/quarter=1/2019-01-01_performance_mobile_tiles.parquet\""
]
},
- {
- "cell_type": "markdown",
- "id": "5991ef2c-5db0-4110-b6a1-b33fcbddad0d",
- "metadata": {},
- "source": [
- "The data used in this example is relatively large. In the cell below, we cache the downloading and preparation of the dataset so that it's faster to run this notebook the second time.\n",
- "\n",
- "We fetch two columns — `avg_d_kbps` and `tile` — from this data file directly from AWS. The `pd.read_parquet` command will perform a network request for these columns from the data file, so it may take a while on a slow network connection. `avg_d_kbps` is the average download speed for that data point in kilobits per second. `tile` is the WKT string representing a given zoom-16 Web Mercator tile.\n",
- "\n",
- "The `tile` column contains _strings_ representing WKT-formatted geometries. We need to parse those strings into geometries. Then for simplicity we'll convert into their centroids."
- ]
- },
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 4,
"id": "7c20cb4c-9746-486f-aef7-95dd2dedd6a5",
"metadata": {},
"outputs": [],
@@ -100,17 +94,9 @@
" gdf.to_parquet(local_path)"
]
},
- {
- "cell_type": "markdown",
- "id": "5852aa94-2d18-4a1b-b379-be19682d57eb",
- "metadata": {},
- "source": [
- "We can take a quick look at this data:"
- ]
- },
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 5,
"id": "4b27e9a4",
"metadata": {},
"outputs": [
@@ -158,12 +144,12 @@
"
\n",
" 3 | \n",
" 2381 | \n",
- " POINT (-160.03510 70.63357) | \n",
+ " POINT (-160.0351 70.63357) | \n",
"
\n",
" \n",
" 4 | \n",
" 3047 | \n",
- " POINT (-160.03510 70.63175) | \n",
+ " POINT (-160.0351 70.63175) | \n",
"
\n",
" \n",
"\n",
@@ -174,11 +160,11 @@
"0 5983 POINT (-160.01862 70.63722)\n",
"1 3748 POINT (-160.04059 70.63357)\n",
"2 3364 POINT (-160.04059 70.63175)\n",
- "3 2381 POINT (-160.03510 70.63357)\n",
- "4 3047 POINT (-160.03510 70.63175)"
+ "3 2381 POINT (-160.0351 70.63357)\n",
+ "4 3047 POINT (-160.0351 70.63175)"
]
},
- "execution_count": 4,
+ "execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
@@ -188,23 +174,711 @@
]
},
{
- "cell_type": "markdown",
- "id": "65436a4a-c498-4f40-ba79-1082062376bf",
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "5bcebd7c-58d4-487e-b191-a27e4ffb6349",
"metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "added_names\n",
+ "[]\n"
+ ]
+ }
+ ],
"source": [
- "To ensure that this demo is snappy on most computers, we'll filter to a bounding box over Europe.\n",
+ "layer = ScatterplotLayer.from_geopandas(gdf)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "8224d03d-8b16-4475-a265-2e2e7ba58858",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test = layer.quak()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "3b3776ce-2a84-4f52-a110-a7a89b8401d2",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "ee0e68d707384d0cb2da95ae9e79813c",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Widget(sql='SELECT * FROM \"df\"', temp_indexes=True)"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "test"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "51c82441-9711-4df8-9715-527fe00aff01",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.filter_categories = [1]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "1b0964f6-fcf2-4b5d-9f70-42615f974d8e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "6c63a10fe1574c639f2cf2536b721cad",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(custom_attribution='', layers=[ScatterplotLayer(extensions=[DataFilterExtension()], filter_categories=[1],…"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "m = Map(layer)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "869dc1e9-aeba-4297-b16e-c8eb893ada8d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ext = DataFilterExtension(category_size=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "9a848d8d-2efd-4ff5-bcee-f0690081abeb",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "ValueError",
+ "evalue": "Cannot handle multiple of the same extension",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[20], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mlayer\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_extension\u001b[49m\u001b[43m(\u001b[49m\u001b[43mext\u001b[49m\u001b[43m)\u001b[49m\n",
+ "File \u001b[0;32m~/github/developmentseed/lonboard/lonboard/_layer.py:159\u001b[0m, in \u001b[0;36mBaseLayer.add_extension\u001b[0;34m(self, extension)\u001b[0m\n\u001b[1;32m 157\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21madd_extension\u001b[39m(\u001b[38;5;28mself\u001b[39m, extension: BaseExtension):\n\u001b[1;32m 158\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28many\u001b[39m(\u001b[38;5;28misinstance\u001b[39m(ext, extension\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m ext \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mextensions):\n\u001b[0;32m--> 159\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot handle multiple of the same extension\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 161\u001b[0m \u001b[38;5;66;03m# Maybe keep a registry of which extensions have already been added?\u001b[39;00m\n\u001b[1;32m 162\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_add_extension_traits([extension])\n",
+ "\u001b[0;31mValueError\u001b[0m: Cannot handle multiple of the same extension"
+ ]
+ }
+ ],
+ "source": [
+ "layer.add_extension(ext)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "df3d4458-5a86-4f41-8317-7a708d428ae1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['_layer_type',\n",
+ " '_model_module',\n",
+ " '_model_module_version',\n",
+ " '_model_name',\n",
+ " '_msg_callbacks',\n",
+ " '_property_lock',\n",
+ " '_states_to_send',\n",
+ " '_view_count',\n",
+ " '_view_module',\n",
+ " '_view_module_version',\n",
+ " '_view_name',\n",
+ " 'antialiasing',\n",
+ " 'auto_highlight',\n",
+ " 'billboard',\n",
+ " 'comm',\n",
+ " 'extensions',\n",
+ " 'filled',\n",
+ " 'filter_categories',\n",
+ " 'filter_enabled',\n",
+ " 'filter_range',\n",
+ " 'filter_soft_range',\n",
+ " 'filter_transform_color',\n",
+ " 'filter_transform_size',\n",
+ " 'get_fill_color',\n",
+ " 'get_filter_category',\n",
+ " 'get_filter_value',\n",
+ " 'get_line_color',\n",
+ " 'get_line_width',\n",
+ " 'get_radius',\n",
+ " 'keys',\n",
+ " 'line_width_max_pixels',\n",
+ " 'line_width_min_pixels',\n",
+ " 'line_width_scale',\n",
+ " 'line_width_units',\n",
+ " 'log',\n",
+ " 'opacity',\n",
+ " 'pickable',\n",
+ " 'radius_max_pixels',\n",
+ " 'radius_min_pixels',\n",
+ " 'radius_scale',\n",
+ " 'radius_units',\n",
+ " 'selected_bounds',\n",
+ " 'selected_index',\n",
+ " 'stroked',\n",
+ " 'table',\n",
+ " 'visible']"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.trait_names()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "0b8d14fd-b087-4d35-a9ef-29afd9bb71b4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "arro3.core.Array"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.get_filter_category"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "6403ec90-6272-4c7f-a1be-d56dd89054ae",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1]"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.filter_categories"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "edc90c01-2c3d-483e-91b3-6e298f52a68c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "1"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.extensions[0].category_size"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "40c2ec2a-73cb-4c56-bf52-05339193cabf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test = "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "77abfe2b-8c6b-4c14-ad4a-9562feb33768",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ext = DataFilterExtension(category_size=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "3764dd90-e512-4a87-89d8-bfc90e05acab",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "calling add traits\n",
+ "{'filter_categories': , 'filter_enabled': , 'filter_range': , 'filter_soft_range': , 'filter_transform_size': , 'filter_transform_color': , 'get_filter_value': , 'get_filter_category': }\n"
+ ]
+ }
+ ],
+ "source": [
+ "layer.add_extension(ext)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "dfc39cc6-200c-4f26-ba93-a446185f0ff3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "table = layer.table\n",
+ "num_rows = table.num_rows\n",
+ "if num_rows <= np.iinfo(np.uint8).max:\n",
+ " row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint8))\n",
+ " filter_arr = np.ones(num_rows, dtype=np.float32)\n",
+ "elif num_rows <= np.iinfo(np.uint16).max:\n",
+ " row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint16))\n",
+ " filter_arr = np.ones(num_rows, dtype=np.float32)\n",
+ "elif num_rows <= np.iinfo(np.uint32).max:\n",
+ " row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint32))\n",
+ " filter_arr = np.ones(num_rows, dtype=np.float32)\n",
+ "else:\n",
+ " row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint64))\n",
+ " filter_arr = np.ones(num_rows, dtype=np.float32)\n",
"\n",
- "If you're on a recent computer, feel free to comment out the next line."
+ "table_with_row_index = table.append_column(\"_row_index\", ChunkedArray(row_index))\n",
+ "quak_widget = quak.Widget(table_with_row_index)"
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "id": "80326895-70ba-4f4b-a7b3-106b4bbd36d9",
+ "execution_count": 10,
+ "id": "9dee0cf6-3082-4403-92b0-124cbb99e667",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "0abc4db7d7bf4b56ad8de415eac03548",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Widget(sql='SELECT * FROM \"df\"', temp_indexes=True)"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "quak_widget"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "bd4431d9-93af-492d-aa4f-e7ca5d394ed0",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "NameError",
+ "evalue": "name 'm' is not defined",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[11], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\n",
+ "\u001b[0;31mNameError\u001b[0m: name 'm' is not defined"
+ ]
+ }
+ ],
+ "source": [
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3cbd865c-3761-4174-a657-367b7922d415",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "a99da0b6-ce83-49d7-b7a8-85b23377dcec",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "89d661be5f4442ebbfef3473045202ab",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(custom_attribution='', layers=[ScatterplotLayer(extensions=[DataFilterExtension()], table=arro3.core.Table…"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "m = Map(layer)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "1a43a9fa-bed7-4eae-97c8-2524eda01781",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([1., 1., 1., ..., 1., 1., 1.], dtype=float32)"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "filter_arr"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "e55c9d4f-26ee-45d4-a006-d2f19967ad1d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[DataFilterExtension()]"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.extensions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "8b94a3ae-ba93-4e7b-9b85-f85125beb712",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# layer.extensions = [ext]\n",
+ "layer.get_filter_category = filter_arr\n",
+ "layer.filter_categories = [1]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "41b96069-4bae-412f-a502-15cc1ee0da92",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def row_index_callback(change):\n",
+ " sql = sqlglot.parse_one(quak_widget.sql, dialect=\"duckdb\")\n",
+ " sql.set(\"expressions\", [sqlglot.column(\"_row_index\")])\n",
+ " row_index_table = quak_widget._conn.query(sql.sql(dialect=\"duckdb\")).arrow()\n",
+ "\n",
+ " # Reset all to 2. We don't use zero because there might be a bug with 0\n",
+ " filter_arr[:] = 2\n",
+ "\n",
+ " # Set the desired _row_index to 1\n",
+ " filter_arr[row_index_table[\"_row_index\"]] = 1\n",
+ "\n",
+ " layer.get_filter_category = filter_arr\n",
+ "\n",
+ "quak_widget.observe(row_index_callback, names=\"sql\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "ef7decf8-5d7f-49f2-be44-9a4198327cdf",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "arro3.core.Array"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.get_filter_category"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "b7ea012c-7ab8-49c6-86bc-859ee345b761",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.send_state()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "0215bbce-97b3-48fb-a185-426a431b3c3a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1]"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.filter_categories"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c64b53f9-5f32-4afe-8bd0-56bd69b453c0",
"metadata": {},
"outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "id": "a238c681-6ebc-4977-9aef-cbd93d8a1640",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf[\"slow\"] = gdf[\"avg_d_kbps\"] < 5000"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "6e3a8ad3-af0e-45e8-b0dc-277ee2a59ccf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from lonboard.layer_extension import DataFilterExtension"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "47517cd2-e886-4aeb-a266-a609c97c0cb6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ext = DataFilterExtension(category_size=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "2daf3b7c-fadb-461f-b1c1-6a834eb71df7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "28b673727f4e4506ae9d8551acbf5fab",
+ "version_major": 2,
+ "version_minor": 1
+ },
+ "text/plain": [
+ "Map(custom_attribution='', layers=[ScatterplotLayer(extensions=[DataFilterExtension()], filter_categories=[0],…"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "gdf = gdf.cx[-11.83:25.5, 34.9:59]"
+ "layer = ScatterplotLayer.from_geopandas(\n",
+ " gdf,\n",
+ " get_filter_category = gdf[\"slow\"].astype(np.float32),\n",
+ " filter_categories = [0],\n",
+ " extensions=[ext]\n",
+ " )\n",
+ "m = Map(layer)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "f6dc26f8-34ac-48f4-b3f4-2c9da083be16",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.filter_categories = [0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "f51b34e8-a903-4a41-a9ad-78120b7bb63d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1]"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "layer.filter_categories"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "e93bcb7f-6de8-4eca-91fe-19febbcd5ae7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([0., 1., 1., ..., 0., 0., 1.], dtype=float32)"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "np.array(layer.get_filter_category)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "66128647-702e-4902-a29d-4edd7c852eed",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ext = DataFilterExtension(category_size=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "2aa68b6c-72a3-4e27-a98a-90b1f404a34c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.extensions = [ext]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "44a4852c-385f-42da-a8a0-115c16135b9f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.get_filter_category = gdf[\"slow\"]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "9eb20ef9-8bf7-4a1c-a6e0-9c56720d20d0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "layer.filter_categories = [0]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "65436a4a-c498-4f40-ba79-1082062376bf",
+ "metadata": {},
+ "source": [
+ "To ensure that this demo is snappy on most computers, we'll filter to a bounding box over Europe.\n",
+ "\n",
+ "If you're on a recent computer, feel free to comment out the next line."
]
},
{
@@ -564,7 +1238,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.4"
+ "version": "3.11.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
diff --git a/lonboard/_layer.py b/lonboard/_layer.py
index f32a7f20..4f970b5c 100644
--- a/lonboard/_layer.py
+++ b/lonboard/_layer.py
@@ -22,8 +22,9 @@
)
import ipywidgets
+import numpy as np
import traitlets
-from arro3.core import Table
+from arro3.core import Array, ChunkedArray, Table
from arro3.core.types import ArrowStreamExportable
from lonboard._base import BaseExtension, BaseWidget
@@ -38,6 +39,7 @@
from lonboard._serialization import infer_rows_per_chunk
from lonboard._utils import auto_downcast as _auto_downcast
from lonboard._utils import get_geometry_column_index, remove_extension_kwargs
+from lonboard.layer_extension import DataFilterExtension
from lonboard.traits import (
ArrowTableTrait,
ColorAccessor,
@@ -48,6 +50,7 @@
if TYPE_CHECKING:
import geopandas as gpd
+ import quak
from lonboard.types.layer import (
BaseLayerKwargs,
@@ -102,6 +105,9 @@ def __init__(self, *, extensions: Sequence[BaseExtension] = (), **kwargs):
self.set_trait(prop_name, prop_value)
added_names.append(prop_name)
+ print("added_names")
+ print(added_names)
+
self.send_state(added_names)
# TODO: validate that only one extension per type is included. E.g. you can't have
@@ -129,6 +135,8 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]):
# the `Widget` implementation, `send_state` will fail, even if the user
# passes in a value, because `send_state` is called before we call
# `super().__init__()`
+ print("calling add traits")
+ print(extension._layer_traits)
traitlets.HasTraits.add_traits(self, **extension._layer_traits)
# Note: This is part of `Widget.add_traits` (in the direct superclass) that
@@ -137,55 +145,52 @@ 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.
+ 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.
+ Any properties for the added extension should also be passed as keyword
+ arguments to this function.
- # Examples:
+ Examples:
- # ```py
- # from lonboard import ScatterplotLayer
- # from lonboard.layer_extension import DataFilterExtension
+ ```py
+ from lonboard import ScatterplotLayer
+ from lonboard.layer_extension import DataFilterExtension
- # gdf = geopandas.GeoDataFrame(...)
- # layer = ScatterplotLayer.from_geopandas(gdf)
+ gdf = geopandas.GeoDataFrame(...)
+ layer = ScatterplotLayer.from_geopandas(gdf)
- # extension = DataFilterExtension(filter_size=1)
- # filter_values = gdf["filter_column"]
+ extension = DataFilterExtension(filter_size=1)
+ filter_values = gdf["filter_column"]
- # layer.add_extension(
- # extension,
- # get_filter_value=filter_values,
- # filter_range=[0, 1]
- # )
- # ```
+ layer.add_extension(
+ extension,
+ get_filter_value=filter_values,
+ filter_range=[0, 1]
+ )
+ ```
- # Args:
- # extension: The new extension to add.
+ 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")
+ 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,)
+ 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)
+ # 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"])
+ self.send_state(added_names + ["extensions"])
pickable = traitlets.Bool(True).tag(sync=True)
"""
@@ -431,6 +436,58 @@ def from_duckdb(
return cls(table=table, **kwargs)
+ def quak(self) -> quak.Widget:
+ import quak
+ import sqlglot
+
+ if not any(isinstance(ext, DataFilterExtension) for ext in self.extensions):
+ self.add_extension(DataFilterExtension(category_size=1))
+
+ table: Table = self.table
+ num_rows = table.num_rows
+ if num_rows <= np.iinfo(np.uint8).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint8))
+ filter_arr = np.ones(num_rows, dtype=np.float32)
+ elif num_rows <= np.iinfo(np.uint16).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint16))
+ filter_arr = np.ones(num_rows, dtype=np.float32)
+ elif num_rows <= np.iinfo(np.uint32).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint32))
+ filter_arr = np.ones(num_rows, dtype=np.float32)
+ else:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint64))
+ filter_arr = np.ones(num_rows, dtype=np.float32)
+
+ table_with_row_index = table.append_column(
+ "_row_index", ChunkedArray(row_index)
+ )
+ quak_widget = quak.Widget(table_with_row_index)
+
+ def row_index_callback(change):
+ global test
+ test = change
+
+ sql = sqlglot.parse_one(quak_widget.sql, dialect="duckdb")
+ sql.set("expressions", [sqlglot.column("_row_index")])
+ row_index_table = quak_widget._conn.query(sql.sql(dialect="duckdb")).arrow()
+
+ # Reset all to 2. We don't use zero because there might be a bug with 0
+ filter_arr[:] = 2
+
+ # Set the desired _row_index to 1
+ filter_arr[row_index_table["_row_index"]] = 1
+
+ self.get_filter_category = filter_arr # type: ignore
+ self.filter_categories = [1] # type: ignore
+
+ quak_widget.observe(row_index_callback, names="sql")
+
+ return quak_widget
+ pass
+
+
+test = None
+
class BitmapLayer(BaseLayer):
"""
diff --git a/poetry.lock b/poetry.lock
index faffffbb..910e3070 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -3868,6 +3868,22 @@ files = [
[package.dependencies]
cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+[[package]]
+name = "quak"
+version = "0.1.8"
+description = ""
+optional = false
+python-versions = "*"
+files = [
+ {file = "quak-0.1.8-py2.py3-none-any.whl", hash = "sha256:2063c551797a7ff2d07a41f20b5ca60ec7447f6acd4ed28a8751782669bac7f7"},
+ {file = "quak-0.1.8.tar.gz", hash = "sha256:0b285c1405f123f0f68f804b408c3fc8d2335878442f5ec93c43f2f92a2c3ee5"},
+]
+
+[package.dependencies]
+anywidget = "*"
+duckdb = ">=1.0.0"
+pyarrow = ">=16.0.0"
+
[[package]]
name = "referencing"
version = "0.35.1"
@@ -4256,6 +4272,21 @@ files = [
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
]
+[[package]]
+name = "sqlglot"
+version = "25.18.0"
+description = "An easily customizable SQL parser and transpiler"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sqlglot-25.18.0-py3-none-any.whl", hash = "sha256:d315874d88a81c48604cf4a9cd8a56e8bd3009aa294c233755ac176a9aceb153"},
+ {file = "sqlglot-25.18.0.tar.gz", hash = "sha256:f79b02b378038e991633911de43eb6e44eba99510fa7144bba7e7532e3d4e00d"},
+]
+
+[package.extras]
+dev = ["duckdb (>=0.6)", "maturin (>=1.4,<2.0)", "mypy", "pandas", "pandas-stubs", "pdoc", "pre-commit", "python-dateutil", "pytz", "ruff (==0.4.3)", "types-python-dateutil", "types-pytz", "typing-extensions"]
+rs = ["sqlglotrs (==0.2.9)"]
+
[[package]]
name = "stack-data"
version = "0.6.3"
@@ -4696,4 +4727,4 @@ geopandas = ["geopandas", "pandas", "shapely"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "b5899cf19b1b2d93344d31411701f1b0992d0b7a4c4c204c8869049a5610668c"
+content-hash = "1173c60daf7a8e9e2e6f38ee42ce74ff0c0d09cf99323ccdf2fa1e713360d8a2"
diff --git a/pyproject.toml b/pyproject.toml
index 2e9e8f9e..63a50ee9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,6 +65,8 @@ pre-commit = ">=3.4.0"
pyarrow = ">=14.0.1"
pyogrio = ">=0.8"
pytest = ">=7.4.2"
+quak = ">=0.1.8"
+sqlglot = ">=25.16.0"
[tool.poetry.group.docs.dependencies]
# We use ruff format ourselves, but mkdocstrings requires black to be installed
diff --git a/tmp4.py b/tmp4.py
new file mode 100644
index 00000000..c9ee6b62
--- /dev/null
+++ b/tmp4.py
@@ -0,0 +1,58 @@
+import geodatasets
+import geopandas as gpd
+import numpy as np
+import quak
+import sqlglot
+from arro3.core import Array, ChunkedArray, Table
+
+from lonboard import PolygonLayer, viz
+
+gdf = gpd.read_file(geodatasets.get_path("nybb"))
+layer = PolygonLayer.from_geopandas(gdf)
+self = layer
+self.extensions
+
+
+def quak_fn(self: PolygonLayer) -> quak.Widget:
+ table: Table = self.table
+ num_rows = table.num_rows
+ if num_rows <= np.iinfo(np.uint8).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint8))
+ filter_arr = np.ones(num_rows, dtype=np.uint8)
+ elif num_rows <= np.iinfo(np.uint16).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint16))
+ filter_arr = np.ones(num_rows, dtype=np.uint16)
+ elif num_rows <= np.iinfo(np.uint32).max:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint32))
+ filter_arr = np.ones(num_rows, dtype=np.uint32)
+ else:
+ row_index = Array.from_numpy(np.arange(num_rows, dtype=np.uint64))
+ filter_arr = np.ones(num_rows, dtype=np.uint64)
+
+ table_with_row_index = table.append_column("_row_index", ChunkedArray(row_index))
+ quak_widget = quak.Widget(table_with_row_index)
+
+ test = None
+
+ def row_index_callback(change):
+ global test
+ test = change
+
+ sql = sqlglot.parse_one(quak_widget.sql, dialect="duckdb")
+ sql.set("expressions", [sqlglot.column("_row_index")])
+ row_index_table = quak_widget._conn.query(sql.sql(dialect="duckdb")).arrow()
+
+ # Reset all to zero
+ filter_arr[:] = 0
+
+ # Set the desired _row_index to 1
+ filter_arr[row_index_table["_row_index"]] = 1
+
+ quak_widget.observe(row_index_callback, names="sql")
+
+ test
+
+
+m = viz(gdf)
+m
+table = m.layers[0].table