Create data visualizations using Observable Plot's declarative grammar of graphics
/plugin marketplace add mberg/claude-skills/plugin install mberg-observable-plot@mberg/claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
TABLE_OF_CONTENTS.mdexamples.mdreference/common-patterns.mdreference/datasets.mdreference/examples/bar-chart.mdreference/examples/choropleth-map.mdreference/examples/density-contour.mdreference/examples/diverging-stacked-bars.mdreference/examples/faceted-plot.mdreference/examples/heatmaps.mdreference/examples/hexbin-heatmap.mdreference/examples/interactive-tips.mdreference/examples/network-diagrams.mdreference/examples/population-pyramid.mdreference/examples/scatterplot.mdreference/examples/time-series.mdreference/examples/vector-fields.mdreference/examples/waffle-charts.mdreference/faceting.mdreference/geographic.mdObservable Plot is a JavaScript library for exploratory data visualization. It uses a declarative grammar of graphics to create meaningful visualizations quickly by composing marks, scales, and transforms.
Observable Plot helps you turn data into visualizations by mapping data columns to visual properties. Instead of specifying chart types, you compose visual elements (marks) with data transformations (transforms) and encodings (scales).
Key principle: Maximize configuration, minimize external JavaScript. Plot handles data processing, layout, axes, and legends through its declarative API.
Marks are visual elements that represent data. Plot provides 30+ mark types:
dot, image, textbarX, barY, cell, rectline, lineX, lineY, area, areaX, areaYruleX, ruleY, tickX, tickY, frame, gridgeo, sphere, graticulearrow, link, tree, vectorcontour, density, hexgrid, rasterboxX, boxY, linearRegressionYaxis, tipScales map abstract data values to visual values (position, color, size, etc.). Plot automatically infers scales from your data and mark specifications.
Scale types:
linear, log, sqrt, pow, time, utcpoint, band, ordinalTransforms process data within the Plot specification, eliminating the need for external data wrangling:
bin, hexbin, groupnormalize, window (moving averages)stack, dodge, shiftfilter, selectsortCompose complex visualizations by layering multiple marks. Each mark can have its own data, transforms, and encodings while sharing the same coordinate system.
Plot works best with tidy data: arrays of objects where each object represents one observation with consistent properties.
// Tidy data format
const data = [
{name: "Alice", age: 25, score: 85},
{name: "Bob", age: 30, score: 92},
{name: "Carol", age: 28, score: 78}
];
import * as Plot from "@observablehq/plot";
// Simple scatterplot
const data = [
{x: 1, y: 2, category: "A"},
{x: 2, y: 5, category: "B"},
{x: 3, y: 3, category: "A"},
{x: 4, y: 7, category: "B"}
];
const plot = Plot.plot({
marks: [
Plot.dot(data, {
x: "x",
y: "y",
fill: "category",
r: 5
})
]
});
document.body.appendChild(plot);
The Observable Plot skill includes an interactive live editor and viewer for developing visualizations:
Launch from your working directory (where you want plots to be created):
# From your project directory
uv run --directory ~/.claude/skills/observable-plot/scripts plot-viewer --plots-dir $(pwd)/plots
Or if you're already in the skills directory structure:
# Shorter version from within skills directory
uv run --directory observable-plot/scripts plot-viewer --plots-dir $(pwd)/plots
This launches a three-pane interface with history sidebar, live preview, and code editor on http://localhost:8765
The viewer automatically detects if it's already running and just opens a new browser tab.
Options:
--plots-dir PATH - Specify plots directory (default: ./plots)--port 8080 - Use a different port--no-browser - Don't auto-open browser--create-example - Create example plot file on startupClaude can write visualization code to JSON files that automatically load in the viewer:
IMPORTANT: Claude should check its current working directory and write plot files to $(pwd)/plots/.
Workflow:
--plots-dir $(pwd)/plotsplots/ subdirectoryplots/filename.jsonFor Claude:
pwdplots subdirectory exists with mkdir -p plotsplots/filename.jsonThe viewer will show which directory it's watching on startup, so you can verify Claude is writing to the correct location.
Claude writes plot files to $(pwd)/plots/ with this structure:
{
"name": "Sales Analysis",
"description": "Monthly revenue by product category",
"code": "const data = [...]; return Plot.plot({...})",
"timestamp": "2025-10-31T12:34:56.789Z"
}
Fields:
name - Descriptive name shown in history (required)description - Brief description shown in history sidebar (optional)code - JavaScript code that returns a Plot.plot() result (required)timestamp - ISO timestamp for tracking creation time (required)Use descriptive filenames that make sense:
sales-by-category-2025.jsontemperature-trends-seattle.jsonpenguins-body-mass-analysis.jsonAvoid generic names like plot1.json or temp.json.
Step 0: Launch the viewer from your working directory
cd /Users/mberg/projects/myapp
uv run --directory ~/.claude/skills/observable-plot/scripts plot-viewer --plots-dir $(pwd)/plots
The viewer will print: Watching plots directory: /Users/mberg/projects/myapp/plots
Step 1: Check current directory and ensure plots folder exists
pwd
# Output: /Users/mberg/projects/myapp
mkdir -p plots
Step 2: Write the plot file
cat > plots/sales-analysis-2025.json << 'EOF'
{
"name": "Sales Analysis",
"description": "Monthly revenue by product category",
"code": "const data = [\n {month: 'Jan', revenue: 1000},\n {month: 'Feb', revenue: 1200}\n];\n\nreturn Plot.plot({\n marks: [\n Plot.barY(data, {x: 'month', y: 'revenue'})\n ]\n})",
"timestamp": "2025-10-31T12:34:56.789Z"
}
EOF
Step 3: Viewer automatically detects and loads the new plot
The viewer polls every 5 seconds for new plots and will automatically:
Step 4: User interaction
Users can then:
plots/ directory for future useWhen running the viewer in the background, Claude can check for errors:
curl -s http://localhost:8765/viewer-status | jq
This returns:
{
"last_error": "Plot evaluation error: data is not defined",
"last_error_time": "2025-10-31T14:23:45.123Z",
"error_count": 3,
"recent_errors": [
{
"message": "Plot evaluation error: data is not defined",
"stack": "ReferenceError: data is not defined...",
"timestamp": "2025-10-31T14:23:45.123Z"
}
],
"plots_dir": "/Users/mberg/projects/myapp/plots",
"plots_dir_exists": true,
"server_time": "2025-10-31T14:25:00.000Z"
}
This allows Claude to:
The code field must:
Plot.plot() result using returnPlot and d3 globalsExample:
// Fetch and visualize data
const data = await fetch(
"https://raw.githubusercontent.com/observablehq/sample-datasets/refs/heads/main/penguins.csv"
).then(r => r.text()).then(text => d3.csvParse(text, d3.autoType));
return Plot.plot({
marks: [
Plot.dot(data, {
x: "flipper_length_mm",
y: "body_mass_g",
fill: "species",
tip: true
})
],
color: { legend: true }
})
Observable provides a collection of real-world datasets perfect for creating example visualizations. These include datasets for time series (stock prices, weather, temperature), categorical data (letters, athletes, cars), and geographic data (US counties, state capitals).
See: reference/datasets.md for complete dataset catalog with direct URLs and usage examples.
When creating visualizations with real data, always use the curated datasets from reference/datasets.md. Do not attempt to fetch arbitrary URLs or datasets from other sources.
Before creating a visualization with external data, verify the data structure:
curl -s "https://raw.githubusercontent.com/observablehq/sample-datasets/refs/heads/main/olympians.csv" | head -20
This shows you:
Proper pattern for CSV files:
const data = await fetch(
"https://raw.githubusercontent.com/observablehq/sample-datasets/refs/heads/main/olympians.csv"
).then(r => r.text()).then(text => d3.csvParse(text, d3.autoType));
return Plot.plot({
marks: [
Plot.dot(data, {x: "weight", y: "height", fill: "sex"})
]
});
Key points:
curl | head -20 before creating the plotfetch(url).then(r => r.text()).then(text => d3.csvParse(text, d3.autoType))fetch(url).then(r => r.json())d3.autoType to automatically infer numeric and date typesExample datasets with common use cases:
Plot.dot() with x/y channelsPlot.barY() or Plot.barX() with grouping/stackingPlot.line() with x/y channels, optional curve optionPlot.cell() with fill encoding or Plot.hexbin() for hexagonal binningPlot.geo() with GeoJSON data and projectionsfx/fy faceting optionsSee reference/transforms.md for detailed transform documentation.
See reference/plot-options.md for layout, margins, and dimensions. See reference/scales.md for color schemes and scale configuration.
See reference/interactions.md for tips, crosshairs, and pointer interactions.
Observable Plot requires modern JavaScript (ES2020+) and works in:
Plot.density() or Plot.hexbin()Plot emphasizes declarative configuration. Here's the general pattern:
Plot.plot({
// Plot-level options
width: 640,
height: 400,
marginLeft: 50,
// Color scale configuration
color: {
scheme: "viridis",
legend: true
},
// Marks array (order determines layering)
marks: [
// Background/reference marks
Plot.gridY(),
Plot.ruleY([0]),
// Data marks with transforms
Plot.barY(data, Plot.groupX({y: "sum"}, {
x: "category",
y: "value",
fill: "subcategory"
})),
// Annotation marks
Plot.text(labels, {x: "x", y: "y", text: "label"})
]
})
For a basic getting-started example and links to all examples, see reference/examples.md.
Browse examples by category:
Comprehensive reference documentation is available for progressive loading:
When helping users create visualizations with Observable Plot:
barY for vertical bars, barX for horizontal barsPlot.hexbin(), Plot.density(), or opacity for large datasetsThis skill is based on Observable Plot v0.6.x API. The library is actively maintained and follows semantic versioning.
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.