11 KiB
11 KiB
Workflows
Bar Chart Workflow
Problem
Display categorical data with quantitative values as vertical bars.
Steps
- Prepare data
const data = [
{category: 'A', value: 30},
{category: 'B', value: 80},
{category: 'C', value: 45}
];
- Create SVG container
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
- Create scales
const xScale = d3.scaleBand()
.domain(data.map(d => d.category))
.range([0, width])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
- Create axes
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
svg.append('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(yScale));
- Draw bars
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => xScale(d.category))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('fill', 'steelblue');
Key Concepts
- scaleBand: Positions categorical bars with automatic spacing
- Inverted Y range:
[height, 0]puts origin at bottom-left - Height calculation:
height - yScale(d.value)computes bar height
Line Chart Workflow
Problem
Show trends over time or continuous data.
Steps
- Prepare temporal data
const data = [
{date: new Date(2020, 0, 1), value: 30},
{date: new Date(2020, 1, 1), value: 80},
{date: new Date(2020, 2, 1), value: 45}
];
- Create scales
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]);
- Create line generator
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
- Draw line
svg.append('path')
.datum(data)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2);
- Add axes
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale).ticks(5));
svg.append('g')
.call(d3.axisLeft(yScale));
Key Concepts
- scaleTime: Handles Date objects automatically
- d3.extent: Returns
[min, max]for domain - .datum() vs .data(): Use
.datum()for single path,.data()for multiple paths - fill='none': Lines need stroke, not fill
Scatter Plot Workflow
Problem
Visualize relationship between two quantitative variables.
Steps
// 1. Prepare data and scales
const data = [{x: 10, y: 20}, {x: 40, y: 90}, {x: 80, y: 50}];
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([height, 0]);
// 2. Draw points
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 5)
.attr('fill', 'steelblue')
.attr('opacity', 0.7);
// 3. Add axes
svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(xScale));
svg.append('g').call(d3.axisLeft(yScale));
Key Concepts
- Both linear scales: X and Y are quantitative
- Opacity for overlaps: Helps see density
Update Pattern Workflow
Problem
Dynamically update visualization when data changes.
Steps
- Initial render
const svg = d3.select('#chart').append('svg')...
function render(data) {
// Update scale domains
yScale.domain([0, d3.max(data, d => d.value)]);
// Update axis
svg.select('.y-axis')
.transition()
.duration(750)
.call(d3.axisLeft(yScale));
// Update bars
svg.selectAll('rect')
.data(data, d => d.id) // Key function!
.join(
enter => enter.append('rect')
.attr('x', d => xScale(d.category))
.attr('y', height)
.attr('width', xScale.bandwidth())
.attr('height', 0)
.attr('fill', 'steelblue')
.call(enter => enter.transition()
.duration(750)
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value))),
update => update
.call(update => update.transition()
.duration(750)
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value))),
exit => exit
.call(exit => exit.transition()
.duration(750)
.attr('y', height)
.attr('height', 0)
.remove())
);
}
// Initial render
render(initialData);
- Update on new data
// Later, when data changes
render(newData);
Key Concepts
- Encapsulate in function: Reusable render logic
- Key function: Tracks element identity across updates
- Enter/Update/Exit: Custom animations for each lifecycle
- Update domain first: Recalculate scales before rendering
Network Visualization Workflow
Problem
Display relationships between entities (nodes and links).
Steps
- Prepare data
const nodes = [
{id: 'A', group: 1},
{id: 'B', group: 1},
{id: 'C', group: 2}
];
const links = [
{source: 'A', target: 'B'},
{source: 'B', target: 'C'}
];
- Create force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide().radius(20));
- Draw links
const link = svg.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke', '#999')
.attr('stroke-width', 2);
- Draw nodes
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 10)
.attr('fill', d => d.group === 1 ? 'steelblue' : 'orange');
- Update positions on tick
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
- Add drag behavior
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
node.call(drag(simulation));
Key Concepts
- forceSimulation: Physics-based layout
- tick handler: Updates positions every frame
- id accessor: Links reference nodes by ID
- fx/fy: Fixed positions during drag
Hierarchy Visualization (Treemap) Workflow
Problem
Show hierarchical data with nested rectangles.
Steps
// 1. Prepare and process hierarchy
const data = {name: 'root', children: [{name: 'A', value: 100}, {name: 'B', value: 200}]};
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
// 2. Apply layout
d3.treemap().size([width, height]).padding(1)(root);
// 3. Draw rectangles
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
svg.selectAll('rect')
.data(root.leaves())
.join('rect')
.attr('x', d => d.x0)
.attr('y', d => d.y0)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => colorScale(d.parent.data.name));
Key Concepts
- d3.hierarchy: Creates hierarchy from nested object
- .sum(): Aggregates values up tree
- x0, y0, x1, y1: Layout computes rectangle bounds
Geographic Map (Choropleth) Workflow
Problem
Visualize regional data on a map.
Steps
// 1. Load data
Promise.all([d3.json('countries.geojson'), d3.csv('data.csv')])
.then(([geojson, csvData]) => {
// 2. Setup projection and path
const projection = d3.geoMercator().fitExtent([[0, 0], [width, height]], geojson);
const path = d3.geoPath().projection(projection);
// 3. Create color scale and data lookup
const colorScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(csvData, d => +d.value)]);
const dataById = new Map(csvData.map(d => [d.id, +d.value]));
// 4. Draw map
svg.selectAll('path')
.data(geojson.features)
.join('path')
.attr('d', path)
.attr('fill', d => dataById.get(d.id) ? colorScale(dataById.get(d.id)) : '#ccc')
.attr('stroke', '#fff');
});
Key Concepts
- fitExtent: Auto-scales projection to fit bounds
- geoPath: Converts GeoJSON to SVG paths
- Map for lookup: Fast data join by ID
Real-Time Updates Workflow
Problem
Continuously update visualization with streaming data.
Steps
// 1. Setup sliding window
const maxPoints = 50;
let data = [];
function update(newValue) {
data.push({time: new Date(), value: newValue});
if (data.length > maxPoints) data.shift();
// 2. Update scales and render
xScale.domain(d3.extent(data, d => d.time));
yScale.domain([0, d3.max(data, d => d.value)]);
svg.select('.x-axis').transition().duration(200).call(d3.axisBottom(xScale));
svg.select('.y-axis').transition().duration(200).call(d3.axisLeft(yScale));
const line = d3.line().x(d => xScale(d.time)).y(d => yScale(d.value));
svg.select('.line').datum(data).transition().duration(200).attr('d', line);
}
// 3. Poll data
setInterval(() => update(Math.random() * 100), 1000);
Key Concepts
- Sliding window: Fixed-size buffer
- Short transitions: 200ms feels responsive
Linked Views Workflow
Problem
Coordinate highlighting across multiple charts.
Steps
// 1. Shared event handlers
function highlight(id) {
svg1.selectAll('circle').attr('opacity', d => d.id === id ? 1 : 0.3);
svg2.selectAll('rect').attr('opacity', d => d.id === id ? 1 : 0.3);
}
function unhighlight() {
svg1.selectAll('circle').attr('opacity', 1);
svg2.selectAll('rect').attr('opacity', 1);
}
// 2. Attach to elements
svg1.selectAll('circle')
.data(data).join('circle')
.on('mouseover', (event, d) => highlight(d.id))
.on('mouseout', unhighlight);
svg2.selectAll('rect')
.data(data).join('rect')
.on('mouseover', (event, d) => highlight(d.id))
.on('mouseout', unhighlight);
Key Concepts
- Shared state: Common ID across views
- Coordinated updates: Single event updates all charts