-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add spatial index widgets examples (#31)
--------- Co-authored-by: Álex Tena <[email protected]> Co-authored-by: Don McCurdy <[email protected]>
- Loading branch information
1 parent
864591f
commit e6084c8
Showing
22 changed files
with
1,142 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,4 +25,7 @@ dist | |
|
||
# Don't lock libraries in examples | ||
yarn.lock | ||
package-lock.json | ||
package-lock.json | ||
|
||
#.DS_Store | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,5 +4,6 @@ | |
"devDependencies": { | ||
"vite": "^4.5.0", | ||
"ocular-dev-tools": "^2.0.0-alpha.15" | ||
} | ||
}, | ||
"packageManager": "[email protected]+sha512.f825273d0689cc9ead3259c14998037662f1dcd06912637b21a450e8da7cfeb4b1965bbee73d16927baa1201054126bc385c6f43ff4aa705c8631d26e12460f1" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.