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("", panel_html + "\n") +with open("out.html", "w", encoding="utf-8") as f: + f.write(html) +``` + +## Notes + +- Use `position: absolute` (not `fixed`) so the panel stays inside the map container when the page is scrolled. +- For a light-theme map (`theme='light'`), swap the background to `rgba(255,255,255,0.95)` and text colors to dark greys. +- Keep the width ≤ 320px to avoid overlapping the kepler.gl side panel when it is open. +- This overlay is static HTML — it does not react to filter state or layer toggles. For dynamic behavior, subscribe to the redux store on `window` in the exported JS. diff --git a/skill/skill-references/trip-animation-map.md b/skill/skill-references/trip-animation-map.md new file mode 100644 index 0000000000..cfab1fc8a5 --- /dev/null +++ b/skill/skill-references/trip-animation-map.md @@ -0,0 +1,130 @@ +# Trip Animation Map + +Animate movement along paths over time. Trip layers render GeoJSON LineString geometries with timestamps to create animated visualizations. + +## Data + +Trip data must be GeoJSON with `LineString` or `MultiLineString` geometry where each coordinate has a timestamp as the fourth value: `[lng, lat, altitude, timestamp]`. + +```python +trip_geojson = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "vendor": "A", + "trip_id": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4194, 37.7749, 0, 1564184363], + [-122.4180, 37.7760, 0, 1564184400], + [-122.4160, 37.7770, 0, 1564184440], + [-122.4140, 37.7780, 0, 1564184480], + [-122.4120, 37.7790, 0, 1564184520] + ] + } + }, + { + "type": "Feature", + "properties": { + "vendor": "B", + "trip_id": 2 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4100, 37.7800, 0, 1564184363], + [-122.4120, 37.7810, 0, 1564184400], + [-122.4140, 37.7820, 0, 1564184440], + [-122.4160, 37.7830, 0, 1564184480] + ] + } + } + ] +} +``` + +Timestamps are Unix epoch seconds (or milliseconds). The coordinates format is `[longitude, latitude, altitude, timestamp]`. + +## Minimal Export + +```python +from keplergl import KeplerGl + +map_1 = KeplerGl(data={'trips': trip_geojson}) +map_1.save_to_html(file_name='trip_map.html', center_map=True) +``` + +## Export with Config + +```python +config = { + 'version': 'v1', + 'config': { + 'visState': { + 'layers': [{ + 'type': 'trip', + 'config': { + 'dataId': 'trips', + 'label': 'Vehicle Trips', + 'isVisible': True, + 'columns': { + 'geojson': '_geojson' + }, + 'color': [18, 147, 154], + 'visConfig': { + 'opacity': 0.8, + 'thickness': 3, + 'trailLength': 180, + 'colorRange': { + 'name': 'Global Warming', + 'type': 'sequential', + 'category': 'Uber', + 'colors': ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300'] + } + } + }, + 'visualChannels': { + 'colorField': {'name': 'vendor', 'type': 'string'}, + 'colorScale': 'ordinal' + } + }], + 'animationConfig': { + 'currentTime': None, + 'speed': 1 + } + }, + 'mapState': { + 'latitude': 37.78, + 'longitude': -122.42, + 'zoom': 13 + }, + 'mapStyle': { + 'styleType': 'dark-matter' + } + } +} + +map_1 = KeplerGl(data={'trips': trip_geojson}, config=config) +map_1.save_to_html(file_name='trip_map_styled.html') +``` + +## Key Config Fields for Trip Layer + +| Field | Description | +|-------|-------------| +| `type` | Must be `'trip'` | +| `columns.geojson` | Must be `'_geojson'` for GeoJSON data | +| `visConfig.trailLength` | Trail length in seconds | +| `visConfig.thickness` | Line width | +| `animationConfig.speed` | Playback speed multiplier | +| `visualChannels.colorField` | Property to map to color | + +## Notes + +- The animation will auto-play when the HTML is opened in a browser. +- `trailLength` controls how many seconds of trail are visible behind the moving point. +- Coordinates must include timestamps as the 4th value: `[lng, lat, alt, timestamp]`.