Build D3 visualizations with Bostock's patterns, Tufte's integrity, Few's clarity, and production-grade frontend craft.
/dataviz
Build data visualizations that are technically correct, visually honest, and worth looking at. Combines Bostock (D3 patterns), Tufte (graphical integrity), Few (information design), and frontend craft into one workflow.
No arguments? Describe this skill and stop. Do not execute.
Before You Write Code
Answer these questions. They determine everything.
1. What decision does this visualization support?
If you can't name the decision, you don't need a visualization — you need a table. Every chart exists to help someone decide or understand something specific.
2. What type of data relationship?
Relationship
Best Display
Never Use
Comparison
Horizontal bar (sorted)
Pie, radar
Trend over time
Line, area, sparkline
Bar (unless discrete periods)
Distribution
Histogram, strip plot, box plot
Pie
Part-to-whole
Stacked bar, treemap
3D pie, donut
Correlation
Scatter plot
Bubble (unless 3rd variable)
KPI vs target
Bullet graph
Gauge, dial, speedometer
Geographic
Choropleth, proportional symbol
3D globe
Current status
Indicator + value
Animation
Pick the simplest display that answers the question. If a table works, use a table.
3. Who is looking at this?
Audience
Precision
Context
Density
Executive
Trends, not decimals ($1.2M)
vs target, vs prior period
Low — 5 metrics max
Analyst
Full precision ($1,234,567)
Drill-down, filter, compare
High — coordinated views
Operations
Current state, real-time
Alerts, thresholds
Medium — status indicators
Building the Visualization
Step 1: Data Structure First
Define the data shape before touching SVG. The data drives everything.
// Define your data contractconst data = [
{ id: "q1", date: newDate("2024-01"), value: 42, category: "A" }
];
// Derive domains from data, never hardcodeconst x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
Domains from data (d3.extent, d3.max), not magic numbers
Key field present for identity (.data(data, d => d.id))
Correct scale type for data type (time, linear, band, ordinal, sqrt)
scaleSqrt() for area encodings — never scaleLinear() on radius
viewBox for responsive sizing, not fixed width/height attributes
Scales use inner width/height
No positioning math elsewhere — margins handle it once
Step 3: Data Joins
Binddata to elements. Handle all three states.
svg.selectAll("rect")
.data(data, d => d.id) // Key function — always
.join(
enter => enter.append("rect")
.attr("x", d =>x(d.date))
.attr("height", 0)
.call(enter => enter.transition()
.attr("height", d => height - y(d.value))),
update => update
.call(update => update.transition()
.attr("x", d =>x(d.date))
.attr("height", d => height - y(d.value))),
exit => exit
.call(exit => exit.transition()
.attr("height", 0)
.remove())
);
Key function on every .data() call when data changes
Enter/update/exit handled explicitly — no selectAll("*").remove() redraws
Transitions show what changed, not decoration
Step 4: Graphical Integrity (Tufte)
Lie Factor must be 0.95–1.05. Check these:
Rule
Violation
Fix
Bar charts start at zero
Truncated Y-axis exaggerates
domain([0, max])
Area = sqrt encoding
Linear radius on bubbles → 4x visual error
d3.scaleSqrt()
Consistent scales
Small multiples with different Y ranges
Shared domain across panels
No dual Y-axes
Misleading correlation
Two charts or normalize
Data-ink ratio > 70%. Remove:
Remove
Replace with
Heavy gridlines
Light gray (#e5e5e5) at 0.5px, or none
Axis domain lines
Just ticks, or nothing
Borders/boxes
White space
Legends
Direct labels on data
Background colors
White/transparent
3D effects
Nothing (2D always)
Direct labeling over legends. Put the label at the data point:
// Label on the line, not in a box somewhereconst lastPoint = data[data.length - 1];
svg.append("text")
.attr("x", x(lastPoint.date) + 4)
.attr("y", y(lastPoint.value))
.attr("dy", "0.35em")
.text(lastPoint.category);
Step 5: Color Discipline (Few)
Default: grayscale. Color is a scarce resource.
// Base palette: graysconst base = "#333"; // text, primary dataconst muted = "#999"; // secondary, axesconst light = "#e5e5e5"; // gridlines, borders// Color ONLY for meaningconst alert = "#d32f2f"; // bad / below targetconst success = "#388e3c"; // good / above targetconst highlight = "#1565c0"; // selected / focus
When everything is colorful, nothing stands out
Always pair color with a value — never traffic lights alone
Colorblind safe: add shape or text, don't rely on red/green distinction
If you need categorical colors, use 5 or fewer with maximum perceptual distance
Step 6: Typography and Craft
Charts are read, not just seen. Typography matters.
Axis labels: Short, no abbreviations the audience wouldn't know. Units in the axis title, not on every tick.
Title: States the insight, not the data. "Revenue grew 23% in Q4" not "Revenue by Quarter"
Numbers: Format for the audience. d3.format("$.2s") for executives (→ $1.2M), d3.format("$,.0f") for analysts (→ $1,234,568).
Font: Use the application's typeface. Don't introduce a new one for charts.