From d6c183ead40f0335e9ccc16dc73ba5be6a4d9233 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Thu, 5 Dec 2024 20:05:37 +0100 Subject: [PATCH 01/18] h3-widgets example first widget working --- h3-widgets/.env | 5 ++ h3-widgets/README.md | 21 ++++++ h3-widgets/images/scale.jpg | Bin 0 -> 3449 bytes h3-widgets/index.html | 54 +++++++++++++++ h3-widgets/index.ts | 129 ++++++++++++++++++++++++++++++++++ h3-widgets/package.json | 26 +++++++ h3-widgets/selectorUtils.ts | 27 ++++++++ h3-widgets/style.css | 133 ++++++++++++++++++++++++++++++++++++ h3-widgets/tsconfig.json | 17 +++++ h3-widgets/utils.ts | 15 ++++ package.json | 3 +- 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 h3-widgets/.env create mode 100644 h3-widgets/README.md create mode 100644 h3-widgets/images/scale.jpg create mode 100644 h3-widgets/index.html create mode 100644 h3-widgets/index.ts create mode 100644 h3-widgets/package.json create mode 100644 h3-widgets/selectorUtils.ts create mode 100644 h3-widgets/style.css create mode 100644 h3-widgets/tsconfig.json create mode 100644 h3-widgets/utils.ts diff --git a/h3-widgets/.env b/h3-widgets/.env new file mode 100644 index 0000000..be525ad --- /dev/null +++ b/h3-widgets/.env @@ -0,0 +1,5 @@ +# API Base URL (copy this from CARTO Workspace -> Developers) +VITE_API_BASE_URL=https://gcp-us-east1.api.carto.com +# This API Access Token only grants access to demo data for the examples. Replace it with your own token. +# Go to app.carto.com -> Developers -> Credentials -> API Access Tokens. +VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiJkOTU4OWMyZiJ9.78MdzU2J6y-J6Far71_Mh7IQO9eYIZD9nECUiZJAVL4 \ No newline at end of file diff --git a/h3-widgets/README.md b/h3-widgets/README.md new file mode 100644 index 0000000..ecbe9fe --- /dev/null +++ b/h3-widgets/README.md @@ -0,0 +1,21 @@ +## Example: Spatial Index H3 + +This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). + +Uses [Vite](https://vitejs.dev/) to bundle and serve files. + +## Usage + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/spatial-features-h3?file=index.ts) + +Or run it locally: + +```bash +npm install +# or +yarn +``` + +Commands: +* `npm dev` is the development target, to serve the app and hot reload. +* `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/h3-widgets/images/scale.jpg b/h3-widgets/images/scale.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08619e703ad35d146498c7434ef3d9d3609f6ab6 GIT binary patch literal 3449 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 Q?k~2U)&+FVdB?#20=jBlYXATM literal 0 HcmV?d00001 diff --git a/h3-widgets/index.html b/h3-widgets/index.html new file mode 100644 index 0000000..72b1491 --- /dev/null +++ b/h3-widgets/index.html @@ -0,0 +1,54 @@ + + + + + CARTO + deck.gl - H3 Spatial Index 12M rows + + +
+ +
+
+

✨👀 You're viewing

+

CARTO Spatial Features datasets in H3 (12M rows).

+

+ More info about + H3 Spatial Index +

+ +
+

Variable

+ +

Agreggation method applied

+

+

More information about aggregationExp

+
+ legend +
+
Less
+
More
+
+
+
+ +
+
+

Total Population

+
+
+
+
+
+

Source: Spatial Features - United States of America (H3 Resolution 8)

+ + + diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts new file mode 100644 index 0000000..abec412 --- /dev/null +++ b/h3-widgets/index.ts @@ -0,0 +1,129 @@ +import './style.css'; +import 'maplibre-gl/dist/maplibre-gl.css'; +import maplibregl from 'maplibre-gl'; +import {Deck, MapViewState} from '@deck.gl/core'; +import {H3TileLayer, BASEMAP, colorBins} from '@deck.gl/carto'; +import { initSelectors } from './selectorUtils'; +import { debounce, getSpatialFilterFromViewState } from './utils'; +import { h3QuerySource } from '@carto/api-client' + +const cartoConfig = { + apiBaseUrl: import.meta.env.VITE_API_BASE_URL, + accessToken: import.meta.env.VITE_API_ACCESS_TOKEN, + connectionName: 'carto_dw' +}; + +const INITIAL_VIEW_STATE: MapViewState = { + latitude: 40.7128, // New York + longitude: -74.006, // New York + zoom: 7, + pitch: 0, + bearing: 0, + minZoom: 3.5, + maxZoom: 15 +} + +type Source = ReturnType; + +// Selectors variables +let selectedVariable = 'population'; +let aggregationExp = `SUM(${selectedVariable})` + +let source: Source; +let viewState = INITIAL_VIEW_STATE + +// DOM elements +const variableSelector = document.getElementById('variable') as HTMLSelectElement; +const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; +const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; + +aggMethodLabel.innerText = aggregationExp; +variableSelector?.addEventListener('change', () => { + const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; + + selectedVariable = variableSelector.value; + aggregationExp = `${aggMethod}(${selectedVariable})`; + aggMethodLabel.innerText = aggregationExp; + + render(); +}); + +function render() { + source = h3QuerySource({ + ...cartoConfig, + aggregationExp: `${aggregationExp} as value`, + sqlQuery: `SELECT * FROM cartobq.public_account.derived_spatialfeatures_usa_h3int_res8_v1_yearly_v2` + }); + renderLayers(); + renderWidgets(); +} + +function renderLayers() { + const layers = [ + new H3TileLayer({ + id: 'h3_layer', + data: source, + opacity: 0.8, + pickable: true, + extruded: false, + getFillColor: colorBins({ + attr: 'value', + domain: [0, 100, 1000, 10000, 100000, 1000000], + colors: 'PinkYl' + }), + lineWidthMinPixels: 0.5, + getLineWidth: 0.5, + getLineColor: [255, 255, 255, 100] + }) + ]; + + deck.setProps({ + layers, + getTooltip: ({object}) => + object && { + html: `Hex ID: ${object.id}
+ ${selectedVariable.toUpperCase()}: ${parseInt( + object.properties.value + )}
+ Aggregation Expression: ${aggregationExp}` + } + }); +} + +async function renderWidgets() { + formulaWidget.innerHTML = 'Loading...' + const { widgetSource } = await source + const formula = await widgetSource.getFormula({ + column: selectedVariable, + operation: 'count', + spatialFilter: getSpatialFilterFromViewState(viewState), + viewState, + }) + formulaWidget.textContent = Intl.NumberFormat('en-US').format(formula.value) +} + +const debouncedRenderWidgets = debounce(renderWidgets, 500); + +// Main execution +const map = new maplibregl.Map({ + container: 'map', + style: BASEMAP.DARK_MATTER, + interactive: false +}); + +const deck = new Deck({ + canvas: 'deck-canvas', + initialViewState: viewState, + controller: true +}); +deck.setProps({ + onViewStateChange: (props) => { + const {longitude, latitude, ...rest} = props.viewState; + map.jumpTo({center: [longitude, latitude], ...rest}); + viewState = props.viewState; + debouncedRenderWidgets() + } +}); + +initSelectors(); +render(); diff --git a/h3-widgets/package.json b/h3-widgets/package.json new file mode 100644 index 0000000..f7fd3ef --- /dev/null +++ b/h3-widgets/package.json @@ -0,0 +1,26 @@ +{ + "name": "carto-deckgl-spatial-index-h3-example", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "dev-local": "vite --config ../vite.config.local.mjs", + "build": "vite build", + "preview": "vite preview", + "link-deck": "yarn link @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", + "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions" + }, + "devDependencies": { + "vite": "^4.5.0" + }, + "dependencies": { + "@deck.gl/aggregation-layers": "^9.0.17", + "@deck.gl/carto": "^9.0.17", + "@deck.gl/core": "^9.0.17", + "@deck.gl/extensions": "^9.0.17", + "@deck.gl/geo-layers": "^9.0.17", + "@deck.gl/layers": "^9.0.17", + "@deck.gl/mesh-layers": "^9.0.17", + "maplibre-gl": "^3.5.2" + } +} diff --git a/h3-widgets/selectorUtils.ts b/h3-widgets/selectorUtils.ts new file mode 100644 index 0000000..2894d13 --- /dev/null +++ b/h3-widgets/selectorUtils.ts @@ -0,0 +1,27 @@ +type DbType = 'FLOAT' | 'STRING' | 'DATE'; +interface Variable { + column_name: string; + name: string; + db_type: DbType; + agg_method: string; +} + +export async function initSelectors() { + const variableSelector = document.getElementById('variable') as HTMLSelectElement; + const data = await getVariables(); + const options = data + .filter((variable: Variable) => variable.db_type === 'FLOAT') + .map((variable: Variable) => { + return ``; + }); + variableSelector.innerHTML = options.join(''); +} + +async function getVariables() { + const response = await fetch( + 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' + ); + return response.json(); +} + + diff --git a/h3-widgets/style.css b/h3-widgets/style.css new file mode 100644 index 0000000..d286268 --- /dev/null +++ b/h3-widgets/style.css @@ -0,0 +1,133 @@ +body, +html { + height: 100%; + width: 100%; + overflow: hidden; + position: fixed; + font-family: Inter, sans-serif; + font-weight: 400; + font-size: 1rem; +} + +body { + margin: 0; +} + +#map { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + max-height: 100%; +} + +#app { + flex: 1 1 auto; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +#top-left { + position: absolute; + top: 0; + left: 0; + margin: 3.5rem 0.75rem; +} + +#story-card { + padding: 1.75rem; + background: #fdfdfe; + box-shadow: 4px 4px 8px #30303099; + border-radius: 4px; + max-width: 480px; + max-height: calc(100vh - 9rem); + z-index: 10; + overflow: scroll; +} + +.layer-controls { + margin-top: 2.25rem; +} + +.overline { + font-size: 0.625rem; + font-weight: 600; + line-height: 1.2; + letter-spacing: 0.125rem; + text-transform: uppercase; +} + +select:hover, +button:hover { + cursor: pointer; +} + +button { + border-radius: 4px; + margin-right: 0.75rem; + padding: 0.4rem 0.75rem; + background-color: #f3f3f3; + border-color: #30303099; + border-width: 1px; +} + +button:hover { + box-shadow: 2px 2px 2px #30303099; + cursor: pointer; +} + +.caption { + margin-top: 2.25rem; + margin-bottom: 0px; + font-size: 0.8rem; +} + +.city-button-group { + display: flex; +} + +#tooltip { + position: absolute; + z-index: 1; + background: #000; + border-radius: 4px; + color: #fff; + font-family: sans-serif; + font-size: 12px; + padding: 0; + padding: 0; + pointer-events: none; +} + +.legend { + width: 100%; + height: 10px; + margin-top: 8px; +} + +.label-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 2px; + font-size: smaller; +} + +.select { + width: 50%; + margin-bottom: 8px; + padding: 4px; + border: #ccc 1px solid; +} + +.widgets { + margin: 2rem 0; +} + +#formula-data { + font-size: 24px; + font-weight: 600; + font-family: monospace; +} diff --git a/h3-widgets/tsconfig.json b/h3-widgets/tsconfig.json new file mode 100644 index 0000000..892a5a5 --- /dev/null +++ b/h3-widgets/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "jsx": "react", + "strict": true, + "noImplicitAny": false, + "allowJs": true, + "checkJs": false, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "baseUrl": "." + } +} diff --git a/h3-widgets/utils.ts b/h3-widgets/utils.ts new file mode 100644 index 0000000..3f8cc6d --- /dev/null +++ b/h3-widgets/utils.ts @@ -0,0 +1,15 @@ +import { createViewportSpatialFilter } from "@carto/api-client"; +import { MapViewState, WebMercatorViewport } from "@deck.gl/core"; + +export function debounce(func, wait) { + let timeout; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +export function getSpatialFilterFromViewState(viewState: MapViewState) { + const viewport = new WebMercatorViewport(viewState); + return createViewportSpatialFilter(viewport.getBounds()) +} diff --git a/package.json b/package.json index 76471e7..167c4d9 100644 --- a/package.json +++ b/package.json @@ -4,5 +4,6 @@ "devDependencies": { "vite": "^4.5.0", "ocular-dev-tools": "^2.0.0-alpha.15" - } + }, + "packageManager": "yarn@4.4.1+sha512.f825273d0689cc9ead3259c14998037662f1dcd06912637b21a450e8da7cfeb4b1965bbee73d16927baa1201054126bc385c6f43ff4aa705c8631d26e12460f1" } From 141e26157d9dfda3a252f4431c946a39e2029a85 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Wed, 11 Dec 2024 11:00:20 +0100 Subject: [PATCH 02/18] first h3 widgets example --- h3-widgets/index.html | 9 ++++-- h3-widgets/index.ts | 71 +++++++++++++++++++++++++++++++++++------ h3-widgets/package.json | 2 ++ h3-widgets/style.css | 20 +++++++++++- 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/h3-widgets/index.html b/h3-widgets/index.html index 72b1491..687d8a7 100644 --- a/h3-widgets/index.html +++ b/h3-widgets/index.html @@ -10,7 +10,7 @@

✨👀 You're viewing

-

CARTO Spatial Features datasets in H3 (12M rows).

+

CARTO Widgets for H3 (12M rows).

More info about CARTO Spatial Features datasets in H3 (12M rows).

-
+

Total Population

+
+

Urbanity categories

+ + +
diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index abec412..af08b84 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -5,18 +5,22 @@ import {Deck, MapViewState} from '@deck.gl/core'; import {H3TileLayer, BASEMAP, colorBins} from '@deck.gl/carto'; import { initSelectors } from './selectorUtils'; import { debounce, getSpatialFilterFromViewState } from './utils'; -import { h3QuerySource } from '@carto/api-client' +import { h3QuerySource, WidgetSource } from '@carto/api-client' +import Chart from 'chart.js/auto'; const cartoConfig = { + // @ts-expect-error misconfigured env variables apiBaseUrl: import.meta.env.VITE_API_BASE_URL, + // @ts-expect-error misconfigured env variables accessToken: import.meta.env.VITE_API_ACCESS_TOKEN, connectionName: 'carto_dw' }; const INITIAL_VIEW_STATE: MapViewState = { - latitude: 40.7128, // New York - longitude: -74.006, // New York - zoom: 7, + // Spain + latitude: 37.3753636, + longitude: -5.9962577, + zoom: 6, pitch: 0, bearing: 0, minZoom: 3.5, @@ -36,6 +40,9 @@ let viewState = INITIAL_VIEW_STATE const variableSelector = document.getElementById('variable') as HTMLSelectElement; const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; +const categoriesWidget = document.getElementById('categories-data') as HTMLCanvasElement; +const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; +let histogramChart: Chart; aggMethodLabel.innerText = aggregationExp; variableSelector?.addEventListener('change', () => { @@ -51,8 +58,8 @@ variableSelector?.addEventListener('change', () => { function render() { source = h3QuerySource({ ...cartoConfig, - aggregationExp: `${aggregationExp} as value`, - sqlQuery: `SELECT * FROM cartobq.public_account.derived_spatialfeatures_usa_h3int_res8_v1_yearly_v2` + aggregationExp: `${aggregationExp} as value`, + sqlQuery: `SELECT * FROM carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2` }); renderLayers(); renderWidgets(); @@ -91,15 +98,61 @@ function renderLayers() { } async function renderWidgets() { - formulaWidget.innerHTML = 'Loading...' const { widgetSource } = await source - const formula = await widgetSource.getFormula({ + await Promise.all([ + renderFormula(widgetSource), + renderHistogram(widgetSource) + ]) +} + +async function renderFormula(ws: WidgetSource) { + formulaWidget.innerHTML = 'Loading...' + const formula = await ws.getFormula({ column: selectedVariable, + operation: 'sum', + spatialFilter: getSpatialFilterFromViewState(viewState), + viewState, + }) + formulaWidget.textContent = Intl.NumberFormat('en-US', { + maximumFractionDigits: 0, + // notation: 'compact' + }).format(formula.value) +} + +const TICKS = [100, 500, 1000, 5000]; + +async function renderHistogram(ws: WidgetSource) { + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', false); + histogramWidget.classList.toggle('hidden', true); + + const categories = await ws.getCategories({ + column: 'urbanity', operation: 'count', spatialFilter: getSpatialFilterFromViewState(viewState), viewState, }) - formulaWidget.textContent = Intl.NumberFormat('en-US').format(formula.value) + + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); + histogramWidget.classList.toggle('hidden', false); + + if (histogramChart) { + histogramChart.data.labels = categories.map((c) => c.name); + histogramChart.data.datasets[0].data = categories.map((c) => c.value); + histogramChart.update(); + } else { + histogramChart = new Chart(histogramWidget, { + type: 'bar', + data: { + labels: categories.map((c) => c.name), + datasets: [ + { + label: 'Urbanity category', + data: categories.map((c) => c.value), + } + ] + }, + }) + } } const debouncedRenderWidgets = debounce(renderWidgets, 500); diff --git a/h3-widgets/package.json b/h3-widgets/package.json index f7fd3ef..06894ed 100644 --- a/h3-widgets/package.json +++ b/h3-widgets/package.json @@ -14,6 +14,7 @@ "vite": "^4.5.0" }, "dependencies": { + "@carto/api-client": "^0.4.1-alpha.0", "@deck.gl/aggregation-layers": "^9.0.17", "@deck.gl/carto": "^9.0.17", "@deck.gl/core": "^9.0.17", @@ -21,6 +22,7 @@ "@deck.gl/geo-layers": "^9.0.17", "@deck.gl/layers": "^9.0.17", "@deck.gl/mesh-layers": "^9.0.17", + "chart.js": "^4.4.7", "maplibre-gl": "^3.5.2" } } diff --git a/h3-widgets/style.css b/h3-widgets/style.css index d286268..8be1dad 100644 --- a/h3-widgets/style.css +++ b/h3-widgets/style.css @@ -122,7 +122,7 @@ button:hover { border: #ccc 1px solid; } -.widgets { +.widget { margin: 2rem 0; } @@ -131,3 +131,21 @@ button:hover { font-weight: 600; font-family: monospace; } + +.relative { position: relative; } + +.loader { + font-size: 14px; + font-weight: 400; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 1; + transition: opacity 0.3s; +} + +.hidden { + opacity: 0; +} From 6f59bd96538ad0ffc912c195cca1a175ec19bf68 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Wed, 11 Dec 2024 15:39:20 +0100 Subject: [PATCH 03/18] use a token that has only access to h3 spatial features demo data --- h3-widgets/.env | 6 +++--- h3-widgets/index.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/h3-widgets/.env b/h3-widgets/.env index be525ad..34986fb 100644 --- a/h3-widgets/.env +++ b/h3-widgets/.env @@ -1,5 +1,5 @@ # API Base URL (copy this from CARTO Workspace -> Developers) VITE_API_BASE_URL=https://gcp-us-east1.api.carto.com -# This API Access Token only grants access to demo data for the examples. Replace it with your own token. -# Go to app.carto.com -> Developers -> Credentials -> API Access Tokens. -VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiJkOTU4OWMyZiJ9.78MdzU2J6y-J6Far71_Mh7IQO9eYIZD9nECUiZJAVL4 \ No newline at end of file +# This API Access Token only grants access to demo data for the examples (h3 spatial features demo data). +# To replace this token with your own, go to app.carto.com -> Developers -> Credentials -> API Access Tokens. +VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfNDd1dW5tZWciLCJqdGkiOiJlOThmMDg0MSJ9.X8emzWzf4i0kJ0jsp7CEzG830MglsAGHmuVIe6rLOtg diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index af08b84..2457993 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -5,7 +5,7 @@ import {Deck, MapViewState} from '@deck.gl/core'; import {H3TileLayer, BASEMAP, colorBins} from '@deck.gl/carto'; import { initSelectors } from './selectorUtils'; import { debounce, getSpatialFilterFromViewState } from './utils'; -import { h3QuerySource, WidgetSource } from '@carto/api-client' +import { h3TableSource, WidgetSource } from '@carto/api-client'; import Chart from 'chart.js/auto'; const cartoConfig = { @@ -27,7 +27,7 @@ const INITIAL_VIEW_STATE: MapViewState = { maxZoom: 15 } -type Source = ReturnType; +type Source = ReturnType; // Selectors variables let selectedVariable = 'population'; @@ -56,10 +56,10 @@ variableSelector?.addEventListener('change', () => { }); function render() { - source = h3QuerySource({ + source = h3TableSource({ ...cartoConfig, aggregationExp: `${aggregationExp} as value`, - sqlQuery: `SELECT * FROM carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2` + tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2' }); renderLayers(); renderWidgets(); From 868a409c90c8246e28b9de7d4633dd33d30d4470 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Wed, 11 Dec 2024 16:15:06 +0100 Subject: [PATCH 04/18] completed h3 example with filtering --- h3-widgets/index.html | 4 +++- h3-widgets/index.ts | 39 +++++++++++++++++++++++++++++++++++---- h3-widgets/style.css | 6 ++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/h3-widgets/index.html b/h3-widgets/index.html index 687d8a7..a81c543 100644 --- a/h3-widgets/index.html +++ b/h3-widgets/index.html @@ -10,7 +10,7 @@

✨👀 You're viewing

-

CARTO Widgets for H3 (12M rows).

+

CARTO Widgets for H3 Spatial Index

More info about CARTO Widgets for H3 (12M rows).

+

Urbanity categories

+

You can click the bars to filter the map with the selected category

diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index 2457993..3b796c4 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -5,7 +5,7 @@ import {Deck, MapViewState} from '@deck.gl/core'; import {H3TileLayer, BASEMAP, colorBins} from '@deck.gl/carto'; import { initSelectors } from './selectorUtils'; import { debounce, getSpatialFilterFromViewState } from './utils'; -import { h3TableSource, WidgetSource } from '@carto/api-client'; +import { addFilter, Filters, FilterType, h3TableSource, removeFilter, WidgetSource } from '@carto/api-client'; import Chart from 'chart.js/auto'; const cartoConfig = { @@ -35,13 +35,19 @@ let aggregationExp = `SUM(${selectedVariable})` let source: Source; let viewState = INITIAL_VIEW_STATE +const filters: Filters = {}; // DOM elements const variableSelector = document.getElementById('variable') as HTMLSelectElement; const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; -const categoriesWidget = document.getElementById('categories-data') as HTMLCanvasElement; const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; +const histogramClearBtn = document.querySelector('.histogram-widget .clear-btn') as HTMLButtonElement; +histogramClearBtn.addEventListener('click', () => { + removeFilter(filters, { column: 'urbanity' }); + render(); +}) + let histogramChart: Chart; aggMethodLabel.innerText = aggregationExp; @@ -58,11 +64,12 @@ variableSelector?.addEventListener('change', () => { function render() { source = h3TableSource({ ...cartoConfig, + filters, aggregationExp: `${aggregationExp} as value`, tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2' }); - renderLayers(); renderWidgets(); + renderLayers(); } function renderLayers() { @@ -119,7 +126,7 @@ async function renderFormula(ws: WidgetSource) { }).format(formula.value) } -const TICKS = [100, 500, 1000, 5000]; +const HISTOGRAM_WIDGET_ID = 'urbanity_widget'; async function renderHistogram(ws: WidgetSource) { histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', false); @@ -128,6 +135,7 @@ async function renderHistogram(ws: WidgetSource) { const categories = await ws.getCategories({ column: 'urbanity', operation: 'count', + filterOwner: HISTOGRAM_WIDGET_ID, spatialFilter: getSpatialFilterFromViewState(viewState), viewState, }) @@ -135,9 +143,13 @@ async function renderHistogram(ws: WidgetSource) { histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); + const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; + const colors = categories.map((c) => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)') + if (histogramChart) { histogramChart.data.labels = categories.map((c) => c.name); histogramChart.data.datasets[0].data = categories.map((c) => c.value); + histogramChart.data.datasets[0].backgroundColor = colors; histogramChart.update(); } else { histogramChart = new Chart(histogramWidget, { @@ -148,9 +160,28 @@ async function renderHistogram(ws: WidgetSource) { { label: 'Urbanity category', data: categories.map((c) => c.value), + backgroundColor: colors, } ] }, + options: { + onClick: async (ev, elems) => { + console.log(elems[0]) + const index = elems[0]?.index + const categoryName = categories[index]?.name + if (!categoryName || categoryName === selectedCategory) { + removeFilter(filters, { column: 'urbanity' }) + } else { + addFilter(filters, { + column: 'urbanity', + type: FilterType.IN, + values: [categoryName], + owner: HISTOGRAM_WIDGET_ID, + }) + } + await render() + } + }, }) } } diff --git a/h3-widgets/style.css b/h3-widgets/style.css index 8be1dad..a9faff8 100644 --- a/h3-widgets/style.css +++ b/h3-widgets/style.css @@ -149,3 +149,9 @@ button:hover { .hidden { opacity: 0; } + +.widget .clear-btn { + position: absolute; + top: -8px; + right: -8px; +} From 63ab0ba40260dc1b735c8c5251a1a6a43f84fb4a Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Wed, 11 Dec 2024 16:29:32 +0100 Subject: [PATCH 05/18] correct title in h3-widgets --- h3-widgets/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h3-widgets/index.html b/h3-widgets/index.html index a81c543..73f57b4 100644 --- a/h3-widgets/index.html +++ b/h3-widgets/index.html @@ -2,7 +2,7 @@ - CARTO + deck.gl - H3 Spatial Index 12M rows + CARTO + deck.gl - H3 Spatial Index Widgets
From 3a7e888f7c65cec636bc9d74a928043fb5f9f5ad Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Wed, 11 Dec 2024 16:29:50 +0100 Subject: [PATCH 06/18] copy h3-widgets to quadbin-widgets --- quadbin-widgets/.env | 5 + quadbin-widgets/README.md | 21 +++ quadbin-widgets/images/scale.jpg | Bin 0 -> 3449 bytes quadbin-widgets/index.html | 61 +++++++++ quadbin-widgets/index.ts | 213 +++++++++++++++++++++++++++++++ quadbin-widgets/package.json | 28 ++++ quadbin-widgets/selectorUtils.ts | 27 ++++ quadbin-widgets/style.css | 157 +++++++++++++++++++++++ quadbin-widgets/tsconfig.json | 17 +++ quadbin-widgets/utils.ts | 15 +++ 10 files changed, 544 insertions(+) create mode 100644 quadbin-widgets/.env create mode 100644 quadbin-widgets/README.md create mode 100644 quadbin-widgets/images/scale.jpg create mode 100644 quadbin-widgets/index.html create mode 100644 quadbin-widgets/index.ts create mode 100644 quadbin-widgets/package.json create mode 100644 quadbin-widgets/selectorUtils.ts create mode 100644 quadbin-widgets/style.css create mode 100644 quadbin-widgets/tsconfig.json create mode 100644 quadbin-widgets/utils.ts diff --git a/quadbin-widgets/.env b/quadbin-widgets/.env new file mode 100644 index 0000000..638c9d2 --- /dev/null +++ b/quadbin-widgets/.env @@ -0,0 +1,5 @@ +# API Base URL (copy this from CARTO Workspace -> Developers) +VITE_API_BASE_URL=https://gcp-us-east1.api.carto.com +# This API Access Token only grants access to demo data for the examples (quadbin spatial features demo data). +# To replace this token with your own, go to app.carto.com -> Developers -> Credentials -> API Access Tokens. +VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfNDd1dW5tZWciLCJqdGkiOiIzOWVhYzFhMiJ9.Q-EcYIHzWzT2zvQ2r0zWvBqmn3WdS-Kgrz0P6uvBdag diff --git a/quadbin-widgets/README.md b/quadbin-widgets/README.md new file mode 100644 index 0000000..ecbe9fe --- /dev/null +++ b/quadbin-widgets/README.md @@ -0,0 +1,21 @@ +## Example: Spatial Index H3 + +This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). + +Uses [Vite](https://vitejs.dev/) to bundle and serve files. + +## Usage + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/spatial-features-h3?file=index.ts) + +Or run it locally: + +```bash +npm install +# or +yarn +``` + +Commands: +* `npm dev` is the development target, to serve the app and hot reload. +* `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/quadbin-widgets/images/scale.jpg b/quadbin-widgets/images/scale.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08619e703ad35d146498c7434ef3d9d3609f6ab6 GIT binary patch literal 3449 zcmd5;c{r478y`yx(kMsP1|v&ihS6e~p)z(t))2-RnJhC|GRJW8M$eum) zF_!EiOIfF^AzQ}I_v&=2>zqHnf4}#-p7*)m=e~dU{oMEOdG6~CHMyX}%_+nQ0)e>o zbkP@qc>oy4_wNPX_Iq?rfIuu5yr!m!o~EXli5I~I?~Vh3WbS!H=fl~q{nqeWCryht z=x#Mp6DynXLdwa_K)>_paihG;xxSLU({Mj&CnX*?Eq+_Gu-CWh98cZTW zZQCd+Ju>%TWrModN`b?{u1ihfco^u$AR;FJZfR+uT)9I>#0Ao1atvQ=FmI@G+=#7h z3Yq)%bhk{hZ!R-C&M*1qR{m0DXZFN6;iiEPuHf1E^>fFMx8#Au&nEWe2~5a;WVsU$ zgkYgZ9X+tXo3AlA`obu&wcHMhiB>$w;ifn))BKZu2f2eAVZ3owL$0Rpkbgn;$| z^I>41pRoSW4m@G|As^p0JZGk*e^92*2^X@&gFqb7 zdgycJB$ip5=17S%2L|+*(K|bl{@q{1&)yn{5%KEdc=4vQnaPd~`;DhCTG67_^N3#K z{j$d;q5<3M+j~CEHb3x7crZRf#(>3tVJKNhzOIx1Ip&|0naO;=;yn~n4HK-fG$dRt{J64-11KB&F3*s&&nxAt) zcU~Tf5m6A~iPx;LU9R7w>Thn)p*nSFim}erZj*L*t>GYR=bnl{vb#0Y0t9bJtB>+s zsWnzNhH&v5meO{2ta-KUupl`%>>b^WIoA5Vd_+;ruB}^Vel+aOuoXvIM8b4=dv|hA z*Ruus0i8bHAg8Uy>bOqs+^PKWykWW*Wv&lX0Sn!jd7UubKJDvOcI>f-zZ!=0kS2&FGMT1)HvklFhzBWX0oiQc~%BmGfBucbPObcB*b;$ol<`R!LdGTZCmnAK*F+W zS^3vv_ZbkA_9|(yqdT>zHm@so(d%qQnOUdSvBQi$Ta?dPY;RlW^?e76h%R#EYf{uN^(9KC9xi;o2wl#)$sKz9Dz26){_qXF4CA z%Q`&AfZ7G)wp&nx+NfT^PxO}XJb<4qLIj_-p^4f_V zarP+T^x8@*Z(3Z4?W5E=774Ow5=dOqSuKgI3^^@xQ!c>XRN1%&VkE$i&p3zuo>)ae_@c5&VD=cbRiDHR*Z_`!Tr)0e^|1>}0aCrGS4|Wgdye zsCIu_W4N;{f$fno@pEX#gIaF5W zY$Uxa;e4$ws%#2w7hNG{E#TK1P>B;}n3WBrXum>FFYlAu2aF6Fwn%3RyTRPc*# z2v=Nb%nQ}xs5OBys=RgEi+swN35^O@iWpZ{j%hPh5~v?_dZBgY{3isj#I)QM?at;u z)Jy`a4WvIyx;{tuI)VF*5v!&VD4T*}^aMrO;=SdJ)5X9`<9W>0o54|x zTSoOiSvLbue3|W3s`R5MPfS$~Q2k%i!iapud;`;{#4(5_<)g*>Y|I)a+eVgkB~`~V z$oF@RRNGBk#|?}5+NGA_Rc`Z-Lt!`0wsOPVW2gV^!3f}^B+ zHkNavE|WnKuua1*2_dlVqZ7L+{lZ?#|74HvAM8E;|7I_WqTI83vhqR>3^jxZ_x;K) zvw$OgW?0Xfu3K-PcX0|)i(#CX%o)v$oW-hZtA91=?yF2#5`B#AXS?+9dV~%0#KJNp zl4fC!w5Z|?J=%bvXp{S>G1XC5XG~*@#?B4NzyZ6m(arhEu=zE}1&d<7R}b$y#;atZ zs16LQvSX~GN(A_%(A_djf*~q#>hxy}C)2iXb7U@v60{xQ&YfkTwUleS=~-hthxS$CD!?Q+m_U_;zYWOK5WNLy?kO{iYWM%LI% zV%>Ywc|4FFG3gT2n(K~A5nV+Jd#eN9(^=Q)=H73liO67SdY>bDOh2dw%16}$+mo)H z4eV)*(gjm~_EL>(@dCz7Q`mNQ0mF#AD8P5jPC!xS-xrSH;tn5CJ)&V6b(F>l7K$9< z4T74s=e1I~c-6v4apOE)`S$$V72liaDt$R(k89}rFd(0M#{9*h%9er;`b)5}RTit= z)DbiaS~^6>AC(tO;*m!S0d3HKpw8C5=~5}(w1MBR4I z-gjHSWinj#PoGOcfYWa`t?aGd)tBU{-Fp^rN_*AGYSV%=^EP^2-+#ARc+|n9Bp>3q z-J*uAR_DzsS!3y|rCluUV&fqmfdeNitiZB!?*}}00%VpJwji(?i$13JIvGsUDtL;T z&z$Kg?8;1o5{=2uTBO;xo&%R)a`R;H1c#{*VT=uRUBO(gAiqZoI9#MY$M%<-H|iKj z4oW)4E;ViO*qJ{y8jQ!8(;qd+r(Y4uhdY9jJCw)^I*@l}$Kg8-E5sevvHUbL(XjaX Q?k~2U)&+FVdB?#20=jBlYXATM literal 0 HcmV?d00001 diff --git a/quadbin-widgets/index.html b/quadbin-widgets/index.html new file mode 100644 index 0000000..f23d5af --- /dev/null +++ b/quadbin-widgets/index.html @@ -0,0 +1,61 @@ + + + + + CARTO + deck.gl - Quadbin Spatial Index Widgets + + +
+ +
+
+

✨👀 You're viewing

+

CARTO Widgets for Quadbin Spatial Index

+

+ More info about + Quadbin Spatial Index +

+ +
+

Variable

+ +

Agreggation method applied

+

+

More information about aggregationExp

+
+ legend +
+
Less
+
More
+
+
+
+ +
+
+

Total Population

+
+
+
+ +

Urbanity categories

+

You can click the bars to filter the map with the selected category

+ + +
+
+
+
+

Source: Spatial Features - United States of America (Quadbin Resolution 15)

+ + + diff --git a/quadbin-widgets/index.ts b/quadbin-widgets/index.ts new file mode 100644 index 0000000..911bff2 --- /dev/null +++ b/quadbin-widgets/index.ts @@ -0,0 +1,213 @@ +import './style.css'; +import 'maplibre-gl/dist/maplibre-gl.css'; +import maplibregl from 'maplibre-gl'; +import {Deck, MapViewState} from '@deck.gl/core'; +import {H3TileLayer, BASEMAP, colorBins, QuadbinTileLayer} from '@deck.gl/carto'; +import { initSelectors } from './selectorUtils'; +import { debounce, getSpatialFilterFromViewState } from './utils'; +import { addFilter, Filters, FilterType, quadbinTableSource, removeFilter, WidgetSource } from '@carto/api-client'; +import Chart from 'chart.js/auto'; + +const cartoConfig = { + // @ts-expect-error misconfigured env variables + apiBaseUrl: import.meta.env.VITE_API_BASE_URL, + // @ts-expect-error misconfigured env variables + accessToken: import.meta.env.VITE_API_ACCESS_TOKEN, + connectionName: 'carto_dw' +}; + +const INITIAL_VIEW_STATE: MapViewState = { + // Spain + latitude: 37.3753636, + longitude: -5.9962577, + zoom: 6, + pitch: 0, + bearing: 0, + minZoom: 3.5, + maxZoom: 15 +} + +type Source = ReturnType; + +// Selectors variables +let selectedVariable = 'population'; +let aggregationExp = `SUM(${selectedVariable})` + +let source: Source; +let viewState = INITIAL_VIEW_STATE +const filters: Filters = {}; + +// DOM elements +const variableSelector = document.getElementById('variable') as HTMLSelectElement; +const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; +const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; +const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; +const histogramClearBtn = document.querySelector('.histogram-widget .clear-btn') as HTMLButtonElement; +histogramClearBtn.addEventListener('click', () => { + removeFilter(filters, { column: 'urbanity' }); + render(); +}) + +let histogramChart: Chart; + +aggMethodLabel.innerText = aggregationExp; +variableSelector?.addEventListener('change', () => { + const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; + + selectedVariable = variableSelector.value; + aggregationExp = `${aggMethod}(${selectedVariable})`; + aggMethodLabel.innerText = aggregationExp; + + render(); +}); + +function render() { + source = quadbinTableSource({ + ...cartoConfig, + filters, + aggregationExp: `${aggregationExp} as value`, + tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_quadbin15_v1_yearly_v2' + }); + renderWidgets(); + renderLayers(); +} + +function renderLayers() { + const layers = [ + new QuadbinTileLayer({ + id: 'quadbin_layer', + data: source, + opacity: 0.8, + pickable: true, + extruded: false, + getFillColor: colorBins({ + attr: 'value', + domain: [0, 100, 1000, 10000, 100000, 1000000], + colors: 'PinkYl' + }), + lineWidthMinPixels: 0.5, + getLineWidth: 0.5, + getLineColor: [255, 255, 255, 100] + }) + ]; + + deck.setProps({ + layers, + getTooltip: ({object}) => + object && { + html: `Hex ID: ${object.id}
+ ${selectedVariable.toUpperCase()}: ${parseInt( + object.properties.value + )}
+ Aggregation Expression: ${aggregationExp}` + } + }); +} + +async function renderWidgets() { + const { widgetSource } = await source + await Promise.all([ + renderFormula(widgetSource), + renderHistogram(widgetSource) + ]) +} + +async function renderFormula(ws: WidgetSource) { + formulaWidget.innerHTML = 'Loading...' + const formula = await ws.getFormula({ + column: selectedVariable, + operation: 'sum', + spatialFilter: getSpatialFilterFromViewState(viewState), + viewState, + }) + formulaWidget.textContent = Intl.NumberFormat('en-US', { + maximumFractionDigits: 0, + // notation: 'compact' + }).format(formula.value) +} + +const HISTOGRAM_WIDGET_ID = 'urbanity_widget'; + +async function renderHistogram(ws: WidgetSource) { + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', false); + histogramWidget.classList.toggle('hidden', true); + + const categories = await ws.getCategories({ + column: 'urbanity', + operation: 'count', + filterOwner: HISTOGRAM_WIDGET_ID, + spatialFilter: getSpatialFilterFromViewState(viewState), + viewState, + }) + + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); + histogramWidget.classList.toggle('hidden', false); + + const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; + const colors = categories.map((c) => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)') + + if (histogramChart) { + histogramChart.data.labels = categories.map((c) => c.name); + histogramChart.data.datasets[0].data = categories.map((c) => c.value); + histogramChart.data.datasets[0].backgroundColor = colors; + histogramChart.update(); + } else { + histogramChart = new Chart(histogramWidget, { + type: 'bar', + data: { + labels: categories.map((c) => c.name), + datasets: [ + { + label: 'Urbanity category', + data: categories.map((c) => c.value), + backgroundColor: colors, + } + ] + }, + options: { + onClick: async (ev, elems) => { + console.log(elems[0]) + const index = elems[0]?.index + const categoryName = categories[index]?.name + if (!categoryName || categoryName === selectedCategory) { + removeFilter(filters, { column: 'urbanity' }) + } else { + addFilter(filters, { + column: 'urbanity', + type: FilterType.IN, + values: [categoryName], + owner: HISTOGRAM_WIDGET_ID, + }) + } + await render() + } + }, + }) + } +} + +const debouncedRenderWidgets = debounce(renderWidgets, 500); + +// Main execution +const map = new maplibregl.Map({ + container: 'map', + style: BASEMAP.DARK_MATTER, + interactive: false +}); + +const deck = new Deck({ + canvas: 'deck-canvas', + initialViewState: viewState, + controller: true +}); +deck.setProps({ + onViewStateChange: (props) => { + const {longitude, latitude, ...rest} = props.viewState; + map.jumpTo({center: [longitude, latitude], ...rest}); + viewState = props.viewState; + debouncedRenderWidgets() + } +}); + +initSelectors(); +render(); diff --git a/quadbin-widgets/package.json b/quadbin-widgets/package.json new file mode 100644 index 0000000..06894ed --- /dev/null +++ b/quadbin-widgets/package.json @@ -0,0 +1,28 @@ +{ + "name": "carto-deckgl-spatial-index-h3-example", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "dev-local": "vite --config ../vite.config.local.mjs", + "build": "vite build", + "preview": "vite preview", + "link-deck": "yarn link @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", + "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions" + }, + "devDependencies": { + "vite": "^4.5.0" + }, + "dependencies": { + "@carto/api-client": "^0.4.1-alpha.0", + "@deck.gl/aggregation-layers": "^9.0.17", + "@deck.gl/carto": "^9.0.17", + "@deck.gl/core": "^9.0.17", + "@deck.gl/extensions": "^9.0.17", + "@deck.gl/geo-layers": "^9.0.17", + "@deck.gl/layers": "^9.0.17", + "@deck.gl/mesh-layers": "^9.0.17", + "chart.js": "^4.4.7", + "maplibre-gl": "^3.5.2" + } +} diff --git a/quadbin-widgets/selectorUtils.ts b/quadbin-widgets/selectorUtils.ts new file mode 100644 index 0000000..2894d13 --- /dev/null +++ b/quadbin-widgets/selectorUtils.ts @@ -0,0 +1,27 @@ +type DbType = 'FLOAT' | 'STRING' | 'DATE'; +interface Variable { + column_name: string; + name: string; + db_type: DbType; + agg_method: string; +} + +export async function initSelectors() { + const variableSelector = document.getElementById('variable') as HTMLSelectElement; + const data = await getVariables(); + const options = data + .filter((variable: Variable) => variable.db_type === 'FLOAT') + .map((variable: Variable) => { + return ``; + }); + variableSelector.innerHTML = options.join(''); +} + +async function getVariables() { + const response = await fetch( + 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' + ); + return response.json(); +} + + diff --git a/quadbin-widgets/style.css b/quadbin-widgets/style.css new file mode 100644 index 0000000..a9faff8 --- /dev/null +++ b/quadbin-widgets/style.css @@ -0,0 +1,157 @@ +body, +html { + height: 100%; + width: 100%; + overflow: hidden; + position: fixed; + font-family: Inter, sans-serif; + font-weight: 400; + font-size: 1rem; +} + +body { + margin: 0; +} + +#map { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + max-height: 100%; +} + +#app { + flex: 1 1 auto; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +#top-left { + position: absolute; + top: 0; + left: 0; + margin: 3.5rem 0.75rem; +} + +#story-card { + padding: 1.75rem; + background: #fdfdfe; + box-shadow: 4px 4px 8px #30303099; + border-radius: 4px; + max-width: 480px; + max-height: calc(100vh - 9rem); + z-index: 10; + overflow: scroll; +} + +.layer-controls { + margin-top: 2.25rem; +} + +.overline { + font-size: 0.625rem; + font-weight: 600; + line-height: 1.2; + letter-spacing: 0.125rem; + text-transform: uppercase; +} + +select:hover, +button:hover { + cursor: pointer; +} + +button { + border-radius: 4px; + margin-right: 0.75rem; + padding: 0.4rem 0.75rem; + background-color: #f3f3f3; + border-color: #30303099; + border-width: 1px; +} + +button:hover { + box-shadow: 2px 2px 2px #30303099; + cursor: pointer; +} + +.caption { + margin-top: 2.25rem; + margin-bottom: 0px; + font-size: 0.8rem; +} + +.city-button-group { + display: flex; +} + +#tooltip { + position: absolute; + z-index: 1; + background: #000; + border-radius: 4px; + color: #fff; + font-family: sans-serif; + font-size: 12px; + padding: 0; + padding: 0; + pointer-events: none; +} + +.legend { + width: 100%; + height: 10px; + margin-top: 8px; +} + +.label-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 2px; + font-size: smaller; +} + +.select { + width: 50%; + margin-bottom: 8px; + padding: 4px; + border: #ccc 1px solid; +} + +.widget { + margin: 2rem 0; +} + +#formula-data { + font-size: 24px; + font-weight: 600; + font-family: monospace; +} + +.relative { position: relative; } + +.loader { + font-size: 14px; + font-weight: 400; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 1; + transition: opacity 0.3s; +} + +.hidden { + opacity: 0; +} + +.widget .clear-btn { + position: absolute; + top: -8px; + right: -8px; +} diff --git a/quadbin-widgets/tsconfig.json b/quadbin-widgets/tsconfig.json new file mode 100644 index 0000000..892a5a5 --- /dev/null +++ b/quadbin-widgets/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "jsx": "react", + "strict": true, + "noImplicitAny": false, + "allowJs": true, + "checkJs": false, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "baseUrl": "." + } +} diff --git a/quadbin-widgets/utils.ts b/quadbin-widgets/utils.ts new file mode 100644 index 0000000..3f8cc6d --- /dev/null +++ b/quadbin-widgets/utils.ts @@ -0,0 +1,15 @@ +import { createViewportSpatialFilter } from "@carto/api-client"; +import { MapViewState, WebMercatorViewport } from "@deck.gl/core"; + +export function debounce(func, wait) { + let timeout; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +export function getSpatialFilterFromViewState(viewState: MapViewState) { + const viewport = new WebMercatorViewport(viewState); + return createViewportSpatialFilter(viewport.getBounds()) +} From af73e0bc432733a91754dc64af5eac35b0ceb009 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Thu, 12 Dec 2024 00:23:40 +0100 Subject: [PATCH 07/18] run prettier --- h3-widgets/README.md | 7 ++- h3-widgets/index.ts | 94 +++++++++++++++++--------------- h3-widgets/package.json | 3 +- h3-widgets/selectorUtils.ts | 36 ++++++------ h3-widgets/utils.ts | 6 +- quadbin-widgets/README.md | 7 ++- quadbin-widgets/index.ts | 94 +++++++++++++++++--------------- quadbin-widgets/package.json | 3 +- quadbin-widgets/selectorUtils.ts | 36 ++++++------ quadbin-widgets/utils.ts | 6 +- 10 files changed, 152 insertions(+), 140 deletions(-) diff --git a/h3-widgets/README.md b/h3-widgets/README.md index ecbe9fe..46abfd0 100644 --- a/h3-widgets/README.md +++ b/h3-widgets/README.md @@ -1,6 +1,6 @@ ## Example: Spatial Index H3 -This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). +This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). Uses [Vite](https://vitejs.dev/) to bundle and serve files. @@ -17,5 +17,6 @@ yarn ``` Commands: -* `npm dev` is the development target, to serve the app and hot reload. -* `npm run build` is the production target, to create the final bundle and write to disk. + +- `npm dev` is the development target, to serve the app and hot reload. +- `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index 3b796c4..63c93d0 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -3,9 +3,16 @@ import 'maplibre-gl/dist/maplibre-gl.css'; import maplibregl from 'maplibre-gl'; import {Deck, MapViewState} from '@deck.gl/core'; import {H3TileLayer, BASEMAP, colorBins} from '@deck.gl/carto'; -import { initSelectors } from './selectorUtils'; -import { debounce, getSpatialFilterFromViewState } from './utils'; -import { addFilter, Filters, FilterType, h3TableSource, removeFilter, WidgetSource } from '@carto/api-client'; +import {initSelectors} from './selectorUtils'; +import {debounce, getSpatialFilterFromViewState} from './utils'; +import { + addFilter, + Filters, + FilterType, + h3TableSource, + removeFilter, + WidgetSource +} from '@carto/api-client'; import Chart from 'chart.js/auto'; const cartoConfig = { @@ -25,16 +32,16 @@ const INITIAL_VIEW_STATE: MapViewState = { bearing: 0, minZoom: 3.5, maxZoom: 15 -} +}; type Source = ReturnType; // Selectors variables let selectedVariable = 'population'; -let aggregationExp = `SUM(${selectedVariable})` +let aggregationExp = `SUM(${selectedVariable})`; let source: Source; -let viewState = INITIAL_VIEW_STATE +let viewState = INITIAL_VIEW_STATE; const filters: Filters = {}; // DOM elements @@ -42,22 +49,24 @@ const variableSelector = document.getElementById('variable') as HTMLSelectElemen const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; -const histogramClearBtn = document.querySelector('.histogram-widget .clear-btn') as HTMLButtonElement; +const histogramClearBtn = document.querySelector( + '.histogram-widget .clear-btn' +) as HTMLButtonElement; histogramClearBtn.addEventListener('click', () => { - removeFilter(filters, { column: 'urbanity' }); + removeFilter(filters, {column: 'urbanity'}); render(); -}) +}); -let histogramChart: Chart; +let histogramChart: Chart; aggMethodLabel.innerText = aggregationExp; variableSelector?.addEventListener('change', () => { const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; - + selectedVariable = variableSelector.value; aggregationExp = `${aggMethod}(${selectedVariable})`; aggMethodLabel.innerText = aggregationExp; - + render(); }); @@ -96,34 +105,29 @@ function renderLayers() { getTooltip: ({object}) => object && { html: `Hex ID: ${object.id}
- ${selectedVariable.toUpperCase()}: ${parseInt( - object.properties.value - )}
+ ${selectedVariable.toUpperCase()}: ${parseInt(object.properties.value)}
Aggregation Expression: ${aggregationExp}` } }); } async function renderWidgets() { - const { widgetSource } = await source - await Promise.all([ - renderFormula(widgetSource), - renderHistogram(widgetSource) - ]) + const {widgetSource} = await source; + await Promise.all([renderFormula(widgetSource), renderHistogram(widgetSource)]); } async function renderFormula(ws: WidgetSource) { - formulaWidget.innerHTML = 'Loading...' + formulaWidget.innerHTML = 'Loading...'; const formula = await ws.getFormula({ column: selectedVariable, operation: 'sum', spatialFilter: getSpatialFilterFromViewState(viewState), - viewState, - }) + viewState + }); formulaWidget.textContent = Intl.NumberFormat('en-US', { - maximumFractionDigits: 0, + maximumFractionDigits: 0 // notation: 'compact' - }).format(formula.value) + }).format(formula.value); } const HISTOGRAM_WIDGET_ID = 'urbanity_widget'; @@ -137,52 +141,54 @@ async function renderHistogram(ws: WidgetSource) { operation: 'count', filterOwner: HISTOGRAM_WIDGET_ID, spatialFilter: getSpatialFilterFromViewState(viewState), - viewState, - }) + viewState + }); histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; - const colors = categories.map((c) => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)') + const colors = categories.map(c => + c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' + ); if (histogramChart) { - histogramChart.data.labels = categories.map((c) => c.name); - histogramChart.data.datasets[0].data = categories.map((c) => c.value); + histogramChart.data.labels = categories.map(c => c.name); + histogramChart.data.datasets[0].data = categories.map(c => c.value); histogramChart.data.datasets[0].backgroundColor = colors; histogramChart.update(); } else { histogramChart = new Chart(histogramWidget, { type: 'bar', data: { - labels: categories.map((c) => c.name), + labels: categories.map(c => c.name), datasets: [ { label: 'Urbanity category', - data: categories.map((c) => c.value), - backgroundColor: colors, + data: categories.map(c => c.value), + backgroundColor: colors } ] }, options: { onClick: async (ev, elems) => { - console.log(elems[0]) - const index = elems[0]?.index - const categoryName = categories[index]?.name + console.log(elems[0]); + const index = elems[0]?.index; + const categoryName = categories[index]?.name; if (!categoryName || categoryName === selectedCategory) { - removeFilter(filters, { column: 'urbanity' }) + removeFilter(filters, {column: 'urbanity'}); } else { addFilter(filters, { column: 'urbanity', type: FilterType.IN, values: [categoryName], - owner: HISTOGRAM_WIDGET_ID, - }) + owner: HISTOGRAM_WIDGET_ID + }); } - await render() + await render(); } - }, - }) + } + }); } } @@ -201,11 +207,11 @@ const deck = new Deck({ controller: true }); deck.setProps({ - onViewStateChange: (props) => { + onViewStateChange: props => { const {longitude, latitude, ...rest} = props.viewState; map.jumpTo({center: [longitude, latitude], ...rest}); viewState = props.viewState; - debouncedRenderWidgets() + debouncedRenderWidgets(); } }); diff --git a/h3-widgets/package.json b/h3-widgets/package.json index 06894ed..1040020 100644 --- a/h3-widgets/package.json +++ b/h3-widgets/package.json @@ -8,7 +8,8 @@ "build": "vite build", "preview": "vite preview", "link-deck": "yarn link @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", - "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions" + "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", + "format": "npx prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.eslintignore --write" }, "devDependencies": { "vite": "^4.5.0" diff --git a/h3-widgets/selectorUtils.ts b/h3-widgets/selectorUtils.ts index 2894d13..72d7a0e 100644 --- a/h3-widgets/selectorUtils.ts +++ b/h3-widgets/selectorUtils.ts @@ -1,27 +1,25 @@ -type DbType = 'FLOAT' | 'STRING' | 'DATE'; +type DbType = 'FLOAT' | 'STRING' | 'DATE'; interface Variable { - column_name: string; - name: string; - db_type: DbType; - agg_method: string; + column_name: string; + name: string; + db_type: DbType; + agg_method: string; } export async function initSelectors() { - const variableSelector = document.getElementById('variable') as HTMLSelectElement; - const data = await getVariables(); - const options = data - .filter((variable: Variable) => variable.db_type === 'FLOAT') - .map((variable: Variable) => { - return ``; - }); - variableSelector.innerHTML = options.join(''); + const variableSelector = document.getElementById('variable') as HTMLSelectElement; + const data = await getVariables(); + const options = data + .filter((variable: Variable) => variable.db_type === 'FLOAT') + .map((variable: Variable) => { + return ``; + }); + variableSelector.innerHTML = options.join(''); } async function getVariables() { - const response = await fetch( - 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' - ); - return response.json(); + const response = await fetch( + 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' + ); + return response.json(); } - - diff --git a/h3-widgets/utils.ts b/h3-widgets/utils.ts index 3f8cc6d..162c9f3 100644 --- a/h3-widgets/utils.ts +++ b/h3-widgets/utils.ts @@ -1,5 +1,5 @@ -import { createViewportSpatialFilter } from "@carto/api-client"; -import { MapViewState, WebMercatorViewport } from "@deck.gl/core"; +import {createViewportSpatialFilter} from '@carto/api-client'; +import {MapViewState, WebMercatorViewport} from '@deck.gl/core'; export function debounce(func, wait) { let timeout; @@ -11,5 +11,5 @@ export function debounce(func, wait) { export function getSpatialFilterFromViewState(viewState: MapViewState) { const viewport = new WebMercatorViewport(viewState); - return createViewportSpatialFilter(viewport.getBounds()) + return createViewportSpatialFilter(viewport.getBounds()); } diff --git a/quadbin-widgets/README.md b/quadbin-widgets/README.md index ecbe9fe..46abfd0 100644 --- a/quadbin-widgets/README.md +++ b/quadbin-widgets/README.md @@ -1,6 +1,6 @@ ## Example: Spatial Index H3 -This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). +This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). Uses [Vite](https://vitejs.dev/) to bundle and serve files. @@ -17,5 +17,6 @@ yarn ``` Commands: -* `npm dev` is the development target, to serve the app and hot reload. -* `npm run build` is the production target, to create the final bundle and write to disk. + +- `npm dev` is the development target, to serve the app and hot reload. +- `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/quadbin-widgets/index.ts b/quadbin-widgets/index.ts index 911bff2..f836c7f 100644 --- a/quadbin-widgets/index.ts +++ b/quadbin-widgets/index.ts @@ -3,9 +3,16 @@ import 'maplibre-gl/dist/maplibre-gl.css'; import maplibregl from 'maplibre-gl'; import {Deck, MapViewState} from '@deck.gl/core'; import {H3TileLayer, BASEMAP, colorBins, QuadbinTileLayer} from '@deck.gl/carto'; -import { initSelectors } from './selectorUtils'; -import { debounce, getSpatialFilterFromViewState } from './utils'; -import { addFilter, Filters, FilterType, quadbinTableSource, removeFilter, WidgetSource } from '@carto/api-client'; +import {initSelectors} from './selectorUtils'; +import {debounce, getSpatialFilterFromViewState} from './utils'; +import { + addFilter, + Filters, + FilterType, + quadbinTableSource, + removeFilter, + WidgetSource +} from '@carto/api-client'; import Chart from 'chart.js/auto'; const cartoConfig = { @@ -25,16 +32,16 @@ const INITIAL_VIEW_STATE: MapViewState = { bearing: 0, minZoom: 3.5, maxZoom: 15 -} +}; type Source = ReturnType; // Selectors variables let selectedVariable = 'population'; -let aggregationExp = `SUM(${selectedVariable})` +let aggregationExp = `SUM(${selectedVariable})`; let source: Source; -let viewState = INITIAL_VIEW_STATE +let viewState = INITIAL_VIEW_STATE; const filters: Filters = {}; // DOM elements @@ -42,22 +49,24 @@ const variableSelector = document.getElementById('variable') as HTMLSelectElemen const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; -const histogramClearBtn = document.querySelector('.histogram-widget .clear-btn') as HTMLButtonElement; +const histogramClearBtn = document.querySelector( + '.histogram-widget .clear-btn' +) as HTMLButtonElement; histogramClearBtn.addEventListener('click', () => { - removeFilter(filters, { column: 'urbanity' }); + removeFilter(filters, {column: 'urbanity'}); render(); -}) +}); -let histogramChart: Chart; +let histogramChart: Chart; aggMethodLabel.innerText = aggregationExp; variableSelector?.addEventListener('change', () => { const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; - + selectedVariable = variableSelector.value; aggregationExp = `${aggMethod}(${selectedVariable})`; aggMethodLabel.innerText = aggregationExp; - + render(); }); @@ -96,34 +105,29 @@ function renderLayers() { getTooltip: ({object}) => object && { html: `Hex ID: ${object.id}
- ${selectedVariable.toUpperCase()}: ${parseInt( - object.properties.value - )}
+ ${selectedVariable.toUpperCase()}: ${parseInt(object.properties.value)}
Aggregation Expression: ${aggregationExp}` } }); } async function renderWidgets() { - const { widgetSource } = await source - await Promise.all([ - renderFormula(widgetSource), - renderHistogram(widgetSource) - ]) + const {widgetSource} = await source; + await Promise.all([renderFormula(widgetSource), renderHistogram(widgetSource)]); } async function renderFormula(ws: WidgetSource) { - formulaWidget.innerHTML = 'Loading...' + formulaWidget.innerHTML = 'Loading...'; const formula = await ws.getFormula({ column: selectedVariable, operation: 'sum', spatialFilter: getSpatialFilterFromViewState(viewState), - viewState, - }) + viewState + }); formulaWidget.textContent = Intl.NumberFormat('en-US', { - maximumFractionDigits: 0, + maximumFractionDigits: 0 // notation: 'compact' - }).format(formula.value) + }).format(formula.value); } const HISTOGRAM_WIDGET_ID = 'urbanity_widget'; @@ -137,52 +141,54 @@ async function renderHistogram(ws: WidgetSource) { operation: 'count', filterOwner: HISTOGRAM_WIDGET_ID, spatialFilter: getSpatialFilterFromViewState(viewState), - viewState, - }) + viewState + }); histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; - const colors = categories.map((c) => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)') + const colors = categories.map(c => + c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' + ); if (histogramChart) { - histogramChart.data.labels = categories.map((c) => c.name); - histogramChart.data.datasets[0].data = categories.map((c) => c.value); + histogramChart.data.labels = categories.map(c => c.name); + histogramChart.data.datasets[0].data = categories.map(c => c.value); histogramChart.data.datasets[0].backgroundColor = colors; histogramChart.update(); } else { histogramChart = new Chart(histogramWidget, { type: 'bar', data: { - labels: categories.map((c) => c.name), + labels: categories.map(c => c.name), datasets: [ { label: 'Urbanity category', - data: categories.map((c) => c.value), - backgroundColor: colors, + data: categories.map(c => c.value), + backgroundColor: colors } ] }, options: { onClick: async (ev, elems) => { - console.log(elems[0]) - const index = elems[0]?.index - const categoryName = categories[index]?.name + console.log(elems[0]); + const index = elems[0]?.index; + const categoryName = categories[index]?.name; if (!categoryName || categoryName === selectedCategory) { - removeFilter(filters, { column: 'urbanity' }) + removeFilter(filters, {column: 'urbanity'}); } else { addFilter(filters, { column: 'urbanity', type: FilterType.IN, values: [categoryName], - owner: HISTOGRAM_WIDGET_ID, - }) + owner: HISTOGRAM_WIDGET_ID + }); } - await render() + await render(); } - }, - }) + } + }); } } @@ -201,11 +207,11 @@ const deck = new Deck({ controller: true }); deck.setProps({ - onViewStateChange: (props) => { + onViewStateChange: props => { const {longitude, latitude, ...rest} = props.viewState; map.jumpTo({center: [longitude, latitude], ...rest}); viewState = props.viewState; - debouncedRenderWidgets() + debouncedRenderWidgets(); } }); diff --git a/quadbin-widgets/package.json b/quadbin-widgets/package.json index 06894ed..1040020 100644 --- a/quadbin-widgets/package.json +++ b/quadbin-widgets/package.json @@ -8,7 +8,8 @@ "build": "vite build", "preview": "vite preview", "link-deck": "yarn link @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", - "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions" + "unlink-deck": "yarn unlink @deck.gl/core && yarn link @deck.gl/layers && yarn link @deck.gl/geo-layers && yarn link @deck.gl/mesh-layers && yarn link @deck.gl/aggregation-layers && yarn link @deck.gl/carto && yarn link @deck.gl/extensions", + "format": "npx prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.eslintignore --write" }, "devDependencies": { "vite": "^4.5.0" diff --git a/quadbin-widgets/selectorUtils.ts b/quadbin-widgets/selectorUtils.ts index 2894d13..72d7a0e 100644 --- a/quadbin-widgets/selectorUtils.ts +++ b/quadbin-widgets/selectorUtils.ts @@ -1,27 +1,25 @@ -type DbType = 'FLOAT' | 'STRING' | 'DATE'; +type DbType = 'FLOAT' | 'STRING' | 'DATE'; interface Variable { - column_name: string; - name: string; - db_type: DbType; - agg_method: string; + column_name: string; + name: string; + db_type: DbType; + agg_method: string; } export async function initSelectors() { - const variableSelector = document.getElementById('variable') as HTMLSelectElement; - const data = await getVariables(); - const options = data - .filter((variable: Variable) => variable.db_type === 'FLOAT') - .map((variable: Variable) => { - return ``; - }); - variableSelector.innerHTML = options.join(''); + const variableSelector = document.getElementById('variable') as HTMLSelectElement; + const data = await getVariables(); + const options = data + .filter((variable: Variable) => variable.db_type === 'FLOAT') + .map((variable: Variable) => { + return ``; + }); + variableSelector.innerHTML = options.join(''); } async function getVariables() { - const response = await fetch( - 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' - ); - return response.json(); + const response = await fetch( + 'https://public.carto.com/api/v4/data/observatory/metadata/datasets/cdb_spatial_fea_94e6b1f/variables?minimal=false' + ); + return response.json(); } - - diff --git a/quadbin-widgets/utils.ts b/quadbin-widgets/utils.ts index 3f8cc6d..162c9f3 100644 --- a/quadbin-widgets/utils.ts +++ b/quadbin-widgets/utils.ts @@ -1,5 +1,5 @@ -import { createViewportSpatialFilter } from "@carto/api-client"; -import { MapViewState, WebMercatorViewport } from "@deck.gl/core"; +import {createViewportSpatialFilter} from '@carto/api-client'; +import {MapViewState, WebMercatorViewport} from '@deck.gl/core'; export function debounce(func, wait) { let timeout; @@ -11,5 +11,5 @@ export function debounce(func, wait) { export function getSpatialFilterFromViewState(viewState: MapViewState) { const viewport = new WebMercatorViewport(viewState); - return createViewportSpatialFilter(viewport.getBounds()) + return createViewportSpatialFilter(viewport.getBounds()); } From 3451397aaee6698041182d6003a99508a7b17918 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Thu, 12 Dec 2024 18:46:10 +0100 Subject: [PATCH 08/18] use latest allpha to read dataResolution prop --- h3-widgets/index.ts | 1 + h3-widgets/package.json | 2 +- quadbin-widgets/index.ts | 1 + quadbin-widgets/package.json | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index 63c93d0..874e28d 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -74,6 +74,7 @@ function render() { source = h3TableSource({ ...cartoConfig, filters, + dataResolution: 8, aggregationExp: `${aggregationExp} as value`, tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2' }); diff --git a/h3-widgets/package.json b/h3-widgets/package.json index 1040020..0820ced 100644 --- a/h3-widgets/package.json +++ b/h3-widgets/package.json @@ -15,7 +15,7 @@ "vite": "^4.5.0" }, "dependencies": { - "@carto/api-client": "^0.4.1-alpha.0", + "@carto/api-client": "^0.4.2-alpha.0", "@deck.gl/aggregation-layers": "^9.0.17", "@deck.gl/carto": "^9.0.17", "@deck.gl/core": "^9.0.17", diff --git a/quadbin-widgets/index.ts b/quadbin-widgets/index.ts index f836c7f..1add7db 100644 --- a/quadbin-widgets/index.ts +++ b/quadbin-widgets/index.ts @@ -75,6 +75,7 @@ function render() { ...cartoConfig, filters, aggregationExp: `${aggregationExp} as value`, + dataResolution: 15, tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_quadbin15_v1_yearly_v2' }); renderWidgets(); diff --git a/quadbin-widgets/package.json b/quadbin-widgets/package.json index 1040020..0820ced 100644 --- a/quadbin-widgets/package.json +++ b/quadbin-widgets/package.json @@ -15,7 +15,7 @@ "vite": "^4.5.0" }, "dependencies": { - "@carto/api-client": "^0.4.1-alpha.0", + "@carto/api-client": "^0.4.2-alpha.0", "@deck.gl/aggregation-layers": "^9.0.17", "@deck.gl/carto": "^9.0.17", "@deck.gl/core": "^9.0.17", From 8822fadaee6389a569d24790a55562e63cdc12d8 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Tue, 17 Dec 2024 14:36:03 +0100 Subject: [PATCH 09/18] fix widgets and color null cells --- h3-widgets/index.ts | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index 874e28d..f89bc2d 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -75,7 +75,7 @@ function render() { ...cartoConfig, filters, dataResolution: 8, - aggregationExp: `${aggregationExp} as value`, + aggregationExp: `${aggregationExp} as value, any_value(urbanity) as urbanity`, tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2' }); renderWidgets(); @@ -83,6 +83,13 @@ function render() { } function renderLayers() { + + const colorScale = colorBins({ + attr: 'value', + domain: [0, 100, 1000, 10000, 100000, 1000000], + colors: 'PinkYl', + }) + const layers = [ new H3TileLayer({ id: 'h3_layer', @@ -90,14 +97,21 @@ function renderLayers() { opacity: 0.8, pickable: true, extruded: false, - getFillColor: colorBins({ - attr: 'value', - domain: [0, 100, 1000, 10000, 100000, 1000000], - colors: 'PinkYl' - }), + getFillColor: (...args) => { + const color = colorScale(...args); + const d = args[0] + const value = Math.floor(d.properties.value) + if (value > 0) { + return color + } + return [0, 0, 0, 255 * 0.25] + }, lineWidthMinPixels: 0.5, getLineWidth: 0.5, - getLineColor: [255, 255, 255, 100] + getLineColor: [255, 255, 255, 100], + onClick: info => { + console.log(info.object) + } }) ]; @@ -106,7 +120,8 @@ function renderLayers() { getTooltip: ({object}) => object && { html: `Hex ID: ${object.id}
- ${selectedVariable.toUpperCase()}: ${parseInt(object.properties.value)}
+ ${selectedVariable.toUpperCase()}: ${Number(object.properties.value).toFixed(2)}
+ Urbanity: ${object.properties.urbanity}
Aggregation Expression: ${aggregationExp}` } }); @@ -145,13 +160,15 @@ async function renderHistogram(ws: WidgetSource) { viewState }); + categories.sort((a, b) => a.name.localeCompare(b.name)); + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; const colors = categories.map(c => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' - ); + ) if (histogramChart) { histogramChart.data.labels = categories.map(c => c.name); @@ -172,10 +189,10 @@ async function renderHistogram(ws: WidgetSource) { ] }, options: { - onClick: async (ev, elems) => { - console.log(elems[0]); + onClick: async (ev, elems, chart) => { + const labels = chart.data.labels as string[]; const index = elems[0]?.index; - const categoryName = categories[index]?.name; + const categoryName = labels[index]; if (!categoryName || categoryName === selectedCategory) { removeFilter(filters, {column: 'urbanity'}); } else { From 4d43afdae53fdce22e1b8935b3e22f4a6a26e9d7 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Tue, 17 Dec 2024 14:44:31 +0100 Subject: [PATCH 10/18] bring fixes to quadbin widgets --- quadbin-widgets/index.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/quadbin-widgets/index.ts b/quadbin-widgets/index.ts index 1add7db..bf25d36 100644 --- a/quadbin-widgets/index.ts +++ b/quadbin-widgets/index.ts @@ -74,7 +74,7 @@ function render() { source = quadbinTableSource({ ...cartoConfig, filters, - aggregationExp: `${aggregationExp} as value`, + aggregationExp: `${aggregationExp} as value, any_value(urbanity) as urbanity`, dataResolution: 15, tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_quadbin15_v1_yearly_v2' }); @@ -83,6 +83,11 @@ function render() { } function renderLayers() { + const colorScale = colorBins({ + attr: 'value', + domain: [0, 100, 1000, 10000, 100000, 1000000], + colors: 'PinkYl' + }) const layers = [ new QuadbinTileLayer({ id: 'quadbin_layer', @@ -90,11 +95,15 @@ function renderLayers() { opacity: 0.8, pickable: true, extruded: false, - getFillColor: colorBins({ - attr: 'value', - domain: [0, 100, 1000, 10000, 100000, 1000000], - colors: 'PinkYl' - }), + getFillColor: (...args) => { + const color = colorScale(...args); + const d = args[0] + const value = Math.floor(d.properties.value) + if (value > 0) { + return color + } + return [0, 0, 0, 255 * 0.25] + }, lineWidthMinPixels: 0.5, getLineWidth: 0.5, getLineColor: [255, 255, 255, 100] @@ -145,6 +154,8 @@ async function renderHistogram(ws: WidgetSource) { viewState }); + categories.sort((a, b) => a.name.localeCompare(b.name)); + histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); @@ -172,10 +183,10 @@ async function renderHistogram(ws: WidgetSource) { ] }, options: { - onClick: async (ev, elems) => { - console.log(elems[0]); + onClick: async (ev, elems, chart) => { + const labels = chart.data.labels as string[]; const index = elems[0]?.index; - const categoryName = categories[index]?.name; + const categoryName = labels[index]; if (!categoryName || categoryName === selectedCategory) { removeFilter(filters, {column: 'urbanity'}); } else { From 6ab14262737a2ca798144c3a3e44c70526c5df2b Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Tue, 17 Dec 2024 14:46:36 +0100 Subject: [PATCH 11/18] run prettier --- h3-widgets/index.ts | 17 ++++++++--------- quadbin-widgets/index.ts | 10 +++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/h3-widgets/index.ts b/h3-widgets/index.ts index f89bc2d..d7b422f 100644 --- a/h3-widgets/index.ts +++ b/h3-widgets/index.ts @@ -83,12 +83,11 @@ function render() { } function renderLayers() { - const colorScale = colorBins({ attr: 'value', domain: [0, 100, 1000, 10000, 100000, 1000000], - colors: 'PinkYl', - }) + colors: 'PinkYl' + }); const layers = [ new H3TileLayer({ @@ -99,18 +98,18 @@ function renderLayers() { extruded: false, getFillColor: (...args) => { const color = colorScale(...args); - const d = args[0] - const value = Math.floor(d.properties.value) + const d = args[0]; + const value = Math.floor(d.properties.value); if (value > 0) { - return color + return color; } - return [0, 0, 0, 255 * 0.25] + return [0, 0, 0, 255 * 0.25]; }, lineWidthMinPixels: 0.5, getLineWidth: 0.5, getLineColor: [255, 255, 255, 100], onClick: info => { - console.log(info.object) + console.log(info.object); } }) ]; @@ -168,7 +167,7 @@ async function renderHistogram(ws: WidgetSource) { const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; const colors = categories.map(c => c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' - ) + ); if (histogramChart) { histogramChart.data.labels = categories.map(c => c.name); diff --git a/quadbin-widgets/index.ts b/quadbin-widgets/index.ts index bf25d36..d214e2d 100644 --- a/quadbin-widgets/index.ts +++ b/quadbin-widgets/index.ts @@ -87,7 +87,7 @@ function renderLayers() { attr: 'value', domain: [0, 100, 1000, 10000, 100000, 1000000], colors: 'PinkYl' - }) + }); const layers = [ new QuadbinTileLayer({ id: 'quadbin_layer', @@ -97,12 +97,12 @@ function renderLayers() { extruded: false, getFillColor: (...args) => { const color = colorScale(...args); - const d = args[0] - const value = Math.floor(d.properties.value) + const d = args[0]; + const value = Math.floor(d.properties.value); if (value > 0) { - return color + return color; } - return [0, 0, 0, 255 * 0.25] + return [0, 0, 0, 255 * 0.25]; }, lineWidthMinPixels: 0.5, getLineWidth: 0.5, From 84e09b04eb8e65872b7781db7f6f5a9e21554699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Mon, 23 Dec 2024 16:41:36 +0100 Subject: [PATCH 12/18] Add DS Store to gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c0cba18..65b6457 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ dist # Don't lock libraries in examples yarn.lock -package-lock.json \ No newline at end of file +package-lock.json + +#.DS_Store +.DS_Store \ No newline at end of file From a8f4bcc713614166972fdc3844d552d35bba4bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Mon, 23 Dec 2024 16:41:53 +0100 Subject: [PATCH 13/18] Rename directories for consistency --- {h3-widgets => widgets-h3}/.env | 0 {h3-widgets => widgets-h3}/README.md | 0 {h3-widgets => widgets-h3}/images/scale.jpg | Bin {h3-widgets => widgets-h3}/index.html | 0 {h3-widgets => widgets-h3}/index.ts | 0 {h3-widgets => widgets-h3}/package.json | 0 {h3-widgets => widgets-h3}/selectorUtils.ts | 0 {h3-widgets => widgets-h3}/style.css | 0 {h3-widgets => widgets-h3}/tsconfig.json | 0 {h3-widgets => widgets-h3}/utils.ts | 0 {quadbin-widgets => widgets-quadbin}/.env | 0 {quadbin-widgets => widgets-quadbin}/README.md | 0 .../images/scale.jpg | Bin {quadbin-widgets => widgets-quadbin}/index.html | 0 {quadbin-widgets => widgets-quadbin}/index.ts | 0 {quadbin-widgets => widgets-quadbin}/package.json | 0 .../selectorUtils.ts | 0 {quadbin-widgets => widgets-quadbin}/style.css | 0 {quadbin-widgets => widgets-quadbin}/tsconfig.json | 0 {quadbin-widgets => widgets-quadbin}/utils.ts | 0 20 files changed, 0 insertions(+), 0 deletions(-) rename {h3-widgets => widgets-h3}/.env (100%) rename {h3-widgets => widgets-h3}/README.md (100%) rename {h3-widgets => widgets-h3}/images/scale.jpg (100%) rename {h3-widgets => widgets-h3}/index.html (100%) rename {h3-widgets => widgets-h3}/index.ts (100%) rename {h3-widgets => widgets-h3}/package.json (100%) rename {h3-widgets => widgets-h3}/selectorUtils.ts (100%) rename {h3-widgets => widgets-h3}/style.css (100%) rename {h3-widgets => widgets-h3}/tsconfig.json (100%) rename {h3-widgets => widgets-h3}/utils.ts (100%) rename {quadbin-widgets => widgets-quadbin}/.env (100%) rename {quadbin-widgets => widgets-quadbin}/README.md (100%) rename {quadbin-widgets => widgets-quadbin}/images/scale.jpg (100%) rename {quadbin-widgets => widgets-quadbin}/index.html (100%) rename {quadbin-widgets => widgets-quadbin}/index.ts (100%) rename {quadbin-widgets => widgets-quadbin}/package.json (100%) rename {quadbin-widgets => widgets-quadbin}/selectorUtils.ts (100%) rename {quadbin-widgets => widgets-quadbin}/style.css (100%) rename {quadbin-widgets => widgets-quadbin}/tsconfig.json (100%) rename {quadbin-widgets => widgets-quadbin}/utils.ts (100%) diff --git a/h3-widgets/.env b/widgets-h3/.env similarity index 100% rename from h3-widgets/.env rename to widgets-h3/.env diff --git a/h3-widgets/README.md b/widgets-h3/README.md similarity index 100% rename from h3-widgets/README.md rename to widgets-h3/README.md diff --git a/h3-widgets/images/scale.jpg b/widgets-h3/images/scale.jpg similarity index 100% rename from h3-widgets/images/scale.jpg rename to widgets-h3/images/scale.jpg diff --git a/h3-widgets/index.html b/widgets-h3/index.html similarity index 100% rename from h3-widgets/index.html rename to widgets-h3/index.html diff --git a/h3-widgets/index.ts b/widgets-h3/index.ts similarity index 100% rename from h3-widgets/index.ts rename to widgets-h3/index.ts diff --git a/h3-widgets/package.json b/widgets-h3/package.json similarity index 100% rename from h3-widgets/package.json rename to widgets-h3/package.json diff --git a/h3-widgets/selectorUtils.ts b/widgets-h3/selectorUtils.ts similarity index 100% rename from h3-widgets/selectorUtils.ts rename to widgets-h3/selectorUtils.ts diff --git a/h3-widgets/style.css b/widgets-h3/style.css similarity index 100% rename from h3-widgets/style.css rename to widgets-h3/style.css diff --git a/h3-widgets/tsconfig.json b/widgets-h3/tsconfig.json similarity index 100% rename from h3-widgets/tsconfig.json rename to widgets-h3/tsconfig.json diff --git a/h3-widgets/utils.ts b/widgets-h3/utils.ts similarity index 100% rename from h3-widgets/utils.ts rename to widgets-h3/utils.ts diff --git a/quadbin-widgets/.env b/widgets-quadbin/.env similarity index 100% rename from quadbin-widgets/.env rename to widgets-quadbin/.env diff --git a/quadbin-widgets/README.md b/widgets-quadbin/README.md similarity index 100% rename from quadbin-widgets/README.md rename to widgets-quadbin/README.md diff --git a/quadbin-widgets/images/scale.jpg b/widgets-quadbin/images/scale.jpg similarity index 100% rename from quadbin-widgets/images/scale.jpg rename to widgets-quadbin/images/scale.jpg diff --git a/quadbin-widgets/index.html b/widgets-quadbin/index.html similarity index 100% rename from quadbin-widgets/index.html rename to widgets-quadbin/index.html diff --git a/quadbin-widgets/index.ts b/widgets-quadbin/index.ts similarity index 100% rename from quadbin-widgets/index.ts rename to widgets-quadbin/index.ts diff --git a/quadbin-widgets/package.json b/widgets-quadbin/package.json similarity index 100% rename from quadbin-widgets/package.json rename to widgets-quadbin/package.json diff --git a/quadbin-widgets/selectorUtils.ts b/widgets-quadbin/selectorUtils.ts similarity index 100% rename from quadbin-widgets/selectorUtils.ts rename to widgets-quadbin/selectorUtils.ts diff --git a/quadbin-widgets/style.css b/widgets-quadbin/style.css similarity index 100% rename from quadbin-widgets/style.css rename to widgets-quadbin/style.css diff --git a/quadbin-widgets/tsconfig.json b/widgets-quadbin/tsconfig.json similarity index 100% rename from quadbin-widgets/tsconfig.json rename to widgets-quadbin/tsconfig.json diff --git a/quadbin-widgets/utils.ts b/widgets-quadbin/utils.ts similarity index 100% rename from quadbin-widgets/utils.ts rename to widgets-quadbin/utils.ts From 8e1809787f0e37046aec766f46994d98b5632b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Mon, 23 Dec 2024 18:05:22 +0100 Subject: [PATCH 14/18] Simplify and polish H3 example --- widgets-h3/README.md | 12 +++++++---- widgets-h3/index.html | 41 +++++++++++++++++++------------------ widgets-h3/index.ts | 39 +++++++++++++++++------------------ widgets-h3/selectorUtils.ts | 2 +- widgets-h3/style.css | 8 ++++---- 5 files changed, 53 insertions(+), 49 deletions(-) diff --git a/widgets-h3/README.md b/widgets-h3/README.md index 46abfd0..546ad97 100644 --- a/widgets-h3/README.md +++ b/widgets-h3/README.md @@ -1,12 +1,16 @@ -## Example: Spatial Index H3 +## Example: CARTO Widgets over H3 Spatial Index sources -This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). +This is an evolution of our [H3 example](https://github.com/CartoDB/deck.gl-examples/tree/master/hello-world), adding charts and filtering capabilities. + +It showcases how to use [Widget models in CARTO](https://docs.carto.com/carto-for-developers/charts-and-widgets) to easily build interactive data visualizations that stay synchronized with the map, with added interactions such as filtering with inputs or by clicking in the charts. And in this case, how to integrate them into spatial index sources, such as H3 and Quadbin, for optimal performance and scalability. + +The UI for the charts is built using [Chart JS](https://www.chartjs.org/) but developers can plug their own charting or data visualization library. Uses [Vite](https://vitejs.dev/) to bundle and serve files. ## Usage -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/spatial-features-h3?file=index.ts) +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/widgets-h3?file=index.ts) Or run it locally: @@ -18,5 +22,5 @@ yarn Commands: -- `npm dev` is the development target, to serve the app and hot reload. +- `npm run dev` is the development target, to serve the app and hot reload. - `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/widgets-h3/index.html b/widgets-h3/index.html index 73f57b4..8bda6bc 100644 --- a/widgets-h3/index.html +++ b/widgets-h3/index.html @@ -2,7 +2,7 @@ - CARTO + deck.gl - H3 Spatial Index Widgets + CARTO + deck.gl - H3 Widgets
@@ -10,52 +10,53 @@

✨👀 You're viewing

-

CARTO Widgets for H3 Spatial Index

+

CARTO Widgets for H3 Sources

- More info about + This example showcases how to build widgets (charts and filters) into visualizations using + CARTO + deck.gl + H3 Spatial Index sources. Learn more about H3 Spatial Indexwidgets in CARTO.

- +

Variable

-

Agreggation method applied

-

-

More information about aggregationExp

- legend + legend
Less
More
- +
-

Total Population

+

Total

-

Urbanity categories

-

You can click the bars to filter the map with the selected category

+

Population by urbanity

- +
-

Source: Spatial Features - United States of America (H3 Resolution 8)

+

+ Source: + Spatial Features - United States of America (H3 Resolution 8) +

diff --git a/widgets-h3/index.ts b/widgets-h3/index.ts index d7b422f..078b28d 100644 --- a/widgets-h3/index.ts +++ b/widgets-h3/index.ts @@ -25,11 +25,11 @@ const cartoConfig = { const INITIAL_VIEW_STATE: MapViewState = { // Spain - latitude: 37.3753636, - longitude: -5.9962577, - zoom: 6, - pitch: 0, - bearing: 0, + latitude: 35.3753636, + longitude: -14.9962577, + zoom: 5.2, + pitch: 50, + bearing: 5, minZoom: 3.5, maxZoom: 15 }; @@ -46,7 +46,6 @@ const filters: Filters = {}; // DOM elements const variableSelector = document.getElementById('variable') as HTMLSelectElement; -const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; const histogramClearBtn = document.querySelector( @@ -59,13 +58,11 @@ histogramClearBtn.addEventListener('click', () => { let histogramChart: Chart; -aggMethodLabel.innerText = aggregationExp; variableSelector?.addEventListener('change', () => { const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; selectedVariable = variableSelector.value; aggregationExp = `${aggMethod}(${selectedVariable})`; - aggMethodLabel.innerText = aggregationExp; render(); }); @@ -93,9 +90,9 @@ function renderLayers() { new H3TileLayer({ id: 'h3_layer', data: source, - opacity: 0.8, + opacity: 0.75, pickable: true, - extruded: false, + extruded: true, getFillColor: (...args) => { const color = colorScale(...args); const d = args[0]; @@ -105,12 +102,15 @@ function renderLayers() { } return [0, 0, 0, 255 * 0.25]; }, + getElevation: (...args) => { + const d = args[0]; + return d.properties.value; + }, + coverage: 0.95, + elevationScale: 0.2, lineWidthMinPixels: 0.5, getLineWidth: 0.5, - getLineColor: [255, 255, 255, 100], - onClick: info => { - console.log(info.object); - } + getLineColor: [255, 255, 255, 100] }) ]; @@ -153,25 +153,24 @@ async function renderHistogram(ws: WidgetSource) { const categories = await ws.getCategories({ column: 'urbanity', - operation: 'count', + operation: 'sum', + operationColumn: selectedVariable, filterOwner: HISTOGRAM_WIDGET_ID, spatialFilter: getSpatialFilterFromViewState(viewState), viewState }); - categories.sort((a, b) => a.name.localeCompare(b.name)); - histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; const colors = categories.map(c => - c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' + c.name === selectedCategory ? 'rgba(255, 99, 132, 0.8)' : 'rgba(54, 162, 235, 0.75)' ); if (histogramChart) { histogramChart.data.labels = categories.map(c => c.name); - histogramChart.data.datasets[0].data = categories.map(c => c.value); + histogramChart.data.datasets[0].data = categories.map(c => Math.floor(c.value)); histogramChart.data.datasets[0].backgroundColor = colors; histogramChart.update(); } else { @@ -182,7 +181,7 @@ async function renderHistogram(ws: WidgetSource) { datasets: [ { label: 'Urbanity category', - data: categories.map(c => c.value), + data: categories.map(c => Math.floor(c.value)), backgroundColor: colors } ] diff --git a/widgets-h3/selectorUtils.ts b/widgets-h3/selectorUtils.ts index 72d7a0e..28994dd 100644 --- a/widgets-h3/selectorUtils.ts +++ b/widgets-h3/selectorUtils.ts @@ -10,7 +10,7 @@ export async function initSelectors() { const variableSelector = document.getElementById('variable') as HTMLSelectElement; const data = await getVariables(); const options = data - .filter((variable: Variable) => variable.db_type === 'FLOAT') + .filter((variable: Variable) => ['population', 'female', 'male'].includes(variable.column_name)) .map((variable: Variable) => { return ``; }); diff --git a/widgets-h3/style.css b/widgets-h3/style.css index a9faff8..92613bf 100644 --- a/widgets-h3/style.css +++ b/widgets-h3/style.css @@ -7,9 +7,7 @@ html { font-family: Inter, sans-serif; font-weight: 400; font-size: 1rem; -} - -body { + line-height: 1.5; margin: 0; } @@ -132,7 +130,9 @@ button:hover { font-family: monospace; } -.relative { position: relative; } +.relative { + position: relative; +} .loader { font-size: 14px; From e7717e9acd822a9b752f3a8238f2d1b39f0a7558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Tue, 24 Dec 2024 11:31:10 +0100 Subject: [PATCH 15/18] Change h3 example to US data --- widgets-h3/.env | 2 +- widgets-h3/index.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/widgets-h3/.env b/widgets-h3/.env index 34986fb..88ed826 100644 --- a/widgets-h3/.env +++ b/widgets-h3/.env @@ -2,4 +2,4 @@ VITE_API_BASE_URL=https://gcp-us-east1.api.carto.com # This API Access Token only grants access to demo data for the examples (h3 spatial features demo data). # To replace this token with your own, go to app.carto.com -> Developers -> Credentials -> API Access Tokens. -VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfNDd1dW5tZWciLCJqdGkiOiJlOThmMDg0MSJ9.X8emzWzf4i0kJ0jsp7CEzG830MglsAGHmuVIe6rLOtg +VITE_API_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiJkOTU4OWMyZiJ9.78MdzU2J6y-J6Far71_Mh7IQO9eYIZD9nECUiZJAVL4 diff --git a/widgets-h3/index.ts b/widgets-h3/index.ts index 078b28d..f135b92 100644 --- a/widgets-h3/index.ts +++ b/widgets-h3/index.ts @@ -9,7 +9,7 @@ import { addFilter, Filters, FilterType, - h3TableSource, + h3QuerySource, removeFilter, WidgetSource } from '@carto/api-client'; @@ -24,17 +24,16 @@ const cartoConfig = { }; const INITIAL_VIEW_STATE: MapViewState = { - // Spain - latitude: 35.3753636, - longitude: -14.9962577, - zoom: 5.2, - pitch: 50, - bearing: 5, + latitude: 35.7128, + longitude: -88.006, + zoom: 5, + pitch: 60, + bearing: 0, minZoom: 3.5, maxZoom: 15 }; -type Source = ReturnType; +type Source = ReturnType; // Selectors variables let selectedVariable = 'population'; @@ -68,12 +67,13 @@ variableSelector?.addEventListener('change', () => { }); function render() { - source = h3TableSource({ + source = h3QuerySource({ ...cartoConfig, filters, dataResolution: 8, aggregationExp: `${aggregationExp} as value, any_value(urbanity) as urbanity`, - tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2' + sqlQuery: + 'SELECT * FROM cartobq.public_account.derived_spatialfeatures_usa_h3int_res8_v1_yearly_v2' }); renderWidgets(); renderLayers(); From 20ad2ac9a38237584bba86e990df07647ac21f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Tue, 24 Dec 2024 11:32:16 +0100 Subject: [PATCH 16/18] fix readme link --- widgets-h3/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgets-h3/README.md b/widgets-h3/README.md index 546ad97..b4d0502 100644 --- a/widgets-h3/README.md +++ b/widgets-h3/README.md @@ -1,6 +1,6 @@ ## Example: CARTO Widgets over H3 Spatial Index sources -This is an evolution of our [H3 example](https://github.com/CartoDB/deck.gl-examples/tree/master/hello-world), adding charts and filtering capabilities. +This is an evolution of our [H3 example](https://github.com/CartoDB/deck.gl-examples/tree/master/spatial-features-h3), adding charts and filtering capabilities. It showcases how to use [Widget models in CARTO](https://docs.carto.com/carto-for-developers/charts-and-widgets) to easily build interactive data visualizations that stay synchronized with the map, with added interactions such as filtering with inputs or by clicking in the charts. And in this case, how to integrate them into spatial index sources, such as H3 and Quadbin, for optimal performance and scalability. From 12436190a4674fafa7d3fa03bad299d8ba3fa960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Tue, 24 Dec 2024 11:56:59 +0100 Subject: [PATCH 17/18] h3 - fixes caption and package --- widgets-h3/index.html | 19 ++++++++++--------- widgets-h3/package.json | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/widgets-h3/index.html b/widgets-h3/index.html index 8bda6bc..729e00e 100644 --- a/widgets-h3/index.html +++ b/widgets-h3/index.html @@ -46,17 +46,18 @@

CARTO Widgets for H3 Sources

+
+

+ Source: + Spatial Features - United States of America (H3 Resolution 8) +

-

- Source: - Spatial Features - United States of America (H3 Resolution 8) -

diff --git a/widgets-h3/package.json b/widgets-h3/package.json index 0820ced..e874b54 100644 --- a/widgets-h3/package.json +++ b/widgets-h3/package.json @@ -1,5 +1,5 @@ { - "name": "carto-deckgl-spatial-index-h3-example", + "name": "carto-deckgl-example-widgets-h3", "version": "0.0.0", "private": true, "scripts": { From 7804afc2fc0c6d075ae49d7964935f0285c0dd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Tena?= Date: Tue, 24 Dec 2024 11:57:54 +0100 Subject: [PATCH 18/18] Updates quadbin example to match h3 (simplified, fixes, copywriting) --- widgets-quadbin/README.md | 10 ++++++--- widgets-quadbin/index.html | 38 +++++++++++++++++--------------- widgets-quadbin/index.ts | 14 +++++------- widgets-quadbin/package.json | 2 +- widgets-quadbin/selectorUtils.ts | 2 +- widgets-quadbin/style.css | 8 +++---- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/widgets-quadbin/README.md b/widgets-quadbin/README.md index 46abfd0..980eb79 100644 --- a/widgets-quadbin/README.md +++ b/widgets-quadbin/README.md @@ -1,12 +1,16 @@ -## Example: Spatial Index H3 +## Example: CARTO Widgets over Quadbin Spatial Index sources -This is a great example on how performant spatial indexes are to visualize and operate with large geospatial datasets. In this case we're using a dataset based in an hexagonal grid (H3), from our CARTO Data Observatory. This datasets includes demographic, financial, and environmental variables across the US. CARTO + deck.gl offer native support for spatial indexes. If you want to learn more about spatial indexes, we recommend you to check our [Spatial Indexes 101 guide](https://go.carto.com/report-spatial-indexes-101). +This is an evolution of our [Quadbin example](https://github.com/CartoDB/deck.gl-examples/tree/master/spatial-features-quadbin), adding charts and filtering capabilities. + +It showcases how to use [Widget models in CARTO](https://docs.carto.com/carto-for-developers/charts-and-widgets) to easily build interactive data visualizations that stay synchronized with the map, with added interactions such as filtering with inputs or by clicking in the charts. And in this case, how to integrate them into spatial index sources, such as H3 and Quadbin, for optimal performance and scalability. + +The UI for the charts is built using [Chart JS](https://www.chartjs.org/) but developers can plug their own charting or data visualization library. Uses [Vite](https://vitejs.dev/) to bundle and serve files. ## Usage -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/spatial-features-h3?file=index.ts) +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/CartoDB/deck.gl-examples/tree/master/widgets-quadbin?file=index.ts) Or run it locally: diff --git a/widgets-quadbin/index.html b/widgets-quadbin/index.html index f23d5af..9430eb0 100644 --- a/widgets-quadbin/index.html +++ b/widgets-quadbin/index.html @@ -12,50 +12,52 @@

✨👀 You're viewing

CARTO Widgets for Quadbin Spatial Index

- More info about + This example showcases how to build widgets (charts and filters) into visualizations using + CARTO + deck.gl + Quadbin Spatial Index sources. Learn more about Quadbin Spatial Indexwidgets in CARTO.

- +

Variable

-

Agreggation method applied

-

-

More information about aggregationExp

- legend + legend
Less
More
- +
-

Total Population

+

Total

-

Urbanity categories

-

You can click the bars to filter the map with the selected category

+

Population by urbanity

- +
+
+

+ Source: + Spatial Features - Spain (Quadbin Resolution 15) +

-

Source: Spatial Features - United States of America (Quadbin Resolution 15)

diff --git a/widgets-quadbin/index.ts b/widgets-quadbin/index.ts index d214e2d..c348ac2 100644 --- a/widgets-quadbin/index.ts +++ b/widgets-quadbin/index.ts @@ -26,8 +26,8 @@ const cartoConfig = { const INITIAL_VIEW_STATE: MapViewState = { // Spain latitude: 37.3753636, - longitude: -5.9962577, - zoom: 6, + longitude: -7.9962577, + zoom: 4.8, pitch: 0, bearing: 0, minZoom: 3.5, @@ -46,7 +46,6 @@ const filters: Filters = {}; // DOM elements const variableSelector = document.getElementById('variable') as HTMLSelectElement; -const aggMethodLabel = document.getElementById('agg-method') as HTMLSelectElement; const formulaWidget = document.getElementById('formula-data') as HTMLDivElement; const histogramWidget = document.getElementById('histogram-data') as HTMLCanvasElement; const histogramClearBtn = document.querySelector( @@ -59,13 +58,11 @@ histogramClearBtn.addEventListener('click', () => { let histogramChart: Chart; -aggMethodLabel.innerText = aggregationExp; variableSelector?.addEventListener('change', () => { const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM'; selectedVariable = variableSelector.value; aggregationExp = `${aggMethod}(${selectedVariable})`; - aggMethodLabel.innerText = aggregationExp; render(); }); @@ -148,20 +145,19 @@ async function renderHistogram(ws: WidgetSource) { const categories = await ws.getCategories({ column: 'urbanity', - operation: 'count', + operation: 'sum', + operationColumn: selectedVariable, filterOwner: HISTOGRAM_WIDGET_ID, spatialFilter: getSpatialFilterFromViewState(viewState), viewState }); - categories.sort((a, b) => a.name.localeCompare(b.name)); - histogramWidget.parentElement?.querySelector('.loader')?.classList.toggle('hidden', true); histogramWidget.classList.toggle('hidden', false); const selectedCategory = filters['urbanity']?.[FilterType.IN]?.values[0]; const colors = categories.map(c => - c.name === selectedCategory ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)' + c.name === selectedCategory ? 'rgba(255, 99, 132, 0.75)' : 'rgba(54, 162, 235, 0.75)' ); if (histogramChart) { diff --git a/widgets-quadbin/package.json b/widgets-quadbin/package.json index 0820ced..fe14c08 100644 --- a/widgets-quadbin/package.json +++ b/widgets-quadbin/package.json @@ -1,5 +1,5 @@ { - "name": "carto-deckgl-spatial-index-h3-example", + "name": "carto-deckgl-example-widgets-quadbin", "version": "0.0.0", "private": true, "scripts": { diff --git a/widgets-quadbin/selectorUtils.ts b/widgets-quadbin/selectorUtils.ts index 72d7a0e..28994dd 100644 --- a/widgets-quadbin/selectorUtils.ts +++ b/widgets-quadbin/selectorUtils.ts @@ -10,7 +10,7 @@ export async function initSelectors() { const variableSelector = document.getElementById('variable') as HTMLSelectElement; const data = await getVariables(); const options = data - .filter((variable: Variable) => variable.db_type === 'FLOAT') + .filter((variable: Variable) => ['population', 'female', 'male'].includes(variable.column_name)) .map((variable: Variable) => { return ``; }); diff --git a/widgets-quadbin/style.css b/widgets-quadbin/style.css index a9faff8..92613bf 100644 --- a/widgets-quadbin/style.css +++ b/widgets-quadbin/style.css @@ -7,9 +7,7 @@ html { font-family: Inter, sans-serif; font-weight: 400; font-size: 1rem; -} - -body { + line-height: 1.5; margin: 0; } @@ -132,7 +130,9 @@ button:hover { font-family: monospace; } -.relative { position: relative; } +.relative { + position: relative; +} .loader { font-size: 14px;