Skip to content

Commit 2364d1b

Browse files
authored
Remove geopandas dependency in viz for shapely input (#607)
1 parent 916f8d2 commit 2364d1b

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

lonboard/_utils.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, TypeVar
3+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, TypeVar
44

55
import numpy as np
66
from arro3.core import Schema
@@ -12,6 +12,7 @@
1212
if TYPE_CHECKING:
1313
import geopandas as gpd
1414
import pandas as pd
15+
from numpy.typing import NDArray
1516

1617
DF = TypeVar("DF", bound=pd.DataFrame)
1718

@@ -120,10 +121,61 @@ def remove_extension_kwargs(
120121

121122
def split_mixed_gdf(gdf: gpd.GeoDataFrame) -> List[gpd.GeoDataFrame]:
122123
"""Split a GeoDataFrame into one or more GeoDataFrames with unique geometry type"""
124+
indices = indices_by_geometry_type(gdf.geometry)
125+
if indices is None:
126+
return [gdf]
127+
128+
point_indices, linestring_indices, polygon_indices = indices
129+
130+
# Here we intentionally check geometries in a specific order.
131+
# Starting from polygons, then linestrings, then points,
132+
# so that the order of generated layers is polygon, then path then scatterplot.
133+
# This ensures that points are rendered on top and polygons on the bottom.
134+
gdfs = []
135+
for single_type_geometry_indices in (
136+
polygon_indices,
137+
linestring_indices,
138+
point_indices,
139+
):
140+
if len(single_type_geometry_indices) > 0:
141+
gdfs.append(gdf.iloc[single_type_geometry_indices])
142+
143+
return gdfs
144+
145+
146+
def split_mixed_shapely_array(
147+
geometry: NDArray[np.object_],
148+
) -> List[NDArray[np.object_]]:
149+
"""Split a shapely array into one or more arrays with unique geometry type"""
150+
indices = indices_by_geometry_type(geometry)
151+
if indices is None:
152+
return [geometry]
153+
154+
point_indices, linestring_indices, polygon_indices = indices
155+
156+
# Here we intentionally check geometries in a specific order.
157+
# Starting from polygons, then linestrings, then points,
158+
# so that the order of generated layers is polygon, then path then scatterplot.
159+
# This ensures that points are rendered on top and polygons on the bottom.
160+
arrays = []
161+
for single_type_geometry_indices in (
162+
polygon_indices,
163+
linestring_indices,
164+
point_indices,
165+
):
166+
if len(single_type_geometry_indices) > 0:
167+
arrays.append(geometry[single_type_geometry_indices])
168+
169+
return arrays
170+
171+
172+
def indices_by_geometry_type(
173+
geometry: NDArray[np.object_],
174+
) -> Tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]] | None:
123175
import shapely
124176
from shapely import GeometryType
125177

126-
type_ids = np.array(shapely.get_type_id(gdf.geometry))
178+
type_ids = np.array(shapely.get_type_id(geometry))
127179
unique_type_ids = set(np.unique(type_ids))
128180

129181
if GeometryType.GEOMETRYCOLLECTION in unique_type_ids:
@@ -133,17 +185,17 @@ def split_mixed_gdf(gdf: gpd.GeoDataFrame) -> List[gpd.GeoDataFrame]:
133185
raise ValueError("LinearRings not currently supported")
134186

135187
if len(unique_type_ids) == 1:
136-
return [gdf]
188+
return None
137189

138190
if len(unique_type_ids) == 2:
139191
if unique_type_ids == {GeometryType.POINT, GeometryType.MULTIPOINT}:
140-
return [gdf]
192+
return None
141193

142194
if unique_type_ids == {GeometryType.LINESTRING, GeometryType.MULTILINESTRING}:
143-
return [gdf]
195+
return None
144196

145197
if unique_type_ids == {GeometryType.POLYGON, GeometryType.MULTIPOLYGON}:
146-
return [gdf]
198+
return None
147199

148200
point_indices = np.where(
149201
(type_ids == GeometryType.POINT) | (type_ids == GeometryType.MULTIPOINT)
@@ -158,17 +210,4 @@ def split_mixed_gdf(gdf: gpd.GeoDataFrame) -> List[gpd.GeoDataFrame]:
158210
(type_ids == GeometryType.POLYGON) | (type_ids == GeometryType.MULTIPOLYGON)
159211
)[0]
160212

161-
# Here we intentionally check geometries in a specific order.
162-
# Starting from polygons, then linestrings, then points,
163-
# so that the order of generated layers is polygon, then path then scatterplot.
164-
# This ensures that points are rendered on top and polygons on the bottom.
165-
gdfs = []
166-
for single_type_geometry_indices in (
167-
polygon_indices,
168-
linestring_indices,
169-
point_indices,
170-
):
171-
if len(single_type_geometry_indices) > 0:
172-
gdfs.append(gdf.iloc[single_type_geometry_indices])
173-
174-
return gdfs
213+
return point_indices, linestring_indices, polygon_indices

lonboard/_viz.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
from lonboard._geoarrow.parse_wkb import parse_serialized_table
2929
from lonboard._layer import PathLayer, PolygonLayer, ScatterplotLayer
3030
from lonboard._map import Map
31-
from lonboard._utils import get_geometry_column_index, split_mixed_gdf
31+
from lonboard._utils import (
32+
get_geometry_column_index,
33+
split_mixed_gdf,
34+
split_mixed_shapely_array,
35+
)
3236
from lonboard.basemap import CartoBasemap
3337

3438
if TYPE_CHECKING:
@@ -361,12 +365,15 @@ def _viz_shapely_scalar(
361365
def _viz_shapely_array(
362366
data: NDArray[np.object_], **kwargs
363367
) -> List[Union[ScatterplotLayer, PathLayer, PolygonLayer]]:
364-
# Note: for now we pass this through a GeoDataFrame to handle mixed-type geometry
365-
# arrays. Longer term we should do this without a GeoPandas dependency.
366-
import geopandas as gpd
368+
layers: List[Union[ScatterplotLayer, PathLayer, PolygonLayer]] = []
369+
for partial_geometry_array in split_mixed_shapely_array(data):
370+
field, geom_arr = construct_geometry_array(
371+
partial_geometry_array,
372+
)
373+
table = Table.from_arrays([geom_arr], schema=Schema([field]))
374+
layers.extend(_viz_geoarrow_table(table, **kwargs))
367375

368-
gdf = gpd.GeoDataFrame(geometry=data) # type: ignore
369-
return _viz_geopandas_geodataframe(gdf, **kwargs)
376+
return layers
370377

371378

372379
def _viz_geo_interface(

0 commit comments

Comments
 (0)