Skip to content

Commit

Permalink
Add HeatmapLayer (#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylebarron authored Nov 7, 2023
1 parent c5fe2f7 commit cf24721
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 6 deletions.
2 changes: 1 addition & 1 deletion lonboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from . import colormap, traits
from ._layer import PathLayer, ScatterplotLayer, SolidPolygonLayer
from ._layer import HeatmapLayer, PathLayer, ScatterplotLayer, SolidPolygonLayer
from ._map import Map
from ._version import __version__
from ._viz import viz
116 changes: 116 additions & 0 deletions lonboard/_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,119 @@ def _validate_accessor_length(self, proposal):
raise traitlets.TraitError("accessor must have same length as table")

return proposal["value"]


class HeatmapLayer(BaseLayer):
"""The `HeatmapLayer` visualizes the spatial distribution of data.
**Example:**
```py
import geopandas as gpd
from lonboard import Map, HeatmapLayer
# A GeoDataFrame with Point geometries
gdf = gpd.GeoDataFrame()
layer = HeatmapLayer.from_geopandas(gdf,)
map_ = Map(layers=[layer])
```
"""

_layer_type = traitlets.Unicode("heatmap").tag(sync=True)

# NOTE: we override the default for _rows_per_chunk because otherwise we render one
# heatmap per _chunk_ not for the entire dataset.
# TODO: on the JS side, rechunk the table into a single contiguous chunk.
@traitlets.default("_rows_per_chunk")
def _default_rows_per_chunk(self):
return len(self.table)

table = PyarrowTableTrait(allowed_geometry_types={EXTENSION_NAME.POINT})

radius_pixels = traitlets.Float(allow_none=True).tag(sync=True)
"""Radius of the circle in pixels, to which the weight of an object is distributed.
- Type: `float`, optional
- Default: `30`
"""

# TODO: stabilize ColormapTrait
# color_range?: Color[];
# """Specified as an array of colors [color1, color2, ...].

# - Default: `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6)
# """

intensity = traitlets.Float(allow_none=True).tag(sync=True)
"""
Value that is multiplied with the total weight at a pixel to obtain the final
weight.
- Type: `float`, optional
- Default: `1`
"""

threshold = traitlets.Float(allow_none=True, min=0, max=1).tag(sync=True)
"""Ratio of the fading weight to the max weight, between `0` and `1`.
For example, `0.1` affects all pixels with weight under 10% of the max.
Ignored when `color_domain` is specified.
- Type: `float`, optional
- Default: `0.05`
"""

color_domain = traitlets.List(
traitlets.Float(), default_value=None, allow_none=True, minlen=2, maxlen=2
).tag(sync=True)
# """
# Controls how weight values are mapped to the `color_range`, as an array of two
# numbers [`min_value`, `max_value`].

# - Type: `(float, float)`, optional
# - Default: `None`
# """

aggregation = traitlets.Unicode(allow_none=True).tag(sync=True)
"""Defines the type of aggregation operation
Valid values are 'SUM', 'MEAN'.
- Type: `str`, optional
- Default: `"SUM"`
"""

weights_texture_size = traitlets.Int(allow_none=True).tag(sync=True)
"""Specifies the size of weight texture.
- Type: `int`, optional
- Default: `2048`
"""

debounce_timeout = traitlets.Int(allow_none=True).tag(sync=True)
"""
Interval in milliseconds during which changes to the viewport don't trigger
aggregation.
- Type: `int`, optional
- Default: `500`
"""

get_weight = FloatAccessor()
"""The weight of each object.
- Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
- If a number is provided, it is used as the outline width for all objects.
- If an array is provided, each value in the array will be used as the outline
width for the object at the same row index.
- Default: `1`.
"""

@traitlets.validate("get_weight")
def _validate_accessor_length(self, proposal):
if isinstance(proposal["value"], (pa.ChunkedArray, pa.Array)):
if len(proposal["value"]) != len(self.table):
raise traitlets.TraitError("accessor must have same length as table")

return proposal["value"]
33 changes: 29 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"@deck.gl/core": "^8.9.32",
"@deck.gl/layers": "^8.9.32",
"@deck.gl/react": "^8.9.32",
"@geoarrow/deck.gl-layers": "^0.3.0-beta.1",
"@geoarrow/deck.gl-layers": "^0.3.0-beta.2",
"apache-arrow": "^13.0.0",
"maplibre-gl": "^3.5.2",
"parquet-wasm": "0.5.0",
Expand Down
6 changes: 6 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { IWidgetManager, WidgetModel } from "@jupyter-widgets/base";
import {
ArcModel,
BaseLayerModel,
HeatmapModel,
PathModel,
ScatterplotModel,
SolidPolygonModel,
Expand Down Expand Up @@ -85,6 +86,11 @@ function getChildModelState(
setStateCounter(new Date())
);
break;
case "heatmap":
newSubModelState[childLayerId] = new HeatmapModel(childModel, () =>
setStateCounter(new Date())
);
break;
default:
console.error(`no layer supported for ${layerType}`);
break;
Expand Down
57 changes: 57 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
GeoArrowSolidPolygonLayerProps,
GeoArrowArcLayer,
GeoArrowArcLayerProps,
GeoArrowHeatmapLayerProps,
GeoArrowHeatmapLayer,
} from "@geoarrow/deck.gl-layers";
import * as arrow from "apache-arrow";
import type { WidgetModel } from "@jupyter-widgets/base";
Expand Down Expand Up @@ -380,3 +382,58 @@ export class ArcModel extends BaseLayerModel {
});
}
}

export class HeatmapModel extends BaseLayerModel {
protected radiusPixels: GeoArrowHeatmapLayerProps["radiusPixels"] | null;
protected colorRange: GeoArrowHeatmapLayerProps["colorRange"] | null;
protected intensity: GeoArrowHeatmapLayerProps["intensity"] | null;
protected threshold: GeoArrowHeatmapLayerProps["threshold"] | null;
protected colorDomain: GeoArrowHeatmapLayerProps["colorDomain"] | null;
protected aggregation: GeoArrowHeatmapLayerProps["aggregation"] | null;
protected weightsTextureSize:
| GeoArrowHeatmapLayerProps["weightsTextureSize"]
| null;
protected debounceTimeout:
| GeoArrowHeatmapLayerProps["debounceTimeout"]
| null;
protected getPosition: GeoArrowHeatmapLayerProps["getPosition"] | null;
protected getWeight: GeoArrowHeatmapLayerProps["getWeight"] | null;

constructor(model: WidgetModel, updateStateCallback: () => void) {
super(model, updateStateCallback);

this.initRegularAttribute("radius_pixels", "radiusPixels");
this.initRegularAttribute("color_range", "colorRange");
this.initRegularAttribute("intensity", "intensity");
this.initRegularAttribute("threshold", "threshold");
this.initRegularAttribute("color_domain", "colorDomain");
this.initRegularAttribute("aggregation", "aggregation");
this.initRegularAttribute("weights_texture_size", "weightsTextureSize");
this.initRegularAttribute("debounce_timeout", "debounceTimeout");

this.initVectorizedAccessor("get_position", "getPosition");
this.initVectorizedAccessor("get_weight", "getWeight");
}

render(): GeoArrowHeatmapLayer {
return new GeoArrowHeatmapLayer({
...this.baseLayerProps(),
// Note: this is included here instead of in baseLayerProps to satisfy
// typing.
data: this.table,

...(this.radiusPixels && { radiusPixels: this.radiusPixels }),
...(this.colorRange && { colorRange: this.colorRange }),
...(this.intensity && { intensity: this.intensity }),
...(this.threshold && { threshold: this.threshold }),
...(this.colorDomain && { colorDomain: this.colorDomain }),
...(this.aggregation && { aggregation: this.aggregation }),
...(this.weightsTextureSize && {
weightsTextureSize: this.weightsTextureSize,
}),
...(this.debounceTimeout && { debounceTimeout: this.debounceTimeout }),
...(this.getPosition && { getPosition: this.getPosition }),
...(this.getWeight && { getWeight: this.getWeight }),
});
}
}

0 comments on commit cf24721

Please sign in to comment.