kepler-gl-app
GitHub管理 kepler.gl 地图可视化,支持创建配置多种图层、动画、对比视图及数据加载。
Trigger Scenarios
Install
npx skills add keplergl/kepler.gl --skill kepler-gl-app -g -y
SKILL.md
Frontmatter
{
"name": "kepler-gl-app",
"description": "Create and configure kepler.gl map layers (point, flow, flowField, arc, line, grid, hexagon, geojson, cluster, heatmap, h3, trip, s2) from datasets, animate them over time, compare them side by side, change the basemap, switch between the 2D, 3D and globe views, update layer colors, load data from a URL, add columns to existing datasets, and read the current map boundary."
}
Kepler map management
Create and configure kepler.gl map layers from datasets, animate them over time, compare them side by side, change the basemap, update layer colors, load data from a URL, add columns to existing datasets, and read the current map boundary. These are direct map-mutation operations on the kepler.gl map surface.
When to use
Use this skill when the user wants to create or reconfigure map layers, change the appearance or configuration of the map itself, animate data over time, or compare layers side by side. Examples:
- "Show the shops as points colored by visits"
- "Create a heatmap of the pickup locations"
- "Animate the trips over time"
- "Compare the 2019 and 2024 layers side by side"
- "Change the basemap to dark/satellite/positron"
- "Update the color of the layer to blue"
- "Load data from this URL"
- "Add a new variable / column to an existing dataset"
- "Change a column's type (e.g. make NOSOUTH a string)"
- "Rename a column on the existing dataset"
- "What's the current map boundary/extent?"
Do NOT use this skill for analysis beyond map mutation — spatial statistics, spatial filtering, colocation, boundary fetching, or charts. Those belong to your host's analysis skills (e.g. a spatial-analysis skill, a filter skill), not this one.
Commands at a glance
| command | purpose |
|---|---|
map.add-layer |
Add a layer (point, flow, flowField, h3, arc, trip, hexagon, grid, cluster, heatmap, geojson, line, s2) to the map. |
map.add-time-filter |
Animate a NON-trip layer over a TIMESTAMP/DATE column. |
map.toggle-time-filter |
Show/hide the enlarged time controller at the bottom of the map. |
map.split-view |
Enable/disable dual-map comparison. |
map.update-layer-color |
Update an existing layer's color palette. |
map.set-basemap |
Change the basemap style. |
map.set-view-mode |
Switch between the flat 2D map, the tilted 3D map and the 3D globe. |
map.load-data |
Load data from a URL (no layer — call map.add-layer). |
map.save-data |
Save a DuckDB table as a map dataset (no layer — call map.add-layer). |
map.add-column |
Add a column to an EXISTING dataset in place (copy or expression). |
map.create-table |
Create a NEW dataset via SQL (original untouched — call map.add-layer). |
map.get-dataset-context |
List all loaded datasets with their fields and layer configurations. |
map.get-boundary |
Read the current map view's bounding box. |
Every command takes a commandId (one of the table above) and an input
object. Issue commands through your host's map-command dispatcher — the exact
envelope is described in your host's tool listing. In the kepler.gl demo
assistant the dispatcher is the executeApi tool with
apiName: "executeCommand" and args: { commandId, input }.
Picking a layer type
-
Individual locations →
point(clusterfor zoom-out density,heatmapfor continuous intensity). -
Density without precomputed cells →
gridorhexagon(auto-binned). -
Pre-aggregated H3 cells →
h3. -
Origin→destination flows →
flow(a dedicated flow layer with locations, clustering and weighted edges; requires two point lat/lng pairs — source + destination). Fall back toarconly when you want straight great-circle lines between points. For weighted flows optionally pass the magnitude column ascountColumn. -
Vector fields — wind, currents, downhill terrain flow →
flowField, animated streamlines over one vector per sample point. This is notflow:flowjoins an origin pair to a destination pair,flowFielddraws many short lines through a field of vectors. -
Polylines / polygons →
line/geojson. A decoded GEOMETRYgeomcolumn can be rendered directly withgeojson— do NOT convert it to lat/lon columns or build an intermediate table. For geojson datasets use geometryColumn"_geojson". -
Time-animated movement →
trip.
Color configuration
colorByis case-sensitive and must match the column name exactly.simpleColoris the string"[r, g, b]"(e.g."[255, 64, 0]"), not an array. It is a single static color for ALL features — NEVER use it for data-driven coloring; usecolorBy+colorType+colorMapfor that.- For continuous data use
colorType: "breaks"with acolorMapwhose last entry hasvalue: null(the color for the highest bucket):[{value: 0, color: "#fee5d9"}, {value: 50, color: "#fcae91"}, {value: 100, color: "#fb6a4a"}, {value: null, color: "#de2d26"}] - For categorical data use
colorType: "unique":[{value: "retail", color: "#1f77b4"}, {value: "restaurant", color: "#ff7f0e"}]HARD RULE: thevalues MUST be the actual unique values in the column. Classify the variable with your host's classification tool first and use the returned unique values verbatim. NEVER guess category labels from the column name —map.add-layerrejects values that do not exist in the data. - For
heatmap, onlycolorRange(fromcolorMap) takes effect. - To compute the break values, classify the variable with your host's
classification tool first and pass the returned breaks as the
colorMapvalues. - Generate ColorBrewer-style palettes automatically when the user does not specify colors.
Suggested palettes
- Sequential green:
#ffffcc #c2e699 #78c679 #31a354 #006837 #004529 - Sequential red:
#fee5d9 #fcae91 #fb6a4a #de2d26 #a50f15 - Categorical (Tableau 10):
#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b
Workflow
1. Create a layer (map.add-layer)
Point layer colored by a continuous variable:
{
"datasetName": "shops",
"layerType": "point",
"layerName": "Shops by visits",
"colorBy": "visits",
"colorType": "breaks",
"colorMap": [
{"value": 3, "color": "#ffffcc"},
{"value": 10, "color": "#c2e699"},
{"value": 25, "color": "#78c679"},
{"value": 50, "color": "#31a354"},
{"value": 100, "color": "#006837"},
{"value": null, "color": "#004529"}
]
}
Point layer with a uniform color:
{
"datasetName": "pizza_stores",
"layerType": "point",
"layerName": "Pizza stores",
"simpleColor": "[255, 64, 0]"
}
H3 layer:
{
"datasetName": "visits_h3_r9",
"layerType": "h3",
"layerName": "Visits density (h3 r9)",
"colorBy": "visit_count",
"colorType": "breaks",
"colorMap": [
{"value": 100, "color": "#fee5d9"},
{"value": 500, "color": "#fcae91"},
{"value": 2000, "color": "#fb6a4a"},
{"value": 10000, "color": "#de2d26"},
{"value": null, "color": "#a50f15"}
]
}
map.add-layer returns dateTimeColumns when the dataset has TIMESTAMP/DATE
fields — see "Time animation" below for the follow-up.
2. Time animation (NON-trip layers)
When the user asks how something changed/evolved/trended over time and the
source dataset has a TIMESTAMP/DATE column, build a SINGLE layer and animate
it with map.add-time-filter — do NOT create a separate static layer per
time step. A stack of snapshot layers does not convey motion and clutters the
map; the animated layer is the deliverable.
- BEFORE creating any layer for a temporal request, DECLARE the expected
layer count: a change-over-time request ⇒ exactly 1 layer. If you find
yourself about to issue a 2nd
map.add-layer, STOP — you have erred. map.add-layerreturnsdateTimeColumnswhen timestamp/date fields are detected. Pass the first one tomap.add-time-filter.- If the column is an integer/string epoch, build a real TIMESTAMP column first (Kepler.gl cannot handle negative epochs) — NEVER pass an integer/string column directly.
map.add-time-filterinput:{ datasetName, dateTimeColumn, interval? }.intervalis auto-detected from the data when omitted.- Do not retry
map.add-time-filtermore than once. If it fails, report the error and continue — do not loop. - LAYER ORDERING: create the time-filtered layer LAST so its animation is not hidden behind later layers.
{
"datasetName": "earthquakes",
"dateTimeColumn": "time"
}
Trip layers (layerType: "trip")
- Trip layers have built-in time animation. Do NOT add a
map.add-time-filterfor a trip layer — it is unnecessary and can break the animation. After creating a trip layer, STOP. - Strict columns:
id(shared across a trip's waypoints),latitude/longitude(DOUBLE),timestamp(epoch milliseconds). If your epoch is in seconds, multiply by 1000. - Densify sparse O-D data (interpolate lon/lat AND timestamp) or trips teleport.
Flow field layer (layerType: "flowField")
Animated streamlines over a vector field — wind, ocean currents, or downhill
terrain flow. Each row of the dataset is one sample point: a position plus a
direction. It is not the origin→destination flow layer — flow joins two
point pairs, flowField draws many short, continuously moving lines through a
field of vectors.
Pick the mode from the columns the dataset has; map.add-layer auto-detects
them in this order and names them in flowField.*Column when they differ:
flowField.mode |
Columns | Direction comes from |
|---|---|---|
UV (default) |
u, v (+ optional altitude) |
east/north vector components |
SPEED_DIR |
speed, direction |
speed plus compass direction (from) |
ELEVATION |
altitude |
the downhill gradient of a height surface |
Position comes from lat/lng columns — auto-detected, or latitudeColumn /
longitudeColumn — or from a GeoJSON geometry column when the dataset has one.
Wind encoded as u/v (no extra parameters needed):
{
"datasetName": "wind_grid",
"layerType": "flowField",
"layerName": "Wind field",
"flowField": {"colorBySpeed": true}
}
Wind encoded as speed + direction under different column names:
{
"datasetName": "wind_grid",
"layerType": "flowField",
"layerName": "Wind field",
"flowField": {
"mode": "SPEED_DIR",
"speedColumn": "wind_speed",
"directionColumn": "wind_dir"
}
}
colorBySpeed: truecolors each streamline by its local speed (using the layer's color range); without it the streamlines usecolor.colorBy/colorMapdo not apply to this layer.- Do not add
map.add-time-filter— the flow field animates continuously on its own, like a trip layer. - Streamline density (
linesPerScreen),strokeWidth,trailLength,cycleSeconds,lineLifetimeandsmoothinguse tuned defaults; they are adjusted in the layer panel, not throughmap.add-layer. - A field layer over a very large grid is expensive: one row per sample point is fine, but downsample rasters to roughly 10k–100k points before loading.
3. Split map comparison (map.split-view)
action: "enable"withlayerIdsForMap0/layerIdsForMap1. Without the layer-id arrays all layers show on both panels (no comparison). Use the SAMEcolorBy/colorTypefor a fair comparison.action: "disable"returns to one map.
{
"action": "enable",
"layerIdsForMap0": ["layer_2019"],
"layerIdsForMap1": ["layer_2024"]
}
4. Toggle the time controller
map.toggle-time-filter with action: "show" enlarges the time controller
at the bottom of the map; action: "hide" collapses it back to the side
panel. A time filter must already exist (created via map.add-time-filter).
5. Basemap style
{ "styleType": "dark" }
Valid styleType values: "no_map", "dark-matter", "positron",
"voyager", "satellite", "dark", "light", "muted", "muted_night".
If the user uses a descriptive name, map it to the closest valid styleType.
If the request is ambiguous, ask the user to clarify.
6. Layer color
{
"layerId": "layer_xxx",
"numberOfColors": 5,
"customColors": ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd"]
}
customColors is an array of hex color strings; numberOfColors must equal
its length. If the user describes colors qualitatively (e.g. "van gogh
starry night", "watercolor"), generate an appropriate palette from the
description. If the user refers to a layer by name rather than id, find the
matching layerId from the dataset context.
7. Load data from URL
{ "url": "https://..." }
This fetches the data, parses it, and adds it to the map as a dataset. It
does NOT create a layer — call map.add-layer afterward to visualize the
data (with the layer type, color, and styling you want).
The dataset is named after the URL filename (e.g. nyc.geojson). If the
user wants a specific dataset name, pass it as datasetName:
{ "url": "https://.../nyc.geojson", "datasetName": "NYC Neighborhoods" }
Local files: a local file must be served over http(s) — the browser cannot
fetch file:// paths. Copy the file into the demo app's served directory
(e.g. examples/demo-app/dist/) and pass /filename.csv, or serve it with
a CORS-enabled local server (npx serve --cors -l 8000) and pass
http://localhost:8000/filename.csv. If you have the file content but
cannot serve it (e.g. a remote browser context), pass it as a data URL —
data:text/csv;base64,<base64 content> (encode with base64 -i file.csv).
This works for small files (up to ~2MB).
For large files (over a few MB), prefer Parquet (.parquet) over
GeoJSON/CSV — it loads much faster and is less likely to time out. Convert
GeoJSON to GeoParquet with geopandas (gdf.to_parquet("data.parquet"));
kepler reads GeoParquet geometry (WKB) natively, so the map renders the
same lines.
Do NOT create a duplicate dataset just to rename it — never run
map.create-table with a plain SELECT * (or any copy) to give the loaded
data a friendlier name. map.create-table creates a SEPARATE new dataset and
is only for real column transformations; the rename belongs in
map.load-data's datasetName (or in the user's hands). Use the loaded
dataset as-is and add the layer to it.
8. Map boundary
{}
Returns the northwest and southeast coordinates of the current map view:
{ boundary: { nw: [lon, lat], se: [lon, lat] } }. When the user says
"current map view", call this to scope spatial queries to the visible area.
9. Add / rename / retype a column in an EXISTING dataset (map.add-column)
map.add-column adds a NEW column to an existing dataset in place — the
dataset keeps its name, id, color and layers, and existing columns are
untouched. The new column's values come from exactly ONE of two sources:
copyFromColumn— copy an existing column's values. This is how you rename a column in place: to renamefaretofare_amount, addfare_amountcopyingfare(the original column stays — the command never removes columns).expression— an SQL expression computed per row, for a derived variable (z-score, ratio, concatenation) OR a type change:- z-score:
{ datasetName, newColumnName: "HR60_Z", expression: "(HR60 - AVG(HR60) OVER()) / STDDEV(HR60) OVER()" } - type change:
{ datasetName, newColumnName: "NOSOUTH_str", expression: "NOSOUTH::VARCHAR" }(orCAST(NOSOUTH AS VARCHAR)). The new column holds the cast values; the original column stays.
- z-score:
{
"datasetName": "natregimes.geojson",
"newColumnName": "NOSOUTH_str",
"expression": "NOSOUTH::VARCHAR"
}
map.create-table is NOT for this — it builds a separate NEW dataset and leaves
the original untouched. Prefer map.add-column whenever the user wants the
change to land in the existing dataset.
View mode: 2D map, 3D map, or globe (map.set-view-mode)
{ "mode": "globe" }
2d— flat map, no tilt. The default for city- and region-scale data.3d— the flat map tilted (pitch 50). For extruded buildings, 3D layers and a more dramatic camera; it is not a globe.globe— the 3D globe with atmosphere. Use it when the data spans the world (global trips, arcs, lines, points, clusters, hexagons, grids, GeoJSON), so the user sees the whole planet instead of a distorted flat world.
Switching to globe clamps zoom into the globe's range; switching back to
2d/3d restores the normal range. The camera mode is independent of the
basemap, so set both when the user asks for a "dark globe" and the like.
- Keep
flowandflowFieldlayers off the globe. Neither is supported there yet — flow arrows collapse when seen edge-on, and the flow field's clipping is mercator-only. Create a global flow field on the 2d map; if the user wants a globe view of global movement, usearc,lineortripinstead, or tell them the field layer needs the flat map. - Do not switch modes unless the user asks for it or the data's global extent makes it clearly better; a map that suddenly becomes a globe is surprising.
Rules
- Commands that add datasets to the map —
map.load-data,map.save-data,map.create-table— do NOT auto-create layers. Always callmap.add-layerafterward to visualize the result. - To add a variable to, rename a column in, or change a column type in an
EXISTING dataset, use
map.add-column(copy or expression) — do NOT create a new dataset.map.create-tableonly when the user wants a SEPARATE new dataset. - A dataset loaded from a URL keeps the URL filename (or the
datasetNameyou pass tomap.load-data). NEVER create a duplicate dataset viamap.create-tablejust to rename it — use the loaded dataset as-is. - For change-over-time requests, create exactly ONE animated layer — never a separate static layer per time step.
- Do not add a
map.add-time-filterto a trip layer — trips animate themselves. - If a basemap style name is ambiguous, ask the user to clarify rather than guessing.
- For layer color updates, the
layerIdmust match an existing layer on the map. If the user refers to a layer by name rather than id, use the layer name to find the matching layer id from the dataset context.
Before your final message
- If you created a layer, confirm the layer type, name, and (if any) the color field/colorType applied.
- If you added a time filter, confirm the animated column and interval.
- If you enabled split view, confirm which layers are on each panel.
- If you changed the basemap, confirm the new
styleType. - If you updated layer colors, confirm the layer id and colors applied.
- If you loaded data, confirm the dataset was loaded and a layer was created.
- If you read the map boundary, report the northwest and southeast coordinates.
- If
map.add-layerreturneddateTimeColumnsand the user asked about change over time, you MUST follow up withmap.add-time-filteron the first returned column — do not leave the temporal hint unactioned.
Cross-surface reference
This plugin also ships the keplergl-python skill, which builds standalone HTML
maps from Python. When a request crosses over — the Python script and the open
app must agree on a layer type, a basemap, or a palette — use the shared tables
below, not the Python-side names.
Layer types
Layer types differ between the two surfaces. A keplergl Python config
names the layer in type; an MCP map.add-layer call names it in layerType.
The same spatial concept often uses a different string, and the H3/hexbin pair
is the classic trap:
| Concept | Python config type |
MCP layerType |
|---|---|---|
| Point | point |
point |
| Cluster | cluster |
cluster |
| Heatmap | heatmap |
heatmap |
| Hexbin (binned from points) | hexagon |
hexagon |
| H3 hexagon (precomputed H3 ids) | hexagonId |
h3 |
| Grid (binned from points) | — | grid |
| S2 | s2 |
s2 |
| Arc (great-circle origin→destination) | arc |
arc |
| Flow (weighted origin→destination) | — | flow |
| Flow field (streamlines over a vector field) | — | flowField |
| Line (polylines) | line |
line |
| GeoJSON / polygon | geojson |
geojson |
| Trip (path + timestamp animation) | trip |
trip |
| Icon | icon |
— |
hexagonIdis nothexagon. In a Python config,hexagonIdis H3 andhexagonis hexbin. Over MCP, H3 ish3andhexagonis still hexbin. Writinghexagonfor H3 ids gives an empty or wrong layer on both surfaces.—means the surface has no layer type for that concept; do not substitute another one silently.- MCP layer types are validated against an enum, so an unknown string is rejected rather than silently guessed.
flowis notflowField.flowjoins an origin point pair to a destination point pair;flowFielddraws animated streamlines through one vector per sample point (wind, currents, downhill terrain).
Basemaps
Basemap ids are the same on both surfaces, but each surface takes them
differently. Python sets config.config.mapStyle.styleType; MCP calls
map.set-basemap with {"styleType": ...}.
styleType |
Needs a Mapbox token |
|---|---|
dark-matter |
no |
positron |
no |
voyager |
no |
no_map |
no |
satellite |
yes |
dark |
yes |
light |
yes |
muted |
yes |
muted_night |
yes |
- Without a Mapbox token only
dark-matter,positron,voyager, andno_maprender. map.set-basemaprejects any id outside this table (its schema is an enum). A PythonmapStyle.styleTypeis passed straight to kepler.gl, so it also accepts custom styles declared inmapStyle.mapStyles.- The
dark-matter-nolabels,positron-nolabels, andvoyager-nolabelsids no longer exist in kepler.gl; hide basemap labels withmapStyle.visibleLayerGroups.labelinstead. - If a requested style name is ambiguous, ask the user rather than guessing.
Trips
Trip layers animate themselves. A trip needs an id shared across its
waypoints, latitude / longitude, and a timestamp.
- Never attach a separate time filter to a trip layer —
map.add-time-filterover MCP, or atimeRangefilter in a Python config. The trip animation and the filter fight over the same clock. - MCP
map.add-layerexpects epoch milliseconds intimestamp. A Python trip GeoJSON carries the timestamp as the fourth coordinate value,[lng, lat, altitude, timestamp](epoch seconds or milliseconds); convert the same data to milliseconds before handing it to MCPmap.add-layer. - Densify sparse origin→destination data — interpolate longitude, latitude, and timestamp together — or the trips teleport between waypoints.
Palettes
Use these palettes when the user does not specify colors. The hex values work unchanged on both surfaces.
- Sequential green:
#ffffcc #c2e699 #78c679 #31a354 #006837 #004529 - Sequential red:
#fee5d9 #fcae91 #fb6a4a #de2d26 #a50f15 - Categorical (Tableau 10):
#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b - Vibrant (the built-in
Global Warmingrange):#5A1846 #900C3F #C70039 #E3611C #F1920E #FFC300
For continuous data use a sequential or diverging palette; for categorical data use distinct hues. Do not reuse one palette for two encodings on the same map.
Version History
- 9d8d17d Current 2026-11-02 11:58


