@sqlrooms/deck
Deck.gl integration for SQLRooms with JSON-driven map specs, dataset registry binding, DuckDB-backed or in-memory Arrow datasets, and GeoArrow-first geometry preparation.
Map resources and dashboard adapters
Worksheet maps are first-class deckMaps resources. The root package export contains the resource slice, renderer, settings, direct DuckDB data adapter, and resource orchestration APIs. It does not require Mosaic.
Mosaic dashboard panel support is opt-in through @sqlrooms/deck/mosaic. Dashboard panels keep their panel storage, query clients, cross-filter selection, and issue translation inside that adapter boundary.
DeckMapSettingsPanel is the shared host-neutral editor for both surfaces. It receives a map config, selected table, available tables, and edit callbacks; worksheet resources and Mosaic panels only adapt their respective stores to that contract. Layer, binding, style, extrusion, and code-view controls therefore stay consistent without putting Mosaic APIs in the worksheet settings path.
Use getDeckMapDataPolicy(...) to resolve a map config into the exported DeckMapDataPolicy runtime row-limit policy.
Worksheet map runtime issues distinguish dataset SQL failures (sql-error) from fit-to-data bounds failures (fit-error), so each issue is cleared only after its corresponding operation recovers.
DeckMapDataAdapter.resolveFitDataset can provide an unsampled source for fit-to-data bounds queries. The direct adapter uses the authored source for bounds while applying the configured row-limit policy only to rendered rows.
Worksheet maps deliberately use independent selection semantics. Their direct data adapter executes each configured SQL/table dataset through the room's DuckDB connector and neither reads nor publishes Mosaic selections. This drops the old incidental intra-map cross-filtering between datasets; a future host-neutral selection adapter can add that behavior without changing map resource ownership.
Installation
npm install @sqlrooms/deck @sqlrooms/duckdb @sqlrooms/uiWhat This Package Does
@sqlrooms/deck is the JSON-spec bridge between SQLRooms data and deck.gl:
- render a DeckGL map from a serializable
DeckJsonMapspec - bind one or more datasets through a
datasetsregistry - generate starter JSON specs from datasets with
createDeckJsonSpecFromDatasets - validate SQLRooms-specific layer bindings under
_sqlroomsBinding - prepare geometry for GeoArrow-native layers from
@geoarrow/deck.gl-geoarrowand GeoJSON fallback layers - support shared declarative color scales through
@sqlrooms/color-scales
Use this package when you want deck.gl layers to be driven by a JSON-like spec instead of hand-constructing deck layer instances in React code.
Quick Start
import {DeckJsonMap} from '@sqlrooms/deck';
const spec = {
initialViewState: {
longitude: -122.4,
latitude: 37.74,
zoom: 10,
pitch: 0,
bearing: 0,
},
controller: true,
layers: [
{
'@@type': 'GeoArrowScatterplotLayer',
id: 'airports',
_sqlroomsBinding: {
dataset: 'airports',
geometryColumn: 'geom',
},
getFillColor: {
'@@function': 'colorScale',
field: 'scalerank',
type: 'sequential',
scheme: 'YlOrRd',
domain: 'auto',
},
getRadius: '@@=6',
radiusMinPixels: 2,
},
],
};
export function AirportsMap() {
return (
<DeckJsonMap
spec={spec}
datasets={{
airports: {
sqlQuery:
'SELECT name, abbrev, scalerank, ST_AsWKB(geom) AS geom FROM airports',
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
}}
mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
/>
);
}Auto Spec Generation
If you want a starter JSON spec instead of writing every layer manually, use createDeckJsonSpecFromDatasets(...):
import {createDeckJsonSpecFromDatasets, DeckJsonMap} from '@sqlrooms/deck';
const datasets = {
earthquakes: {
arrowTable,
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
};
const spec = createDeckJsonSpecFromDatasets({datasets});By default, the helper is conservative:
- point / multipoint ->
GeoArrowScatterplotLayer - linestring / multilinestring ->
GeoArrowPathLayer - polygon / multipolygon ->
GeoArrowPolygonLayer - mixed, unknown, or unsupported ->
GeoJsonLayer
You can provide semantic hints for special layers:
const spec = createDeckJsonSpecFromDatasets({
datasets,
hints: {
earthquakes: {prefer: 'heatmap'},
trips: {
type: 'GeoArrowTripsLayer',
timestampColumn: 'timestamps',
},
flows: {
type: 'GeoArrowArcLayer',
sourceGeometryColumn: 'source_geom',
targetGeometryColumn: 'target_geom',
},
hexes: {
type: 'GeoArrowH3HexagonLayer',
hexagonColumn: 'h3',
},
},
});Mosaic Dashboard Renderer
@sqlrooms/deck can contribute a deck-json-map panel renderer to @sqlrooms/mosaic dashboards without making the Mosaic package depend on deck.gl or MapLibre. Pass the renderer when creating the Mosaic dashboard slice.
The dashboard renderer exposes DeckMapDashboardSettings through its renderer definition. DeckMapBlockSettings is also exported for block-document hosts that embed maps as stateful blocks.
import {
createDeckMapDashboardPanelConfig,
DECK_MAP_DASHBOARD_PANEL_TYPE,
deckMapDashboardPanelRenderer,
} from '@sqlrooms/deck';
import {
createDefaultMosaicDashboardPanelRenderers,
createMosaicDashboardSlice,
MosaicDashboard,
} from '@sqlrooms/mosaic';
const dashboardSlice = createMosaicDashboardSlice({
panelRenderers: createDefaultMosaicDashboardPanelRenderers({
[DECK_MAP_DASHBOARD_PANEL_TYPE]: deckMapDashboardPanelRenderer,
}),
});
function Dashboard() {
return <MosaicDashboard dashboardId="geo" />;
}
const mapPanel = createDeckMapDashboardPanelConfig({
title: 'Earthquakes map',
spec: {
initialViewState: {longitude: -119.5, latitude: 37, zoom: 4.5},
layers: [
{
'@@type': 'GeoArrowScatterplotLayer',
id: 'earthquakes',
_sqlroomsBinding: {dataset: 'earthquakes'},
},
],
},
datasets: {
earthquakes: {
source: {
sqlQuery:
'SELECT *, ST_AsWKB(ST_Point(Longitude, Latitude)) AS geom FROM earthquakes',
},
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
},
fitToData: {
dataset: 'earthquakes',
longitudeColumn: 'Longitude',
latitudeColumn: 'Latitude',
padding: 40,
maxZoom: 12,
},
});The dashboard renderer uses useMosaicClient, receives Arrow tables directly, and passes them to DeckJsonMap as Arrow-backed datasets. Dataset sources fall back from dataset-level source, to panel source, to the dashboard selected table. When fitToData is provided, the renderer asks DuckDB Spatial for the dataset extent using the declared longitude/latitude columns and fits the initial map view once, instead of inferring bounds from the loaded Arrow payload in React.
Use createDeckMapPanelFromNativeConfig(...) when a host surface already has a native Deck map config, for example from AI tooling, and needs the same dashboard-compatible deck-json-map panel shape that the dashboard map tool creates.
Config Mode
The optional configMode field ('basic' | 'custom') on DeckMapDashboardPanelConfig controls how the map was authored and what editing UI is available:
'basic'(default when absent) — the config uses only properties that the settings panel can represent (layer type, color scale, radius, geometry bindings). The UI settings panel is enabled for user tweaks.'custom'— the config may use any deck.gl JSON props, including those not representable in the UI configurator. Dashboard and document map settings keep the basic controls disabled so dataset or layer edits cannot rewrite the authored config.
AI tools set this field automatically based on request complexity.
Embeddable Map Blocks
Host applications that expose document-like block surfaces can render maps as durable resources without creating a dashboard. Compose createDeckMapsSlice() into the room store, call ensureDeckMapResourceState(...) for a durable map id, and render it with DeckMapBlockRenderer.
Runtime issue recovery can call deckMaps.clearMapIssue(mapId, kind) to clear only a matching issue kind; omit kind when the map state should clear any stale issue. Replacing a map config clears its prior render issue, while data issues remain until the corresponding dataset recovery is reported. Direct worksheet maps automatically fit the configured dataset when the map or its source first becomes ready; the header action remains available for manual refitting.
Hosts that expose direct worksheet-map AI capability should include getDeckMapResourceAiInstructions() in the responsible agent and tool instructions. createOrUpdateDeckMapResource(...) validates the fully merged resource before any durable block or map write: each dataset needs a source.tableName or source.sqlQuery, and each layer needs an explicit _sqlroomsBinding.dataset. Use mergeDeckMapResourceConfigPatch(...) in host preparation so sparse updates retain durable dataset sources and layers. Pass {replaceLayers: true} when the incoming spec.layers array is the complete desired list and omitted existing layers should be removed; the default remains additive for sparse layer updates. Pass {replaceDatasets: true} when the incoming datasets object is the complete desired registry and omitted existing datasets should be removed; use both flags when replacing a complete multi-dataset layer set.
createDeckMapBlockDocumentType(...) and createDeckMapBlockDocumentCommandType(...) provide the reusable registration metadata for block-document hosts. They register a map stateful block with resizable height, scroll-modifier behavior, map settings, and owned state creation wired through ensureDeckMapResourceState(...):
import {
createDeckMapBlockDocumentCommandType,
createDeckMapBlockDocumentType,
} from '@sqlrooms/deck';
const mapBlockType = createDeckMapBlockDocumentType({
getState: () => roomStore.getState(),
defaultTitle: 'Embedded Map',
});
const mapCommandType = createDeckMapBlockDocumentCommandType({
defaultTitle: 'Embedded Map',
});Hosts still own renderer registration, deletion cleanup, and product-specific side effects. Use afterEnsureState for app-local metadata updates after the map resource is created.
createOrUpdateDeckMapResource(...) is the durable orchestration helper for commands and AI tools. It uses only resource and block callbacks:
import {createOrUpdateDeckMapResource} from '@sqlrooms/deck';
const result = await createOrUpdateDeckMapResource(
{
ensureBlockDocument,
findMapBlock,
findMap,
createMapBlock,
updateBlockMetadata,
ensureMap,
writeMap,
findTable,
prepareConfig,
},
{
blockDocumentId,
mapId,
config,
tableName,
title,
intent,
},
);On create, callers must provide either mapId or createMapId. On update, the default behavior is intentionally strict: missing map blocks and SQL-only dataset sources without a resolvable tableName throw so command paths do not silently retarget stale IDs. AI create flows can opt into missingMapBlockBehavior: 'create'; a supplied mapId is retained, with createMapId used only as its fallback.
Title handling is conservative for Ask AI edits: when title is omitted, createOrUpdateDeckMapResource(...) preserves the existing non-blank block caption or resource title. Passing an explicit title updates the durable map title and uses it as the default block caption. Block metadata is written only after the map write succeeds.
Map authoring helpers such as normalizeDeckMapPointConfig(...), normalizeDeckMapFillColor(...), regenerateMapConfigForTable(...), and dataset-source helpers such as getFirstDatasetSourceTableName(...) are exported so hosts can normalize AI-authored configs before calling createOrUpdateDeckMapResource(...). normalizeDeckMapPointConfig(...) only adds the standard lon/lat point transform to table-backed datasets that do not already declare geometryColumn, source.sqlQuery, or source.transformSql and whose resolved table does not expose a native geometry column; native geometry, polygon, line, and pre-transformed datasets are preserved. When regenerating a map with one existing dataset, its dataset ID is retained and geometry bindings are refreshed so custom layers continue to address the same dataset after a table switch. Non-geospatial tables and multi-dataset maps return the existing config unchanged so callers can keep the current selection when a safe target cannot be inferred. Maps without datasets adopt the generated dataset and layer spec after a valid table is selected.
Core Concepts
DeckJsonMap
DeckJsonMap is the main React component exported by this package. It takes:
spec: a JSON-like deck.gl spec object or JSON stringdatasets: a dataset registry keyed by dataset idinterleaved: when true, deck layers render in MapLibre's own WebGL context instead of a separate overlay canvas. This halves the number of WebGL contexts per map panel (from 2 to 1), which matters because browsers limit active contexts to ~8–16 per page. Default:truedeckProps: runtime-only deck props such asgetTooltip,onHover,onClickmapProps: runtime-only MapLibre propsshowLegends: whether SQLRooms-generated color legends should render
spec stays serializable; callbacks and runtime behavior belong in deckProps or mapProps.
By default, deck.gl renders interleaved into MapLibre's layer stack, sharing a single WebGL context. This allows deck layers to be inserted between basemap layers (e.g. render points under map labels) and reduces WebGL context usage. Set interleaved to false to render deck layers in a separate overlay canvas on top of all basemap layers (uses an additional WebGL context per map).
{
/* Default (interleaved): */
}
<DeckJsonMap spec={spec} datasets={datasets} />;
{
/* Opt out to separate overlay canvas: */
}
<DeckJsonMap spec={spec} datasets={datasets} interleaved={false} />;Dataset Registry
Each SQLRooms-managed layer binds to exactly one dataset through _sqlroomsBinding.dataset.
<DeckJsonMap
spec={spec}
datasets={{
earthquakes: {tableName: 'earthquakes'},
faults: {tableName: 'faults'},
}}
/>Dataset ids are layer-binding labels. Internally, prepared geometry is cached by the resolved data identity, not by dataset id, so multiple maps or layers can reuse the same preparation work when they point at the same table/query.
Dataset Input Kinds
Each dataset entry is one of:
datasets={{
airports: {
sqlQuery: 'SELECT * FROM airports',
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
earthquakePoints: {
tableName: 'earthquakes',
transformSql: `
SELECT *, ST_AsWKB(ST_Point(longitude, latitude)) AS geom
FROM __sqlrooms_source
WHERE longitude IS NOT NULL AND latitude IS NOT NULL
`,
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
faults: {
tableName: 'faults',
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
preview: {
arrowTable,
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
}}sqlQueryRuns a standalone literal query through the DuckDB slice execution path. This query is not rewritten by dashboard table selection.tableNameReads directly from a table or schema-qualified table reference.tableName+transformSqlReads a structured table source through a SQL transform.transformSqlmust be a completeSELECTstatement that reads from SQLRooms' reserved__sqlrooms_sourcerelation. SQLRooms binds that relation to the quotedtableNameat execution time, so dashboards can swap the table source without editing authored SQL.arrowTableUses an already available Apache Arrow table. This is the right input for Arrow-native SQLRooms hooks such asuseSqlanduseMosaicClient.
For in-memory Arrow datasets, arrowTable may be temporarily undefined while data is still loading. DeckJsonMap will keep rendering the basemap and treat that dataset as loading until a table is provided.
Use onDatasetStatesChange when the surrounding UI needs dataset loading, ready, or error state:
<DeckJsonMap
spec={spec}
datasets={datasets}
onDatasetStatesChange={(states) => setDatasetStates(states)}
/>SQLRooms Layer Bindings
SQLRooms-specific layer metadata lives under _sqlroomsBinding:
{
'@@type': 'GeoArrowScatterplotLayer',
id: 'earthquakes',
_sqlroomsBinding: {
dataset: 'earthquakes',
geometryColumn: 'geom',
geometryEncodingHint: 'wkb',
},
getFillColor: {
'@@function': 'colorScale',
field: 'Magnitude',
type: 'sequential',
scheme: 'YlOrRd',
domain: 'auto',
},
}Currently supported SQLRooms binding fields are:
dataset: binds the layer to one dataset idgeometryColumn: overrides geometry column detection for that layergeometryEncodingHint: helps geometry detection when the source table needs itsourceGeometryColumn: source point geometry forGeoArrowArcLayertargetGeometryColumn: target point geometry forGeoArrowArcLayertimestampColumn: timestamp list column forGeoArrowTripsLayerhexagonColumn: H3 index column forGeoArrowH3HexagonLayer
The surrounding deck spec remains intentionally loose so normal deck.gl JSON props still pass through, while _sqlroomsBinding is validated strictly.
Color Scales and Legends
You can ask SQLRooms to derive colors from a field with the colorScale JSON function instead of writing long @@= color expressions:
getFillColor: {
'@@function': 'colorScale',
field: 'Magnitude',
type: 'sequential',
scheme: 'YlOrRd',
domain: 'auto',
clamp: true,
}Discrete numeric palettes are supported too:
getFillColor: {
'@@function': 'colorScale',
field: 'Magnitude',
type: 'quantize',
scheme: 'PuBuGn',
domain: [0, 8],
bins: 5,
}DeckJsonMap renders SQLRooms-generated legends by default for layers that use colorScale. To disable them globally:
<DeckJsonMap spec={spec} datasets={datasets} showLegends={false} />To override the title:
getFillColor: {
'@@function': 'colorScale',
field: 'Magnitude',
type: 'sequential',
scheme: 'YlOrRd',
domain: 'auto',
legend: {
title: 'Magnitude (Mw)',
},
}Supported scale types come from @sqlrooms/color-scales:
sequentialdivergingquantizequantilethresholdcategorical
When domain is set to 'auto', the domain is computed from the currently bound dataset, so colors may shift as filters change. Use explicit domains when you want colors to stay stable across filtering.
Geometry Preparation
prepareDeckDataset(...) is the deck-specific preparation step behind the scenes. It accepts resolved Arrow tables with geometry stored as:
- native GeoArrow
- WKB / GeoArrow WKB
- WKT / GeoArrow WKT
It then produces canonical deck-facing geometry outputs for:
- GeoArrow-native layers such as
GeoArrowScatterplotLayer - GeoJSON-binary fallback layers such as
GeoJsonLayer
Specialized layers such as GeoArrowArcLayer, GeoArrowTripsLayer, and GeoArrowH3HexagonLayer reuse the prepared table but bind additional configured columns on top for source/target geometry, timestamps, or index cells.
This work is cached internally in a module-global prepared dataset store. That cache is separate from any upstream query cache:
- Mosaic-driven queries already benefit from Mosaic's own query cache
- DuckDB SQL datasets still use the DuckDB slice execution path
Deck caches only the expensive geometry preparation layer on top.
Supported Layers
The current curated layer set is:
GeoArrowScatterplotLayerGeoArrowHeatmapLayerGeoArrowColumnLayerGeoArrowPathLayerGeoArrowPolygonLayerGeoArrowSolidPolygonLayerGeoArrowArcLayerGeoArrowTripsLayerGeoArrowH3HexagonLayerGeoJsonLayer
GeoArrow-native geometry columns are the efficient path. WKB/WKT geometry falls back to decoding and GeoJSON-binary preparation, with promotion available for point-focused GeoArrow layers such as GeoArrowScatterplotLayer, GeoArrowHeatmapLayer, and GeoArrowColumnLayer, plus polygon promotion for GeoArrowPolygonLayer and GeoArrowSolidPolygonLayer.
The GeoArrow layer implementations themselves come from @geoarrow/deck.gl-geoarrow.
When querying DuckDB spatial GEOMETRY columns directly, convert them first with ST_AsWKB(...) or ST_AsText(...). DuckDB's internal geometry payload is not the same as standard WKB.
Runtime Props and Children
Keep the spec serializable, then pass runtime behavior separately:
deckPropsfor deck callbacks such asgetTooltip,onHover,onClickmapPropsfor MapLibre props such asprojectionchildrenfor controls, overlays, and popups rendered inside the map
This lets the spec stay stable for storage, validation, and future AI-assisted generation while still supporting interactive React behavior at runtime.
Classes
Interfaces
Type Aliases
- ColorLegendConfig
- ColorLegendConfig
- ColorScaleConfig
- ColorScaleConfig
- DeckGeometryEncodingHint
- ColorScaleFunction
- LayerBindingConfig
- LayerBindingProps
- DeckJsonMapLayerSpec
- DeckJsonMapSpec
- DeckMapStyle
- DeckMapDefaultStyles
- DeckMapDataAdapter
- DeckMapSurfaceProps
- DeckMapResource
- DeckMapsSliceConfig
- DeckMapRuntimeIssue
- DeckMapRuntimeIssueReporter
- DeckMapsSliceState
- DeckMapBlockRendererProps
- DeckMapBlockDocumentRegistrationOptions
- CreateOrUpdateDeckMapResourceHost
- CreateOrUpdateDeckMapResourceParams
- CreateOrUpdateDeckMapResourceResult
- DeckMapDatasetSourceConfig
- DeckMapDataPolicyOverride
- DeckMapDatasetSource
- DeckMapDatasetConfig
- DeckMapInteractionConfig
- DeckMapFitToDataConfig
- DeckMapConfigMode
- DeckMapConfig
- DeckMapDashboardPanelConfig
- DeckMapDashboardDatasetConfig
- DeckMapDashboardInteractionConfig
- DeckMapDashboardFitToDataConfig
- CreateDeckMapDashboardPanelConfigOptions
- DeckMapConfigColumn
- DeckMapTableReference
- DeckMapFillColor
- DeckMapDataPolicy
- DeckMapResourceConfigIssue
- DeckMapResourceConfigValidationOptions
- DeckMapResourceConfigMergeOptions
- GeometryEncodingHint
- ResolvedGeometryEncoding
- ResolvedGeometryColumn
- PreparedGeoArrowLayerData
- PreparedDeckDataset
- DeckAutoLayerType
- DeckSqlDatasetInput
- DeckTableDatasetInput
- DeckTable
- DeckArrowTableDatasetInput
- DeckDatasetInput
- DeckJsonSpecDatasetHint
- CreateDeckJsonSpecFromDatasetsOptions
- PreparedDeckDatasetState
- DeckJsonMapHandle
- DeckJsonMapProps
Variables
- DeckJsonMap
- DeckGeometryEncodingHint
- ColorScaleFunction
- LayerBindingConfig
- LayerBindingProps
- DeckJsonMapLayerSpec
- DeckJsonMapSpec
- DeckMapDefaultStylesProvider
- directDeckMapDataAdapter
- DeckMapResourceSchema
- DeckMapsSliceConfig
- DeckMapSettingsPanel
- DECK_MAP_BLOCK_TYPE
- DECK_MAP_BLOCK_DEFAULT_TITLE
- DECK_MAP_BLOCK_DEFAULT_HEIGHT
- DECK_TABLE_DATASET_SOURCE_RELATION
- DeckMapResourceConfigParameter
- DeckMapResourceToolParameters
- DECK_MAP_DASHBOARD_PANEL_TYPE
- DEFAULT_DECK_MAP_MAX_DATA_POINTS
- isDeckMapDashboardSqlDatasetSource
- isDeckMapDashboardTableDatasetSource
- createDeckMapDashboardConfigForTable
Functions
- ColorScaleLegend
- DeckMapBlockSettings
- useDeckMapDefaultStyles
- resolveDeckMapStyle
- DeckMapSurface
- createDeckMapsSlice
- useStoreWithDeckMaps
- ensureDeckMapResourceState
- DeckMapBlockRenderer
- createDeckMapBlockDocumentType
- createDeckMapBlockDocumentCommandType
- createDeckJsonSpecFromDatasets
- createOrUpdateDeckMapResource
- getFirstDatasetSourceTableName
- hasSqlOnlyDatasetSource
- createDeckTableDatasetSql
- createDeckJsonConfiguration
- createEmptyDeckMapConfig
- createDeckMapDashboardPanelConfig
- asDeckJsonMapConfig
- findDeckMapLongitudeLatitudeColumns
- findLongitudeLatitudeColumns
- findGeometryColumn
- quoteDeckMapSqlIdentifier
- quoteDeckMapSqlTableReference
- createDeckMapPointTransformSql
- normalizeDeckMapPointConfig
- normalizeDeckMapFillColor
- createDeckMapConfigForTable
- createDeckMapDashboardPanelConfigForTable
- regenerateMapConfigForTable
- getDeckMapDataPolicy
- mergeDeckMapResourceConfigPatch
- getDeckMapResourceConfigIssues
- assertDeckMapResourceConfig
- getDeckMapResourceAiInstructions
- prepareDeckDataset
- isSqlDatasetInput
- isTableDatasetInput
- isArrowTableDatasetInput