500 lines
11 KiB
Markdown
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)
|