diff --git a/.github/workflows/publish-skill.yml b/.github/workflows/publish-skill.yml
new file mode 100644
index 0000000000..bd10b1bcf2
--- /dev/null
+++ b/.github/workflows/publish-skill.yml
@@ -0,0 +1,77 @@
+name: Publish kepler.gl Skill
+
+on:
+ push:
+ paths:
+ - 'skill/SKILL.md'
+ - 'skill/skill-references/**'
+ - 'skill/agents/**'
+ - 'skill/plugin.json'
+ tags:
+ - 'skill-v*'
+ workflow_dispatch:
+
+jobs:
+ build-skills:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Read skill version
+ id: version
+ run: |
+ VERSION=$(python3 -c "import json; print(json.load(open('skill/plugin.json'))['version'])")
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - name: Build Claude skill package
+ run: |
+ STAGING=$(mktemp -d)
+ SKILL_DIR="$STAGING/kepler.gl"
+ mkdir -p "$SKILL_DIR"
+
+ cp skill/SKILL.md "$SKILL_DIR/SKILL.md"
+ cp -r skill/skill-references "$SKILL_DIR/skill-references"
+
+ (cd "$STAGING" && zip -r "$OLDPWD/kepler.gl-claude.zip" kepler.gl)
+ rm -rf "$STAGING"
+
+ - name: Build Codex skill package
+ run: |
+ STAGING=$(mktemp -d)
+ SKILL_DIR="$STAGING/kepler.gl"
+ mkdir -p "$SKILL_DIR"
+
+ cp skill/SKILL.md "$SKILL_DIR/SKILL.md"
+ cp -r skill/skill-references "$SKILL_DIR/skill-references"
+ cp -r skill/agents "$SKILL_DIR/agents"
+
+ (cd "$STAGING" && zip -r "$OLDPWD/kepler.gl-codex.zip" kepler.gl)
+ rm -rf "$STAGING"
+
+ - name: Upload Claude skill artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: kepler.gl-claude-v${{ steps.version.outputs.version }}
+ path: kepler.gl-claude.zip
+
+ - name: Upload Codex skill artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: kepler.gl-codex-v${{ steps.version.outputs.version }}
+ path: kepler.gl-codex.zip
+
+ - name: Build source package
+ run: |
+ zip -r kepler.gl-skill-source.zip skill/
+
+ - name: Attach packages to release
+ if: startsWith(github.ref, 'refs/tags/skill-v')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ kepler.gl-claude.zip
+ kepler.gl-codex.zip
+ kepler.gl-skill-source.zip
diff --git a/bindings/python/README.md b/bindings/python/README.md
index a59ba827ef..0c1c1c3684 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -32,6 +32,10 @@ map.add_data(data=df, name='my_data')
map
```
+## Use with AI Coding Assistants
+
+This package works with AI coding assistants via the **kepler.gl skill**. See [`skill/`](../../skill/) for installation instructions and reference documentation.
+
## Documentation
For full documentation, visit [https://docs.kepler.gl/docs/keplergl-jupyter](https://docs.kepler.gl/docs/keplergl-jupyter).
diff --git a/skill/README.md b/skill/README.md
new file mode 100644
index 0000000000..72f489d47b
--- /dev/null
+++ b/skill/README.md
@@ -0,0 +1,41 @@
+# kepler.gl Skill for AI Coding Assistants
+
+This directory contains the **kepler.gl** agent skill — a set of instructions and references that help AI coding assistants (Claude Code, Codex, Cursor, etc.) generate `keplergl` map scripts and exports consistently.
+
+## Skill References
+
+| Reference | Description |
+|-----------|-------------|
+| [point-map.md](skill-references/point-map.md) | Scatter plot from lat/lng |
+| [geojson-polygon-map.md](skill-references/geojson-polygon-map.md) | Polygons & lines from GeoJSON / GeoDataFrame |
+| [h3-hexagon-map.md](skill-references/h3-hexagon-map.md) | H3 spatial index hexagons |
+| [arc-line-map.md](skill-references/arc-line-map.md) | Origin–destination arcs & lines |
+| [heatmap.md](skill-references/heatmap.md) | Density heatmap from points |
+| [hexbin-aggregation-map.md](skill-references/hexbin-aggregation-map.md) | Spatial binning into hexagons |
+| [trip-animation-map.md](skill-references/trip-animation-map.md) | Animated trips along paths |
+| [summary-panel.md](skill-references/summary-panel.md) | SampleMapPanel-style info overlay in exported HTML |
+
+## Quick Setup via AI Agent
+
+The easiest way to get started is to prompt your AI agent:
+
+> Help me install the kepler.gl skill from
+
+- claude: https://github.com/keplergl/kepler.gl/releases/download/skill-v0.0.1/kepler.gl-claude.zip
+- codex: https://github.com/keplergl/kepler.gl/releases/download/skill-v0.0.1/kepler.gl-codex.zip
+- source: https://github.com/keplergl/kepler.gl/releases/download/skill-v0.0.1/kepler.gl-skill-source.zip
+
+or just "kepler.gl github repo"
+
+The agent will locate the skill file and set it up for you automatically.
+
+## Example Prompt
+
+> Create a map showing distribution of HR60 from natregimes.geojson.
+> Color the polygons by population, use a light theme.
+
+The agent will generate a Python script and run it, producing a standalone HTML map file you can open in any browser.
+
+## Versioning
+
+The skill version is tracked in [`plugin.json`](plugin.json). Releases use the tag pattern `skill-v*` (e.g. `skill-v1.0.0`).
diff --git a/skill/SKILL.md b/skill/SKILL.md
new file mode 100644
index 0000000000..b53ac57fdd
--- /dev/null
+++ b/skill/SKILL.md
@@ -0,0 +1,209 @@
+---
+name: kepler.gl
+description: Create interactive map visualizations and export to standalone HTML using the keplergl Python package. Use when the user wants to create maps, visualize geospatial data, plot locations on a map, or generate HTML map files from DataFrames, GeoDataFrames, GeoJSON, or CSV data with coordinates.
+---
+
+# Create Maps with keplergl
+
+Use the `keplergl` Python package to create standalone, interactive HTML map files from geospatial data. The exported HTML loads kepler.gl from CDN — no JavaScript build or server is needed. The resulting `.html` file can be opened directly in any browser.
+
+## Installation
+
+```bash
+pip install keplergl
+```
+
+Requires `kepler.gl-jupyter >= 0.4.0`. Earlier versions use a different widget/serialization API and the examples in this skill will not work. Requirements: Python >= 3.9. Dependencies (`pandas`, `geopandas`, `shapely`) are installed automatically.
+
+## Instructions
+
+1. Import `KeplerGl` from `keplergl`
+2. Load data as a DataFrame, GeoDataFrame, GeoJSON dict, or CSV string
+3. Create a map with `KeplerGl(data={'name': data_object})`
+4. Optionally configure layers, colors, and map state via a `config` dict (default to quantile color scale and a vibrant palette for quantitative color encoding when the user does not specify)
+5. Export with `map.save_to_html(file_name='output.html', center_map=True)`
+6. The output HTML is fully standalone — open it in any browser
+
+## API Reference
+
+### `KeplerGl(data=None, config=None, height=400, mapbox_token="", use_arrow=False, show_docs=False, theme="", app_name="kepler.gl", **kwargs)`
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `height` | int | 400 | Map height in pixels |
+| `data` | dict | None | `{"dataset_name": data_object}` |
+| `config` | dict | None | Map configuration (layers, filters, map state) |
+| `mapbox_token` | str | "" | Mapbox token (only for Mapbox basemap styles) |
+| `use_arrow` | bool | False | Serialize DataFrames as Arrow IPC (more compact, preserves types) |
+| `show_docs` | bool | False | Deprecated (kept for compatibility) |
+| `theme` | str | "" | `"light"`, `"dark"`, `"base"`, or `""` (default dark) |
+| `app_name` | str | "kepler.gl" | App name in header and HTML title |
+
+### `.add_data(data, name)`
+
+- `data`: DataFrame, GeoDataFrame, CSV string, GeoJSON dict, or GeoJSON string
+- `name`: Dataset identifier — must match `dataId` in config if using a config
+
+### `.save_to_html(file_name="keplergl_map.html", data=None, config=None, read_only=False, center_map=True, mapbox_token="", json_encoder=str, app_name=None, theme=None)`
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `file_name` | str | required | Output file path |
+| `data` | dict | None | Data override for export (uses current widget data when None) |
+| `config` | dict | None | Config override for export (uses current widget config when None) |
+| `read_only` | bool | True | True = hide side panel |
+| `center_map` | bool | True | True = auto-fit map to data bounds |
+| `mapbox_token` | str | "" | Mapbox token override for export |
+| `json_encoder` | callable | str | Fallback encoder for non-JSON-native values in GeoDataFrames |
+| `app_name` | str | None | App name override for export title/header |
+| `theme` | str | None | Theme override for export (`"light"`, `"dark"`, `"base"`, or `""`) |
+
+### `.config`
+
+Read or set the map configuration dict. Use `map.config` after customizing in Jupyter UI, then save and reuse.
+
+## Key Rules
+
+- **`dataId` must match the dataset `name`** — every layer and filter references a dataset by `dataId`; this must match the key in the `data` dict or the `name` passed to `add_data()`.
+- **GeoJSON columns use `_geojson`** — when data is loaded as GeoJSON, the geometry column is internally named `_geojson` in configs.
+- **`colorField` / `colorScale` / `sizeField` / `heightField` etc. belong under `visualChannels`, NOT under `config`.** Putting them under `config` is silently ignored — the layer will render but the "Color Based On (field)" input shows empty. The layer object must have two siblings: `config` (for `dataId`, `columns`, `visConfig`, …) and `visualChannels` (for all field-to-channel mappings).
+- Columns named `latitude`/`lat`/`lng`/`longitude` are auto-detected as coordinates.
+- H3 hex IDs are auto-detected if a column contains valid H3 strings.
+- Use `center_map=True` to auto-fit map bounds. Use `read_only=True` to hide the side panel.
+- For numeric color encoding, if the user does not specify a color scale, use `visualChannels.colorScale: 'quantile'`.
+- For numeric color encoding, if the user does not specify a palette, use a vibrant sequential/diverging palette (for example, `colorRange.name: 'Global Warming'`).
+- If the user asks for custom class breaks, compute breakpoints in Python first (for example with `pygeoda`), add a derived classified/bin column to the dataset, and map colors using that derived field.
+- **No `SampleMapPanel` in standalone exports.** The `SampleMapPanel` React component lives in the kepler.gl demo app, not in the UMD bundle used by `save_to_html()`. To show a summary/legend overlay, inject an HTML+CSS `
` into the exported file (position it at `right: 56px` or `left: 66px` so it doesn't block map controls). See [Summary Panel Overlay](skill-references/summary-panel.md).
+
+## Supported Data Formats
+
+| Format | How to Load |
+|--------|-------------|
+| pandas DataFrame | Columns with `lat`/`lng` (or similar) for point data |
+| geopandas GeoDataFrame | Geometry column auto-detected. Interactive widget serialization uses GeoArrow (no CRS reprojection); HTML export path re-projects to EPSG:4326 when needed. |
+| CSV string | Raw CSV text with lat/lng or geometry columns |
+| GeoJSON dict | `Feature` or `FeatureCollection` as Python dict |
+| GeoJSON string | JSON string of GeoJSON |
+| WKT in DataFrame | DataFrame column containing WKT geometry strings |
+
+## Layer Types
+
+| Layer Type | Config `type` | Typical Data |
+|------------|---------------|--------------|
+| Point | `"point"` | DataFrame with lat/lng columns |
+| Arc | `"arc"` | DataFrame with origin/destination lat/lng |
+| Line | `"line"` | DataFrame with origin/destination lat/lng |
+| Hexbin | `"hexagon"` | DataFrame with lat/lng (aggregated spatially) |
+| Heatmap | `"heatmap"` | DataFrame with lat/lng |
+| H3 Hexagon | `"hexagonId"` | DataFrame with H3 hex ID column |
+| GeoJSON / Polygon | `"geojson"` | GeoJSON or GeoDataFrame with polygon/line geometries |
+| Cluster | `"cluster"` | DataFrame with lat/lng |
+| Icon | `"icon"` | DataFrame with lat/lng |
+| Trip | `"trip"` | GeoJSON with LineString + timestamps |
+| S2 | `"s2"` | DataFrame with S2 token column |
+
+## Config Structure
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [...], # Layer definitions
+ 'filters': [...], # Data filters
+ 'interactionConfig': {}, # Tooltips, brush, geocoder
+ 'splitMaps': [], # Split map views
+ 'layerBlending': 'normal' # 'normal', 'additive', 'subtractive'
+ },
+ 'mapState': {
+ 'latitude': 37.76,
+ 'longitude': -122.4,
+ 'zoom': 11,
+ 'bearing': 0,
+ 'pitch': 0,
+ 'dragRotate': False,
+ 'isSplit': False
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+```
+
+## Basemap Styles
+
+Free (no token needed): `dark-matter`, `positron`, `voyager`, `dark-matter-nolabels`, `positron-nolabels`, `voyager-nolabels`
+
+Mapbox (require `mapbox_token`): `dark`, `light`, `muted`, `muted_night`
+
+## Additional Resources
+
+For detailed per-layer-type examples with full config, see supporting files:
+
+- [Point Map](skill-references/point-map.md) — Scatter plot from lat/lng
+- [GeoJSON / Polygon Map](skill-references/geojson-polygon-map.md) — Polygons, lines from GeoJSON or GeoDataFrame
+- [H3 Hexagon Map](skill-references/h3-hexagon-map.md) — H3 spatial index hexagons
+- [Arc / Line Map](skill-references/arc-line-map.md) — Origin-destination connections
+- [Heatmap](skill-references/heatmap.md) — Density heatmap from points
+- [Hexbin Aggregation Map](skill-references/hexbin-aggregation-map.md) — Spatial binning into hexagons
+- [Trip Animation Map](skill-references/trip-animation-map.md) — Animated trips along paths
+- [Summary Panel Overlay](skill-references/summary-panel.md) — Inject a SampleMapPanel-style info overlay into the exported HTML (for LISA/cluster counts, model summaries, custom legends)
+
+## Examples
+
+For full config examples per layer type, see:
+- [Point Map](skill-references/point-map.md) — includes quantile color + vibrant palette config
+- [GeoJSON / Polygon Map](skill-references/geojson-polygon-map.md) — includes choropleth config with `visualChannels`
+
+### Quick start (auto-detected layers, no config needed)
+
+```python
+from keplergl import KeplerGl
+import pandas as pd
+
+df = pd.DataFrame({
+ 'lat': [37.7749, 34.0522, 40.7128],
+ 'lng': [-122.4194, -118.2437, -74.0060],
+ 'name': ['San Francisco', 'Los Angeles', 'New York'],
+ 'value': [15, 42, 27]
+})
+
+map_1 = KeplerGl(data={'cities': df})
+map_1.save_to_html(file_name='cities_map.html', center_map=True)
+```
+
+### GeoDataFrame from shapefile
+
+```python
+from keplergl import KeplerGl
+import geopandas as gpd
+
+gdf = gpd.read_file('shapefile.shp')
+map_1 = KeplerGl(data={'regions': gdf})
+map_1.save_to_html(file_name='regions_map.html', read_only=True, center_map=True)
+```
+
+### Multiple datasets
+
+```python
+map_1 = KeplerGl(data={
+ 'locations': points_df,
+ 'routes': routes_df
+})
+map_1.save_to_html(file_name='combined_map.html', center_map=True)
+```
+
+### Save and reuse config
+
+```python
+import json
+# Save
+with open('my_config.json', 'w') as f:
+ json.dump(map_1.config, f)
+# Load
+with open('my_config.json', 'r') as f:
+ config = json.load(f)
+map_2 = KeplerGl(data={'data_1': df}, config=config)
+map_2.save_to_html(file_name='map.html')
+```
diff --git a/skill/agents/assets/icon-large.svg b/skill/agents/assets/icon-large.svg
new file mode 100644
index 0000000000..ab469458b3
--- /dev/null
+++ b/skill/agents/assets/icon-large.svg
@@ -0,0 +1,6 @@
+
diff --git a/skill/agents/assets/icon-small.svg b/skill/agents/assets/icon-small.svg
new file mode 100644
index 0000000000..00d4e63f1b
--- /dev/null
+++ b/skill/agents/assets/icon-small.svg
@@ -0,0 +1,6 @@
+
diff --git a/skill/agents/openai.yaml b/skill/agents/openai.yaml
new file mode 100644
index 0000000000..14de76a339
--- /dev/null
+++ b/skill/agents/openai.yaml
@@ -0,0 +1,13 @@
+interface:
+ display_name: "kepler.gl Map"
+ short_description: "Create interactive map visualizations with kepler.gl"
+ icon_small: "./assets/icon-small.svg"
+ icon_large: "./assets/icon-large.svg"
+ brand_color: "#1FBAD6"
+ default_prompt: "Create an interactive map visualization"
+
+policy:
+ allow_implicit_invocation: true
+
+dependencies:
+ tools: []
diff --git a/skill/cursor-plugin.json b/skill/cursor-plugin.json
new file mode 100644
index 0000000000..dd58b1e796
--- /dev/null
+++ b/skill/cursor-plugin.json
@@ -0,0 +1,38 @@
+{
+ "name": "kepler.gl",
+ "displayName": "kepler.gl Map",
+ "version": "1.0.0",
+ "description": "Create interactive map visualizations and export to standalone HTML using the keplergl Python package.",
+ "publisher": "keplergl",
+ "icon": "agents/assets/icon-large.svg",
+ "categories": ["Data Science", "Visualization", "Maps"],
+ "keywords": [
+ "maps",
+ "geospatial",
+ "visualization",
+ "kepler.gl",
+ "GeoJSON",
+ "DataFrame",
+ "HTML",
+ "interactive",
+ "choropleth",
+ "heatmap"
+ ],
+ "homepage": "https://kepler.gl",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/keplergl/kepler.gl",
+ "directory": "bindings/python"
+ },
+ "license": "MIT",
+ "skills": [
+ {
+ "path": "SKILL.md",
+ "name": "kepler.gl",
+ "description": "Create interactive map visualizations and export to standalone HTML using the keplergl Python package."
+ }
+ ],
+ "engines": {
+ "cursor": ">=1.0.0"
+ }
+}
diff --git a/skill/plugin.json b/skill/plugin.json
new file mode 100644
index 0000000000..9153f8ab53
--- /dev/null
+++ b/skill/plugin.json
@@ -0,0 +1,28 @@
+{
+ "name": "kepler.gl",
+ "version": "0.0.1",
+ "description": "Create interactive map visualizations and export to standalone HTML using the keplergl Python package. Use when the user wants to create maps, visualize geospatial data, plot locations on a map, or generate HTML map files.",
+ "author": "kepler.gl",
+ "license": "MIT",
+ "homepage": "https://kepler.gl",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/keplergl/kepler.gl",
+ "directory": "bindings/python"
+ },
+ "skills": [
+ {
+ "path": "./SKILL.md"
+ }
+ ],
+ "keywords": [
+ "maps",
+ "geospatial",
+ "visualization",
+ "kepler.gl",
+ "GeoJSON",
+ "DataFrame",
+ "HTML",
+ "interactive"
+ ]
+}
diff --git a/skill/skill-references/arc-line-map.md b/skill/skill-references/arc-line-map.md
new file mode 100644
index 0000000000..c9a17b2be1
--- /dev/null
+++ b/skill/skill-references/arc-line-map.md
@@ -0,0 +1,108 @@
+# Arc / Line Map
+
+Show connections between origin and destination points. Arc layers render 3D arcs; line layers render flat lines.
+
+## Data
+
+A pandas DataFrame with origin and destination latitude/longitude columns.
+
+```python
+import pandas as pd
+
+df = pd.DataFrame({
+ 'origin_lat': [37.7749, 40.7128, 41.8781],
+ 'origin_lng': [-122.4194, -74.0060, -87.6298],
+ 'dest_lat': [34.0522, 37.7749, 40.7128],
+ 'dest_lng': [-118.2437, -122.4194, -74.0060],
+ 'origin_city': ['San Francisco', 'New York', 'Chicago'],
+ 'dest_city': ['Los Angeles', 'San Francisco', 'New York'],
+ 'flights': [150, 200, 180]
+})
+```
+
+## Minimal Export
+
+```python
+from keplergl import KeplerGl
+
+map_1 = KeplerGl(data={'routes': df})
+map_1.save_to_html(file_name='arc_map.html', center_map=True)
+```
+
+## Arc Layer with Config
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'arc',
+ 'config': {
+ 'dataId': 'routes',
+ 'label': 'Flight Routes',
+ 'isVisible': True,
+ 'columns': {
+ 'lat0': 'origin_lat',
+ 'lng0': 'origin_lng',
+ 'lat1': 'dest_lat',
+ 'lng1': 'dest_lng'
+ },
+ 'visConfig': {
+ 'opacity': 0.8,
+ 'thickness': 2,
+ 'targetColor': [255, 203, 153]
+ },
+ 'color': [18, 147, 154]
+ },
+ 'visualChannels': {
+ 'sizeField': {'name': 'flights', 'type': 'integer'},
+ 'sizeScale': 'linear'
+ }
+ }],
+ 'interactionConfig': {
+ 'tooltip': {
+ 'enabled': True,
+ 'fieldsToShow': {
+ 'routes': ['origin_city', 'dest_city', 'flights']
+ }
+ }
+ }
+ },
+ 'mapState': {
+ 'latitude': 38.0,
+ 'longitude': -97.0,
+ 'zoom': 4,
+ 'pitch': 30,
+ 'bearing': 0,
+ 'dragRotate': True
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'routes': df}, config=config)
+map_1.save_to_html(file_name='arc_map_styled.html')
+```
+
+## Line Layer (Flat)
+
+Replace `'type': 'arc'` with `'type': 'line'` in the config. The columns are the same.
+
+```python
+config['config']['visState']['layers'][0]['type'] = 'line'
+```
+
+## Key Config Fields
+
+| Field | Description |
+|-------|-------------|
+| `type` | `'arc'` for 3D arcs, `'line'` for flat lines |
+| `columns.lat0` / `columns.lng0` | Origin latitude/longitude column |
+| `columns.lat1` / `columns.lng1` | Destination latitude/longitude column |
+| `visConfig.thickness` | Line width |
+| `visConfig.targetColor` | RGB array for destination end color |
+| `color` | RGB array for origin end color |
+| `visualChannels.sizeField` | Column to map to line thickness |
diff --git a/skill/skill-references/geojson-polygon-map.md b/skill/skill-references/geojson-polygon-map.md
new file mode 100644
index 0000000000..9a820cf7a4
--- /dev/null
+++ b/skill/skill-references/geojson-polygon-map.md
@@ -0,0 +1,150 @@
+# GeoJSON / Polygon Map
+
+Display polygon, line, or point geometries from GeoJSON or GeoDataFrame data.
+
+## Data Options
+
+### Option 1: GeoJSON dict or string
+
+```python
+import json
+
+with open('boundaries.geojson', 'r') as f:
+ geojson = json.load(f)
+
+# geojson can be a FeatureCollection or a single Feature
+```
+
+### Option 2: GeoDataFrame
+
+```python
+import geopandas as gpd
+
+gdf = gpd.read_file('shapefile.shp')
+# or from a GeoJSON file:
+gdf = gpd.read_file('boundaries.geojson')
+```
+
+### Option 3: Inline GeoJSON
+
+```python
+geojson = {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"name": "Area A", "value": 100},
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-122.4, 37.8], [-122.4, 37.7],
+ [-122.3, 37.7], [-122.3, 37.8],
+ [-122.4, 37.8]
+ ]]
+ }
+ }
+ ]
+}
+```
+
+## Minimal Export
+
+```python
+from keplergl import KeplerGl
+
+# From GeoJSON
+map_1 = KeplerGl(data={'zones': geojson})
+map_1.save_to_html(file_name='polygon_map.html', center_map=True)
+
+# From GeoDataFrame
+map_1 = KeplerGl(data={'zones': gdf})
+map_1.save_to_html(file_name='polygon_map.html', center_map=True)
+```
+
+## Export with Config
+
+**Important:** When data is loaded as GeoJSON, the geometry column in config must be `_geojson`.
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'geojson',
+ 'config': {
+ 'dataId': 'zones',
+ 'label': 'Zone Boundaries',
+ 'isVisible': True,
+ 'columns': {
+ 'geojson': '_geojson'
+ },
+ 'visConfig': {
+ 'opacity': 0.8,
+ 'filled': True,
+ 'stroked': True,
+ 'thickness': 1.5,
+ 'strokeColor': [255, 255, 255],
+ 'colorRange': {
+ 'name': 'Sunrise 8',
+ 'type': 'sequential',
+ 'category': 'Uber',
+ 'colors': [
+ '#194266', '#355C7D', '#63617F', '#916681',
+ '#C06C84', '#D28389', '#E59A8F', '#F8B195'
+ ]
+ },
+ 'enable3d': False,
+ 'elevationScale': 5
+ }
+ },
+ 'visualChannels': {
+ 'colorField': {'name': 'value', 'type': 'integer'},
+ 'colorScale': 'quantize'
+ }
+ }],
+ 'interactionConfig': {
+ 'tooltip': {
+ 'enabled': True,
+ 'fieldsToShow': {
+ 'zones': ['name', 'value']
+ }
+ }
+ }
+ },
+ 'mapStyle': {
+ 'styleType': 'positron'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'zones': geojson}, config=config, theme='light')
+map_1.save_to_html(file_name='polygon_map_styled.html')
+```
+
+## 3D Extruded Polygons
+
+Set `enable3d: True` and map a `heightField` to extrude polygons:
+
+```python
+config['config']['visState']['layers'][0]['config']['visConfig']['enable3d'] = True
+config['config']['visState']['layers'][0]['config']['visConfig']['elevationScale'] = 10
+config['config']['visState']['layers'][0]['visualChannels']['heightField'] = {
+ 'name': 'value', 'type': 'integer'
+}
+config['config']['visState']['layers'][0]['visualChannels']['heightScale'] = 'linear'
+```
+
+## Key Config Fields for GeoJSON Layer
+
+| Field | Description |
+|-------|-------------|
+| `columns.geojson` | Must be `'_geojson'` for GeoJSON data |
+| `visConfig.filled` | Fill polygons with color |
+| `visConfig.stroked` | Show polygon outlines |
+| `visConfig.thickness` | Outline stroke width |
+| `visConfig.strokeColor` | RGB array for outline color |
+| `visConfig.enable3d` | Extrude polygons in 3D |
+| `visConfig.elevationScale` | Height multiplier for 3D |
+| `visualChannels.colorField` | Property to map to fill color |
+| `visualChannels.heightField` | Property to map to extrusion height |
diff --git a/skill/skill-references/h3-hexagon-map.md b/skill/skill-references/h3-hexagon-map.md
new file mode 100644
index 0000000000..1222007bbc
--- /dev/null
+++ b/skill/skill-references/h3-hexagon-map.md
@@ -0,0 +1,113 @@
+# H3 Hexagon Map
+
+Visualize data using [H3 spatial index](https://h3geo.org/) hexagons. Each row has an H3 hex ID and associated values.
+
+## Data
+
+A pandas DataFrame with a column containing H3 hex ID strings and value columns.
+
+```python
+import pandas as pd
+
+df = pd.DataFrame({
+ 'hex_id': [
+ '89283082c2fffff', '8928308288fffff', '89283082c07ffff',
+ '89283082817ffff', '89283082c3bffff', '89283082883ffff'
+ ],
+ 'value': [64, 73, 65, 74, 66, 50]
+})
+```
+
+## Minimal Export
+
+```python
+from keplergl import KeplerGl
+
+map_1 = KeplerGl(data={'hex_data': df})
+map_1.save_to_html(file_name='h3_map.html', center_map=True)
+```
+
+kepler.gl auto-detects H3 hex ID columns and creates a hexagonId layer.
+
+## Export with Config (Colored + 3D Extruded)
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'hexagonId',
+ 'config': {
+ 'dataId': 'hex_data',
+ 'label': 'H3 Hexagons',
+ 'isVisible': True,
+ 'columns': {
+ 'hex_id': 'hex_id'
+ },
+ 'visConfig': {
+ 'opacity': 0.8,
+ 'coverage': 1,
+ 'enable3d': True,
+ 'sizeRange': [0, 500],
+ 'elevationScale': 5,
+ 'colorRange': {
+ 'name': 'Sunrise 8',
+ 'type': 'sequential',
+ 'category': 'Uber',
+ 'colors': [
+ '#194266', '#355C7D', '#63617F', '#916681',
+ '#C06C84', '#D28389', '#E59A8F', '#F8B195'
+ ],
+ 'reversed': False
+ }
+ }
+ },
+ 'visualChannels': {
+ 'colorField': {'name': 'value', 'type': 'integer'},
+ 'colorScale': 'quantize',
+ 'sizeField': {'name': 'value', 'type': 'integer'},
+ 'sizeScale': 'linear',
+ 'coverageField': None,
+ 'coverageScale': 'linear'
+ }
+ }],
+ 'interactionConfig': {
+ 'tooltip': {
+ 'enabled': True,
+ 'fieldsToShow': {
+ 'hex_data': ['hex_id', 'value']
+ }
+ }
+ }
+ },
+ 'mapState': {
+ 'latitude': 37.76,
+ 'longitude': -122.43,
+ 'zoom': 12,
+ 'bearing': 2.6,
+ 'pitch': 37.4,
+ 'dragRotate': True
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'hex_data': df}, config=config)
+map_1.save_to_html(file_name='h3_map_3d.html')
+```
+
+## Key Config Fields for H3 Layer
+
+| Field | Description |
+|-------|-------------|
+| `type` | Must be `'hexagonId'` |
+| `columns.hex_id` | Column name containing H3 hex ID strings |
+| `visConfig.coverage` | 0 to 1, how much of hex cell is filled |
+| `visConfig.enable3d` | Extrude hexagons by value |
+| `visConfig.sizeRange` | `[min, max]` height range for 3D |
+| `visConfig.elevationScale` | Height multiplier |
+| `visualChannels.colorField` | Column to map to color |
+| `visualChannels.sizeField` | Column to map to extrusion height |
diff --git a/skill/skill-references/heatmap.md b/skill/skill-references/heatmap.md
new file mode 100644
index 0000000000..2729cd8ca0
--- /dev/null
+++ b/skill/skill-references/heatmap.md
@@ -0,0 +1,93 @@
+# Heatmap
+
+Density heatmap visualization from point data. Points are aggregated into a continuous density surface.
+
+## Data
+
+A pandas DataFrame with latitude and longitude columns. Optionally a weight column.
+
+```python
+import pandas as pd
+import numpy as np
+
+np.random.seed(42)
+n = 1000
+df = pd.DataFrame({
+ 'lat': np.random.normal(37.76, 0.02, n),
+ 'lng': np.random.normal(-122.42, 0.02, n),
+ 'magnitude': np.random.uniform(1, 10, n)
+})
+```
+
+## Minimal Export
+
+```python
+from keplergl import KeplerGl
+
+map_1 = KeplerGl(data={'events': df})
+map_1.save_to_html(file_name='heatmap.html', center_map=True)
+```
+
+Note: kepler.gl will auto-detect lat/lng columns but may create a point layer by default. Use a config to force a heatmap layer.
+
+## Export with Heatmap Config
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'heatmap',
+ 'config': {
+ 'dataId': 'events',
+ 'label': 'Event Density',
+ 'isVisible': True,
+ 'columns': {
+ 'lat': 'lat',
+ 'lng': 'lng'
+ },
+ 'visConfig': {
+ 'opacity': 0.8,
+ 'radius': 20,
+ 'intensity': 1,
+ 'colorRange': {
+ 'name': 'Global Warming',
+ 'type': 'sequential',
+ 'category': 'Uber',
+ 'colors': ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ }
+ }
+ },
+ 'visualChannels': {
+ 'weightField': {'name': 'magnitude', 'type': 'real'},
+ 'weightScale': 'linear'
+ }
+ }]
+ },
+ 'mapState': {
+ 'latitude': 37.76,
+ 'longitude': -122.42,
+ 'zoom': 13
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'events': df}, config=config)
+map_1.save_to_html(file_name='heatmap_styled.html')
+```
+
+## Key Config Fields for Heatmap Layer
+
+| Field | Description |
+|-------|-------------|
+| `type` | Must be `'heatmap'` |
+| `columns.lat` | Latitude column name |
+| `columns.lng` | Longitude column name |
+| `visConfig.radius` | Influence radius of each point |
+| `visConfig.intensity` | Heat intensity multiplier |
+| `visConfig.opacity` | 0.0 to 1.0 |
+| `visualChannels.weightField` | Column to use as weight (optional) |
diff --git a/skill/skill-references/hexbin-aggregation-map.md b/skill/skill-references/hexbin-aggregation-map.md
new file mode 100644
index 0000000000..6dce62262b
--- /dev/null
+++ b/skill/skill-references/hexbin-aggregation-map.md
@@ -0,0 +1,107 @@
+# Hexbin Aggregation Map
+
+Aggregate point data into hexagonal bins. Unlike H3 maps (which use pre-computed H3 IDs), hexbin layers take raw lat/lng points and aggregate them spatially.
+
+## Data
+
+A pandas DataFrame with latitude and longitude columns.
+
+```python
+import pandas as pd
+import numpy as np
+
+np.random.seed(42)
+n = 5000
+df = pd.DataFrame({
+ 'lat': np.random.normal(37.76, 0.05, n),
+ 'lng': np.random.normal(-122.42, 0.05, n),
+ 'value': np.random.randint(1, 100, n)
+})
+```
+
+## Minimal Export
+
+```python
+from keplergl import KeplerGl
+
+map_1 = KeplerGl(data={'points': df})
+map_1.save_to_html(file_name='hexbin_map.html', center_map=True)
+```
+
+Note: kepler.gl may create a point layer by default. Use a config to specify a hexagon aggregation layer.
+
+## Export with Config
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'hexagon',
+ 'config': {
+ 'dataId': 'points',
+ 'label': 'Hexbin Density',
+ 'isVisible': True,
+ 'columns': {
+ 'lat': 'lat',
+ 'lng': 'lng'
+ },
+ 'visConfig': {
+ 'opacity': 0.8,
+ 'worldUnitSize': 0.5,
+ 'resolution': 8,
+ 'coverage': 1,
+ 'enable3d': True,
+ 'sizeRange': [0, 500],
+ 'elevationScale': 5,
+ 'colorRange': {
+ 'name': 'Global Warming',
+ 'type': 'sequential',
+ 'category': 'Uber',
+ 'colors': ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ },
+ 'colorAggregation': 'average'
+ }
+ },
+ 'visualChannels': {
+ 'colorField': {'name': 'value', 'type': 'integer'},
+ 'colorScale': 'quantile',
+ 'sizeField': {'name': 'value', 'type': 'integer'},
+ 'sizeScale': 'linear'
+ }
+ }]
+ },
+ 'mapState': {
+ 'latitude': 37.76,
+ 'longitude': -122.42,
+ 'zoom': 12,
+ 'pitch': 40,
+ 'bearing': 0,
+ 'dragRotate': True
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'points': df}, config=config)
+map_1.save_to_html(file_name='hexbin_map_3d.html')
+```
+
+## Key Config Fields for Hexbin Layer
+
+| Field | Description |
+|-------|-------------|
+| `type` | Must be `'hexagon'` |
+| `columns.lat` | Latitude column name |
+| `columns.lng` | Longitude column name |
+| `visConfig.worldUnitSize` | Hexagon bin radius in km |
+| `visConfig.coverage` | 0 to 1, fill ratio of each hex |
+| `visConfig.enable3d` | Extrude hexagons by aggregated value |
+| `visConfig.sizeRange` | `[min, max]` height range for 3D |
+| `visConfig.elevationScale` | Height multiplier |
+| `visConfig.colorAggregation` | `'average'`, `'sum'`, `'min'`, `'max'`, `'count'` |
+| `visualChannels.colorField` | Column to aggregate for color |
+| `visualChannels.sizeField` | Column to aggregate for height |
diff --git a/skill/skill-references/point-map.md b/skill/skill-references/point-map.md
new file mode 100644
index 0000000000..57662c286d
--- /dev/null
+++ b/skill/skill-references/point-map.md
@@ -0,0 +1,102 @@
+# Point Map
+
+Scatter plot of locations using latitude/longitude columns from a DataFrame.
+
+## Data
+
+A pandas DataFrame with numeric latitude and longitude columns.
+
+```python
+import pandas as pd
+
+df = pd.DataFrame({
+ 'name': ['San Francisco', 'Los Angeles', 'New York', 'Chicago', 'Houston'],
+ 'lat': [37.7749, 34.0522, 40.7128, 41.8781, 29.7604],
+ 'lng': [-122.4194, -118.2437, -74.0060, -87.6298, -95.3698],
+ 'population': [884363, 3979576, 8336817, 2693976, 2320268],
+ 'category': ['West', 'West', 'East', 'Midwest', 'South']
+})
+```
+
+## Minimal Export (auto-detected layers)
+
+```python
+from keplergl import KeplerGl
+
+map_1 = KeplerGl(data={'cities': df})
+map_1.save_to_html(file_name='point_map.html', center_map=True)
+```
+
+## Export with Config
+
+```python
+config = {
+ 'version': 'v1',
+ 'config': {
+ 'visState': {
+ 'layers': [{
+ 'type': 'point',
+ 'config': {
+ 'dataId': 'cities',
+ 'label': 'City Locations',
+ 'isVisible': True,
+ 'columns': {
+ 'lat': 'lat',
+ 'lng': 'lng',
+ 'altitude': None
+ },
+ 'visConfig': {
+ 'radius': 20,
+ 'opacity': 0.8,
+ 'filled': True,
+ 'colorRange': {
+ 'name': 'Global Warming',
+ 'type': 'sequential',
+ 'category': 'Uber',
+ 'colors': ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ }
+ }
+ },
+ 'visualChannels': {
+ 'colorField': {'name': 'population', 'type': 'integer'},
+ 'colorScale': 'quantile',
+ 'sizeField': {'name': 'population', 'type': 'integer'},
+ 'sizeScale': 'sqrt'
+ }
+ }],
+ 'interactionConfig': {
+ 'tooltip': {
+ 'enabled': True,
+ 'fieldsToShow': {
+ 'cities': ['name', 'population', 'category']
+ }
+ }
+ }
+ },
+ 'mapState': {
+ 'latitude': 38.0,
+ 'longitude': -97.0,
+ 'zoom': 4
+ },
+ 'mapStyle': {
+ 'styleType': 'dark-matter'
+ }
+ }
+}
+
+map_1 = KeplerGl(data={'cities': df}, config=config)
+map_1.save_to_html(file_name='point_map_styled.html')
+```
+
+## Key Config Fields for Point Layer
+
+| Field | Description |
+|-------|-------------|
+| `columns.lat` | Column name for latitude |
+| `columns.lng` | Column name for longitude |
+| `visConfig.radius` | Point radius in pixels |
+| `visConfig.opacity` | 0.0 to 1.0 |
+| `visConfig.filled` | Whether points are filled |
+| `visualChannels.colorField` | Column to map to color |
+| `visualChannels.sizeField` | Column to map to point size |
+| `visualChannels.colorScale` | Common options: `'quantile'`, `'quantize'`, `'ordinal'` (and `'custom'` for color channels in supported contexts) |
diff --git a/skill/skill-references/summary-panel.md b/skill/skill-references/summary-panel.md
new file mode 100644
index 0000000000..28a6be1386
--- /dev/null
+++ b/skill/skill-references/summary-panel.md
@@ -0,0 +1,99 @@
+# Summary Panel Overlay (SampleMapPanel-style)
+
+Add a small informational panel on top of a kepler.gl map exported via `save_to_html()` — useful for summary stats, legends of custom classifications, model metadata, or LISA / cluster counts.
+
+## Why not a real React component?
+
+The `SampleMapPanel` used in the kepler.gl demo app is part of that demo's source and is **not** exposed by the UMD bundle loaded in the standalone HTML export. The pragmatic equivalent is to inject an HTML/CSS overlay into the exported file after calling `save_to_html()`.
+
+## Positioning
+
+The map's zoom/rotate/split/legend control column sits against the right edge of the viewport and is roughly 40–50px wide. Place the panel at `right: 56px` (or `left: 66px` to sit next to the side panel toggle) so it does not block those controls.
+
+| Placement | CSS |
+|-----------|-----|
+| Top-right, clear of map controls | `top: 16px; right: 56px;` |
+| Top-left, clear of side-panel toggle | `top: 16px; left: 66px;` |
+| Bottom-left, above attribution | `bottom: 36px; left: 16px;` |
+
+## Pattern
+
+1. Call `map.save_to_html(file_name='out.html')` as usual.
+2. Build a small `
` + `
+"""
+
+with open("out.html", "r", encoding="utf-8") as f:
+ html = f.read()
+html = html.replace("