Skip to content

Commit b76d90c

Browse files
authored
feat: Support passing ChunkedArray/ArrayReader into viz (#824)
### Change list Currently, passing a `ChunkedArray`/`ArrayReader` into `viz` fails because any `ArrowStreamExportable` it tries to import as a table. This adds a helper for checking the field of an input stream. If the field is struct but **not** a GeoArrow extension type, then it assumes the input is a `Table`. We need to check for the extension type because that ensures that we can render a struct-encoded point column or a `geoarrow.box` column. Also: - Update `test_viz.py` for latest geoarrow-rust-core version 0.4. - Rename `geoarrow` test folder so that when starting a Python kernel in the above directory it doesn't search in the test files for the `geoarrow` python library. - Update some other dev deps #823 will be a follow up
1 parent b7b6482 commit b76d90c

File tree

8 files changed

+163
-51
lines changed

8 files changed

+163
-51
lines changed

lonboard/_geoarrow/c_stream_import.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Import Arrow data from a C stream into a Table."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from arro3.core import ArrayReader, ChunkedArray, DataType, Table
8+
9+
if TYPE_CHECKING:
10+
from arro3.core.types import ArrowStreamExportable
11+
12+
13+
def import_arrow_c_stream(stream: ArrowStreamExportable) -> Table | ChunkedArray:
14+
"""Import an Arrow stream into a Table.
15+
16+
If the stream is a struct but **not** a geoarrow extension type, then we assume it's
17+
table input. Otherwise, we assume it's an array input, which we then coerce into a
18+
Table with a single field named "geometry".
19+
"""
20+
array_reader = ArrayReader.from_arrow(stream)
21+
22+
if DataType.is_struct(
23+
array_reader.field.type,
24+
) and not array_reader.field.metadata_str.get(
25+
"ARROW:extension:name",
26+
"",
27+
).startswith("geoarrow"):
28+
# If the field is a struct but **not** a geoarrow extension type, then we assume
29+
# it's table input.
30+
return Table.from_arrow(array_reader)
31+
32+
return ChunkedArray.from_arrow(array_reader)

lonboard/_viz.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from lonboard._compat import check_pandas_version
2222
from lonboard._constants import EXTENSION_NAME
23+
from lonboard._geoarrow.c_stream_import import import_arrow_c_stream
2324
from lonboard._geoarrow.extension_types import construct_geometry_array
2425
from lonboard._geoarrow.geopandas_interop import geopandas_to_geoarrow
2526
from lonboard._geoarrow.parse_wkb import parse_serialized_table
@@ -275,12 +276,19 @@ def create_layers_from_data_input(
275276
# Anything with __arrow_c_array__
276277
if hasattr(data, "__arrow_c_array__"):
277278
data = cast("ArrowArrayExportable", data)
278-
return _viz_geoarrow_array(data, **kwargs)
279+
array = Array.from_arrow(data)
280+
ca = ChunkedArray([array])
281+
return _viz_geoarrow_chunked_array(ca, **kwargs)
279282

280283
# Anything with __arrow_c_stream__
281284
if hasattr(data, "__arrow_c_stream__"):
282285
data = cast("ArrowStreamExportable", data)
283-
return _viz_geoarrow_table(Table.from_arrow(data), **kwargs)
286+
imported_stream = import_arrow_c_stream(data)
287+
if isinstance(imported_stream, Table):
288+
return _viz_geoarrow_table(imported_stream, **kwargs)
289+
290+
assert isinstance(imported_stream, ChunkedArray)
291+
return _viz_geoarrow_chunked_array(imported_stream, **kwargs)
284292

285293
# Anything with __geo_interface__
286294
if hasattr(data, "__geo_interface__"):
@@ -432,16 +440,15 @@ def _viz_geo_interface(
432440
raise ValueError(f"type '{geo_interface_type}' not supported.")
433441

434442

435-
def _viz_geoarrow_array(
436-
data: ArrowArrayExportable,
443+
def _viz_geoarrow_chunked_array(
444+
ca: ChunkedArray,
437445
**kwargs: Any,
438446
) -> list[ScatterplotLayer | PathLayer | PolygonLayer]:
439-
array = Array.from_arrow(data)
440-
field = array.field.with_name("geometry")
447+
field = ca.field.with_name("geometry")
441448
schema = Schema([field])
442-
table = Table.from_arrays([array], schema=schema)
449+
table = Table.from_arrays([ca], schema=schema)
443450

444-
num_rows = len(array)
451+
num_rows = len(ca)
445452
if num_rows <= np.iinfo(np.uint8).max:
446453
arange_col = Array(np.arange(num_rows, dtype=np.uint8))
447454
elif num_rows <= np.iinfo(np.uint16).max:
@@ -575,4 +582,4 @@ def _viz_geoarrow_table(
575582

576583
return [PolygonLayer(table=table, **polygon_kwargs)]
577584

578-
raise ValueError(f"Unsupported extension type: '{geometry_ext_type}'.")
585+
raise ValueError(f"Unsupported extension type: '{geometry_ext_type!r}'.")

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,21 @@ docs = [
7272
"black>=24.8.0",
7373
]
7474
dev = [
75-
"duckdb>=1.2.0",
75+
"duckdb>=1.3.0",
7676
"fiona<1.10",
7777
"geoarrow-pyarrow>=0.2.0",
78-
"geoarrow-rust-core>=0.3.0",
78+
"geoarrow-rust-core>=0.4.0",
7979
"geodatasets>=2024.8.0",
8080
"jupyterlab>=4.3.3",
8181
"matplotlib>=3.7.5",
8282
"movingpandas>=0.20.0",
8383
"palettable>=3.3.3",
84+
"pandas-stubs>=2.2.2.240807",
8485
"pre-commit>=3.5.0",
8586
"pyarrow>=17.0.0",
8687
"pyogrio>=0.9.0",
8788
"pytest>=8.3.4",
88-
"ruff>=0.11.0",
89+
"ruff>=0.12.0",
8990
"sidecar>=0.7.0",
9091
"types-shapely>=2.1.0.20250512",
9192
]
File renamed without changes.
File renamed without changes.

tests/test_viz.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4-
from typing import TYPE_CHECKING, cast
54

65
import geoarrow.pyarrow as gap
76
import geodatasets
87
import numpy as np
98
import pyarrow as pa
109
import pyarrow.parquet as pq
1110
import pytest
12-
from geoarrow.rust.core import geometry_col, read_pyogrio, to_wkb
13-
from pyogrio.raw import read_arrow
11+
from geoarrow.rust.core import to_wkb
12+
from pyogrio import read_arrow
1413

1514
from lonboard import PathLayer, PolygonLayer, ScatterplotLayer, viz
1615
from lonboard._constants import EXTENSION_NAME
@@ -19,9 +18,6 @@
1918

2019
fixtures_dir = Path(__file__).parent / "fixtures"
2120

22-
if TYPE_CHECKING:
23-
from arro3.core import Table
24-
2521

2622
def mixed_shapely_geoms():
2723
shapely = pytest.importorskip("shapely")
@@ -156,31 +152,31 @@ def test_viz_shapely_mixed_array():
156152
assert isinstance(map_.layers[2], PolygonLayer)
157153

158154

159-
# read_pyogrio currently keeps geometries as WKB
160155
@pytest.mark.skipif(not compat.HAS_SHAPELY, reason="shapely not available")
161-
def test_viz_geoarrow_rust_table():
162-
table = read_pyogrio(geodatasets.get_path("naturalearth.land"))
156+
def test_viz_pyogrio_table():
157+
_meta, table = read_arrow(geodatasets.get_path("naturalearth.land"))
163158
map_ = viz(table)
164159
assert isinstance(map_.layers[0], PolygonLayer)
165160

166161

167-
# read_pyogrio currently keeps geometries as WKB
168162
@pytest.mark.skipif(not compat.HAS_SHAPELY, reason="shapely not available")
169-
def test_viz_geoarrow_rust_array():
170-
# `read_pyogrio` has incorrect typing currently
171-
# https://github.com/geoarrow/geoarrow-rs/pull/807
172-
table = cast("Table", read_pyogrio(geodatasets.get_path("naturalearth.land")))
173-
map_ = viz(geometry_col(table).chunk(0))
163+
def test_viz_pyogrio_chunked_array():
164+
_meta, table = read_arrow(geodatasets.get_path("naturalearth.land"))
165+
map_ = viz(table["wkb_geometry"])
166+
assert isinstance(map_.layers[0], PolygonLayer)
167+
168+
169+
@pytest.mark.skipif(not compat.HAS_SHAPELY, reason="shapely not available")
170+
def test_viz_pyogrio_array():
171+
_meta, table = read_arrow(geodatasets.get_path("naturalearth.land"))
172+
map_ = viz(table["wkb_geometry"].chunk(0))
174173
assert isinstance(map_.layers[0], PolygonLayer)
175174

176175

177-
# read_pyogrio currently keeps geometries as WKB
178-
@pytest.mark.skip("to_wkb gives panic on todo!(), not yet implemented")
179176
@pytest.mark.skipif(not compat.HAS_SHAPELY, reason="shapely not available")
180-
def test_viz_geoarrow_rust_wkb_array():
181-
table = read_pyogrio(geodatasets.get_path("naturalearth.land"))
182-
arr = geometry_col(table).chunk(0)
183-
wkb_arr = to_wkb(arr)
177+
def test_viz_array_reader():
178+
_meta, table = read_arrow(geodatasets.get_path("naturalearth.land"))
179+
wkb_arr = to_wkb(table["wkb_geometry"])
184180
map_ = viz(wkb_arr)
185181
assert isinstance(map_.layers[0], PolygonLayer)
186182

0 commit comments

Comments
 (0)