From d3778cf7f5e73c14c21ef424f85e2c7ab0d961ed Mon Sep 17 00:00:00 2001
From: "Juan D. Jara"
Date: Thu, 23 Jan 2025 13:05:03 +0100
Subject: [PATCH 1/6] WIP tileset example
---
packages/create-react/package.json | 2 +-
.../src/components/views/IncomeView.tsx | 194 +++++++++++++++
packages/create-react/src/routes.tsx | 4 +
yarn.lock | 222 +++++++++++++++++-
4 files changed, 418 insertions(+), 4 deletions(-)
create mode 100644 packages/create-react/src/components/views/IncomeView.tsx
diff --git a/packages/create-react/package.json b/packages/create-react/package.json
index 849237a..c0a500a 100644
--- a/packages/create-react/package.json
+++ b/packages/create-react/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
- "@carto/api-client": "^0.4.5",
+ "@carto/api-client": "0.5.0-alpha.1",
"@carto/create-common": "^0.1.3",
"@deck.gl/aggregation-layers": "^9.0.40",
"@deck.gl/carto": "^9.0.40",
diff --git a/packages/create-react/src/components/views/IncomeView.tsx b/packages/create-react/src/components/views/IncomeView.tsx
new file mode 100644
index 0000000..ebac334
--- /dev/null
+++ b/packages/create-react/src/components/views/IncomeView.tsx
@@ -0,0 +1,194 @@
+import { Color, MapView, MapViewState, WebMercatorViewport } from "@deck.gl/core";
+import { useContext, useEffect, useMemo, useState } from "react";
+import { AppContext } from "../../context";
+import { useDebouncedState } from "../../hooks/useDebouncedState";
+import { createViewportSpatialFilter, vectorTilesetSource } from "@carto/api-client";
+import { VectorTileLayer } from "@deck.gl/carto";
+import { Card } from "../Card";
+import { FormulaWidget } from "../widgets/FormulaWidget";
+import DeckGL from "@deck.gl/react";
+import { Map } from 'react-map-gl/maplibre';
+import { Layers } from "../Layers";
+import { LegendEntryCategorical } from "../legends/LegendEntryCategorical";
+
+const MAP_VIEW = new MapView({ repeat: true });
+const MAP_STYLE =
+ 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
+
+const INITIAL_VIEW_STATE: MapViewState = {
+ latitude: 31.8028,
+ longitude: -103.0078,
+ zoom: 2,
+};
+
+const histogramTicks = [15000, 20000, 23000, 26000, 30000, 34000, 40000, 50000];
+
+const colors = [
+ '#f7fcf0',
+ '#e0f3db',
+ '#ccebc5',
+ '#a8ddb5',
+ '#7bccc4',
+ '#4eb3d3',
+ '#2b8cbe',
+ '#08589e',
+].map((hex) => hexToRgb(hex));
+
+function hexToRgb(hex: string) {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return [r, g, b];
+}
+
+/**
+ * Example application page, showing U.S. income by block group.
+ */
+export default function IncomeView() {
+ // With authentication enabled, access token may change.
+ const { accessToken, apiBaseUrl } = useContext(AppContext);
+ const [attributionHTML, setAttributionHTML] = useState('');
+ // Debounce view state to avoid excessive re-renders during pan and zoom.
+ const [viewState, setViewState] = useDebouncedState(INITIAL_VIEW_STATE, 200);
+
+ /****************************************************************************
+ * Sources (https://deck.gl/docs/api-reference/carto/data-sources)
+ */
+
+ const data = useMemo(
+ () =>
+ vectorTilesetSource({
+ accessToken,
+ apiBaseUrl,
+ connectionName: 'carto_dw',
+ tableName: 'carto-demo-data.demo_tilesets.sociodemographics_usa_blockgroup',
+ }),
+ [accessToken, apiBaseUrl],
+ );
+
+ /****************************************************************************
+ * Layers (https://deck.gl/docs/api-reference/carto/overview#carto-layers)
+ */
+
+ const LAYER_ID = 'Income by block group'
+
+ // Layer visibility represented as name/visibility pairs, managed by the Layers component.
+ const [layerVisibility, setLayerVisibility] = useState<
+ Record
+ >({
+ [LAYER_ID]: true,
+ });
+
+ // Update layers when data or visualization parameters change.
+ const layers = useMemo(() => {
+ return [
+ new VectorTileLayer({
+ id: LAYER_ID,
+ pickable: true,
+ visible: layerVisibility[LAYER_ID],
+ data,
+ getLineColor: [0, 0 ,0],
+ lineWidthMinPixels: 0.3,
+ getFillColor: d => {
+ const n = d.properties.income_per_capita;
+ const index = histogramTicks.slice().reverse().findIndex((tick) => n >= tick);
+ const color = colors[index] || colors[colors.length - 1];
+ return color as Color;
+ },
+ onViewportLoad(tiles) {
+ data?.then((res) => {
+ res.widgetSource.loadTiles(tiles)
+ const bbox = new WebMercatorViewport(viewState).getBounds()
+ const spatialFilter = createViewportSpatialFilter(bbox)
+ if (spatialFilter) {
+ res.widgetSource.extractTileFeatures({ spatialFilter })
+ }
+ })
+ },
+ }),
+ ];
+ }, [data, layerVisibility]);
+
+ /****************************************************************************
+ * Attribution
+ */
+
+ useEffect(() => {
+ data?.then(({ attribution }) => setAttributionHTML(attribution));
+ }, [data]);
+
+ useEffect(() => {
+ if (data && viewState) {
+ data?.then((res) => {
+ const bbox = new WebMercatorViewport(viewState).getBounds()
+ const spatialFilter = createViewportSpatialFilter(bbox)
+ if (spatialFilter) {
+ res.widgetSource.extractTileFeatures({ spatialFilter })
+ }
+ })
+ }
+ }, [data, viewState])
+
+ return (
+ <>
+
+
+ setViewState(viewState)}
+ >
+
+
+
+
+
+ colors[histogramTicks.indexOf(Number(value))] as Color
+ }
+ />
+
+
+
+ >
+ )
+}
diff --git a/packages/create-react/src/routes.tsx b/packages/create-react/src/routes.tsx
index 4e54c7f..a0a4cc3 100644
--- a/packages/create-react/src/routes.tsx
+++ b/packages/create-react/src/routes.tsx
@@ -6,6 +6,7 @@ import PopulationView from './components/views/PopulationView';
import NotFoundView from './components/views/NotFoundView';
import LoginView from './components/views/LoginView';
import LogoutView from './components/views/LogoutView';
+import IncomeView from './components/views/IncomeView';
/**
* Available paths (URLs) in the application.
@@ -13,6 +14,7 @@ import LogoutView from './components/views/LogoutView';
export const RoutePath = {
CELL_TOWERS: '/',
POPULATION: '/usa-population',
+ INCOME: '/income',
LOGIN: '/login',
LOGOUT: '/logout',
NOT_FOUND: '/404',
@@ -24,6 +26,7 @@ export const RoutePath = {
export const NAV_ROUTES: { text: string; path: string }[] = [
{ text: 'Cell towers', path: RoutePath.CELL_TOWERS },
{ text: 'U.S. population', path: RoutePath.POPULATION },
+ { text: 'Income', path: RoutePath.INCOME },
];
/**
@@ -40,6 +43,7 @@ export const routes: RouteObject[] = [
children: [
{ path: RoutePath.CELL_TOWERS, element: },
{ path: RoutePath.POPULATION, element: },
+ { path: RoutePath.INCOME, element: },
],
},
{ path: RoutePath.LOGIN, element: },
diff --git a/yarn.lock b/yarn.lock
index d3535a8..71d0b74 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2586,6 +2586,25 @@ __metadata:
languageName: node
linkType: hard
+"@carto/api-client@npm:0.5.0-alpha.1":
+ version: 0.5.0-alpha.1
+ resolution: "@carto/api-client@npm:0.5.0-alpha.1"
+ dependencies:
+ "@loaders.gl/schema": "npm:^4.3.3"
+ "@turf/bbox-clip": "npm:^7.1.0"
+ "@turf/bbox-polygon": "npm:^7.1.0"
+ "@turf/boolean-intersects": "npm:^7.1.0"
+ "@turf/boolean-within": "npm:^7.1.0"
+ "@turf/helpers": "npm:^7.1.0"
+ "@turf/intersect": "npm:^7.1.0"
+ "@turf/invariant": "npm:^7.1.0"
+ "@turf/union": "npm:^7.1.0"
+ "@types/geojson": "npm:^7946.0.15"
+ h3-js: "npm:4.1.0"
+ checksum: 10c0/8b8cfbfe5d3a73bdfb2491a792d5e007e1605473dd4f824c3a7d56f0ec811ca1e9fbeeb35c799b72136ad5b76643004ac2c2c683ece5d22d285948d93de2e6df
+ languageName: node
+ linkType: hard
+
"@carto/api-client@npm:^0.4.5":
version: 0.4.5
resolution: "@carto/api-client@npm:0.4.5"
@@ -2658,7 +2677,7 @@ __metadata:
resolution: "@carto/create-react@workspace:packages/create-react"
dependencies:
"@auth0/auth0-react": "npm:^2.2.4"
- "@carto/api-client": "npm:^0.4.5"
+ "@carto/api-client": "npm:0.5.0-alpha.1"
"@carto/create-common": "npm:^0.1.3"
"@deck.gl/aggregation-layers": "npm:^9.0.40"
"@deck.gl/carto": "npm:^9.0.40"
@@ -4017,7 +4036,7 @@ __metadata:
languageName: node
linkType: hard
-"@loaders.gl/schema@npm:4.3.3":
+"@loaders.gl/schema@npm:4.3.3, @loaders.gl/schema@npm:^4.3.3":
version: 4.3.3
resolution: "@loaders.gl/schema@npm:4.3.3"
dependencies:
@@ -5295,6 +5314,18 @@ __metadata:
languageName: node
linkType: hard
+"@turf/bbox@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/bbox@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/meta": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/766d59d5f75c272481e971cd4004e139962607e8f34391b2abfb15bb34f9544a0479ceb14772565e005e4a12fdd82adf0d440ab1c9e0decbde6de50a5706db43
+ languageName: node
+ linkType: hard
+
"@turf/boolean-clockwise@npm:^5.1.5":
version: 5.1.5
resolution: "@turf/boolean-clockwise@npm:5.1.5"
@@ -5305,6 +5336,74 @@ __metadata:
languageName: node
linkType: hard
+"@turf/boolean-disjoint@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/boolean-disjoint@npm:7.2.0"
+ dependencies:
+ "@turf/boolean-point-in-polygon": "npm:^7.2.0"
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/line-intersect": "npm:^7.2.0"
+ "@turf/meta": "npm:^7.2.0"
+ "@turf/polygon-to-line": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/f9e27f3b46fdddd5e96e67e7f130e25678922c843afd5d898a1c13b599285b126a8ab9df5bd5e76454e88deacc359ebc1fa47d50cbd21f64c0547201d0fb9118
+ languageName: node
+ linkType: hard
+
+"@turf/boolean-intersects@npm:^7.1.0":
+ version: 7.2.0
+ resolution: "@turf/boolean-intersects@npm:7.2.0"
+ dependencies:
+ "@turf/boolean-disjoint": "npm:^7.2.0"
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/meta": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/2b4ef09eb77fa02db9671e5c4d0c0c4133a815bbd9a744e70e65a3b37a8b6a13c26d51dc77de0822f39ed2b882646f416fa07561888d4813bb4d31b7003fcba5
+ languageName: node
+ linkType: hard
+
+"@turf/boolean-point-in-polygon@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/boolean-point-in-polygon@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/invariant": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ point-in-polygon-hao: "npm:^1.1.0"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/eeb431f70175e466205b27076dbc331b862d3e6867d43730ac4099ecd7e6d6f0fed9e3301fd29f6375f340e379f477489f09060e6aec9fc0f7afa01ac58d5c09
+ languageName: node
+ linkType: hard
+
+"@turf/boolean-point-on-line@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/boolean-point-on-line@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/invariant": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/a7f81170ff795025f209602559a289330304adf046340133e1b18f950aca386e8f2d4f1f91b85c2456306d95359fdd19b8323c13f88327491dbf2f196fa1e635
+ languageName: node
+ linkType: hard
+
+"@turf/boolean-within@npm:^7.1.0":
+ version: 7.2.0
+ resolution: "@turf/boolean-within@npm:7.2.0"
+ dependencies:
+ "@turf/bbox": "npm:^7.2.0"
+ "@turf/boolean-point-in-polygon": "npm:^7.2.0"
+ "@turf/boolean-point-on-line": "npm:^7.2.0"
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/invariant": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/2aed21d55b820a19d1d7f22f521fdd9c780ba8ab036828ac2a316efea984d3c70c71190aeaa5766fce922b5dcd52987d86dc0fcb27a0d50306a9e7e270cf80ce
+ languageName: node
+ linkType: hard
+
"@turf/clone@npm:^5.1.5":
version: 5.1.5
resolution: "@turf/clone@npm:5.1.5"
@@ -5331,6 +5430,29 @@ __metadata:
languageName: node
linkType: hard
+"@turf/helpers@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/helpers@npm:7.2.0"
+ dependencies:
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/4d6f57164cca00ec7a18e2d3c0200d0274e4ab2b6b3201c6a867b867d899f3f618a82ae242617d467efb04f32740cec150ae06a0e07ee39318397ebc34914687
+ languageName: node
+ linkType: hard
+
+"@turf/intersect@npm:^7.1.0":
+ version: 7.2.0
+ resolution: "@turf/intersect@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/meta": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ polyclip-ts: "npm:^0.16.8"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/16c39a87819c0173572e1030e29eca1c522115f9aa2c07a3baa00b6b28626573e6e54b428cd919d055f80e49ab1dd90c17166f5e8ba46eb3da738d1a8c8bbd71
+ languageName: node
+ linkType: hard
+
"@turf/invariant@npm:^5.1.5":
version: 5.2.0
resolution: "@turf/invariant@npm:5.2.0"
@@ -5351,6 +5473,29 @@ __metadata:
languageName: node
linkType: hard
+"@turf/invariant@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/invariant@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/6968b766d5522488bb2b149a72f863e3f0d4bb0342b14f3992e3e920d9e32c252e1e07e84213732cb51609aef7a82833a8fe76b1c7d0db4cd384954cfedaa4e5
+ languageName: node
+ linkType: hard
+
+"@turf/line-intersect@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/line-intersect@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ sweepline-intersections: "npm:^1.5.0"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/d2ed0159ce84e179f999ed461c5481f063c813bedfdfb4af45e46432503b0acd240128be5c6c2d324e05edc4981fd806a41ee0282567c5d0c80c223497e40cb4
+ languageName: node
+ linkType: hard
+
"@turf/meta@npm:^5.1.5":
version: 5.2.0
resolution: "@turf/meta@npm:5.2.0"
@@ -5370,6 +5515,28 @@ __metadata:
languageName: node
linkType: hard
+"@turf/meta@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/meta@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ checksum: 10c0/707ed63ba64fe48769806bf2419f5c0cd2ebf821a6467aeffb784ba7ebd6a63ec98d4192b97915948529c00304ed46ddc83842a80714fb1f2018fd4e3c455498
+ languageName: node
+ linkType: hard
+
+"@turf/polygon-to-line@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "@turf/polygon-to-line@npm:7.2.0"
+ dependencies:
+ "@turf/helpers": "npm:^7.2.0"
+ "@turf/invariant": "npm:^7.2.0"
+ "@types/geojson": "npm:^7946.0.10"
+ tslib: "npm:^2.8.1"
+ checksum: 10c0/b840b218e631a252655d233b2c569607d4d3e8d3715a1eab7d26394e4acc1f325b736837d2a82e5c4f678600b2ddc32004782ed3825ca5b1027dce12a4e4cde9
+ languageName: node
+ linkType: hard
+
"@turf/rewind@npm:^5.1.5":
version: 5.1.5
resolution: "@turf/rewind@npm:5.1.5"
@@ -7149,6 +7316,13 @@ __metadata:
languageName: node
linkType: hard
+"bignumber.js@npm:^9.1.0":
+ version: 9.1.2
+ resolution: "bignumber.js@npm:9.1.2"
+ checksum: 10c0/e17786545433f3110b868725c449fa9625366a6e675cd70eb39b60938d6adbd0158cb4b3ad4f306ce817165d37e63f4aa3098ba4110db1d9a3b9f66abfbaf10d
+ languageName: node
+ linkType: hard
+
"bin-links@npm:^4.0.4":
version: 4.0.4
resolution: "bin-links@npm:4.0.4"
@@ -10428,7 +10602,7 @@ __metadata:
languageName: node
linkType: hard
-"h3-js@npm:^4.1.0":
+"h3-js@npm:4.1.0, h3-js@npm:^4.1.0":
version: 4.1.0
resolution: "h3-js@npm:4.1.0"
checksum: 10c0/58adb7c7a19ff6e6959ef97a60b383bb06c560ffd12826ce603470b25bb34bb26b9bedee6e406591b97017fb888a6194b4043d16abd0d100e6cb51d449e9c3d1
@@ -14152,6 +14326,25 @@ __metadata:
languageName: node
linkType: hard
+"point-in-polygon-hao@npm:^1.1.0":
+ version: 1.2.4
+ resolution: "point-in-polygon-hao@npm:1.2.4"
+ dependencies:
+ robust-predicates: "npm:^3.0.2"
+ checksum: 10c0/12badef8b15216acdae7d609a8aca1b916d6a9cac2f53ace2928e3b521bee57224489ea8fba41d001c0c05c50e64152c175e3d6583d8cad6fe7af929f082b49a
+ languageName: node
+ linkType: hard
+
+"polyclip-ts@npm:^0.16.8":
+ version: 0.16.8
+ resolution: "polyclip-ts@npm:0.16.8"
+ dependencies:
+ bignumber.js: "npm:^9.1.0"
+ splaytree-ts: "npm:^1.0.2"
+ checksum: 10c0/8de02c32e566421875c70890fe6d3d7bd55a92fc39867446e65999e042c4632fb27ab1a7e106e96edd12d63b32a643f3a690d9a980aeda655a41ddf4e8750f5d
+ languageName: node
+ linkType: hard
+
"polygon-clipping@npm:^0.15.3":
version: 0.15.7
resolution: "polygon-clipping@npm:0.15.7"
@@ -16224,6 +16417,13 @@ __metadata:
languageName: node
linkType: hard
+"splaytree-ts@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "splaytree-ts@npm:1.0.2"
+ checksum: 10c0/4c34573891a749055ffe22381ea841ae03dbf7b55a3ea4a1d7df5013bfbe128e79f6c468579358ec573734cdbdf5f77a97ae7185f155248eb8be58151428c85e
+ languageName: node
+ linkType: hard
+
"splaytree@npm:^3.1.0":
version: 3.1.2
resolution: "splaytree@npm:3.1.2"
@@ -16584,6 +16784,15 @@ __metadata:
languageName: node
linkType: hard
+"sweepline-intersections@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "sweepline-intersections@npm:1.5.0"
+ dependencies:
+ tinyqueue: "npm:^2.0.0"
+ checksum: 10c0/587a597c75b787e61054ef88b98463af47f60855265b7829fa8acc5ebe68fb4bc3d148a80e9f72c69c16a0241bfed38d3fbbe93a735ea5a2276c00116adc5283
+ languageName: node
+ linkType: hard
+
"symbol-observable@npm:4.0.0":
version: 4.0.0
resolution: "symbol-observable@npm:4.0.0"
@@ -16772,6 +16981,13 @@ __metadata:
languageName: node
linkType: hard
+"tinyqueue@npm:^2.0.0":
+ version: 2.0.3
+ resolution: "tinyqueue@npm:2.0.3"
+ checksum: 10c0/d7b590088f015a94a17132fa209c2f2a80c45158259af5474901fdf5932e95ea13ff6f034bcc725a6d5f66d3e5b888b048c310229beb25ad5bebb4f0a635abf2
+ languageName: node
+ linkType: hard
+
"tinyqueue@npm:^3.0.0":
version: 3.0.0
resolution: "tinyqueue@npm:3.0.0"
From 37dcb4988f5d081edc7a59d14e6a10da20e16748 Mon Sep 17 00:00:00 2001
From: "Juan D. Jara"
Date: Wed, 29 Jan 2025 16:51:53 +0100
Subject: [PATCH 2/6] dropped features data
---
.../src/components/views/IncomeView.tsx | 30 +++++++++++++++++--
1 file changed, 28 insertions(+), 2 deletions(-)
diff --git a/packages/create-react/src/components/views/IncomeView.tsx b/packages/create-react/src/components/views/IncomeView.tsx
index ebac334..eaa47e5 100644
--- a/packages/create-react/src/components/views/IncomeView.tsx
+++ b/packages/create-react/src/components/views/IncomeView.tsx
@@ -48,6 +48,12 @@ export default function IncomeView() {
// With authentication enabled, access token may change.
const { accessToken, apiBaseUrl } = useContext(AppContext);
const [attributionHTML, setAttributionHTML] = useState('');
+
+ // data to calculate feature dropping for each zoom level
+ const [fractionsDropped, setFractionsDropped] = useState([]);
+ const [minZoom, setMinZoom] = useState(0);
+ const [maxZoom, setMaxZoom] = useState(20);
+
// Debounce view state to avoid excessive re-renders during pan and zoom.
const [viewState, setViewState] = useDebouncedState(INITIAL_VIEW_STATE, 200);
@@ -114,7 +120,13 @@ export default function IncomeView() {
*/
useEffect(() => {
- data?.then(({ attribution }) => setAttributionHTML(attribution));
+ data?.then((res) => {
+ const { fraction_dropped_per_zoom, minzoom, maxzoom, attribution } = res
+ setFractionsDropped(fraction_dropped_per_zoom ?? [])
+ setMinZoom(minzoom ?? 0)
+ setMaxZoom(maxzoom ?? 20)
+ setAttributionHTML(attribution)
+ })
}, [data]);
useEffect(() => {
@@ -128,7 +140,21 @@ export default function IncomeView() {
})
}
}, [data, viewState])
-
+
+ function clamp(n: number, min: number, max: number) {
+ return Math.min(Math.max(n, min), max);
+ }
+
+ const droppingPercent = useMemo(() => {
+ if (!fractionsDropped.length) {
+ return 0
+ }
+ const roundedZoom = Math.round(viewState.zoom)
+ const clampedZoom = clamp(roundedZoom, minZoom, maxZoom)
+ const percent = fractionsDropped[clampedZoom]
+ return percent
+ }, [minZoom, maxZoom, fractionsDropped, viewState.zoom])
+
return (
<>
-
-
-
-
-
+ {tilesLoaded && (
+ <>
+ {droppingPercent > 0 && droppingPercent <= 0.05 && (
+
+ Warning: There may be some data missing at this zoom level because of the tileset dropping features.
+
+ )}
+ {droppingPercent > 0.05 && (
+
+ Warning: There is an important amount of data missing at this zoom level because of the tileset dropping features. Widget calculations will not be accurate.
+
+ )}
+
+
+
+ {/*
+
+ */}
+ >
+ )}
;
+ /** Column containing a value to be aggregated. */
+ operationColumn: string;
/** Map view state. If specified, widget will be filtered to the view. */
viewState?: MapViewState;
/** Filter state. If specified, widget will be filtered. */
@@ -42,6 +44,7 @@ export function CategoryWidget({
data,
column,
operation,
+ operationColumn,
viewState,
filters,
onFiltersChange,
@@ -66,6 +69,7 @@ export function CategoryWidget({
widgetSource.getCategories({
column,
operation,
+ operationColumn,
spatialFilter: viewState && createSpatialFilter(viewState),
abortController,
filterOwner: owner,
From 09b826d5bd9204d4745b6b434c212c840c736d2e Mon Sep 17 00:00:00 2001
From: "Juan D. Jara"
Date: Thu, 6 Feb 2025 15:16:09 +0100
Subject: [PATCH 4/6] use same tileset as deck.gl example
---
packages/create-react/.env | 2 +-
.../views/{IncomeView.tsx => RiversView.tsx} | 45 ++++++++++---------
packages/create-react/src/routes.tsx | 8 ++--
3 files changed, 30 insertions(+), 25 deletions(-)
rename packages/create-react/src/components/views/{IncomeView.tsx => RiversView.tsx} (83%)
diff --git a/packages/create-react/.env b/packages/create-react/.env
index ad068d2..1809182 100644
--- a/packages/create-react/.env
+++ b/packages/create-react/.env
@@ -1,5 +1,5 @@
VITE_APP_TITLE="React template"
-VITE_ACCESS_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfNDd1dW5tZWciLCJqdGkiOiJhZjRlM2QxMSJ9.e1GDIOtg3Jy2zcwbFpAxsvK38RqycRrWII1NVTH7KtQ"
+VITE_ACCESS_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfN3hoZnd5bWwiLCJqdGkiOiIwNjU0ZTQ5MyJ9.WoBKztKg5ExQFkUsbAFbwjSZK9nV0ESE9S0_hWS5WgE"
VITE_API_BASE_URL="https://gcp-us-east1.api.carto.com"
VITE_AUTH_ENABLED="false"
diff --git a/packages/create-react/src/components/views/IncomeView.tsx b/packages/create-react/src/components/views/RiversView.tsx
similarity index 83%
rename from packages/create-react/src/components/views/IncomeView.tsx
rename to packages/create-react/src/components/views/RiversView.tsx
index eaa7964..664d995 100644
--- a/packages/create-react/src/components/views/IncomeView.tsx
+++ b/packages/create-react/src/components/views/RiversView.tsx
@@ -3,18 +3,18 @@ import { useContext, useEffect, useMemo, useState } from "react";
import { AppContext } from "../../context";
import { useDebouncedState } from "../../hooks/useDebouncedState";
import { createViewportSpatialFilter, vectorTilesetSource } from "@carto/api-client";
-import { VectorTileLayer } from "@deck.gl/carto";
+import { BASEMAP, VectorTileLayer } from "@deck.gl/carto";
import { Card } from "../Card";
import { FormulaWidget } from "../widgets/FormulaWidget";
import DeckGL from "@deck.gl/react";
import { Map } from 'react-map-gl/maplibre';
import { Layers } from "../Layers";
import { LegendEntryCategorical } from "../legends/LegendEntryCategorical";
-import { CategoryWidget } from "../widgets/CategoryWidget";
+// import { CategoryWidget } from "../widgets/CategoryWidget";
+const CONNECTION_NAME = 'amanzanares-pm-bq';
+const TILESET_NAME = 'cartodb-on-gcp-pm-team.amanzanares_opensource_demo.national_water_model_tileset_final_test_4';
const MAP_VIEW = new MapView({ repeat: true });
-const MAP_STYLE =
- 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
const INITIAL_VIEW_STATE: MapViewState = {
latitude: 31.8028,
@@ -43,7 +43,7 @@ function hexToRgb(hex: string) {
}
/**
- * Example application page, showing U.S. income by block group.
+ * Example application page, showing U.S. streams network.
*/
export default function IncomeView() {
// With authentication enabled, access token may change.
@@ -68,8 +68,8 @@ export default function IncomeView() {
vectorTilesetSource({
accessToken,
apiBaseUrl,
- connectionName: 'carto_dw',
- tableName: 'carto-demo-data.demo_tilesets.sociodemographics_usa_blockgroup',
+ connectionName: CONNECTION_NAME,
+ tableName: TILESET_NAME,
}),
[accessToken, apiBaseUrl],
);
@@ -95,14 +95,19 @@ export default function IncomeView() {
pickable: true,
visible: layerVisibility[LAYER_ID],
data,
- getLineColor: [0, 0 ,0],
- lineWidthMinPixels: 0.3,
- getFillColor: d => {
- const n = d.properties.income_per_capita;
- const index = histogramTicks.slice().reverse().findIndex((tick) => n >= tick);
- const color = colors[index] || colors[colors.length - 1];
- return color as Color;
+ getLineColor: d => {
+ const [r, g, b] = hexToRgb('#d5d5d7');
+ const n = d.properties.streamOrder;
+ const alphaPart = Math.min(n / 10, 1);
+ const alpha = 120 + 128 * alphaPart;
+ return [r, g, b, alpha];
},
+ getLineWidth: d => {
+ const n = d.properties.streamOrder;
+ return n * 0.5;
+ },
+ lineWidthUnits: 'pixels',
+ lineWidthMinPixels: 1,
onViewportLoad(tiles) {
data?.then((res) => {
setTilesLoaded(true)
@@ -163,7 +168,7 @@ export default function IncomeView() {
},
{ path: RoutePath.POPULATION, element: },
- { path: RoutePath.INCOME, element: },
+ { path: RoutePath.RIVERS, element: },
],
},
{ path: RoutePath.LOGIN, element: },
From 6dda023b64d29326c2b377d5561d3781a7b99cae Mon Sep 17 00:00:00 2001
From: "Juan D. Jara"
Date: Thu, 6 Feb 2025 15:26:06 +0100
Subject: [PATCH 5/6] adapt new widgets interface
---
packages/create-react/package.json | 2 +-
.../src/components/widgets/CategoryWidget.tsx | 5 +++--
.../src/components/widgets/FormulaWidget.tsx | 6 +++---
yarn.lock | 10 +++++-----
4 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/packages/create-react/package.json b/packages/create-react/package.json
index c0a500a..a34e9ef 100644
--- a/packages/create-react/package.json
+++ b/packages/create-react/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
- "@carto/api-client": "0.5.0-alpha.1",
+ "@carto/api-client": "0.5.0-alpha.2",
"@carto/create-common": "^0.1.3",
"@deck.gl/aggregation-layers": "^9.0.40",
"@deck.gl/carto": "^9.0.40",
diff --git a/packages/create-react/src/components/widgets/CategoryWidget.tsx b/packages/create-react/src/components/widgets/CategoryWidget.tsx
index f9ea644..59c6803 100644
--- a/packages/create-react/src/components/widgets/CategoryWidget.tsx
+++ b/packages/create-react/src/components/widgets/CategoryWidget.tsx
@@ -9,6 +9,7 @@ import {
removeFilter,
getFilter,
hasFilter,
+ WidgetSourceProps,
} from '@carto/api-client';
import {
createSpatialFilter,
@@ -21,7 +22,7 @@ const { IN } = FilterType;
export interface CategoryWidgetProps {
/** Widget-compatible data source, from vectorTableSource, vectorQuerySource, etc. */
- data: Promise<{ widgetSource: WidgetSource }>;
+ data: Promise<{ widgetSource: WidgetSource }>;
/** Column containing category names. */
column: string;
/** Operation used to aggregate features in each category. */
@@ -88,7 +89,7 @@ export function CategoryWidget({
setStatus('loading');
return () => abortController.abort();
- }, [data, column, operation, viewState, owner]);
+ }, [data, column, operation, operationColumn, viewState, owner]);
// Compute min/max over category values.
const [min, max] = useMemo(() => {
diff --git a/packages/create-react/src/components/widgets/FormulaWidget.tsx b/packages/create-react/src/components/widgets/FormulaWidget.tsx
index 4aee4de..20f83e5 100644
--- a/packages/create-react/src/components/widgets/FormulaWidget.tsx
+++ b/packages/create-react/src/components/widgets/FormulaWidget.tsx
@@ -1,5 +1,5 @@
import { MapViewState } from '@deck.gl/core';
-import { AggregationType, WidgetSource } from '@carto/api-client';
+import { AggregationType, WidgetSource, WidgetSourceProps } from '@carto/api-client';
import { useEffect, useState } from 'react';
import {
createSpatialFilter,
@@ -9,7 +9,7 @@ import {
export interface FormulaWidgetProps {
/** Widget-compatible data source, from vectorTableSource, vectorQuerySource, etc. */
- data: Promise<{ widgetSource: WidgetSource }>;
+ data: Promise<{ widgetSource: WidgetSource }>;
/** Column containing a value to be aggregated. */
column: string;
/** Operation used to aggregate the specified column. */
@@ -45,7 +45,7 @@ export function FormulaWidget({
}),
)
.then((response) => {
- setValue(response.value);
+ setValue(response.value ?? 0);
setStatus('complete');
})
.catch(() => {
diff --git a/yarn.lock b/yarn.lock
index 71d0b74..659ce05 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2586,9 +2586,9 @@ __metadata:
languageName: node
linkType: hard
-"@carto/api-client@npm:0.5.0-alpha.1":
- version: 0.5.0-alpha.1
- resolution: "@carto/api-client@npm:0.5.0-alpha.1"
+"@carto/api-client@npm:0.5.0-alpha.2":
+ version: 0.5.0-alpha.2
+ resolution: "@carto/api-client@npm:0.5.0-alpha.2"
dependencies:
"@loaders.gl/schema": "npm:^4.3.3"
"@turf/bbox-clip": "npm:^7.1.0"
@@ -2601,7 +2601,7 @@ __metadata:
"@turf/union": "npm:^7.1.0"
"@types/geojson": "npm:^7946.0.15"
h3-js: "npm:4.1.0"
- checksum: 10c0/8b8cfbfe5d3a73bdfb2491a792d5e007e1605473dd4f824c3a7d56f0ec811ca1e9fbeeb35c799b72136ad5b76643004ac2c2c683ece5d22d285948d93de2e6df
+ checksum: 10c0/fc67454045f90311bdbba361f28a650b096bae3c5fdb4befcde595a8b1b492e551c95935b9c10a10aeb83166240649d59533c07d7d7d725db06be764fd768ca7
languageName: node
linkType: hard
@@ -2677,7 +2677,7 @@ __metadata:
resolution: "@carto/create-react@workspace:packages/create-react"
dependencies:
"@auth0/auth0-react": "npm:^2.2.4"
- "@carto/api-client": "npm:0.5.0-alpha.1"
+ "@carto/api-client": "npm:0.5.0-alpha.2"
"@carto/create-common": "npm:^0.1.3"
"@deck.gl/aggregation-layers": "npm:^9.0.40"
"@deck.gl/carto": "npm:^9.0.40"
From b92d767f5041b6879bd94662011b20ff1a97c7d0 Mon Sep 17 00:00:00 2001
From: "Juan D. Jara"
Date: Thu, 6 Feb 2025 16:51:23 +0100
Subject: [PATCH 6/6] first try at histogram widget for app templates
---
packages/create-react/package.json | 1 +
.../src/components/views/RiversView.tsx | 37 ++--
.../src/components/widgets/FormulaWidget.tsx | 4 +-
.../components/widgets/HistogramWidget.tsx | 184 ++++++++++++++++++
yarn.lock | 27 +++
5 files changed, 232 insertions(+), 21 deletions(-)
create mode 100644 packages/create-react/src/components/widgets/HistogramWidget.tsx
diff --git a/packages/create-react/package.json b/packages/create-react/package.json
index a34e9ef..24f0ccb 100644
--- a/packages/create-react/package.json
+++ b/packages/create-react/package.json
@@ -30,6 +30,7 @@
"@loaders.gl/core": "^4.3.3",
"@luma.gl/core": "^9.0.27",
"@luma.gl/engine": "^9.0.27",
+ "echarts": "^5.6.0",
"maplibre-gl": "^4.7.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/packages/create-react/src/components/views/RiversView.tsx b/packages/create-react/src/components/views/RiversView.tsx
index 664d995..b07df1a 100644
--- a/packages/create-react/src/components/views/RiversView.tsx
+++ b/packages/create-react/src/components/views/RiversView.tsx
@@ -1,4 +1,4 @@
-import { Color, MapView, MapViewState, WebMercatorViewport } from "@deck.gl/core";
+import { MapView, MapViewState, WebMercatorViewport } from "@deck.gl/core";
import { useContext, useEffect, useMemo, useState } from "react";
import { AppContext } from "../../context";
import { useDebouncedState } from "../../hooks/useDebouncedState";
@@ -9,8 +9,7 @@ import { FormulaWidget } from "../widgets/FormulaWidget";
import DeckGL from "@deck.gl/react";
import { Map } from 'react-map-gl/maplibre';
import { Layers } from "../Layers";
-import { LegendEntryCategorical } from "../legends/LegendEntryCategorical";
-// import { CategoryWidget } from "../widgets/CategoryWidget";
+import { HistogramWidget } from "../widgets/HistogramWidget";
const CONNECTION_NAME = 'amanzanares-pm-bq';
const TILESET_NAME = 'cartodb-on-gcp-pm-team.amanzanares_opensource_demo.national_water_model_tileset_final_test_4';
@@ -19,21 +18,10 @@ const MAP_VIEW = new MapView({ repeat: true });
const INITIAL_VIEW_STATE: MapViewState = {
latitude: 31.8028,
longitude: -103.0078,
- zoom: 2,
+ zoom: 4,
};
-const histogramTicks = [15000, 20000, 23000, 26000, 30000, 34000, 40000, 50000];
-
-const colors = [
- '#f7fcf0',
- '#e0f3db',
- '#ccebc5',
- '#a8ddb5',
- '#7bccc4',
- '#4eb3d3',
- '#2b8cbe',
- '#08589e',
-].map((hex) => hexToRgb(hex));
+const histogramTicks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function hexToRgb(hex: string) {
const r = parseInt(hex.slice(1, 3), 16);
@@ -194,14 +182,23 @@ export default function IncomeView() {
Warning: There is an important amount of data ({(droppingPercent * 100).toFixed(2)}%) missing at this zoom level ({Math.round(viewState.zoom)}) because of the tileset dropping features. Widget calculations will not be accurate.
)}
-
+
+
+
+
{/*
-
+ {/*
-
+ */}