Appearance
Cross-filtering
The map and the rest of the dashboard talk to each other through dashboard variables. Add a variable, map something to it under Cross-filtering, and the map becomes a control for every other panel.
There are four kinds of mapping, and they behave quite differently.

| Source | Direction | What lands in the variable |
|---|---|---|
| Filter | map ↔ variable | the value of a kepler filter |
| Click | map → variable | the clicked entity's value of a column |
| Coordinates | map → variables | the clicked place, as a lat/lng pair |
| Center | variables → map | nothing; it reads a pair and moves the viewport |
| Chart | map → variable | what a chart's cross-filter holds on a column |
Why variables and not Grafana's own cross-filtering
Because a variable is read from the URL, which has two consequences that matter.
The map reacts to a variable even when its own query never mentions it — so you can filter the map from a dropdown, a text box, or a data link on a table row, without the map re-querying. And a shared link carries the state: send someone a URL and they see the filter you were looking at.
Filter — the two-way one
The default. Pick a column and a variable; a select, multi-select or text filter set on that column in kepler's filter panel writes its value to the variable, and changing the variable applies the matching filter back on the map.
Cross-filtering
Field: vehicle_type
Variable: $vehicle_typeNow filtering the map to buses filters every panel whose query says WHERE type IN ($vehicle_type), and picking buses from the variable picker filters the map.
Numeric ranges
Name a second variable and the mapping becomes a range mapping: a numeric range filter on the column publishes its bounds as a min/max pair, exactly the way the time window does.
sql
WHERE speed BETWEEN $speed_min AND $speed_maxWhen the filter is removed, both variables are written blank rather than left holding their old values. That is deliberate: with the map at "no filter" and the variables at the old pair, neither side would ever match the last agreement again and the variables could never drive the map again.
The "All" value
Grafana's $__all and the empty string are both read as "no value", so a multi-value variable set to All clears the filter rather than filtering for the literal string $__all.
Click — publish the entity
Switch a mapping's source to Click. Now clicking a point or a trajectory writes that entity's value of the column into the variable.
sql
-- on any consuming panel
WHERE vehicle_id = '$vehicle'One way only, map → variable. Clicking empty map clears the variable — but only when the running selection was published from this panel, so a shared link's preset value survives a stray click on the sea.
Shift+click — several at once
When the variable is multi-value (a custom variable with Multi-value on), Shift+click adds the entity to what it holds instead of replacing it, and Shift+click on one already in it takes it out. A plain click still replaces the whole selection with one entity; a Shift+click on empty map clears nothing, so a slip does not cost what you built. Consume it as a list:
sql
WHERE vehicle_id IN (${vehicle:singlequote})On a single-value variable Shift+click is a click like any other, since the variable cannot hold two.
Coordinates — publish the place
Switch a mapping's source to Coordinates. Instead of a column value, the pair of variables named under Lat and Lng receives the coordinate that was clicked.
That is what lets another panel run a spatial query around the click, with $radius an ordinary dashboard variable:
sql
WHERE $lat != ''
AND ST_DWithin(geom, ST_MakePoint($lng, $lat)::geography, $radius)The computed coverage comes back to the map as one more query, so the panel needs no spatial logic of its own.
How pinning works
The panel switches kepler's coordinate interaction on by itself for this — and switches it back on if you turn it off, since the mapping would otherwise silently stop working.
Any click pins the coordinate. Clicking again unpins it on the map, but the variables keep the last spot until the next pin, so the consuming query never loses its centre and never runs with an empty geometry.
While the draw toolbar is engaged, clicks belong to the drawing and publish nothing.
Values are rounded to six decimal places, about 0.1 m — enough that re-pinning the same spot does not rewrite the variable and re-run the dashboard.
Center — read a pair back
Switch a mapping's source to Center and the same kind of pair works the other way: when the variables change from outside the map, the viewport centres on them, optionally jumping to the zoom level named in the mapping.
This is what turns a table into a map control. Give the table's query the coordinates as hidden columns and a per-row data link that writes them:
?var-lat=${__data.fields.latitude}&var-lng=${__data.fields.longitude}Clicking a row flies the map to that feature. A pair of text boxes works the same way.
Two behaviours keep it sane: the map's own clicks are recognised and never re-centre it, and on load the saved viewport wins — the pair moves the map only when someone changes it.
Fit to data when a variable changes
A centre needs a pair and keeps the zoom the mapping names, which suits a feature, not a region. When a variable moves the whole dataset somewhere else — a state picker, a route, a city — name it under Cross-filtering → Fit to data when these change instead: when it changes, the map frames its new rows as it does on load, at whatever zoom they need, from California to Rhode Island. Nothing else moves the viewport: a refresh, a time change or any other variable leaves it where the user put it.
The rows a change brings can arrive in steps — a chain of browser datasets answers once per dataset, first with the old rows or with none. The map waits for rows somewhere else than the old ones, frames them, and frames any further step in the next few seconds; after that the map is the user's. Leave out variables that only filter what is already on screen, or every change would pull the map back to the whole dataset.
Chart — a chart's cross-filter
Switch a mapping's source to Chart cross-filter and pick a column: when a chart in kepler's charts panel cross-filters that column, the variable receives the value. A heatmap filters two columns at once, so two rows — one per axis — turn a click on a cell into two variables: on the tide gauges dashboard, the port and the three-hour block, which a time series under the map then draws minute by minute.
It works one way only. The variable never writes a filter back onto the map, clicking the cell again empties the variable, and a dashboard opened with the variable already in its URL keeps it until a chart changes. A Filter mapping on the same column still ignores chart filters.
Combining them
The five kinds are exclusive per mapping row, but a panel can have several rows. A common arrangement is a Click mapping for the entity, a Coordinates mapping for spatial queries, and a Filter mapping for a category — three rows, three variables, one map driving the whole dashboard.
Select from the popup
On a map where selecting is cheap, a click that publishes straight away is exactly right. On a map where it reloads a tab, re-runs a heavy query or swaps a whole dashboard's worth of panels, it makes looking expensive — and a reader who cannot look without committing stops exploring.
Turn on Select from the popup, under Cross-filtering, and the click is split in two: it opens kepler's popup with the entity's details and publishes nothing. The popup's Select button is what writes the variables, and Clear is what empties them.
The option appears once the panel has a Click mapping, or a drawn-area variable with Click to set the area on — those are the channels a click would otherwise publish.
It also tells you what is already selected
The popup knows whether the entity showing is the one the variables already hold, so the button reads as a selection rather than as a suggestion.
The same popup carries the entity's data links and actions, above the Select button.
Seeing what is selected
Whatever the click channels have published is ringed on the map in amber: a ring around a selected point, an outline around a selected polygon or path. It needs no configuring — a panel with a click mapping has it.
Three things about it are deliberate:
- It is read from the variables, not from kepler's clicked entity. kepler forgets what was clicked on every data refresh, and never knew it at all on a dashboard opened from a link. The variables survive both, so the halo does too — which is what makes a shared link show which entity the numbers beside the map are about.
- No click can land on it. The rings are drawn above every layer so they are never buried, and they are deaf to the pointer, so clicking a ringed feature still hits the feature.
- It follows the columns, not one mapping. A panel that maps both a zone and a station rings the selected station and every row of the selected zone — the union, which is what a reader expects from two filters at once.
In a split map it is drawn only in the pane where the layer it belongs to is visible.
A selection the query flags
A click publishes one entity, and a variable can hold a few. A selection of thousands — every tract a chart's brush caught — is better worked out by the query than carried in the URL, where a shared link that long fails. Name a column under Cross-filtering → Highlight rows where: the rows where it is true (or 1) are ringed like a click's, beside whatever the click mappings published. The query computes it from what the dashboard holds, for instance a brushed box's four edges:
sql
SELECT geoid, geojson, share,
z BETWEEN $x0 AND $x1 AND lag BETWEEN $y0 AND $y1 AS selected
FROM tractsThe four variables are short whatever the brush catches. As everywhere on this page, they come from another panel: a map that filtered on its own selection could never show the rest again.
Dim the rest
Cross-filtering → Dim the rest covers every row outside the selection under a veil of the base map's tone — dark on the dark styles and satellite, light on the others — so the selection stands out the way it does in GeoDa. It works with either kind of selection, and only while the selection marks something on that pane: a selection that lies elsewhere dims nothing. The veil is deaf to the pointer like the rings, so a dimmed feature still answers a click.
Under the veil the amber outline is more than a selection needs, so Cross-filtering → Selection outline set to Auto (the default) draws one thin line in the base map's contrast colour — white on a dark style, near-black on a light one — round a handful of shapes, and no outline at all beyond 50 on a pane, where outlines of small shapes merge into blots and the veil already says it all. Without the veil, Auto is the amber it has always been. Amber, Thin and None pick one style whatever the selection.
Linked to a Mosaic selection
Variables travel through the URL, and every panel that reads one runs its query again once the slider comes to rest. With the Chaski app there is a faster path, the one its Mosaic and filters panels take among themselves: a Mosaic Selection, shared by name on the dashboard. Name it under Cross-filtering → Mosaic selection, and the map takes part in it the way a brush does:
- Map → Mosaic. Each range, time or multi-select filter on the map is a clause on its column, sent while the slider moves. The Mosaic panels redraw under the pointer, from pre-aggregated tables, as they do for their own brushes.
- Mosaic → map. A brush or a pick in a Mosaic or filters panel, on a column the map's data also has, lands on the map's filter of that column — making the filter if the map had none, and taking it away again when the brush clears. It is a filter in the browser: the map's query does not run again.
- One window per column. A histogram of the same column in a Mosaic panel shows the map's window as its brush, and the last one moved wins. The map is never filtered by its own clauses.
Mosaic location columns (lng, lat) name the longitude and latitude columns of the Selection's data, and add the map's place:
- Filter by the drawn area (on by default): the polygon or rectangle drawn on the map keeps the rows inside it, in every Mosaic panel. Draw another and it takes over; move a vertex and the panels follow; delete it and they show everything again. With several figures, the latest one drawn or edited counts, as for the drawn-area variable.
- Filter by the view (off by default): what the map shows filters the Selection as a bounding box on those two columns while you pan and zoom.
The area needs DuckDB's spatial extension, which Chaski loads.
The column names are the link: a clause on fare lands on the map's fare, so name the map's columns as the Chaski dataset names them. The map's data can come from any datasource; the clauses filter the Mosaic panels, which read Chaski. It needs the Chaski app (2.2 or later) on the Grafana and a Chaski dataset on the dashboard, and works alongside the variable mappings above, which still carry the state in the URL. A time filter that follows the dashboard time range (Time sync set to To map or Two-way) stays out of the Selection: a brush elsewhere would move the time range of the whole dashboard.
What else the map can publish
The bounding box of the current view, and any polygon you draw on it — see Publishing the viewport and drawn areas.