developer guide
Geospatial Data in a JavaScript Data Grid
A column can hold a shape rather than a coordinate pair: filter it with four spatial operators, push that filtering into DuckDB from a GeoParquet file, and draw it straight onto a map.
The geometry column type
Import the geometry module and a column can be declared type: 'geometry'. It reads a point,
line or polygon from WKB, WKT or GeoJSON and stores one GeoJSON geometry per cell, so the shape a table
already keeps a code or a pair of numbers for becomes a value the grid can filter, sort and export in
its own right. A cell reads as a plain-language summary, in the grid's locale, unless you ask for the
raw form back.
import '@toclocoinc/lattice-grid/modules/geometry';
const grid = createGrid(el, {
columns: [
{ field: 'site', title: 'Site', type: 'geometry' },
{ field: 'reading', title: 'Reading', type: 'number' },
],
rows,
});
// A cell reads WKB (bytes, hex or base64), WKT (including EWKT's SRID=
// prefix) or GeoJSON (an object or JSON text), and stores one GeoJSON
// geometry per cell, longitude first.
Four spatial filter operators
A geometry column takes four operators no other column type offers, evaluated in memory for any source that does not push them down, and offered in the column menu and the tool panel like any other filter.
grid.filters.set({ col: 'site', op: 'withinBbox', value: [-4, 50, 2, 56] }); // [minLon, minLat, maxLon, maxLat]
grid.filters.set({ col: 'site', op: 'withinPolygon', value: polygonGeoJson }); // holes respected
grid.filters.set({ col: 'site', op: 'intersects', value: otherGeometry });
grid.filters.set({ col: 'site', op: 'withinDistance', value: { lon: -0.1278, lat: 51.5074, metres: 5000 } });
Pushed into DuckDB from a GeoParquet file
duckdbAdapter's spatial option loads DuckDB's own spatial extension once per
connection, finds the geometry column, and runs the same four operators in the engine, over a GeoParquet
file or any other source DuckDB can read. A column in another coordinate system reprojects to longitude
and latitude in the query itself.
import { createPushdownSource, duckdbAdapter } from '@toclocoinc/lattice-grid';
import * as compute from '@toclocoinc/lattice-grid';
const source = createPushdownSource({
adapter: duckdbAdapter({
connection, // your own DuckDB connection
from: "read_parquet('data/sites.parquet')", // a GeoParquet file works as-is
spatial: true, // load the spatial extension, find the geometry column
}),
compute,
pageSize: 100,
});
// withinBbox, withinPolygon, intersects and withinDistance now run in DuckDB,
// with the same answers the grid's own evaluator gives.
Maps that read a geometry column
markermap, bubblemap and choropleth take geometry: '<col>'
in place of separate longitude and latitude columns.
createChart({ grid, container, type: 'markermap', geometry: 'site', value: 'reading' });
createChart({ grid, container, type: 'choropleth', geometry: 'site', y: 'reading', label: 'name' });
// A Point row places a marker at its own coordinates; a LineString, Polygon or
// Multi* row places at its centroid, except on a choropleth, which draws the
// polygon itself. choropleth needs no separate geometry pack: the shape is
// the cell value.
The map's own pan and zoom, as a filter
Turn on viewportFilter and the box the reader is looking at becomes a condition on the grid
the map reads from, so every other viewer over the same grid narrows along with the map, in both
directions.
createChart({
grid, container, type: 'markermap', geometry: 'site', value: 'reading',
viewportFilter: true, // or { column, debounce } to narrow either
});
// Pan or zoom, and the chart writes ONE withinBbox condition to the grid's
// own filters, replacing only its own previous one. Set a matching condition
// from elsewhere - a saved view, another panel - and the map pans to it
// instead of being overwritten.
Provenance: what was drawn, and where it was computed
chart.provenance() reports what a map or a chart actually did: how many rows it drew against
how many matched, whether it fell back to density cells, and whether the figure came from the engine or
from the browser.
const p = chart.provenance();
p.viewport; // { rows, matched, cap, binned, bbox } for a markermap, bubblemap or choropleth
p.measures; // per measure, on a grouped chart: { col, fn, computed: 'engine' | 'client' }
Drawing with deck.gl instead
The built-in maps cover markers, bubbles and choropleths; a page wanting its own deck.gl layers over the
same rows imports the deck.gl module instead. bindDeck hands your
own layers function exactly the rows the grid shows, redrawing as they change, and turns the
map's own pan and zoom into the same viewport filter the built-in maps write.
See it running: a value filled straight into each country's own boundary, panning a map to narrow the grid beside it, a spatial filter pushed into DuckDB from a GeoParquet file, and deck.gl drawing straight from the grid over a real basemap. The DuckDB adapter guide covers pushdown in full, and the charts guide covers every other chart type.