Lattice Grid Buy a licence

api reference

Leaflet API Reference

bindLeaflet: Leaflet layers as one more viewer of a grid, the map's view as the grid's viewport filter, and a click as its selection. Leaflet stays the page's; nothing of it is imported.

API reference › The Leaflet binding

All 18 pages Everything on one page → Developer guide →

The Leaflet binding

modules/leaflet makes Leaflet layers one more viewer of a grid, with the same contract as the deck.gl adapter: the grid decides which rows, your code decides how they look, the map's view can filter the grid back, and a click on a feature selects its row. Leaflet stays yours - the module imports none of it, ships no dependency, and is handed your L.Map and a function that builds layers with your own L.

import { bindLeaflet } from '@toclocoinc/lattice-grid/modules/leaflet';

const map = L.map('map').setView([51.5, -0.1], 11);
const binding = bindLeaflet(grid, {
  map,
  position: { geometry: 'place' },                       // or { lon: 'lon', lat: 'lat' }
  layers: (rows, ctx) => ctx.binned
    ? rows.map((c) => L.rectangle([[c.south, c.west], [c.north, c.east]], { weight: 0, fillOpacity: 0.4 }))
    : [L.geoJSON(ctx.features, {
        pointToLayer: (f, at) => L.circleMarker(at, { radius: ctx.selected.includes(f.id) ? 8 : 4 }),
      })],
  viewportFilter: true,                                  // the map's view filters the grid
});

Grid → map. layers(rows, ctx) is called with the rows the grid shows - every leaf row after its filter and sort for a grid that holds them all - on every change of the rows or of the selection, once per frame however many changes land. The layers it returned last time are removed from the map and the new ones added; binding.update() rebuilds now and binding.layers() returns what is on the map. ctx.features carries one GeoJSON Feature per placed row (its id the row key, the row its properties), the place read through the geometry type's own reader whatever the cell holds - a GeoJSON object, a parsed geometry, WKT, WKB or GeoJSON text - or from a lon/lat pair. ctx.provenance.rows is always ctx.features.length; a row whose place is blank or unreadable is counted in provenance.skipped. A paged pushdown grid hands over the rows inside the map's view from the engine, and past viewportCap (default 20,000) the engine's density cells with ctx.binned true - exactly as bindDeck does, through the same code.

Map → grid. With viewportFilter: true (or { debounce }, default 150 ms) every moveend/zoomend writes the map's getBounds() as one condition on the grid - a withinBbox on a geometry position, a between pair on lon/lat - the map viewport filter's own condition. That includes your own setView, fitBounds and flyTo: Leaflet ends each with moveend, so a view set in code filters the grid just as a drag does. A matching condition set from elsewhere moves the map to its box instead of being overwritten.

Selection. A click on a feature of a layer built from ctx.features (an L.geoJSON(ctx.features), or a marker you give a feature with the row's id) selects its row; for a layer that carries no feature, call binding.select(key) (null clears). ctx.selected holds the selected keys, and selection:changed re-runs layers. The grid must be created with selection: 'single' or 'multiple': a row it does not take warns leaflet:selection rather than doing nothing silently. destroy() removes the layers, the condition and every listener.

Refused by name. A map without addLayer, removeLayer, on, off and getBounds (leaflet:map), a layers that is not a function (leaflet:layers), or a position naming no column of the grid (leaflet:position) warns once and binds nothing. A view over the cap that the engine cannot bin warns leaflet:over and hands over no rows rather than a random subset.

Rows to the map, a filter followed, a setView written back as the grid's filter, a click selecting, select() for a host layer, and destroy

const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { bindLeaflet } = await import('../packages/modules/leaflet/index.js');

const grid = createHeadlessGrid({
  rowKey: 'id',
  selection: 'single',
  columns: [{ field: 'id' }, { field: 'mag', type: 'number' },
    { field: 'lon', type: 'number' }, { field: 'lat', type: 'number' }],
  rows: Array.from({ length: 40 }, (_, i) => ({ id: 'r' + i, mag: i % 4, lon: -100 + i * 5, lat: 10 })),
});

// A stand-in for L.map: layers kept in a set, a box we set, and setView ending
// with moveend as Leaflet's does. On a page this is L.map(...), and the layer
// is L.geoJSON(ctx.features).
let box = [-180, -85, 180, 85];
const on = {};
const map = {
  layers: new Set(),
  addLayer(l) { map.layers.add(l); }, removeLayer(l) { map.layers.delete(l); },
  on(t, fn) { on[t] = fn; }, off(t) { delete on[t]; },
  getBounds: () => ({ getWest: () => box[0], getSouth: () => box[1], getEast: () => box[2], getNorth: () => box[3] }),
  setView(next) { box = next; on.moveend(); },
};
const geoJson = (features) => {
  const children = features.map((feature) => ({ feature, on(t, fn) { this.click = fn; }, off() {} }));
  return { children, eachLayer: (fn) => children.forEach(fn) };
};

const binding = bindLeaflet(grid, {
  map,
  position: { lon: 'lon', lat: 'lat' },
  layers: (rows, ctx) => [geoJson(ctx.features)],
  viewportFilter: { debounce: 0 },
  viewportCap: 20000,
});
const seen = [binding.layers()[0].children.length];

grid.filters.set({ col: 'mag', op: 'gte', value: 2 });
binding.update();                                  // now, rather than on the next frame
seen.push(binding.layers()[0].children.length);

map.setView([-100, 0, -50, 20]);                   // the page moves the map in code
await new Promise((r) => setTimeout(r, 30));
seen.push(grid.rows.matchCount());

const hit = binding.layers()[0].children.find((c) => c.feature.id === 'r7');
hit.click({ target: hit });                        // a click on a feature
seen.push(grid.selection.keys()[0]);

binding.select('r3');                              // for a layer that carries no feature
seen.push(grid.selection.keys()[0]);

binding.destroy();                                 // the box condition goes with it
seen.push(grid.rows.matchCount());
return seen.join(' | ');                           // 40 | 20 | 5 | r7 | r3 | 20

Type reference

Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.

The Leaflet binding

LeafletBindingOptions

What {@link bindLeaflet} takes: the same contract as `bindDeck`. Leaflet is the page's: the module imports none of it, and is handed the page's `L.Map` and a function that builds layers with the page's own `L`.

PropertyTypeDescription
map{ addLayer(layer: unknown): unknown; removeLayer(layer: unknown): unknown; on(type: string, fn: (e: unknown) => void): unknown; off(type: string, fn: (e: unknown) => void): unknown; getBounds(): { getWest(): number; getSouth(): number; getEast(): number; getNorth(): number }; fitBounds?(bounds: [[number, number], [number, number]], options?: Record<string, unknown>): unknown; }The page's Leaflet map (`L.map(...)`). The binding calls its `addLayer`/`removeLayer`, listens for `moveend`/`zoomend` with `on`/`off`, reads its view from `getBounds()` and moves it with `fitBounds()`. Missing any of the first five: nothing is bound and `leaflet:map` warns.
position{ lon: string; lat: string } | { geometry: string }The column(s) rows are placed by: a `lon`/`lat` pair, or a `geometry` column, whose cells may hold GeoJSON, a parsed geometry, or WKT, WKB or GeoJSON text. A name that is not a column of the grid: nothing is bound and `leaflet:position` warns.
viewportFilterboolean | { debounce?: number }The map's view as a filter on the grid, exactly as a map chart's {@link ChartSpec.viewportFilter}: after every `moveend`/`zoomend` - a drag or zoom by the reader, or the page's own `setView`, `fitBounds` or `flyTo` - debounced (default 150 ms), the binding writes the map's `getBounds()` as ONE condition: a `withinBbox` on a geometry column, or a `between` pair on `lon`/`lat`. A matching condition set from elsewhere moves the map to its box. Off by default. (optional)
viewportCapnumberThe most rows fetched for one view of a paged pushdown grid before the engine's density cells are handed over instead; as {@link ChartSpec.viewportCap}, default 20,000. (optional)
MethodSignatureParametersReturnsDescription
layers(rows: Array<Record<string, unknown>>, ctx: LeafletLayerContext) => unknown[]rows: Array<Record<string, unknown>>
ctx: LeafletLayerContext
=> unknown[]Builds the layers from the rows the grid shows, called on every change of the rows or the selection (one call per frame): every leaf row after its filter and sort for a grid that holds them all; for a paged pushdown grid, the rows inside the map's view fetched from the engine, or - past {@link LeafletBindingOptions.viewportCap} - the engine's density cells, with `ctx.binned` true. The layers it returned last time are removed from the map and these added. A layer built from `ctx.features` (an `L.geoJSON(ctx.features)`) selects a row when one of its features is clicked. Not a function: nothing is bound and `leaflet:layers` warns.

LeafletLayerContext

The second argument of {@link LeafletBindingOptions.layers}.

PropertyTypeDescription
gridGridThe bound grid.
mapLeafletBindingOptions['map']The page's map.
position{ lon?: string; lat?: string; geometry?: string }The position, as given.
binnedbooleanTrue when `rows` are density cells (`{ west, south, east, north, count }`), not rows.
pendingbooleanTrue while an engine request for the view is in flight; `rows` is the last answer.
featuresArray<{ type: 'Feature'; id: string | number; geometry: Record<string, unknown>; properties: Record<string, unknown> }>One GeoJSON `Feature` per placed row: its `id` the row key, the row its `properties`; empty when binned.
provenanceChartViewportProvenance & { skipped: number }What was handed over - the shape of `chart.provenance().viewport`, where `rows` is the rows drawn and always equals `features.length`, plus `skipped`: the rows handed over whose position is blank or could not be read, counted here and never as drawn; 0 when binned.
selectedArray<string | number>The keys of the grid's selected rows, for the layers to mark.

LeafletBinding

A live binding between a grid and a Leaflet map.

Properties

No properties.

Methods
MethodSignatureParametersReturnsDescription
update(): void - voidRebuild the layers and swap them onto the map now, rather than on the next frame.
layers(): unknown[] - unknown[]The layers the binding last put on the map.
select(key: string | number | null): voidkey: string | number | nullvoidSelect a row by key, for a layer the page made that carries no feature (a click on a feature of an `L.geoJSON(ctx.features)` selects on its own); `null` clears the selection. The grid must be created with `selection: 'single'` or `'multiple'`; a row it does not take warns `leaflet:selection`.
destroy(): void - voidRemove the binding's layers, its viewport condition, and its map and grid listeners.
Events

No events.