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"). Your words are matched against the ontology's object types, so you never need exact type slugs: "drones" finds both the Drone entity and its DroneObservation positions, "bases" finds MilitaryBase, and the answer states how each word was matched. Asking for kinds of things plots only those layers; asking for "everything" plots every mappable type.
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.
Line-geometry layers (routes)
In addition to point markers, MapView can render polyline routes —
useful for cable routes, transit corridors, or any linear feature. A layer may
carry both markers and lines simultaneously; the toggle hides both.
MapLayer.lines
Add a lines array to any layer. Each entry is a MapBlockLine:
import { MapView, type MapLayer, type MapBlockLine } from '@scrydon/ui/map-viewer'
const layers: MapLayer[] = [
{
id: 'cables',
label: 'Submarine cables',
color: '#e84040',
markers: [], // anchor points, if any
lines: [
{
id: 'cable-1',
label: 'North Atlantic Express',
path: [[-73.9, 40.7], [-20.1, 64.1], [-6.3, 53.3]], // [lng, lat] pairs
description: 'Trans-Atlantic cable system',
},
],
},
]
return <MapView layers={layers} height={480} />The path is an array of [longitude, latitude] pairs (GeoJSON coordinate
order). MapView renders each line with the layer color and includes line
vertices when auto-fitting the viewport bounds.
Authoring a line-geometry data source
To expose route geometry through the ontology pipeline, add a pathJson
column (string) to your data source table. The platform's geo query route
recognises the keys pathJson, routeJson, and geometryJson and parses
them as [[lng, lat], …] arrays.
defineDataSource({
kind: 'table',
id: 'my-cable-source',
vendor: 'my-vendor',
scope: 'org',
table: {
name: 'cable_route',
columns: [
{ name: 'cableId', dataType: 'string', isPrimaryKey: true },
{ name: 'cableName', dataType: 'string' },
{ name: 'anchorLatitude', dataType: 'decimal' }, // midpoint for bounds/label
{ name: 'anchorLongitude', dataType: 'decimal' },
{ name: 'pathJson', dataType: 'string' }, // JSON [[lng,lat],…]
{ name: 'updatedAt', dataType: 'timestamp' },
],
primaryKey: ['cableId'],
timestampColumn: 'updatedAt',
},
ingest: { mode: 'poll', intervalSec: 86400, writeMode: 'upsert' },
})Declare pathJson as a string property on the corresponding object type and
include it in the silver_table columnMap. The geo route will parse and
validate the path automatically; malformed or out-of-range coordinates are
silently dropped.
defineObjectType({
slug: 'CableRoute',
displayName: 'Cable route',
kind: 'reference',
properties: [
defineProperty({ slug: 'cableId', displayName: 'Cable ID', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'cableName', displayName: 'Name', scalarType: 'string', cardinality: 'optional' }),
defineProperty({ slug: 'anchorLatitude', displayName: 'Anchor lat', scalarType: 'float', cardinality: 'optional' }),
defineProperty({ slug: 'anchorLongitude', displayName: 'Anchor lng', scalarType: 'float', cardinality: 'optional' }),
defineProperty({ slug: 'pathJson', displayName: 'Route path', scalarType: 'string', cardinality: 'optional' }),
],
})When a query_map response includes features with a resolved path, those
features are split into a lines array on the layer rather than markers.
No tool configuration is required — the agent's query_map call handles the
split automatically.
ODbL attribution for OpenStreetMap-derived layers
If your route data is derived from OpenStreetMap (e.g. submarine cables extracted via Overpass), you must include attribution wherever the layer is displayed or distributed:
© OpenStreetMap contributors, licensed under the Open Database License (ODbL).
The ODbL also requires share-alike: any database derived from ODbL data must be released under the same license. See opendatacommons.org/licenses/odbl for the full text. Non-commercial-only data sources (such as TeleGeography's Submarine Cable Map, licensed CC BY-NC-SA) are not suitable for use in customer deployments — use OSM/ODbL instead.