9.4 KiB
9.4 KiB
Shapes & Layouts
Why Shape Generators Matter
The Problem: Creating SVG paths manually for lines, areas, and arcs requires complex math and string concatenation. <path d="M0,50 L10,40 L20,60..."> is tedious and error-prone.
D3's Solution: Shape generators convert data arrays into SVG path strings automatically. You configure the generator, pass data, get path string.
Key Principle: "Configure generator once, reuse for all data." Separation of shape logic from data.
Line Generator
Basic Usage
const data = [
{x: 0, y: 50},
{x: 100, y: 80},
{x: 200, y: 40},
{x: 300, y: 90}
];
const line = d3.line()
.x(d => d.x)
.y(d => d.y);
svg.append('path')
.datum(data) // Use .datum() for single item
.attr('d', line) // line(data) generates path string
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2);
Key Methods:
.x(accessor): Function returning x coordinate.y(accessor): Function returning y coordinate- Returns path string when called with data
With Scales
const xScale = d3.scaleLinear().domain([0, 300]).range([0, width]);
const yScale = d3.scaleLinear().domain([0, 100]).range([height, 0]);
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
Curves
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX); // Smooth curve
Curve Types:
d3.curveLinear: Straight lines (default)d3.curveMonotoneX: Smooth, preserves monotonicityd3.curveCatmullRom: Smooth, passes through pointsd3.curveStep: Step functiond3.curveBasis: B-spline
Missing Data
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.defined(d => d.y !== null); // Skip null values
Area Generator
Basic Usage
const area = d3.area()
.x(d => xScale(d.x))
.y0(height) // Baseline (bottom)
.y1(d => yScale(d.y)); // Top line
svg.append('path')
.datum(data)
.attr('d', area)
.attr('fill', 'steelblue')
.attr('opacity', 0.5);
Key Methods:
.y0(): Baseline (constant or function).y1(): Top boundary (usually data-driven)
Stacked Area Chart
const stack = d3.stack()
.keys(['series1', 'series2', 'series3']);
const series = stack(data); // Returns array of series
const area = d3.area()
.x(d => xScale(d.data.x))
.y0(d => yScale(d[0])) // Lower bound
.y1(d => yScale(d[1])); // Upper bound
svg.selectAll('path')
.data(series)
.join('path')
.attr('d', area)
.attr('fill', (d, i) => colorScale(i));
Arc Generator
Basic Usage
const arc = d3.arc()
.innerRadius(0) // 0 = pie, >0 = donut
.outerRadius(100);
const arcData = {
startAngle: 0,
endAngle: Math.PI / 2 // 90 degrees
};
svg.append('path')
.datum(arcData)
.attr('d', arc)
.attr('fill', 'steelblue');
Angle Units: Radians (0 to 2π)
Donut Chart
const arc = d3.arc()
.innerRadius(50)
.outerRadius(100);
const pie = d3.pie()
.value(d => d.value);
const arcs = pie(data); // Generates angles
svg.selectAll('path')
.data(arcs)
.join('path')
.attr('d', arc)
.attr('fill', (d, i) => colorScale(i));
Rounded Corners
const arc = d3.arc()
.innerRadius(50)
.outerRadius(100)
.cornerRadius(5); // Rounded edges
Labels
// Use centroid for label positioning
svg.selectAll('text')
.data(arcs)
.join('text')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('text-anchor', 'middle')
.text(d => d.data.label);
Pie Generator
Basic Usage
const data = [30, 80, 45, 60, 20];
const pie = d3.pie();
const arcs = pie(data);
// Returns: [{data: 30, startAngle: 0, endAngle: 0.53, ...}, ...]
What Pie Does: Converts values → angles. Use with arc generator for rendering.
With Objects
const data = [
{name: 'A', value: 30},
{name: 'B', value: 80},
{name: 'C', value: 45}
];
const pie = d3.pie()
.value(d => d.value);
const arcs = pie(data);
Sorting
const pie = d3.pie()
.value(d => d.value)
.sort((a, b) => b.value - a.value); // Descending
Padding
const pie = d3.pie()
.value(d => d.value)
.padAngle(0.02); // Gap between slices
Stack Generator
Basic Usage
const data = [
{month: 'Jan', apples: 30, oranges: 20, bananas: 40},
{month: 'Feb', apples: 50, oranges: 30, bananas: 35},
{month: 'Mar', apples: 40, oranges: 25, bananas: 45}
];
const stack = d3.stack()
.keys(['apples', 'oranges', 'bananas']);
const series = stack(data);
// Returns: [[{0: 0, 1: 30, data: {month: 'Jan', ...}}, ...], [...], [...]]
Output: Array of series, each with [lower, upper] bounds for stacking.
Stacked Bar Chart
const xScale = d3.scaleBand()
.domain(data.map(d => d.month))
.range([0, width])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(series, s => d3.max(s, d => d[1]))])
.range([height, 0]);
svg.selectAll('g')
.data(series)
.join('g')
.attr('fill', (d, i) => colorScale(i))
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => xScale(d.data.month))
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]))
.attr('width', xScale.bandwidth());
Offsets
// Default: stacked on top of each other
const stack = d3.stack().keys(keys);
// Normalized (0-1)
const stack = d3.stack().keys(keys).offset(d3.stackOffsetExpand);
// Centered (streamgraph)
const stack = d3.stack().keys(keys).offset(d3.stackOffsetWiggle);
Symbol Generator
Basic Usage
const symbol = d3.symbol()
.type(d3.symbolCircle)
.size(100); // Area in square pixels
svg.append('path')
.attr('d', symbol())
.attr('fill', 'steelblue');
Symbol Types:
symbolCircle,symbolCross,symbolDiamond,symbolSquare,symbolStar,symbolTriangle,symbolWye
In Scatter Plots
const symbol = d3.symbol()
.type(d => d3.symbolCircle)
.size(d => sizeScale(d.value));
svg.selectAll('path')
.data(data)
.join('path')
.attr('d', symbol)
.attr('transform', d => `translate(${xScale(d.x)}, ${yScale(d.y)})`);
Complete Examples
Line Chart
const xScale = d3.scaleTime().domain(d3.extent(data, d => d.date)).range([0, width]);
const yScale = d3.scaleLinear().domain([0, d3.max(data, d => d.value)]).range([height, 0]);
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
svg.append('path')
.datum(data)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2);
svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(xScale));
svg.append('g').call(d3.axisLeft(yScale));
Area Chart
const area = d3.area()
.x(d => xScale(d.date))
.y0(height)
.y1(d => yScale(d.value))
.curve(d3.curveMonotoneX);
svg.append('path')
.datum(data)
.attr('d', area)
.attr('fill', 'steelblue')
.attr('opacity', 0.5);
Donut Chart
const pie = d3.pie().value(d => d.value);
const arc = d3.arc().innerRadius(50).outerRadius(100);
const arcs = pie(data);
svg.selectAll('path')
.data(arcs)
.join('path')
.attr('d', arc)
.attr('fill', (d, i) => colorScale(i))
.attr('stroke', 'white')
.attr('stroke-width', 2);
svg.selectAll('text')
.data(arcs)
.join('text')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('text-anchor', 'middle')
.text(d => d.data.label);
Stacked Bar Chart
const stack = d3.stack().keys(['series1', 'series2', 'series3']);
const series = stack(data);
const xScale = d3.scaleBand().domain(data.map(d => d.category)).range([0, width]).padding(0.1);
const yScale = d3.scaleLinear().domain([0, d3.max(series, s => d3.max(s, d => d[1]))]).range([height, 0]);
svg.selectAll('g')
.data(series)
.join('g')
.attr('fill', (d, i) => colorScale(i))
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => xScale(d.data.category))
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]))
.attr('width', xScale.bandwidth());
Common Pitfalls
Pitfall 1: Using .data() Instead of .datum()
// WRONG - creates path per data point
svg.selectAll('path').data(data).join('path').attr('d', line);
// CORRECT - single path for entire dataset
svg.append('path').datum(data).attr('d', line);
Pitfall 2: Forgetting fill="none" for Lines
// WRONG - area filled by default
svg.append('path').datum(data).attr('d', line);
// CORRECT
svg.append('path').datum(data).attr('d', line).attr('fill', 'none').attr('stroke', 'steelblue');
Pitfall 3: Wrong Angle Units
// WRONG - degrees
arc.startAngle(90).endAngle(180);
// CORRECT - radians
arc.startAngle(Math.PI / 2).endAngle(Math.PI);
Next Steps
- Apply to real data: Workflows
- Add transitions: Transitions & Interactions
- Combine with layouts: Advanced Layouts