Map visualizations
Ask Chat to plot geo data on an interactive map — wiring ontology bindings with lat/lng properties to the built-in map block.
Chat can answer geospatial questions with an inline interactive map — markers rendered on a MapLibre GL / OpenStreetMap canvas, with a per-layer toggle and click-to-popup.
Map blocks require the ontology to have at least one object type with latitude and longitude float properties, backed by a silver_table binding to a live data source. See Authoring: Ontologies for how to declare them.
How it works
When Chat calls the query_map tool in response to a geospatial question (e.g. "show the drones and military aircraft near the eastern flank"), it:
- Queries the ontology for the requested object types through the platform's geo query route.
- Normalizes each instance to
{ lat, lng, label, properties }. - Builds a
mapcontent block with one layer per asset category. - Streams the block to the chat panel, where the map renders inline with layer toggles.
No map configuration is required from the operator — the tool picks up whatever latitude/longitude properties exist on the object type and auto-fits the viewport to the data.
Triggering a map
Ask Chat naturally. Phrases that reliably trigger the map tool include:
- "show me the drones and military aircraft near the eastern flank on a map"
- "plot all aircraft positions over the last 15 minutes"
- "map the drone activity in the area of interest"
The tool selects which layers to show based on the question — you can also ask for a single layer ("just the drones").
The map content block
A map block has this shape:
{
"type": "map",
"title": "Airborne activity — Suwałki Gap AOI",
"center": { "lat": 54.1, "lng": 23.0 },
"zoom": 6,
"layers": [
{
"id": "drones",
"label": "Drones",
"color": "#ffb107",
"markers": [
{ "id": "d1", "lat": 54.3, "lng": 23.2, "label": "UAV-ALPHA", "description": "Bayraktar TB2 · friendly" }
]
},
{
"id": "aircraft",
"label": "Aircraft",
"color": "#0069cc",
"markers": [
{ "id": "a1", "lat": 54.0, "lng": 23.5, "label": "NATO01", "description": "30000 ft" }
]
}
]
}The MapView component renders this block. Its props are:
| Prop | Type | Description |
|---|---|---|
layers | MapLayer[] | Required. Each layer has id, label, color, and markers. |
center | { lat, lng } | Optional. If omitted, the viewport auto-fits to the markers. |
zoom | number | Default 6. Initial zoom level (ignored when auto-fitting). |
height | number | string | Default 420. Canvas height in pixels or a CSS value. |
Import MapView from @scrydon/ui/map-viewer:
import { MapView } from '@scrydon/ui/map-viewer'
import type { MapLayer } from '@scrydon/ui/map-viewer'Wiring geo data through the ontology
Map blocks are powered by data that flows through the platform's ontology pipeline. The full wiring:
Data source → StarRocks table → silver_table binding → ontology object type
↓
lat/lng properties projected
↓
query_map tool → map blockDefine a data source that writes rows with latitude and longitude columns. The logical table name (e.g. drone_observation) must match byte-for-byte between the source and the binding.
// In a pack (declarative JSON source) or in-platform (code source with produce())
defineDataSource({
kind: 'table',
id: 'my-asset-tracker',
vendor: 'my-vendor',
scope: 'org',
table: {
name: 'asset_position',
columns: [
{ name: 'assetId', dataType: 'string', isPrimaryKey: true },
{ name: 'latitude', dataType: 'decimal' },
{ name: 'longitude', dataType: 'decimal' },
{ name: 'observedAt', dataType: 'timestamp', isPrimaryKey: true },
],
primaryKey: ['assetId', 'observedAt'],
timestampColumn: 'observedAt',
},
ingest: { mode: 'poll', intervalSec: 60, writeMode: 'append' },
})Declare the object type with latitude and longitude float properties in your ontology manifest.
defineObjectType({
slug: 'AssetPosition',
displayName: 'Asset position',
kind: 'transactional',
properties: [
defineProperty({ slug: 'assetId', displayName: 'Asset ID', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'latitude', displayName: 'Latitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'longitude', displayName: 'Longitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'observedAt', displayName: 'Observed at', scalarType: 'timestamp', cardinality: 'required' }),
],
})Add a silver_table binding that maps the table columns to your object type's properties. The source.table value must match the data source's table.name.
defineBinding({
objectTypeSlug: 'AssetPosition',
kind: 'silver_table',
source: { table: 'asset_position' },
identitySpec: { identityColumns: ['assetId', 'observedAt'] },
columnMap: {
assetId: 'assetId',
latitude: 'latitude',
longitude: 'longitude',
observedAt: 'observedAt',
},
})Add identity rules so instances de-duplicate correctly.
defineIdentityRule({
objectTypeSlug: 'AssetPosition',
deterministicKeys: ['assetId', 'observedAt'],
})Build and install the pack. Once installed, ask Chat to plot the data:
"Show AssetPosition objects on a map."
Chat resolves the ontology binding, projects the lat/lng properties, and renders a map block inline.
Property name conventions
The geo query route resolves coordinates with a multi-name fallback. Use standard names to ensure automatic resolution:
| Coordinate | Accepted property slugs |
|---|---|
| Latitude | latitude, lat |
| Longitude | longitude, lng, lon |
The first matching finite float in the priority order above is used. Properties outside this list are ignored for coordinate resolution (they still appear in marker popups via properties).
Use callsign, droneId, registration, or icao24 as the primary display label — these are resolved first when building the marker label. Any other string property falls back to the instance id.
Using MapView directly
If you are building a custom React surface (e.g. an analytics dashboard or a pack-bundled micro-app), import MapView directly from @scrydon/ui/map-viewer. The component is client-only — it must not be server-rendered.
import { MapView, type MapLayer } from '@scrydon/ui/map-viewer'
const layers: MapLayer[] = [
{
id: 'assets',
label: 'My assets',
color: '#0069cc',
markers: features.map((f) => ({
id: f.id,
lat: f.lat,
lng: f.lng,
label: f.label,
description: f.description,
})),
},
]
return <MapView layers={layers} center={{ lat: 54.1, lng: 23.0 }} zoom={6} height={480} />Add @import "maplibre-gl/dist/maplibre-gl.css" to your app's root stylesheet. Without it the map canvas renders but navigation controls and markers may not display correctly.
The MapView component:
- Uses free OpenStreetMap raster tiles — no API key required.
- Auto-fits the viewport to all visible markers on first render when
centeris omitted. - Renders a layer toggle bar at the top of the card for multi-layer datasets.
- Cleans up the MapLibre instance on unmount.