Skip to content

Commit 432406c

Browse files
authored
refactor: Improve H3 layer code reuse (#1006)
### Change list - Python `H3HexagonLayer` subclasses from `PolygonLayer`, so we don't have to redefine all the polygon properties - JS `H3HexagonModel` subclasses from core `BasePolygonModel`, so we don't hve to redefine props - Split JS models into separate files. - Remove H3 index validation because the validation failed for kontur data from https://data.humdata.org/dataset/kontur-population-dataset-22km - Improve H3 layer docs Closes #1002
1 parent 38088b2 commit 432406c

File tree

20 files changed

+1696
-1601
lines changed

20 files changed

+1696
-1601
lines changed

lonboard/layer/_h3.py

Lines changed: 43 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from lonboard._geoarrow.ops import Bbox, WeightedCentroid
1010
from lonboard._utils import auto_downcast as _auto_downcast
11-
from lonboard.layer._base import BaseArrowLayer
12-
from lonboard.traits import ArrowTableTrait, ColorAccessor, FloatAccessor, H3Accessor
11+
from lonboard.layer._polygon import PolygonLayer
12+
from lonboard.traits import ArrowTableTrait, H3Accessor
1313

1414
if TYPE_CHECKING:
1515
import sys
@@ -82,37 +82,38 @@ def default_h3_viewport(ca: ChunkedArray) -> tuple[Bbox, WeightedCentroid] | Non
8282
)
8383

8484

85-
class H3HexagonLayer(BaseArrowLayer):
86-
"""The `H3HexagonLayer` renders H3 hexagons.
85+
class H3HexagonLayer(PolygonLayer):
86+
"""The `H3HexagonLayer` renders hexagons from the [H3](https://h3geo.org/) geospatial indexing system.
8787
8888
**Example:**
8989
90-
From GeoPandas:
90+
From Pandas:
9191
9292
```py
93-
import geopandas as gpd
93+
import pandas as pd
9494
from lonboard import Map, H3HexagonLayer
9595
96-
# A GeoDataFrame with Polygon or MultiPolygon geometries
97-
gdf = gpd.GeoDataFrame()
98-
layer = H3HexagonLayer.from_geopandas(
99-
gdf,
100-
get_fill_color=[255, 0, 0],
96+
# A DataFrame with H3 cell identifiers
97+
df = pd.DataFrame({
98+
"h3_index": ["8928308280fffff", "8928308280bffff", ...],
99+
"other_attributes": [...],
100+
})
101+
layer = H3HexagonLayer.from_pandas(
102+
df,
103+
get_hexagon=df["h3_index"],
101104
)
102105
m = Map(layer)
103106
```
104107
105-
From an Arrow-compatible source like [pyogrio][pyogrio] or [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest):
108+
Or, you can pass in an Arrow table directly
106109
107110
```py
108-
from geoarrow.rust.io import read_flatgeobuf
109111
from lonboard import Map, H3HexagonLayer
110112
111-
# Example: A FlatGeobuf file with Polygon or MultiPolygon geometries
112-
table = read_flatgeobuf("path/to/file.fgb")
113+
# Example: An Arrow table with H3 identifiers as a column
113114
layer = H3HexagonLayer(
114115
table,
115-
get_fill_color=[255, 0, 0],
116+
get_hexagon=table["h3_index"],
116117
)
117118
m = Map(layer)
118119
```
@@ -162,11 +163,11 @@ def from_pandas(
162163
"""Create a new H3HexagonLayer from a pandas DataFrame.
163164
164165
Args:
165-
df: _description_
166+
df: a Pandas DataFrame with properties to associate with H3 hexagons.
166167
167168
Keyword Args:
168-
get_hexagon: _description_
169-
auto_downcast: _description_. Defaults to True.
169+
get_hexagon: H3 cell identifier of each H3 hexagon.
170+
auto_downcast: Whether to save memory on input by casting to smaller types. Defaults to True.
170171
kwargs: Extra args passed down as H3HexagonLayer attributes.
171172
172173
Raises:
@@ -194,173 +195,46 @@ def from_pandas(
194195
_layer_type = t.Unicode("h3-hexagon").tag(sync=True)
195196

196197
table = ArrowTableTrait(geometry_required=False)
198+
"""An Arrow table with properties to associate with the H3 hexagons.
197199
198-
get_hexagon = H3Accessor()
199-
"""
200-
todo
201-
"""
202-
203-
high_precision = t.Bool(None, allow_none=True).tag(sync=True)
204-
205-
stroked = t.Bool(None, allow_none=True).tag(sync=True)
206-
"""Whether to draw an outline around the polygon (solid fill).
207-
208-
Note that both the outer polygon as well the outlines of any holes will be drawn.
209-
210-
- Type: `bool`, optional
211-
- Default: `True`
212-
"""
213-
214-
filled = t.Bool(None, allow_none=True).tag(sync=True)
215-
"""Whether to draw a filled polygon (solid fill).
216-
217-
Note that only the area between the outer polygon and any holes will be filled.
218-
219-
- Type: `bool`, optional
220-
- Default: `True`
200+
If you have a Pandas `DataFrame`, use
201+
[`from_pandas`][lonboard.H3HexagonLayer.from_pandas] instead.
221202
"""
222203

223-
extruded = t.Bool(None, allow_none=True).tag(sync=True)
224-
"""Whether to extrude the polygons.
225-
226-
Based on the elevations provided by the `getElevation` accessor.
227-
228-
If set to `false`, all polygons will be flat, this generates less geometry and is
229-
faster than simply returning 0 from getElevation.
230-
231-
- Type: `bool`, optional
232-
- Default: `False`
233-
"""
234-
235-
wireframe = t.Bool(None, allow_none=True).tag(sync=True)
236-
"""
237-
Whether to generate a line wireframe of the polygon. The outline will have
238-
"horizontal" lines closing the top and bottom polygons and a vertical line
239-
(a "strut") for each vertex on the polygon.
240-
241-
- Type: `bool`, optional
242-
- Default: `False`
243-
244-
**Remarks:**
245-
246-
- These lines are rendered with `GL.LINE` and will thus always be 1 pixel wide.
247-
- Wireframe and solid extrusions are exclusive, you'll need to create two layers
248-
with the same data if you want a combined rendering effect.
249-
"""
250-
251-
elevation_scale = t.Float(None, allow_none=True, min=0).tag(sync=True)
252-
"""Elevation multiplier.
204+
get_hexagon = H3Accessor()
205+
"""The cell identifier of each H3 hexagon.
253206
254-
The final elevation is calculated by `elevationScale * getElevation(d)`.
255-
`elevationScale` is a handy property to scale all elevation without updating the
256-
data.
207+
Accepts either an array of strings or uint64 integers representing H3 cell IDs.
257208
258-
- Type: `float`, optional
259-
- Default: `1`
209+
- Type: [H3Accessor][lonboard.traits.H3Accessor]
260210
"""
261211

262-
line_width_units = t.Unicode(None, allow_none=True).tag(sync=True)
263-
"""
264-
The units of the outline width, one of `'meters'`, `'common'`, and `'pixels'`. See
265-
[unit
266-
system](https://deck.gl/docs/developer-guide/coordinate-systems#supported-units).
267-
268-
- Type: `str`, optional
269-
- Default: `'meters'`
270-
"""
212+
high_precision = t.Bool(None, allow_none=True).tag(sync=True)
213+
"""Whether to render H3 hexagons in high-precision mode.
271214
272-
line_width_scale = t.Float(None, allow_none=True, min=0).tag(sync=True)
273-
"""
274-
The outline width multiplier that multiplied to all outlines of `Polygon` and
275-
`MultiPolygon` features if the `stroked` attribute is true.
215+
Each hexagon in the H3 indexing system is [slightly different in shape](https://h3geo.org/docs/core-library/coordsystems). To draw a large number of hexagons efficiently, the `H3HexagonLayer` may choose to use instanced drawing by assuming that all hexagons within the current viewport have the same shape as the one at the center of the current viewport. The discrepancy is usually too small to be visible.
276216
277-
- Type: `float`, optional
278-
- Default: `1`
279-
"""
217+
There are several cases in which high-precision mode is required. In these cases, `H3HexagonLayer` may choose to switch to high-precision mode, where it trades performance for accuracy:
280218
281-
line_width_min_pixels = t.Float(None, allow_none=True, min=0).tag(sync=True)
282-
"""
283-
The minimum outline width in pixels. This can be used to prevent the outline from
284-
getting too small when zoomed out.
219+
* The input set contains a pentagon. There are 12 pentagons world wide at each resolution, and these cells and their immediate neighbors have significant differences in shape.
220+
* The input set is at a coarse resolution (res `0` through res `5`). These cells have larger differences in shape, particularly when using a Mercator projection.
221+
* The input set contains hexagons with different resolutions.
285222
286-
- Type: `float`, optional
287-
- Default: `0`
288-
"""
223+
Possible values:
289224
290-
line_width_max_pixels = t.Float(None, allow_none=True, min=0).tag(sync=True)
291-
"""
292-
The maximum outline width in pixels. This can be used to prevent the outline from
293-
getting too big when zoomed in.
225+
* `None`: The layer chooses the mode automatically. High-precision rendering is only used if an edge case is encountered in the data.
226+
* `True`: Always use high-precision rendering.
227+
* `False`: Always use instanced rendering, regardless of the characteristics of the data.
294228
295-
- Type: `float`, optional
229+
- Type: `bool | None`, optional
296230
- Default: `None`
297231
"""
298232

299-
line_joint_rounded = t.Bool(None, allow_none=True).tag(sync=True)
300-
"""Type of joint. If `true`, draw round joints. Otherwise draw miter joints.
301-
302-
- Type: `bool`, optional
303-
- Default: `False`
304-
"""
233+
coverage = t.Float(None, allow_none=True, min=0, max=1).tag(sync=True)
234+
"""Hexagon radius multiplier, between 0 - 1.
305235
306-
line_miter_limit = t.Float(None, allow_none=True, min=0).tag(sync=True)
307-
"""The maximum extent of a joint in ratio to the stroke width.
308-
309-
Only works if `line_joint_rounded` is false.
236+
When coverage = 1, hexagon is rendered with actual size, by specifying a different value (between 0 and 1) hexagon can be scaled down.
310237
311238
- Type: `float`, optional
312-
- Default: `4`
313-
"""
314-
315-
get_fill_color = ColorAccessor(None, allow_none=True)
316-
"""
317-
The fill color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a
318-
number between 0-255 and `a` is 255 if not supplied.
319-
320-
- Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional
321-
- If a single `list` or `tuple` is provided, it is used as the fill color for
322-
all polygons.
323-
- If a numpy or pyarrow array is provided, each value in the array will be used
324-
as the fill color for the polygon at the same row index.
325-
- Default: `[0, 0, 0, 255]`.
326-
"""
327-
328-
get_line_color = ColorAccessor(None, allow_none=True)
329-
"""
330-
The outline color of each polygon in the format of `[r, g, b, [a]]`. Each channel is
331-
a number between 0-255 and `a` is 255 if not supplied.
332-
333-
Only applies if `stroked=True`.
334-
335-
- Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional
336-
- If a single `list` or `tuple` is provided, it is used as the outline color for
337-
all polygons.
338-
- If a numpy or pyarrow array is provided, each value in the array will be used
339-
as the outline color for the polygon at the same row index.
340-
- Default: `[0, 0, 0, 255]`.
341-
"""
342-
343-
get_line_width = FloatAccessor(None, allow_none=True)
344-
"""
345-
The width of the outline of each polygon, in units specified by `line_width_units`
346-
(default `'meters'`).
347-
348-
- Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
349-
- If a number is provided, it is used as the outline width for all polygons.
350-
- If an array is provided, each value in the array will be used as the outline
351-
width for the polygon at the same row index.
352-
- Default: `1`.
353-
"""
354-
355-
get_elevation = FloatAccessor(None, allow_none=True)
356-
"""
357-
The elevation to extrude each polygon with, in meters.
358-
359-
Only applies if `extruded=True`.
360-
361-
- Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
362-
- If a number is provided, it is used as the width for all polygons.
363-
- If an array is provided, each value in the array will be used as the width for
364-
the polygon at the same row index.
365-
- Default: `1000`.
239+
- Default: `1`
366240
"""

lonboard/traits/_h3.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy as np
88
from arro3.core import Array, ChunkedArray, DataType
99

10-
from lonboard._h3 import str_to_h3, validate_h3_indices
10+
from lonboard._h3 import str_to_h3
1111
from lonboard._serialization import ACCESSOR_SERIALIZATION
1212
from lonboard.traits._base import FixedErrorTraitType
1313

@@ -125,13 +125,8 @@ def validate(self, obj: BaseArrowLayer, value: Any) -> ChunkedArray:
125125
info="H3 Arrow array must be uint64 type.",
126126
)
127127

128-
try:
129-
validate_h3_indices(value.to_numpy())
130-
except ValueError as e:
131-
self.error(
132-
obj,
133-
value,
134-
info=f"H3 index validation error: {e}",
135-
)
128+
# Ideally we would validate the H3 indices here, but I hit spurious validation
129+
# errors with the kontur 22km dataset
130+
# https://data.humdata.org/dataset/kontur-population-dataset-22km
136131

137132
return value.rechunk(max_chunksize=obj._rows_per_chunk)

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@deck.gl/mapbox": "^9.2.2",
99
"@deck.gl/mesh-layers": "^9.2.2",
1010
"@deck.gl/react": "^9.2.2",
11-
"@geoarrow/deck.gl-layers": "^0.4.0-beta.4",
11+
"@geoarrow/deck.gl-layers": "^0.4.0-beta.5",
1212
"@geoarrow/geoarrow-js": "^0.3.2",
1313
"@nextui-org/react": "^2.4.8",
1414
"@xstate/react": "^6.0.0",

src/model/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
export { BaseModel } from "./base.js";
2-
export { BaseLayerModel } from "./base-layer.js";
3-
export {
4-
PathModel,
5-
ScatterplotModel,
6-
SolidPolygonModel,
7-
initializeLayer,
8-
} from "./layer.js";
2+
export { BaseLayerModel } from "./layer/base.js";
3+
export { initializeLayer } from "./layer/index.js";
94
export {
105
BaseExtensionModel,
116
BrushingExtension,

0 commit comments

Comments
 (0)