From 3c02d4c7a36d09870b65bc39a9b8c3b618e8bdb6 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 26 Jan 2024 11:37:53 -0500 Subject: [PATCH 01/10] bump deck-layers and apache-arrow --- package-lock.json | 116 +++++++++++++++++++++------------------------- package.json | 4 +- 2 files changed, 56 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0691ce8..3c40c7d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "@deck.gl/extensions": "^8.9.33", "@deck.gl/layers": "^8.9.33", "@deck.gl/react": "^8.9.33", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.8", - "apache-arrow": "^14.0.2", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.10", + "apache-arrow": "^15.0.0", "maplibre-gl": "^3.5.2", "parquet-wasm": "0.5.0", "react": "^18.2.0", @@ -592,11 +592,11 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.3.0-beta.8", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.8.tgz", - "integrity": "sha512-h4dJh7pY4S7WxrbwTTg716WXXSytggCzbKweasxbVrVK3H/2oz82N900+1mveQ16x7Ja9kuyy1YtFnAXL3YWqg==", + "version": "0.3.0-beta.11", + "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.11.tgz", + "integrity": "sha512-J4AXdDxjFQfWSZZj16miNhuWJ1KHn7UFPwdkMTz9a+RKphsVnyokxAJ+FCYBwUt6jZq07uvMDxbbDEnJNIA/kA==", "dependencies": { - "@geoarrow/geoarrow-js": "^0.2.0", + "@geoarrow/geoarrow-js": "^0.3.0", "threads": "^1.7.0" }, "peerDependencies": { @@ -605,19 +605,20 @@ "@deck.gl/geo-layers": "^8.9.23", "@deck.gl/layers": "^8.9.23", "@math.gl/polygon": "^3.6.2", - "apache-arrow": ">=14" + "apache-arrow": ">=15" } }, "node_modules/@geoarrow/geoarrow-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@geoarrow/geoarrow-js/-/geoarrow-js-0.2.0.tgz", - "integrity": "sha512-83a4Mj73yPrptJuOnNvclMuRoQGXipNx3UmoMSg56kPS5Jia6t9vvAjWgeomWeHbNNXbQoCswCK9uLezk7Rwkw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@geoarrow/geoarrow-js/-/geoarrow-js-0.3.0.tgz", + "integrity": "sha512-ECTdS+OmgJ67eIpZ3YK55xDdzfYkbDXs1Ji3JIzgcMsooWLesbEmbjCTu4N5kkgA+BGMsd+a7LTUp9ljPNpC2Q==", "dependencies": { "@math.gl/polygon": "^4.0.0", - "proj4": "^2.9.2" + "proj4": "^2.9.2", + "threads": "^1.7.0" }, "peerDependencies": { - "apache-arrow": ">=14" + "apache-arrow": ">=15" } }, "node_modules/@geoarrow/geoarrow-js/node_modules/@math.gl/core": { @@ -1611,6 +1612,14 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@turf/boolean-clockwise": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-5.1.5.tgz", @@ -1678,14 +1687,14 @@ } }, "node_modules/@types/command-line-args": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", - "integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==" }, "node_modules/@types/command-line-usage": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.2.tgz", - "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==" + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==" }, "node_modules/@types/estree": { "version": "1.0.5", @@ -1742,20 +1751,18 @@ } }, "node_modules/@types/node": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", - "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==" + "version": "20.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", + "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" }, - "node_modules/@types/pad-left": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/pad-left/-/pad-left-2.1.1.tgz", - "integrity": "sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA==" - }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", @@ -1956,23 +1963,22 @@ } }, "node_modules/apache-arrow": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-14.0.2.tgz", - "integrity": "sha512-EBO2xJN36/XoY81nhLcwCJgFwkboDZeyNQ+OPsG7bCoQjc2BT0aTyH/MR6SrL+LirSNz+cYqjGRlupMMlP1aEg==", - "dependencies": { - "@types/command-line-args": "5.2.0", - "@types/command-line-usage": "5.0.2", - "@types/node": "20.3.0", - "@types/pad-left": "2.1.1", - "command-line-args": "5.2.1", - "command-line-usage": "7.0.1", - "flatbuffers": "23.5.26", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-15.0.0.tgz", + "integrity": "sha512-e6aunxNKM+woQf137ny3tp/xbLjFJS2oGQxQhYGqW6dGeIwNV1jOeEAeR6sS2jwAI2qLO83gYIP2MBz02Gw5Xw==", + "dependencies": { + "@swc/helpers": "^0.5.2", + "@types/command-line-args": "^5.2.1", + "@types/command-line-usage": "^5.0.2", + "@types/node": "^20.6.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^23.5.26", "json-bignum": "^0.0.3", - "pad-left": "^2.1.0", - "tslib": "^2.5.3" + "tslib": "^2.6.2" }, "bin": { - "arrow2csv": "bin/arrow2csv.js" + "arrow2csv": "bin/arrow2csv.cjs" } }, "node_modules/argparse": { @@ -3314,17 +3320,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pad-left": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz", - "integrity": "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parquet-wasm": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/parquet-wasm/-/parquet-wasm-0.5.0.tgz", @@ -3476,9 +3471,9 @@ } }, "node_modules/proj4": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.9.2.tgz", - "integrity": "sha512-bdyfNmtlWjQN/rHEHEiqFvpTUHhuzDaeQ6Uu1G4sPGqk+Xkxae6ahh865fClJokSGPBmlDOQWWaO6465TCfv5Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.10.0.tgz", + "integrity": "sha512-0eyB8h1PDoWxucnq88/EZqt7UZlvjhcfbXCcINpE7hqRN0iRPWE/4mXINGulNa/FAvK+Ie7F+l2OxH/0uKV36A==", "dependencies": { "mgrs": "1.0.0", "wkt-parser": "^1.3.3" @@ -3584,14 +3579,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4084,6 +4071,11 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/package.json b/package.json index 15e03af3..56aae434 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@deck.gl/extensions": "^8.9.33", "@deck.gl/layers": "^8.9.33", "@deck.gl/react": "^8.9.33", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.8", - "apache-arrow": "^14.0.2", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.10", + "apache-arrow": "^15.0.0", "maplibre-gl": "^3.5.2", "parquet-wasm": "0.5.0", "react": "^18.2.0", From 18e3278240e42b4975db6d109e0ad9dba86157af Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 26 Jan 2024 12:17:02 -0500 Subject: [PATCH 02/10] WIP: PolygonLayer --- lonboard/__init__.py | 1 + lonboard/_layer.py | 204 +++++++++++++++++++++++++++++++++++++++++-- src/model/layer.ts | 104 ++++++++++++++++++++-- 3 files changed, 297 insertions(+), 12 deletions(-) diff --git a/lonboard/__init__.py b/lonboard/__init__.py index 852bdb67..00af15b5 100644 --- a/lonboard/__init__.py +++ b/lonboard/__init__.py @@ -9,6 +9,7 @@ BitmapTileLayer, HeatmapLayer, PathLayer, + PolygonLayer, ScatterplotLayer, SolidPolygonLayer, ) diff --git a/lonboard/_layer.py b/lonboard/_layer.py index 872bb6a8..6fcdde78 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -547,6 +547,198 @@ class BitmapTileLayer(BaseLayer): """ +class PolygonLayer(BaseArrowLayer): + """The `PolygonLayer` renders filled, stroked and/or extruded polygons.""" + + _layer_type = traitlets.Unicode("polygon").tag(sync=True) + + table = PyarrowTableTrait( + allowed_geometry_types={EXTENSION_NAME.POLYGON, EXTENSION_NAME.MULTIPOLYGON} + ) + """A GeoArrow table with a Polygon or MultiPolygon column. + + This is the fastest way to plot data from an existing GeoArrow source, such as + [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest) or + [geoarrow-pyarrow](https://geoarrow.github.io/geoarrow-python/main/index.html). + + If you have a GeoPandas `GeoDataFrame`, use + [`from_geopandas`][lonboard.PolygonLayer.from_geopandas] instead. + """ + + stroked = traitlets.Bool(allow_none=True).tag(sync=True) + """Whether to draw an outline around the polygon (solid fill). + + Note that both the outer polygon as well the outlines of any holes will be drawn. + + - Type: `bool`, optional + - Default: `True` + """ + + filled = traitlets.Bool(allow_none=True).tag(sync=True) + """Whether to draw a filled polygon (solid fill). + + Note that only the area between the outer polygon and any holes will be filled. + + - Type: `bool`, optional + - Default: `True` + """ + + extruded = traitlets.Bool(allow_none=True).tag(sync=True) + """Whether to extrude the polygons. + + Based on the elevations provided by the `getElevation` accessor. + + If set to `false`, all polygons will be flat, this generates less geometry and is + faster than simply returning 0 from getElevation. + + - Type: `bool`, optional + - Default: `False` + """ + + wireframe = traitlets.Bool(allow_none=True).tag(sync=True) + """ + Whether to generate a line wireframe of the polygon. The outline will have + "horizontal" lines closing the top and bottom polygons and a vertical line + (a "strut") for each vertex on the polygon. + + - Type: `bool`, optional + - Default: `False` + + **Remarks:** + + - These lines are rendered with `GL.LINE` and will thus always be 1 pixel wide. + - Wireframe and solid extrusions are exclusive, you'll need to create two layers + with the same data if you want a combined rendering effect. + """ + + elevation_scale = traitlets.Float(allow_none=True, min=0).tag(sync=True) + """Elevation multiplier. + + The final elevation is calculated by `elevationScale * getElevation(d)`. + `elevationScale` is a handy property to scale all elevation without updating the + data. + + - Type: `float`, optional + - Default: `1` + """ + + line_width_units = traitlets.Unicode("meters", allow_none=True).tag(sync=True) + """ + The units of the line width, one of `'meters'`, `'common'`, and `'pixels'`. See + [unit + system](https://deck.gl/docs/developer-guide/coordinate-systems#supported-units). + + - Type: `str`, optional + - Default: `'meters'` + """ + + line_width_scale = traitlets.Float(allow_none=True, min=0).tag(sync=True) + """ + The line width multiplier that multiplied to all outlines of `Polygon` and + `MultiPolygon` features if the `stroked` attribute is true. + + - Type: `float`, optional + - Default: `1` + """ + + line_width_min_pixels = traitlets.Float(allow_none=True, min=0).tag(sync=True) + """ + The minimum line width in pixels. This can be used to prevent the line from getting + too small when zoomed out. + + - Type: `float`, optional + - Default: `0` + """ + + line_width_max_pixels = traitlets.Float(allow_none=True, min=0).tag(sync=True) + """ + The maximum line width in pixels. This can be used to prevent the line from getting + too big when zoomed in. + + - Type: `float`, optional + - Default: `None` + """ + + line_joint_rounded = traitlets.Bool(allow_none=True).tag(sync=True) + """Type of joint. If `true`, draw round joints. Otherwise draw miter joints. + + - Type: `bool`, optional + - Default: `False` + """ + + line_miter_limit = traitlets.Float(allow_none=True, min=0).tag(sync=True) + """The maximum extent of a joint in ratio to the stroke width. + + Only works if `line_joint_rounded` is false. + + - Type: `float`, optional + - Default: `4` + """ + + get_fill_color = ColorAccessor() + """ + The fill color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a + number between 0-255 and `a` is 255 if not supplied. + + - Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional + - If a single `list` or `tuple` is provided, it is used as the fill color for + all polygons. + - If a numpy or pyarrow array is provided, each value in the array will be used + as the fill color for the polygon at the same row index. + - Default: `[0, 0, 0, 255]`. + """ + + get_line_color = ColorAccessor() + """ + The line color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a + number between 0-255 and `a` is 255 if not supplied. + + Only applies if `extruded=True`. + + - Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional + - If a single `list` or `tuple` is provided, it is used as the line color for + all polygons. + - If a numpy or pyarrow array is provided, each value in the array will be used + as the line color for the polygon at the same row index. + - Default: `[0, 0, 0, 255]`. + """ + + get_line_width = FloatAccessor() + """ + The width of the outline of each polygon, in units specified by `line_width_units` + (default `'meters'`). + + - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional + - If a number is provided, it is used as the outline width for all polygons. + - If an array is provided, each value in the array will be used as the outline + width for the polygon at the same row index. + - Default: `1`. + """ + + get_elevation = FloatAccessor() + """ + The elevation to extrude each polygon with, in meters. + + Only applies if `extruded=True`. + + - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional + - If a number is provided, it is used as the width for all polygons. + - If an array is provided, each value in the array will be used as the width for + the polygon at the same row index. + - Default: `1000`. + """ + + @traitlets.validate( + "get_fill_color", "get_line_color", "get_line_width", "get_elevation" + ) + 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"] + + class ScatterplotLayer(BaseArrowLayer): """The `ScatterplotLayer` renders circles at given coordinates. @@ -1006,6 +1198,12 @@ class SolidPolygonLayer(BaseArrowLayer): - Type: `bool`, optional - Default: `False` + + **Remarks:** + + - These lines are rendered with `GL.LINE` and will thus always be 1 pixel wide. + - Wireframe and solid extrusions are exclusive, you'll need to create two layers + with the same data if you want a combined rendering effect. """ elevation_scale = traitlets.Float(allow_none=True, min=0).tag(sync=True) @@ -1016,12 +1214,6 @@ class SolidPolygonLayer(BaseArrowLayer): - Type: `float`, optional - Default: `1` - - **Remarks:** - - - These lines are rendered with `GL.LINE` and will thus always be 1 pixel wide. - - Wireframe and solid extrusions are exclusive, you'll need to create two layers - with the same data if you want a combined rendering effect. """ get_elevation = FloatAccessor() diff --git a/src/model/layer.ts b/src/model/layer.ts index b272eb56..368c2d37 100644 --- a/src/model/layer.ts +++ b/src/model/layer.ts @@ -1,17 +1,21 @@ import { GeoArrowArcLayer, - GeoArrowArcLayerProps, GeoArrowColumnLayer, - GeoArrowColumnLayerProps, GeoArrowHeatmapLayer, - GeoArrowHeatmapLayerProps, GeoArrowPathLayer, - GeoArrowPathLayerProps, + GeoArrowPolygonLayer, GeoArrowScatterplotLayer, - GeoArrowScatterplotLayerProps, GeoArrowSolidPolygonLayer, - GeoArrowSolidPolygonLayerProps, _GeoArrowTextLayer as GeoArrowTextLayer, +} from "@geoarrow/deck.gl-layers"; +import type { + GeoArrowArcLayerProps, + GeoArrowColumnLayerProps, + GeoArrowHeatmapLayerProps, + GeoArrowPathLayerProps, + GeoArrowPolygonLayerProps, + GeoArrowScatterplotLayerProps, + GeoArrowSolidPolygonLayerProps, _GeoArrowTextLayerProps as GeoArrowTextLayerProps, } from "@geoarrow/deck.gl-layers"; import type { WidgetModel } from "@jupyter-widgets/base"; @@ -457,6 +461,90 @@ export class PathModel extends BaseArrowLayerModel { }); } } + +export class PolygonModel extends BaseArrowLayerModel { + static layerType = "polygon"; + + protected stroked: GeoArrowPolygonLayerProps["stroked"] | null; + protected filled: GeoArrowPolygonLayerProps["filled"] | null; + protected extruded: GeoArrowPolygonLayerProps["extruded"] | null; + protected wireframe: GeoArrowPolygonLayerProps["wireframe"] | null; + protected elevationScale: GeoArrowPolygonLayerProps["elevationScale"] | null; + protected lineWidthUnits: GeoArrowPolygonLayerProps["lineWidthUnits"] | null; + protected lineWidthScale: GeoArrowPolygonLayerProps["lineWidthScale"] | null; + protected lineWidthMinPixels: + | GeoArrowPolygonLayerProps["lineWidthMinPixels"] + | null; + protected lineWidthMaxPixels: + | GeoArrowPolygonLayerProps["lineWidthMaxPixels"] + | null; + protected lineJointRounded: + | GeoArrowPolygonLayerProps["lineJointRounded"] + | null; + protected lineMiterLimit: GeoArrowPolygonLayerProps["lineMiterLimit"] | null; + + protected getFillColor: GeoArrowPolygonLayerProps["getFillColor"] | null; + protected getLineColor: GeoArrowPolygonLayerProps["getLineColor"] | null; + protected getLineWidth: GeoArrowPolygonLayerProps["getLineWidth"] | null; + protected getElevation: GeoArrowPolygonLayerProps["getElevation"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("stroked", "stroked"); + this.initRegularAttribute("filled", "filled"); + this.initRegularAttribute("extruded", "extruded"); + this.initRegularAttribute("wireframe", "wireframe"); + this.initRegularAttribute("elevation_scale", "elevationScale"); + this.initRegularAttribute("line_width_units", "lineWidthUnits"); + this.initRegularAttribute("line_width_scale", "lineWidthScale"); + this.initRegularAttribute("line_width_min_pixels", "lineWidthMinPixels"); + this.initRegularAttribute("line_width_max_pixels", "lineWidthMaxPixels"); + this.initRegularAttribute("line_joint_rounded", "lineJointRounded"); + this.initRegularAttribute("line_miter_limit", "lineMiterLimit"); + + this.initVectorizedAccessor("get_fill_color", "getFillColor"); + this.initVectorizedAccessor("get_line_color", "getLineColor"); + this.initVectorizedAccessor("get_line_width", "getLineWidth"); + this.initVectorizedAccessor("get_elevation", "getElevation"); + } + + layerProps(): Omit { + console.log("table", this.table); + console.log('filled', this.filled); + + return { + data: this.table, + ...(this.stroked && { stroked: this.stroked }), + ...(this.filled && { filled: this.filled }), + ...(this.extruded && { extruded: this.extruded }), + ...(this.wireframe && { wireframe: this.wireframe }), + ...(this.elevationScale && { elevationScale: this.elevationScale }), + ...(this.lineWidthUnits && { lineWidthUnits: this.lineWidthUnits }), + ...(this.lineWidthScale && { lineWidthScale: this.lineWidthScale }), + ...(this.lineWidthMinPixels && { + lineWidthMinPixels: this.lineWidthMinPixels, + }), + ...(this.lineWidthMaxPixels && { + lineWidthMaxPixels: this.lineWidthMaxPixels, + }), + ...(this.lineJointRounded && { lineJointRounded: this.lineJointRounded }), + ...(this.lineMiterLimit && { lineMiterLimit: this.lineMiterLimit }), + ...(this.getFillColor && { getFillColor: this.getFillColor }), + ...(this.getLineColor && { getLineColor: this.getLineColor }), + ...(this.getLineWidth && { getLineWidth: this.getLineWidth }), + ...(this.getElevation && { getElevation: this.getElevation }), + }; + } + + render(): GeoArrowPolygonLayer { + return new GeoArrowPolygonLayer({ + ...this.baseLayerProps(), + ...this.layerProps(), + }); + } +} + export class ScatterplotModel extends BaseArrowLayerModel { static layerType = "scatterplot"; @@ -744,6 +832,10 @@ export async function initializeLayer( layerModel = new PathModel(model, updateStateCallback); break; + case PolygonModel.layerType: + layerModel = new PolygonModel(model, updateStateCallback); + break; + case ScatterplotModel.layerType: layerModel = new ScatterplotModel(model, updateStateCallback); break; From 57d0ca7d4cc33996e4e468a1fc4b35c387bb32d8 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 9 Feb 2024 13:56:49 -0500 Subject: [PATCH 03/10] Bump deck.gl-layers --- package-lock.json | 8 ++++---- package.json | 2 +- src/model/layer.ts | 42 ++++++++++++++++++++++++++---------------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4314485f..d60607f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@deck.gl/extensions": "^8.9.34", "@deck.gl/layers": "^8.9.34", "@deck.gl/react": "^8.9.34", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.11", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.12", "apache-arrow": "^15.0.0", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", @@ -596,9 +596,9 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.3.0-beta.11", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.11.tgz", - "integrity": "sha512-J4AXdDxjFQfWSZZj16miNhuWJ1KHn7UFPwdkMTz9a+RKphsVnyokxAJ+FCYBwUt6jZq07uvMDxbbDEnJNIA/kA==", + "version": "0.3.0-beta.12", + "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.12.tgz", + "integrity": "sha512-UBwkklHuuLAbqvuTfTh9zNegealfrSGOMqHBcLyOWUeOW3lCgZDst6wOUwbjHtD21wuW5rUE7yOT+CVXwV5jvQ==", "dependencies": { "@geoarrow/geoarrow-js": "^0.3.0", "threads": "^1.7.0" diff --git a/package.json b/package.json index 94afb24c..63e02672 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@deck.gl/extensions": "^8.9.34", "@deck.gl/layers": "^8.9.34", "@deck.gl/react": "^8.9.34", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.11", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.12", "apache-arrow": "^15.0.0", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", diff --git a/src/model/layer.ts b/src/model/layer.ts index eed5c614..e643075e 100644 --- a/src/model/layer.ts +++ b/src/model/layer.ts @@ -545,29 +545,39 @@ export class PolygonModel extends BaseArrowLayerModel { layerProps(): Omit { console.log("table", this.table); - console.log('filled', this.filled); + console.log("filled", this.filled); return { data: this.table, - ...(this.stroked && { stroked: this.stroked }), - ...(this.filled && { filled: this.filled }), - ...(this.extruded && { extruded: this.extruded }), - ...(this.wireframe && { wireframe: this.wireframe }), - ...(this.elevationScale && { elevationScale: this.elevationScale }), - ...(this.lineWidthUnits && { lineWidthUnits: this.lineWidthUnits }), - ...(this.lineWidthScale && { lineWidthScale: this.lineWidthScale }), - ...(this.lineWidthMinPixels && { + ...(isDefined(this.stroked) && { stroked: this.stroked }), + ...(isDefined(this.filled) && { filled: this.filled }), + ...(isDefined(this.extruded) && { extruded: this.extruded }), + ...(isDefined(this.wireframe) && { wireframe: this.wireframe }), + ...(isDefined(this.elevationScale) && { + elevationScale: this.elevationScale, + }), + ...(isDefined(this.lineWidthUnits) && { + lineWidthUnits: this.lineWidthUnits, + }), + ...(isDefined(this.lineWidthScale) && { + lineWidthScale: this.lineWidthScale, + }), + ...(isDefined(this.lineWidthMinPixels) && { lineWidthMinPixels: this.lineWidthMinPixels, }), - ...(this.lineWidthMaxPixels && { + ...(isDefined(this.lineWidthMaxPixels) && { lineWidthMaxPixels: this.lineWidthMaxPixels, }), - ...(this.lineJointRounded && { lineJointRounded: this.lineJointRounded }), - ...(this.lineMiterLimit && { lineMiterLimit: this.lineMiterLimit }), - ...(this.getFillColor && { getFillColor: this.getFillColor }), - ...(this.getLineColor && { getLineColor: this.getLineColor }), - ...(this.getLineWidth && { getLineWidth: this.getLineWidth }), - ...(this.getElevation && { getElevation: this.getElevation }), + ...(isDefined(this.lineJointRounded) && { + lineJointRounded: this.lineJointRounded, + }), + ...(isDefined(this.lineMiterLimit) && { + lineMiterLimit: this.lineMiterLimit, + }), + ...(isDefined(this.getFillColor) && { getFillColor: this.getFillColor }), + ...(isDefined(this.getLineColor) && { getLineColor: this.getLineColor }), + ...(isDefined(this.getLineWidth) && { getLineWidth: this.getLineWidth }), + ...(isDefined(this.getElevation) && { getElevation: this.getElevation }), }; } From aa9f186cf2e01e6d582e6185a20920dbdfdbb0f1 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 12 Feb 2024 15:52:51 -0500 Subject: [PATCH 04/10] Fix defaultxs --- lonboard/_layer.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/lonboard/_layer.py b/lonboard/_layer.py index 0de4bb54..9ab77646 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -582,7 +582,7 @@ class PolygonLayer(BaseArrowLayer): [`from_geopandas`][lonboard.PolygonLayer.from_geopandas] instead. """ - stroked = traitlets.Bool(allow_none=True).tag(sync=True) + stroked = traitlets.Bool(None, allow_none=True).tag(sync=True) """Whether to draw an outline around the polygon (solid fill). Note that both the outer polygon as well the outlines of any holes will be drawn. @@ -591,7 +591,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `True` """ - filled = traitlets.Bool(allow_none=True).tag(sync=True) + filled = traitlets.Bool(None, allow_none=True).tag(sync=True) """Whether to draw a filled polygon (solid fill). Note that only the area between the outer polygon and any holes will be filled. @@ -600,7 +600,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `True` """ - extruded = traitlets.Bool(allow_none=True).tag(sync=True) + extruded = traitlets.Bool(None, allow_none=True).tag(sync=True) """Whether to extrude the polygons. Based on the elevations provided by the `getElevation` accessor. @@ -612,7 +612,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `False` """ - wireframe = traitlets.Bool(allow_none=True).tag(sync=True) + wireframe = traitlets.Bool(None, allow_none=True).tag(sync=True) """ Whether to generate a line wireframe of the polygon. The outline will have "horizontal" lines closing the top and bottom polygons and a vertical line @@ -628,7 +628,7 @@ class PolygonLayer(BaseArrowLayer): with the same data if you want a combined rendering effect. """ - elevation_scale = traitlets.Float(allow_none=True, min=0).tag(sync=True) + elevation_scale = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) """Elevation multiplier. The final elevation is calculated by `elevationScale * getElevation(d)`. @@ -639,7 +639,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `1` """ - line_width_units = traitlets.Unicode("meters", allow_none=True).tag(sync=True) + line_width_units = traitlets.Unicode(None, allow_none=True).tag(sync=True) """ The units of the line width, one of `'meters'`, `'common'`, and `'pixels'`. See [unit @@ -649,7 +649,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `'meters'` """ - line_width_scale = traitlets.Float(allow_none=True, min=0).tag(sync=True) + line_width_scale = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) """ The line width multiplier that multiplied to all outlines of `Polygon` and `MultiPolygon` features if the `stroked` attribute is true. @@ -658,7 +658,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `1` """ - line_width_min_pixels = traitlets.Float(allow_none=True, min=0).tag(sync=True) + line_width_min_pixels = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) """ The minimum line width in pixels. This can be used to prevent the line from getting too small when zoomed out. @@ -667,7 +667,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `0` """ - line_width_max_pixels = traitlets.Float(allow_none=True, min=0).tag(sync=True) + line_width_max_pixels = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) """ The maximum line width in pixels. This can be used to prevent the line from getting too big when zoomed in. @@ -676,14 +676,14 @@ class PolygonLayer(BaseArrowLayer): - Default: `None` """ - line_joint_rounded = traitlets.Bool(allow_none=True).tag(sync=True) + line_joint_rounded = traitlets.Bool(None, allow_none=True).tag(sync=True) """Type of joint. If `true`, draw round joints. Otherwise draw miter joints. - Type: `bool`, optional - Default: `False` """ - line_miter_limit = traitlets.Float(allow_none=True, min=0).tag(sync=True) + line_miter_limit = traitlets.Float(None, allow_none=True, min=0).tag(sync=True) """The maximum extent of a joint in ratio to the stroke width. Only works if `line_joint_rounded` is false. @@ -692,7 +692,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `4` """ - get_fill_color = ColorAccessor() + get_fill_color = ColorAccessor(None, allow_none=True) """ The fill color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a number between 0-255 and `a` is 255 if not supplied. @@ -705,7 +705,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `[0, 0, 0, 255]`. """ - get_line_color = ColorAccessor() + get_line_color = ColorAccessor(None, allow_none=True) """ The line color of each polygon in the format of `[r, g, b, [a]]`. Each channel is a number between 0-255 and `a` is 255 if not supplied. @@ -720,7 +720,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `[0, 0, 0, 255]`. """ - get_line_width = FloatAccessor() + get_line_width = FloatAccessor(None, allow_none=True) """ The width of the outline of each polygon, in units specified by `line_width_units` (default `'meters'`). @@ -732,7 +732,7 @@ class PolygonLayer(BaseArrowLayer): - Default: `1`. """ - get_elevation = FloatAccessor() + get_elevation = FloatAccessor(None, allow_none=True) """ The elevation to extrude each polygon with, in meters. @@ -745,16 +745,6 @@ class PolygonLayer(BaseArrowLayer): - Default: `1000`. """ - @traitlets.validate( - "get_fill_color", "get_line_color", "get_line_width", "get_elevation" - ) - 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"] - class ScatterplotLayer(BaseArrowLayer): """The `ScatterplotLayer` renders circles at given coordinates. From 5ac66eb8694bc14c501ab9f879e6c0b01bd3a0ee Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 25 Mar 2024 14:36:35 -0400 Subject: [PATCH 05/10] Bump deck.gl-layers version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 870aedb1..d586f6dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@deck.gl/extensions": "^8.9.35", "@deck.gl/layers": "^8.9.35", "@deck.gl/react": "^8.9.35", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.15", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.16", "apache-arrow": "^15.0.1", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", @@ -596,9 +596,9 @@ } }, "node_modules/@geoarrow/deck.gl-layers": { - "version": "0.3.0-beta.15", - "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.15.tgz", - "integrity": "sha512-SQhPbSll7Ac5r+Cpiz2dp+mKEoZPQlL2t6g9AQn+rwRZFdNSdTgWjBE9iL4yG08E+dr/jRJCzKJNyEPKZUEyBQ==", + "version": "0.3.0-beta.16", + "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-layers/-/deck.gl-layers-0.3.0-beta.16.tgz", + "integrity": "sha512-KvBlMhmcoHYvrP/YpXv9F9A4OZ5jjO3FoRSdX4bZM+z+4bN2xj6ZBfE4Wfp47cS6Q3dotUACO+VwY5dZDixl6A==", "dependencies": { "@geoarrow/geoarrow-js": "^0.3.0", "threads": "^1.7.0" diff --git a/package.json b/package.json index 3b94b4ae..2b369ad0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@deck.gl/extensions": "^8.9.35", "@deck.gl/layers": "^8.9.35", "@deck.gl/react": "^8.9.35", - "@geoarrow/deck.gl-layers": "^0.3.0-beta.15", + "@geoarrow/deck.gl-layers": "^0.3.0-beta.16", "apache-arrow": "^15.0.1", "maplibre-gl": "^3.6.2", "parquet-wasm": "0.5.0", From 46341bfaccadf549241a0d107eda7135e3cb97d0 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 25 Mar 2024 14:52:18 -0400 Subject: [PATCH 06/10] Use PolygonLayer by default in viz --- lonboard/_layer.py | 28 +++++++++++++++++++- lonboard/_viz.py | 58 ++++++++++++++++++++++++++--------------- lonboard/types/layer.py | 18 +++++++++++++ 3 files changed, 82 insertions(+), 22 deletions(-) diff --git a/lonboard/_layer.py b/lonboard/_layer.py index c88f0af4..c7eba9b6 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -47,6 +47,7 @@ HeatmapLayerKwargs, PathLayerKwargs, PointCloudLayerKwargs, + PolygonLayerKwargs, ScatterplotLayerKwargs, SolidPolygonLayerKwargs, ) @@ -607,7 +608,32 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs): class PolygonLayer(BaseArrowLayer): - """The `PolygonLayer` renders filled, stroked and/or extruded polygons.""" + """The `PolygonLayer` renders filled, stroked and/or extruded polygons. + + This layer is essentially a combination of a [`PathLayer`][lonboard.PathLayer] and a + [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]. This has some overhead beyond a + `SolidPolygonLayer`, so if you're looking for the maximum performance with large + data, you may want to use a `SolidPolygonLayer` directly. + """ + + def __init__( + self, + *, + table: pa.Table, + _rows_per_chunk: Optional[int] = None, + **kwargs: Unpack[PolygonLayerKwargs], + ): + super().__init__(table=table, _rows_per_chunk=_rows_per_chunk, **kwargs) + + @classmethod + def from_geopandas( + cls, + gdf: gpd.GeoDataFrame, + *, + auto_downcast: bool = True, + **kwargs: Unpack[PolygonLayerKwargs], + ) -> Self: + return super().from_geopandas(gdf=gdf, auto_downcast=auto_downcast, **kwargs) _layer_type = traitlets.Unicode("polygon").tag(sync=True) diff --git a/lonboard/_viz.py b/lonboard/_viz.py index 38fcdb8d..6b95b6a4 100644 --- a/lonboard/_viz.py +++ b/lonboard/_viz.py @@ -28,14 +28,14 @@ from lonboard._geoarrow.geopandas_interop import geopandas_to_geoarrow from lonboard._geoarrow.parse_wkb import parse_wkb_table from lonboard._geoarrow.sanitize import remove_extension_classes -from lonboard._layer import PathLayer, ScatterplotLayer, SolidPolygonLayer +from lonboard._layer import PathLayer, PolygonLayer, ScatterplotLayer from lonboard._map import Map from lonboard._utils import get_geometry_column_index from lonboard.basemap import CartoBasemap from lonboard.types.layer import ( PathLayerKwargs, + PolygonLayerKwargs, ScatterplotLayerKwargs, - SolidPolygonLayerKwargs, ) from lonboard.types.map import MapKwargs @@ -86,7 +86,7 @@ def viz( *, scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, - solid_polygon_kwargs: Optional[SolidPolygonLayerKwargs] = None, + polygon_kwargs: Optional[PolygonLayerKwargs] = None, map_kwargs: Optional[MapKwargs] = None, ) -> Map: """A high-level function to plot your data easily. @@ -115,8 +115,8 @@ def viz( [`ScatterplotLayer`][lonboard.ScatterplotLayer]s. path_kwargs: a `dict` of parameters to pass down to all generated [`PathLayer`][lonboard.PathLayer]s. - solid_polygon_kwargs: a `dict` of parameters to pass down to all generated - [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]s. + polygon_kwargs: a `dict` of parameters to pass down to all generated + [`PolygonLayer`][lonboard.PolygonLayer]s. map_kwargs: a `dict` of parameters to pass down to the generated [`Map`][lonboard.Map]. @@ -135,7 +135,7 @@ def viz( _viz_color=color_ordering[i % len(color_ordering)], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, - solid_polygon_kwargs=solid_polygon_kwargs, + polygon_kwargs=polygon_kwargs, ) for i, item in enumerate(data) ] @@ -146,7 +146,7 @@ def viz( _viz_color=color_ordering[0], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, - solid_polygon_kwargs=solid_polygon_kwargs, + polygon_kwargs=polygon_kwargs, ) ] @@ -160,7 +160,7 @@ def viz( def create_layer_from_data_input( data: VizDataInput, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: # geopandas GeoDataFrame if ( data.__class__.__module__.startswith("geopandas") @@ -222,14 +222,14 @@ def create_layer_from_data_input( def _viz_geopandas_geodataframe( data: gpd.GeoDataFrame, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: table = geopandas_to_geoarrow(data) return _viz_geoarrow_table(table, **kwargs) def _viz_geopandas_geoseries( data: gpd.GeoSeries, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: import geopandas as gpd gdf = gpd.GeoDataFrame(geometry=data) @@ -239,13 +239,13 @@ def _viz_geopandas_geoseries( def _viz_shapely_scalar( data: shapely.geometry.base.BaseGeometry, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: return _viz_shapely_array(np.array([data]), **kwargs) def _viz_shapely_array( data: NDArray[np.object_], **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: # TODO: pass include_z? field, geom_arr = construct_geometry_array(data) schema = pa.schema([field]) @@ -255,7 +255,7 @@ def _viz_shapely_array( def _viz_geo_interface( data: dict, **kwargs -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: if data["type"] in [ "Point", "LineString", @@ -302,8 +302,8 @@ def _viz_geoarrow_table( _viz_color: Optional[str] = None, scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, - solid_polygon_kwargs: Optional[SolidPolygonLayerKwargs] = None, -) -> Union[ScatterplotLayer, PathLayer, SolidPolygonLayer]: + polygon_kwargs: Optional[PolygonLayerKwargs] = None, +) -> Union[ScatterplotLayer, PathLayer, PolygonLayer]: table = remove_extension_classes(table) table = parse_wkb_table(table) @@ -327,6 +327,14 @@ def _viz_geoarrow_table( else: scatterplot_kwargs["radius_min_pixels"] = 0.2 + if "opacity" not in scatterplot_kwargs.keys(): + if len(table) <= 10_000: + scatterplot_kwargs["opacity"] = 0.9 + elif len(table) <= 100_000: + scatterplot_kwargs["opacity"] = 0.7 + elif len(table) <= 1_000_000: + scatterplot_kwargs["opacity"] = 0.5 + return ScatterplotLayer(table=table, **scatterplot_kwargs) elif geometry_ext_type in [ @@ -348,17 +356,25 @@ def _viz_geoarrow_table( else: path_kwargs["width_min_pixels"] = 0.5 + if "opacity" not in path_kwargs.keys(): + if len(table) <= 1_000: + path_kwargs["opacity"] = 0.9 + elif len(table) <= 10_000: + path_kwargs["opacity"] = 0.7 + elif len(table) <= 100_000: + path_kwargs["opacity"] = 0.5 + return PathLayer(table=table, **path_kwargs) elif geometry_ext_type in [EXTENSION_NAME.POLYGON, EXTENSION_NAME.MULTIPOLYGON]: - solid_polygon_kwargs = {} if not solid_polygon_kwargs else solid_polygon_kwargs + polygon_kwargs = {} if not polygon_kwargs else polygon_kwargs - if "get_fill_color" not in solid_polygon_kwargs.keys(): - solid_polygon_kwargs["get_fill_color"] = _viz_color + if "get_fill_color" not in polygon_kwargs.keys(): + polygon_kwargs["get_fill_color"] = _viz_color - if "opacity" not in solid_polygon_kwargs.keys(): - solid_polygon_kwargs["opacity"] = 0.6 + if "opacity" not in polygon_kwargs.keys(): + polygon_kwargs["opacity"] = 0.6 - return SolidPolygonLayer(table=table, **solid_polygon_kwargs) + return PolygonLayer(table=table, **polygon_kwargs) raise ValueError(f"Unsupported extension type: '{geometry_ext_type}'.") diff --git a/lonboard/types/layer.py b/lonboard/types/layer.py index 2417c496..49ec5190 100644 --- a/lonboard/types/layer.py +++ b/lonboard/types/layer.py @@ -131,6 +131,24 @@ class PointCloudLayerKwargs(BaseLayerKwargs, total=False): get_normal: NormalAccessorInput +class PolygonLayerKwargs(BaseLayerKwargs, total=False): + stroked: bool + filled: bool + extruded: bool + wireframe: bool + elevation_scale: IntFloat + line_width_units: Units + line_width_scale: IntFloat + line_width_min_pixels: IntFloat + line_width_max_pixels: IntFloat + line_joint_rounded: bool + line_miter_limit: IntFloat + get_fill_color: ColorAccessorInput + get_line_color: ColorAccessorInput + get_line_width: FloatAccessorInput + get_elevation: FloatAccessorInput + + class ScatterplotLayerKwargs(BaseLayerKwargs, total=False): radius_units: Units radius_scale: IntFloat From 4ce5156e16399baa20ba025ec795f345e0aa2302 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 28 Mar 2024 10:09:54 -0400 Subject: [PATCH 07/10] render stroke by default --- lonboard/_viz.py | 53 +++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/lonboard/_viz.py b/lonboard/_viz.py index 1df37eea..a6260c38 100644 --- a/lonboard/_viz.py +++ b/lonboard/_viz.py @@ -71,18 +71,20 @@ def __arrow_c_stream__( # From mbview # https://github.com/mapbox/mbview/blob/e64bd86cfe4a63e6af4ea1d310bd49be4f162a43/views/vector.ejs#L75-L87 -COLORS = [ - "#FC49A3", # pink - "#CC66FF", # purple-ish - "#66CCFF", # sky blue - "#66FFCC", # teal - "#00FF00", # lime green - "#FFCC66", # light orange - "#FF6666", # salmon - "#FF0000", # red - "#FF8000", # orange - "#FFFF66", # yellow - "#00FFFF", # turquoise +# (primary_color, secondary_color) +# Chosen by ChatGPT at this point. +COLORS: List[Tuple[str, str]] = [ + ("#FC49A3", "#A2FFCD"), # pink + ("#CC66FF", "#66CCFF"), # purple-ish + ("#66CCFF", "#FFCC66"), # sky blue + ("#66FFCC", "#FF6666"), # teal + ("#00FF00", "#FF8000"), # lime green + ("#FFCC66", "#66FFCC"), # light orange + ("#FF6666", "#66CCFF"), # salmon + ("#FF0000", "#66FFCC"), # red + ("#FF8000", "#FFFF66"), # orange + ("#FFFF66", "#FF8000"), # yellow + ("#00FFFF", "#FF6666"), # turquoise ] @@ -137,7 +139,7 @@ def viz( layers = [ create_layer_from_data_input( item, - _viz_color=color_ordering[i % len(color_ordering)], + _viz_colors=color_ordering[i % len(color_ordering)], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, polygon_kwargs=polygon_kwargs, @@ -148,7 +150,7 @@ def viz( layers = [ create_layer_from_data_input( data, - _viz_color=color_ordering[0], + _viz_colors=color_ordering[0], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, polygon_kwargs=polygon_kwargs, @@ -355,7 +357,7 @@ def __arrow_c_array__(self, requested_schema): def _viz_geoarrow_table( table: pa.Table, *, - _viz_color: Optional[str] = None, + _viz_colors: Tuple[str, str], scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, polygon_kwargs: Optional[PolygonLayerKwargs] = None, @@ -371,7 +373,7 @@ def _viz_geoarrow_table( scatterplot_kwargs = {} if not scatterplot_kwargs else scatterplot_kwargs if "get_fill_color" not in scatterplot_kwargs.keys(): - scatterplot_kwargs["get_fill_color"] = _viz_color + scatterplot_kwargs["get_fill_color"] = _viz_colors[0] if "radius_min_pixels" not in scatterplot_kwargs.keys(): if len(table) <= 10_000: @@ -400,7 +402,7 @@ def _viz_geoarrow_table( path_kwargs = {} if not path_kwargs else path_kwargs if "get_color" not in path_kwargs.keys(): - path_kwargs["get_color"] = _viz_color + path_kwargs["get_color"] = _viz_colors[0] if "width_min_pixels" not in path_kwargs.keys(): if len(table) <= 1_000: @@ -426,10 +428,23 @@ def _viz_geoarrow_table( polygon_kwargs = {} if not polygon_kwargs else polygon_kwargs if "get_fill_color" not in polygon_kwargs.keys(): - polygon_kwargs["get_fill_color"] = _viz_color + polygon_kwargs["get_fill_color"] = _viz_colors[0] + + if "get_line_color" not in polygon_kwargs.keys(): + polygon_kwargs["get_line_color"] = _viz_colors[1] if "opacity" not in polygon_kwargs.keys(): - polygon_kwargs["opacity"] = 0.6 + polygon_kwargs["opacity"] = 0.5 + + if "line_width_min_pixels" not in polygon_kwargs.keys(): + if len(table) <= 1_000: + polygon_kwargs["line_width_min_pixels"] = 1 + elif len(table) <= 10_000: + polygon_kwargs["line_width_min_pixels"] = 0.8 + elif len(table) <= 100_000: + polygon_kwargs["line_width_min_pixels"] = 0.7 + else: + polygon_kwargs["line_width_min_pixels"] = 0.5 return PolygonLayer(table=table, **polygon_kwargs) From a52ca2dea129ef74f6bd9e6a086f1251e7c66d55 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 28 Mar 2024 10:33:38 -0400 Subject: [PATCH 08/10] Updated docs --- README.md | 2 +- docs/api/layers/polygon-layer.md | 5 ++++ lonboard/_layer.py | 51 +++++++++++++++++++++++++++++--- mkdocs.yml | 1 + 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 docs/api/layers/polygon-layer.md diff --git a/README.md b/README.md index a173aabc..c8ab668c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ gdf = gpd.GeoDataFrame(...) viz(gdf) ``` -Under the hood, this delegates to a [`ScatterplotLayer`](https://developmentseed.org/lonboard/latest/api/layers/scatterplot-layer/), [`PathLayer`](https://developmentseed.org/lonboard/latest/api/layers/path-layer/), or [`SolidPolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/solid-polygon-layer/). Refer to the [documentation](https://developmentseed.org/lonboard/) and [examples](https://developmentseed.org/lonboard/latest/examples/internet-speeds/) for more control over rendering. +Under the hood, this delegates to a [`ScatterplotLayer`](https://developmentseed.org/lonboard/latest/api/layers/scatterplot-layer/), [`PathLayer`](https://developmentseed.org/lonboard/latest/api/layers/path-layer/), or [`PolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/polygon-layer/). Refer to the [documentation](https://developmentseed.org/lonboard/) and [examples](https://developmentseed.org/lonboard/latest/examples/internet-speeds/) for more control over rendering. ## Documentation diff --git a/docs/api/layers/polygon-layer.md b/docs/api/layers/polygon-layer.md new file mode 100644 index 00000000..aceff289 --- /dev/null +++ b/docs/api/layers/polygon-layer.md @@ -0,0 +1,5 @@ +# PolygonLayer + +::: lonboard.PolygonLayer + options: + inherited_members: true diff --git a/lonboard/_layer.py b/lonboard/_layer.py index c7eba9b6..1476e991 100644 --- a/lonboard/_layer.py +++ b/lonboard/_layer.py @@ -610,10 +610,46 @@ def __init__(self, **kwargs: BitmapTileLayerKwargs): class PolygonLayer(BaseArrowLayer): """The `PolygonLayer` renders filled, stroked and/or extruded polygons. - This layer is essentially a combination of a [`PathLayer`][lonboard.PathLayer] and a - [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]. This has some overhead beyond a - `SolidPolygonLayer`, so if you're looking for the maximum performance with large - data, you may want to use a `SolidPolygonLayer` directly. + !!! note + + This layer is essentially a combination of a [`PathLayer`][lonboard.PathLayer] + and a [`SolidPolygonLayer`][lonboard.SolidPolygonLayer]. This has some overhead + beyond a `SolidPolygonLayer`, so if you're looking for the maximum performance + with large data, you may want to use a `SolidPolygonLayer` directly. + + **Example:** + + From GeoPandas: + + ```py + import geopandas as gpd + from lonboard import Map, PolygonLayer + + # A GeoDataFrame with Polygon or MultiPolygon geometries + gdf = gpd.GeoDataFrame() + layer = PolygonLayer.from_geopandas( + gdf, + get_fill_color=[255, 0, 0], + get_line_color=[0, 100, 100, 150], + ) + m = Map(layer) + ``` + + From [geoarrow-rust](https://geoarrow.github.io/geoarrow-rs/python/latest): + + ```py + from geoarrow.rust.core import read_parquet + from lonboard import Map, PolygonLayer + + # Example: A GeoParquet file with Polygon or MultiPolygon geometries + table = read_parquet("path/to/file.parquet") + layer = PolygonLayer( + table=table, + get_fill_color=[255, 0, 0], + get_line_color=[0, 100, 100, 150], + ) + m = Map(layer) + ``` """ def __init__( @@ -1323,6 +1359,13 @@ class SolidPolygonLayer(BaseArrowLayer): """ The `SolidPolygonLayer` renders filled and/or extruded polygons. + !!! note + + This layer is similar to the [`PolygonLayer`][lonboard.PolygonLayer] but will + not render an outline around polygons. In most cases, you'll want to use the + `PolygonLayer` directly, but for very large datasets not drawing the outline can + significantly improve performance, in which case you may want to use this layer. + **Example:** From GeoPandas: diff --git a/mkdocs.yml b/mkdocs.yml index 997b7b31..609eb1b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ nav: - api/layers/heatmap-layer.md - api/layers/path-layer.md - api/layers/point-cloud-layer.md + - api/layers/polygon-layer.md - api/layers/scatterplot-layer.md - api/layers/solid-polygon-layer.md - api/layers/base-layer.md From 812be904bccf12671fcb4e0218099e679c07e5f5 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 28 Mar 2024 10:45:21 -0400 Subject: [PATCH 09/10] switch to black outline --- lonboard/_viz.py | 53 +++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/lonboard/_viz.py b/lonboard/_viz.py index a6260c38..6a10fa54 100644 --- a/lonboard/_viz.py +++ b/lonboard/_viz.py @@ -71,21 +71,20 @@ def __arrow_c_stream__( # From mbview # https://github.com/mapbox/mbview/blob/e64bd86cfe4a63e6af4ea1d310bd49be4f162a43/views/vector.ejs#L75-L87 -# (primary_color, secondary_color) -# Chosen by ChatGPT at this point. -COLORS: List[Tuple[str, str]] = [ - ("#FC49A3", "#A2FFCD"), # pink - ("#CC66FF", "#66CCFF"), # purple-ish - ("#66CCFF", "#FFCC66"), # sky blue - ("#66FFCC", "#FF6666"), # teal - ("#00FF00", "#FF8000"), # lime green - ("#FFCC66", "#66FFCC"), # light orange - ("#FF6666", "#66CCFF"), # salmon - ("#FF0000", "#66FFCC"), # red - ("#FF8000", "#FFFF66"), # orange - ("#FFFF66", "#FF8000"), # yellow - ("#00FFFF", "#FF6666"), # turquoise +COLORS = [ + "#FC49A3", # pink + "#CC66FF", # purple-ish + "#66CCFF", # sky blue + "#66FFCC", # teal + "#00FF00", # lime green + "#FFCC66", # light orange + "#FF6666", # salmon + "#FF0000", # red + "#FF8000", # orange + "#FFFF66", # yellow + "#00FFFF", # turquoise ] +DEFAULT_POLYGON_LINE_COLOR = [0, 0, 0, 200] def viz( @@ -139,7 +138,7 @@ def viz( layers = [ create_layer_from_data_input( item, - _viz_colors=color_ordering[i % len(color_ordering)], + _viz_color=color_ordering[i % len(color_ordering)], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, polygon_kwargs=polygon_kwargs, @@ -150,7 +149,7 @@ def viz( layers = [ create_layer_from_data_input( data, - _viz_colors=color_ordering[0], + _viz_color=color_ordering[0], scatterplot_kwargs=scatterplot_kwargs, path_kwargs=path_kwargs, polygon_kwargs=polygon_kwargs, @@ -357,7 +356,7 @@ def __arrow_c_array__(self, requested_schema): def _viz_geoarrow_table( table: pa.Table, *, - _viz_colors: Tuple[str, str], + _viz_color: str, scatterplot_kwargs: Optional[ScatterplotLayerKwargs] = None, path_kwargs: Optional[PathLayerKwargs] = None, polygon_kwargs: Optional[PolygonLayerKwargs] = None, @@ -373,7 +372,7 @@ def _viz_geoarrow_table( scatterplot_kwargs = {} if not scatterplot_kwargs else scatterplot_kwargs if "get_fill_color" not in scatterplot_kwargs.keys(): - scatterplot_kwargs["get_fill_color"] = _viz_colors[0] + scatterplot_kwargs["get_fill_color"] = _viz_color if "radius_min_pixels" not in scatterplot_kwargs.keys(): if len(table) <= 10_000: @@ -402,7 +401,7 @@ def _viz_geoarrow_table( path_kwargs = {} if not path_kwargs else path_kwargs if "get_color" not in path_kwargs.keys(): - path_kwargs["get_color"] = _viz_colors[0] + path_kwargs["get_color"] = _viz_color if "width_min_pixels" not in path_kwargs.keys(): if len(table) <= 1_000: @@ -428,23 +427,27 @@ def _viz_geoarrow_table( polygon_kwargs = {} if not polygon_kwargs else polygon_kwargs if "get_fill_color" not in polygon_kwargs.keys(): - polygon_kwargs["get_fill_color"] = _viz_colors[0] + polygon_kwargs["get_fill_color"] = _viz_color if "get_line_color" not in polygon_kwargs.keys(): - polygon_kwargs["get_line_color"] = _viz_colors[1] + polygon_kwargs["get_line_color"] = DEFAULT_POLYGON_LINE_COLOR if "opacity" not in polygon_kwargs.keys(): polygon_kwargs["opacity"] = 0.5 if "line_width_min_pixels" not in polygon_kwargs.keys(): + if len(table) <= 100: + polygon_kwargs["line_width_min_pixels"] = 0.5 if len(table) <= 1_000: - polygon_kwargs["line_width_min_pixels"] = 1 + polygon_kwargs["line_width_min_pixels"] = 0.45 + if len(table) <= 5_000: + polygon_kwargs["line_width_min_pixels"] = 0.4 elif len(table) <= 10_000: - polygon_kwargs["line_width_min_pixels"] = 0.8 + polygon_kwargs["line_width_min_pixels"] = 0.3 elif len(table) <= 100_000: - polygon_kwargs["line_width_min_pixels"] = 0.7 + polygon_kwargs["line_width_min_pixels"] = 0.25 else: - polygon_kwargs["line_width_min_pixels"] = 0.5 + polygon_kwargs["line_width_min_pixels"] = 0.2 return PolygonLayer(table=table, **polygon_kwargs) From 68d537ec144bda1319920e10325ddc4dcec508f9 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 28 Mar 2024 10:47:32 -0400 Subject: [PATCH 10/10] fix tests --- tests/test_viz.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_viz.py b/tests/test_viz.py index 9a547b04..a3f58777 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -3,14 +3,14 @@ from geoarrow.rust.core import read_pyogrio from pyogrio.raw import read_arrow -from lonboard import SolidPolygonLayer, viz +from lonboard import PolygonLayer, viz def test_viz_wkb_pyarrow(): path = geodatasets.get_path("naturalearth.land") meta, table = read_arrow(path) map_ = viz(table) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_reproject(): @@ -37,19 +37,19 @@ def __geo_interface__(self): geo_interface_obj = GeoInterfaceHolder(gdf.geometry[0]) map_ = viz(geo_interface_obj) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_table(): table = read_pyogrio(geodatasets.get_path("naturalearth.land")) map_ = viz(table) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_array(): table = read_pyogrio(geodatasets.get_path("naturalearth.land")) map_ = viz(table.geometry.chunk(0)) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer) def test_viz_geoarrow_rust_wkb_array(): @@ -57,4 +57,4 @@ def test_viz_geoarrow_rust_wkb_array(): arr = table.geometry.chunk(0) wkb_arr = arr.to_wkb() map_ = viz(wkb_arr) - assert isinstance(map_.layers[0], SolidPolygonLayer) + assert isinstance(map_.layers[0], PolygonLayer)