Files
gh-lyndonkl-claude/skills/d3-visualization/resources/workflows.md
2025-11-30 08:38:26 +08:00

500 lines
11 KiB
Markdown

# Workflows
## Bar Chart Workflow
### Problem
Display categorical data with quantitative values as vertical bars.
### Steps
1. **Prepare data**
```javascript
const data = [
{category: 'A', value: 30},
{category: 'B', value: 80},
{category: 'C', value: 45}
];
```
2. **Create SVG container**
```javascript
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})`);
```
3. **Create scales**
```javascript
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]);
```
4. **Create axes**
```javascript
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));
```
5. **Draw bars**
```javascript
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
1. **Prepare temporal data**
```javascript
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}
];
```
2. **Create scales**
```javascript
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]);
```
3. **Create line generator**
```javascript
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
```
4. **Draw line**
```javascript
svg.append('path')
.datum(data)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2);
```
5. **Add axes**
```javascript
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
```javascript
// 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
1. **Initial render**
```javascript
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);
```
2. **Update on new data**
```javascript
// 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
1. **Prepare data**
```javascript
const nodes = [
{id: 'A', group: 1},
{id: 'B', group: 1},
{id: 'C', group: 2}
];
const links = [
{source: 'A', target: 'B'},
{source: 'B', target: 'C'}
];
```
2. **Create force simulation**
```javascript
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));
```
3. **Draw links**
```javascript
const link = svg.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke', '#999')
.attr('stroke-width', 2);
```
4. **Draw nodes**
```javascript
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 10)
.attr('fill', d => d.group === 1 ? 'steelblue' : 'orange');
```
5. **Update positions on tick**
```javascript
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);
});
```
6. **Add drag behavior**
```javascript
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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
## Next Steps
- [Getting Started](getting-started.md) | [Common Patterns](common-patterns.md) | [Selections](selections-datajoins.md) | [Scales](scales-axes.md) | [Shapes](shapes-layouts.md)