Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spatial index widgets examples #31

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
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),
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
});
}
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();
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.2-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"
}
}
Loading