원클릭으로
dataviz
Build D3 visualizations with Bostock's patterns, Tufte's integrity, Few's clarity, and production-grade frontend craft.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Build D3 visualizations with Bostock's patterns, Tufte's integrity, Few's clarity, and production-grade frontend craft.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | dataviz |
| description | Build D3 visualizations with Bostock's patterns, Tufte's integrity, Few's clarity, and production-grade frontend craft. |
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.
Answer these questions. They determine everything.
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.
| 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.
| 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 |
Define the data shape before touching SVG. The data drives everything.
// Define your data contract
const data = [
{ id: "q1", date: new Date("2024-01"), value: 42, category: "A" }
];
// Derive domains from data, never hardcode
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
d3.extent, d3.max), not magic numbers.data(data, d => d.id))scaleSqrt() for area encodings — never scaleLinear() on radiusEvery chart starts here:
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const width = containerWidth - margin.left - margin.right;
const height = containerHeight - margin.top - margin.bottom;
const svg = d3.select(el).append("svg")
.attr("viewBox", `0 0 ${containerWidth} ${containerHeight}`)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
viewBox for responsive sizing, not fixed width/height attributeswidth/heightBinddata 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())
);
.data() call when data changesselectAll("*").remove() redrawsLie 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 somewhere
const 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);
Default: grayscale. Color is a scarce resource.
// Base palette: grays
const base = "#333"; // text, primary data
const muted = "#999"; // secondary, axes
const light = "#e5e5e5"; // gridlines, borders
// Color ONLY for meaning
const alert = "#d32f2f"; // bad / below target
const success = "#388e3c"; // good / above target
const highlight = "#1565c0"; // selected / focus
Charts are read, not just seen. Typography matters.
d3.format("$.2s") for executives (→ $1.2M), d3.format("$,.0f") for analysts (→ $1,234,568).// Tick formatting
xAxis.tickFormat(d3.timeFormat("%b")); // "Jan", "Feb"
yAxis.tickFormat(d3.format("$.2s")); // "$1.2M"
yAxis.ticks(5); // Don't crowd
viewBox on SVG, no fixed dimensionsrole="img" + aria-label on SVG container<title> and <desc> elements inside SVGprefers-reduced-motion check before transitionsprefers-color-scheme for dark mode supportconst prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const t = svg.transition()
.duration(prefersReducedMotion ? 0 : 750);
If this chart will be used more than once, extract it:
function bulletChart() {
let width = 300;
let height = 30;
let ranges = [0.5, 0.75, 1.0];
function chart(selection) {
selection.each(function(d) {
// Build chart using width, height, ranges, d
});
}
chart.width = function(v) {
return arguments.length ? (width = v, chart) : width;
};
chart.height = function(v) {
return arguments.length ? (height = v, chart) : height;
};
chart.ranges = function(v) {
return arguments.length ? (ranges = v, chart) : ranges;
};
return chart;
}
// Usage
d3.selectAll(".bullet").datum(d => d).call(bulletChart().width(400));
Closure pattern, not classes. Getter-setter methods. Callable via selection.call().
If building a dashboard, not a single chart:
| Anti-Pattern | Why | Do Instead |
|---|---|---|
| Pie charts | Humans are bad at comparing angles | Horizontal bar, sorted |
| Gauges/dials | One number, massive footprint | Bullet graph |
| 3D anything | Distorts perception, always | 2D |
| Rainbow color scales | No perceptual ordering | Sequential single-hue or diverging |
| Dual Y-axes | Implies false correlation | Two charts or normalize |
| Full redraws on update | Destroys transitions, wastes DOM | Data joins with enter/update/exit |
| Index-based data joins | Elements track position, not data | Key functions: .data(d, d => d.id) |
| Decorative animation | Slows comprehension | Transition only on data change |
| Legend for <8 series | Forces cognitive round-trip | Direct labels |
| Area encoding with linear scale | 4x visual exaggeration | scaleSqrt() |
Integrity
Clarity
Craft
prefers-reduced-motion respectedDashboard (if applicable)