Skip to content

Commit

Permalink
Add spatial index widgets examples (#31)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Álex Tena <[email protected]>
Co-authored-by: Don McCurdy <[email protected]>
  • Loading branch information
3 people authored Jan 3, 2025
1 parent 864591f commit e6084c8
Show file tree
Hide file tree
Showing 22 changed files with 1,142 additions and 2 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ dist

# Don't lock libraries in examples
yarn.lock
package-lock.json
package-lock.json

#.DS_Store
.DS_Store
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"devDependencies": {
"vite": "^4.5.0",
"ocular-dev-tools": "^2.0.0-alpha.15"
}
},
"packageManager": "[email protected]+sha512.f825273d0689cc9ead3259c14998037662f1dcd06912637b21a450e8da7cfeb4b1965bbee73d16927baa1201054126bc385c6f43ff4aa705c8631d26e12460f1"
}
5 changes: 5 additions & 0 deletions widgets-h3/.env
Original file line number Diff line number Diff line change
@@ -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 (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.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiJkOTU4OWMyZiJ9.78MdzU2J6y-J6Far71_Mh7IQO9eYIZD9nECUiZJAVL4
26 changes: 26 additions & 0 deletions widgets-h3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## 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/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.

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/widgets-h3?file=index.ts)

Or run it locally:

```bash
npm install
# or
yarn
```

Commands:

- `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.
Binary file added widgets-h3/images/scale.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions widgets-h3/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>CARTO + deck.gl - H3 Widgets</title>
</head>
<body>
<div id="map"></div>
<canvas id="deck-canvas"></canvas>
<div id="top-left">
<div id="story-card">
<p class="overline">✨👀 You're viewing</p>
<h2>CARTO Widgets for H3 Sources</h2>
<p>
This example showcases how to build widgets (charts and filters) into visualizations using
CARTO + deck.gl + H3 Spatial Index sources. Learn more about
<a
href="https://docs.carto.com/carto-for-developers/reference/carto-widgets-reference/"
rel="noopener noreferrer"
target="_blank"
>widgets in CARTO.</a
>
</p>

<div class="layer-controls">
<p class="overline">Variable</p>
<select name="variable" id="variable" class="select"></select>
<div>
<img class="legend" src="./images/scale.jpg" alt="legend" />
<div class="label-container">
<div>Less</div>
<div>More</div>
</div>
</div>
</div>

<div class="widgets">
<div class="widget formula-widget">
<p class="overline">Total</p>
<div id="formula-data"></div>
</div>
<div class="widget histogram-widget relative">
<button class="clear-btn">Clear filter</button>
<p class="overline">Population by urbanity</p>
<p class="loader hidden">Loading...</p>
<canvas id="histogram-data" height="200px"></canvas>
</div>
</div>
<hr />
<p class="caption">
Source:
<a
href="https://carto.com/spatial-data-catalog/browser/dataset/cdb_spatial_fea_94e6b1f/"
rel="noopener noreferrer"
target="_blank"
>Spatial Features - United States of America (H3 Resolution 8)</a
>
</p>
</div>
</div>
<script type="module" src="./index.ts"></script>
</body>
</html>
235 changes: 235 additions & 0 deletions widgets-h3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
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 {
addFilter,
Filters,
FilterType,
h3QuerySource,
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 = {
latitude: 35.7128,
longitude: -88.006,
zoom: 5,
pitch: 60,
bearing: 0,
minZoom: 3.5,
maxZoom: 15
};

type Source = ReturnType<typeof h3QuerySource>;

// 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 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;

variableSelector?.addEventListener('change', () => {
const aggMethod = variableSelector.selectedOptions[0].dataset.aggMethod || 'SUM';

selectedVariable = variableSelector.value;
aggregationExp = `${aggMethod}(${selectedVariable})`;

render();
});

function render() {
source = h3QuerySource({
...cartoConfig,
filters,
dataResolution: 8,
aggregationExp: `${aggregationExp} as value, any_value(urbanity) as urbanity`,
sqlQuery:
'SELECT * FROM cartobq.public_account.derived_spatialfeatures_usa_h3int_res8_v1_yearly_v2'
});
renderWidgets();
renderLayers();
}

function renderLayers() {
const colorScale = colorBins({
attr: 'value',
domain: [0, 100, 1000, 10000, 100000, 1000000],
colors: 'PinkYl'
});

const layers = [
new H3TileLayer({
id: 'h3_layer',
data: source,
opacity: 0.75,
pickable: true,
extruded: true,
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];
},
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]
})
];

deck.setProps({
layers,
getTooltip: ({object}) =>
object && {
html: `Hex ID: ${object.id}</br>
${selectedVariable.toUpperCase()}: ${Number(object.properties.value).toFixed(2)}</br>
Urbanity: ${object.properties.urbanity}</br>
Aggregation Expression: ${aggregationExp}`
}
});
}

async function renderWidgets() {
const {widgetSource} = await source;
await Promise.all([renderFormula(widgetSource), renderHistogram(widgetSource)]);
}

async function renderFormula(ws: WidgetSource) {
formulaWidget.innerHTML = '<span style="font-weight: 400; font-size: 14px;">Loading...</span>';
const formula = await ws.getFormula({
column: selectedVariable,
operation: 'sum',
spatialFilter: getSpatialFilterFromViewState(viewState),
spatialIndexReferenceViewState: 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: 'sum',
operationColumn: selectedVariable,
filterOwner: HISTOGRAM_WIDGET_ID,
spatialFilter: getSpatialFilterFromViewState(viewState),
spatialIndexReferenceViewState: 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.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 => Math.floor(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 => Math.floor(c.value)),
backgroundColor: colors
}
]
},
options: {
onClick: async (ev, elems, chart) => {
const labels = chart.data.labels as string[];
const index = elems[0]?.index;
const categoryName = labels[index];
if (!categoryName || categoryName === selectedCategory) {
removeFilter(filters, {column: 'urbanity'});
} else {
addFilter(filters, {
column: 'urbanity',
type: FilterType.IN,
values: [categoryName],
owner: HISTOGRAM_WIDGET_ID
});
}
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();
29 changes: 29 additions & 0 deletions widgets-h3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "carto-deckgl-example-widgets-h3",
"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",
"format": "npx prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.eslintignore --write"
},
"devDependencies": {
"vite": "^4.5.0"
},
"dependencies": {
"@carto/api-client": "^0.4.4",
"@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"
}
}
Loading

0 comments on commit e6084c8

Please sign in to comment.