Initial commit
This commit is contained in:
464
skills/d3-visualization/resources/advanced-layouts.md
Normal file
464
skills/d3-visualization/resources/advanced-layouts.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# Advanced Layouts
|
||||
|
||||
## Why Layouts Matter
|
||||
|
||||
**The Problem**: Calculating positions for complex visualizations (network graphs, treemaps, maps) involves sophisticated algorithms—force-directed simulation, spatial partitioning, map projections—that are mathematically complex.
|
||||
|
||||
**D3's Solution**: Layout generators compute positions/sizes/angles automatically. You provide data, configure layout, receive computed coordinates.
|
||||
|
||||
**Key Principle**: "Layouts transform data, don't render." Layouts add properties (`x`, `y`, `width`, etc.) that you bind to visual elements.
|
||||
|
||||
---
|
||||
|
||||
## Force Simulation
|
||||
|
||||
### Why Force Layouts
|
||||
|
||||
**Use Case**: Network diagrams, organic clustering where fixed positions are unnatural.
|
||||
|
||||
**How It Works**: Physics simulation with forces (repulsion, attraction, gravity) that iteratively compute node positions.
|
||||
|
||||
---
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```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'}
|
||||
];
|
||||
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id))
|
||||
.force('charge', d3.forceManyBody())
|
||||
.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Forces
|
||||
|
||||
**forceLink**: Maintains fixed distance between connected nodes
|
||||
```javascript
|
||||
.force('link', d3.forceLink(links)
|
||||
.id(d => d.id)
|
||||
.distance(50))
|
||||
```
|
||||
|
||||
**forceManyBody**: Repulsion (negative) or attraction (positive)
|
||||
```javascript
|
||||
.force('charge', d3.forceManyBody().strength(-100))
|
||||
```
|
||||
|
||||
**forceCenter**: Pulls nodes toward center point
|
||||
```javascript
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
```
|
||||
|
||||
**forceCollide**: Prevents overlapping circles
|
||||
```javascript
|
||||
.force('collide', d3.forceCollide().radius(20))
|
||||
```
|
||||
|
||||
**forceX / forceY**: Attracts to specific coordinates
|
||||
```javascript
|
||||
.force('x', d3.forceX(width / 2).strength(0.1))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rendering
|
||||
|
||||
```javascript
|
||||
const link = svg.selectAll('line')
|
||||
.data(links)
|
||||
.join('line')
|
||||
.attr('stroke', '#999');
|
||||
|
||||
const node = svg.selectAll('circle')
|
||||
.data(nodes)
|
||||
.join('circle')
|
||||
.attr('r', 10)
|
||||
.attr('fill', d => colorScale(d.group));
|
||||
|
||||
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);
|
||||
});
|
||||
```
|
||||
|
||||
**Key**: Update positions in `tick` handler as simulation runs.
|
||||
|
||||
---
|
||||
|
||||
### 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));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hierarchies
|
||||
|
||||
### Creating Hierarchies
|
||||
|
||||
```javascript
|
||||
const data = {
|
||||
name: 'root',
|
||||
children: [
|
||||
{name: 'child1', value: 10},
|
||||
{name: 'child2', value: 20, children: [{name: 'grandchild', value: 5}]}
|
||||
]
|
||||
};
|
||||
|
||||
const root = d3.hierarchy(data)
|
||||
.sum(d => d.value) // Aggregate values up tree
|
||||
.sort((a, b) => b.value - a.value);
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
- `hierarchy(data)`: Creates hierarchy from nested object
|
||||
- `.sum(accessor)`: Computes values (leaf → root)
|
||||
- `.sort(comparator)`: Orders siblings
|
||||
- `.descendants()`: All nodes (breadth-first)
|
||||
- `.leaves()`: Leaf nodes only
|
||||
|
||||
---
|
||||
|
||||
### Tree Layout
|
||||
|
||||
**Use**: Node-link diagrams (org charts, file systems)
|
||||
|
||||
```javascript
|
||||
const tree = d3.tree().size([width, height]);
|
||||
tree(root);
|
||||
|
||||
// Creates x, y properties on each node
|
||||
svg.selectAll('circle')
|
||||
.data(root.descendants())
|
||||
.join('circle')
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y)
|
||||
.attr('r', 5);
|
||||
|
||||
// Links
|
||||
svg.selectAll('line')
|
||||
.data(root.links())
|
||||
.join('line')
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Treemap Layout
|
||||
|
||||
**Use**: Space-filling rectangles (disk usage, budget allocation)
|
||||
|
||||
```javascript
|
||||
const treemap = d3.treemap()
|
||||
.size([width, height])
|
||||
.padding(1);
|
||||
|
||||
treemap(root);
|
||||
|
||||
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.value));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pack Layout
|
||||
|
||||
**Use**: Circle packing (bubble charts)
|
||||
|
||||
```javascript
|
||||
const pack = d3.pack().size([width, height]).padding(3);
|
||||
pack(root);
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(root.descendants())
|
||||
.join('circle')
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y)
|
||||
.attr('r', d => d.r)
|
||||
.attr('fill', d => d.children ? '#ccc' : colorScale(d.value));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Partition Layout
|
||||
|
||||
**Use**: Sunburst, icicle charts
|
||||
|
||||
```javascript
|
||||
const partition = d3.partition().size([2 * Math.PI, radius]);
|
||||
partition(root);
|
||||
|
||||
const arc = d3.arc()
|
||||
.startAngle(d => d.x0)
|
||||
.endAngle(d => d.x1)
|
||||
.innerRadius(d => d.y0)
|
||||
.outerRadius(d => d.y1);
|
||||
|
||||
svg.selectAll('path')
|
||||
.data(root.descendants())
|
||||
.join('path')
|
||||
.attr('d', arc)
|
||||
.attr('fill', d => colorScale(d.depth));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geographic Maps
|
||||
|
||||
### GeoJSON
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[-100, 40], [-100, 50], [-90, 50], [-90, 40], [-100, 40]]]
|
||||
},
|
||||
"properties": {"name": "Region A"}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Projections
|
||||
|
||||
```javascript
|
||||
// Mercator (world maps)
|
||||
const projection = d3.geoMercator()
|
||||
.center([0, 0])
|
||||
.scale(150)
|
||||
.translate([width / 2, height / 2]);
|
||||
|
||||
// Albers (US maps)
|
||||
const projection = d3.geoAlbersUsa().scale(1000).translate([width / 2, height / 2]);
|
||||
|
||||
// Orthographic (globe)
|
||||
const projection = d3.geoOrthographic().scale(250).translate([width / 2, height / 2]);
|
||||
```
|
||||
|
||||
**Common Projections**: Mercator, Albers, Equirectangular, Orthographic, Azimuthal, Conic
|
||||
|
||||
---
|
||||
|
||||
### Path Generator
|
||||
|
||||
```javascript
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
d3.json('countries.geojson').then(geojson => {
|
||||
svg.selectAll('path')
|
||||
.data(geojson.features)
|
||||
.join('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', '#ccc')
|
||||
.attr('stroke', '#fff');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Auto-Fit
|
||||
|
||||
```javascript
|
||||
const projection = d3.geoMercator()
|
||||
.fitExtent([[0, 0], [width, height]], geojson);
|
||||
```
|
||||
|
||||
**fitExtent**: Automatically scales/centers projection to fit data in bounds.
|
||||
|
||||
---
|
||||
|
||||
### Choropleth
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleSequential(d3.interpolateBlues)
|
||||
.domain([0, d3.max(data, d => d.value)]);
|
||||
|
||||
svg.selectAll('path')
|
||||
.data(geojson.features)
|
||||
.join('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', d => {
|
||||
const value = data.find(v => v.id === d.id)?.value;
|
||||
return value ? colorScale(value) : '#ccc';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chord Diagrams
|
||||
|
||||
### Use Case
|
||||
Visualize flows/relationships between entities (migrations, trade, connections).
|
||||
|
||||
---
|
||||
|
||||
### Data Format
|
||||
|
||||
```javascript
|
||||
const matrix = [
|
||||
[0, 10, 20], // From A to: A, B, C
|
||||
[15, 0, 5], // From B to: A, B, C
|
||||
[25, 30, 0] // From C to: A, B, C
|
||||
];
|
||||
```
|
||||
|
||||
**Matrix[i][j]**: Flow from entity i to entity j.
|
||||
|
||||
---
|
||||
|
||||
### Creating Chord
|
||||
|
||||
```javascript
|
||||
const chord = d3.chord()
|
||||
.padAngle(0.05)
|
||||
.sortSubgroups(d3.descending);
|
||||
|
||||
const chords = chord(matrix);
|
||||
|
||||
const arc = d3.arc()
|
||||
.innerRadius(200)
|
||||
.outerRadius(220);
|
||||
|
||||
const ribbon = d3.ribbon()
|
||||
.radius(200);
|
||||
|
||||
// Outer arcs (groups)
|
||||
svg.selectAll('g')
|
||||
.data(chords.groups)
|
||||
.join('path')
|
||||
.attr('d', arc)
|
||||
.attr('fill', (d, i) => colorScale(i));
|
||||
|
||||
// Inner ribbons (connections)
|
||||
svg.selectAll('path.ribbon')
|
||||
.data(chords)
|
||||
.join('path')
|
||||
.attr('class', 'ribbon')
|
||||
.attr('d', ribbon)
|
||||
.attr('fill', d => colorScale(d.source.index))
|
||||
.attr('opacity', 0.7);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Choosing Layouts
|
||||
|
||||
| Visualization | Layout |
|
||||
|---------------|--------|
|
||||
| Network graph, organic clusters | Force simulation |
|
||||
| Org chart, file tree (node-link) | Tree layout |
|
||||
| Space-filling rectangles | Treemap |
|
||||
| Bubble chart, circle packing | Pack layout |
|
||||
| Sunburst, icicle chart | Partition layout |
|
||||
| World/regional maps | Geographic projection |
|
||||
| Flow diagram (migration, trade) | Chord diagram |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Not Handling Tick Updates
|
||||
|
||||
```javascript
|
||||
// WRONG - positions never update
|
||||
const node = svg.selectAll('circle').data(nodes).join('circle');
|
||||
simulation.on('tick', () => {}); // Empty!
|
||||
|
||||
// CORRECT
|
||||
simulation.on('tick', () => {
|
||||
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Wrong Hierarchy Accessor
|
||||
|
||||
```javascript
|
||||
// WRONG - d3.hierarchy expects nested structure
|
||||
const root = d3.hierarchy(flatArray);
|
||||
|
||||
// CORRECT - nest data first or use stratify
|
||||
const root = d3.hierarchy(nestedObject);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Forgetting to Apply Layout
|
||||
|
||||
```javascript
|
||||
// WRONG - root has no x, y properties
|
||||
const root = d3.hierarchy(data);
|
||||
svg.selectAll('circle').data(root.descendants()).join('circle').attr('cx', d => d.x);
|
||||
|
||||
// CORRECT - apply layout first
|
||||
const tree = d3.tree().size([width, height]);
|
||||
tree(root); // Now root.descendants() have x, y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See complete workflows: [Workflows](workflows.md)
|
||||
- Add interactions: [Transitions & Interactions](transitions-interactions.md)
|
||||
- Use code templates: [Common Patterns](common-patterns.md)
|
||||
221
skills/d3-visualization/resources/common-patterns.md
Normal file
221
skills/d3-visualization/resources/common-patterns.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Common D3 Patterns - Code Templates
|
||||
|
||||
## Bar Chart Template
|
||||
|
||||
```javascript
|
||||
const data = [{label: 'A', value: 30}, {label: 'B', value: 80}, {label: 'C', value: 45}];
|
||||
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(data.map(d => d.label))
|
||||
.range([0, width])
|
||||
.padding(0.1);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(data, d => d.value)])
|
||||
.range([height, 0]);
|
||||
|
||||
svg.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', d => xScale(d.label))
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => height - yScale(d.value))
|
||||
.attr('fill', 'steelblue');
|
||||
|
||||
svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(xScale));
|
||||
svg.append('g').call(d3.axisLeft(yScale));
|
||||
```
|
||||
|
||||
## Line Chart Template
|
||||
|
||||
```javascript
|
||||
const parseDate = d3.timeParse('%Y-%m-%d');
|
||||
data.forEach(d => { d.date = parseDate(d.date); });
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
```
|
||||
|
||||
## Scatter Plot Template
|
||||
|
||||
```javascript
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain(d3.extent(data, d => d.x))
|
||||
.range([0, width]);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain(d3.extent(data, d => d.y))
|
||||
.range([height, 0]);
|
||||
|
||||
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('cx', d => xScale(d.x))
|
||||
.attr('cy', d => yScale(d.y))
|
||||
.attr('r', 5)
|
||||
.attr('fill', d => colorScale(d.category))
|
||||
.attr('opacity', 0.7);
|
||||
```
|
||||
|
||||
## Network Graph Template
|
||||
|
||||
```javascript
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id))
|
||||
.force('charge', d3.forceManyBody().strength(-100))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
|
||||
const link = svg.selectAll('line')
|
||||
.data(links)
|
||||
.join('line')
|
||||
.attr('stroke', '#999');
|
||||
|
||||
const node = svg.selectAll('circle')
|
||||
.data(nodes)
|
||||
.join('circle')
|
||||
.attr('r', 10)
|
||||
.attr('fill', d => d3.schemeCategory10[d.group]);
|
||||
|
||||
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);
|
||||
});
|
||||
```
|
||||
|
||||
## Treemap Template
|
||||
|
||||
```javascript
|
||||
const root = d3.hierarchy(data)
|
||||
.sum(d => d.value)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const treemap = d3.treemap().size([width, height]).padding(1);
|
||||
treemap(root);
|
||||
|
||||
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 => d3.interpolateBlues(d.value / root.value));
|
||||
|
||||
svg.selectAll('text')
|
||||
.data(root.leaves())
|
||||
.join('text')
|
||||
.attr('x', d => d.x0 + 5)
|
||||
.attr('y', d => d.y0 + 15)
|
||||
.text(d => d.data.name);
|
||||
```
|
||||
|
||||
## Geographic Map Template
|
||||
|
||||
```javascript
|
||||
d3.json('world.geojson').then(geojson => {
|
||||
const projection = d3.geoMercator()
|
||||
.fitExtent([[0, 0], [width, height]], geojson);
|
||||
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
svg.selectAll('path')
|
||||
.data(geojson.features)
|
||||
.join('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', '#ccc')
|
||||
.attr('stroke', '#fff');
|
||||
});
|
||||
```
|
||||
|
||||
## Zoom and Pan Pattern
|
||||
|
||||
```javascript
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.5, 5])
|
||||
.on('zoom', (event) => g.attr('transform', event.transform));
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
const g = svg.append('g');
|
||||
g.selectAll('circle').data(data).join('circle')...
|
||||
```
|
||||
|
||||
## Brush Selection Pattern
|
||||
|
||||
```javascript
|
||||
const brush = d3.brush()
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on('end', (event) => {
|
||||
if (!event.selection) return;
|
||||
const [[x0, y0], [x1, y1]] = event.selection;
|
||||
const selected = data.filter(d => {
|
||||
const x = xScale(d.x), y = yScale(d.y);
|
||||
return x >= x0 && x <= x1 && y >= y0 && y <= y1;
|
||||
});
|
||||
updateChart(selected);
|
||||
});
|
||||
|
||||
svg.append('g').attr('class', 'brush').call(brush);
|
||||
```
|
||||
|
||||
## Transition Pattern
|
||||
|
||||
```javascript
|
||||
function update(newData) {
|
||||
yScale.domain([0, d3.max(newData, d => d.value)]);
|
||||
|
||||
svg.selectAll('rect')
|
||||
.data(newData)
|
||||
.join('rect')
|
||||
.transition().duration(750).ease(d3.easeCubicOut)
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('height', d => height - yScale(d.value));
|
||||
|
||||
svg.select('.y-axis').transition().duration(750).call(d3.axisLeft(yScale));
|
||||
}
|
||||
```
|
||||
|
||||
## Tooltip Pattern
|
||||
|
||||
```javascript
|
||||
const tooltip = d3.select('body').append('div')
|
||||
.attr('class', 'tooltip')
|
||||
.style('position', 'absolute')
|
||||
.style('visibility', 'hidden');
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('r', 5)
|
||||
.on('mouseover', (event, d) => {
|
||||
tooltip.style('visibility', 'visible').html(`Value: ${d.value}`);
|
||||
})
|
||||
.on('mousemove', (event) => {
|
||||
tooltip.style('top', (event.pageY - 10) + 'px')
|
||||
.style('left', (event.pageX + 10) + 'px');
|
||||
})
|
||||
.on('mouseout', () => tooltip.style('visibility', 'hidden'));
|
||||
```
|
||||
87
skills/d3-visualization/resources/evaluation-rubric.json
Normal file
87
skills/d3-visualization/resources/evaluation-rubric.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"skill_name": "d3-visualization",
|
||||
"version": "1.0",
|
||||
"threshold": 3.5,
|
||||
"criteria": [
|
||||
{
|
||||
"name": "Completeness",
|
||||
"weight": 1.5,
|
||||
"description": "Covers full D3 workflow from setup to advanced layouts",
|
||||
"levels": {
|
||||
"5": "Complete coverage: selections, data joins, scales, shapes, layouts, transitions, interactions with examples",
|
||||
"3": "Core concepts covered but missing advanced topics (force layouts, geographic maps) or practical examples",
|
||||
"1": "Incomplete: missing critical concepts like scales or data joins"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Clarity",
|
||||
"weight": 1.4,
|
||||
"description": "Explanations are clear with WHY sections and code examples",
|
||||
"levels": {
|
||||
"5": "Every concept has WHY explanation, clear examples, and common pitfalls documented",
|
||||
"3": "Most concepts explained but some lack context or examples",
|
||||
"1": "Unclear explanations, missing examples, jargon without definitions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Actionability",
|
||||
"weight": 1.5,
|
||||
"description": "Provides copy-paste templates and step-by-step workflows",
|
||||
"levels": {
|
||||
"5": "Complete code templates for all common charts, step-by-step workflows with checklists",
|
||||
"3": "Some templates and workflows but incomplete or missing key patterns",
|
||||
"1": "Theoretical only, no actionable code or workflows"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Structure",
|
||||
"weight": 1.3,
|
||||
"description": "Organized with clear navigation and progressive learning path",
|
||||
"levels": {
|
||||
"5": "Interactive hub, clear workflows, logical resource organization, all content linked",
|
||||
"3": "Organized but navigation unclear or some content orphaned",
|
||||
"1": "Disorganized, hard to navigate, no clear entry points"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Triggers",
|
||||
"weight": 1.4,
|
||||
"description": "YAML description clearly defines WHEN to use skill",
|
||||
"levels": {
|
||||
"5": "Trigger-focused description with specific use cases, anti-triggers documented",
|
||||
"3": "Some triggers mentioned but vague or missing anti-triggers",
|
||||
"1": "Generic description, unclear when to use"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Resource Quality",
|
||||
"weight": 1.3,
|
||||
"description": "Resources are self-contained, under 500 lines, with WHY/WHAT structure",
|
||||
"levels": {
|
||||
"5": "All resources <500 lines, WHY sections present, no orphaned content",
|
||||
"3": "Some resources over limit or missing WHY sections",
|
||||
"1": "Resources over 500 lines, missing structure"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "User Collaboration",
|
||||
"weight": 1.2,
|
||||
"description": "Skill creation involved user at decision points",
|
||||
"levels": {
|
||||
"5": "User approved at each major step, validated structure and scope",
|
||||
"3": "Some user input but limited collaboration",
|
||||
"1": "No user collaboration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "File Size",
|
||||
"weight": 1.4,
|
||||
"description": "All files under size limits",
|
||||
"levels": {
|
||||
"5": "SKILL.md <500 lines, all resources <500 lines",
|
||||
"3": "Some files over limits",
|
||||
"1": "Multiple files significantly over limits"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
496
skills/d3-visualization/resources/getting-started.md
Normal file
496
skills/d3-visualization/resources/getting-started.md
Normal file
@@ -0,0 +1,496 @@
|
||||
# Getting Started with D3.js
|
||||
|
||||
## Why Learn D3
|
||||
|
||||
**Problem D3 Solves**: Chart libraries (Chart.js, Highcharts, Plotly) offer pre-made chart types with limited customization. When you need bespoke visualizations—unusual chart types, specific interactions, custom layouts—you need low-level control. D3 provides primitive building blocks that compose into any visualization.
|
||||
|
||||
**Trade-off**: Steeper learning curve and more code than chart libraries, but unlimited customization and flexibility.
|
||||
|
||||
**When D3 is the Right Choice**:
|
||||
- Custom visualization designs not available in libraries
|
||||
- Complex interactions (linked views, custom brushing, coordinated highlighting)
|
||||
- Network graphs, force-directed layouts, geographic maps
|
||||
- Full control over visual encoding, transitions, animations
|
||||
- Integration with data pipelines for real-time dashboards
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
D3 assumes you know these web technologies:
|
||||
|
||||
**Required**:
|
||||
- **HTML**: Document structure, elements, attributes
|
||||
- **SVG**: Scalable Vector Graphics (`<svg>`, `<circle>`, `<rect>`, `<path>`, `viewBox`, transforms)
|
||||
- **CSS**: Styling, selectors, specificity
|
||||
- **JavaScript**: ES6 syntax, functions, arrays, objects, arrow functions, promises, async/await
|
||||
|
||||
**Helpful**:
|
||||
- Modern frameworks (React, Vue) for integration patterns
|
||||
- Data formats (CSV, JSON, GeoJSON)
|
||||
- Basic statistics (distributions, correlations) for visualization design
|
||||
|
||||
**If you lack prerequisites**: Learn HTML/SVG/CSS/JavaScript fundamentals first. D3 documentation assumes this knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### Option 1: CodePen (Recommended for Learning)
|
||||
|
||||
**Quick start without tooling**:
|
||||
|
||||
1. **Create HTML file** (`index.html`):
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>D3 Visualization</title>
|
||||
<style>
|
||||
svg { border: 1px solid #ccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg width="600" height="400"></svg>
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create JavaScript file** (`index.js`):
|
||||
```javascript
|
||||
// Import from Skypack CDN (ESM)
|
||||
import * as d3 from 'https://cdn.skypack.dev/d3@7';
|
||||
|
||||
// Your D3 code here
|
||||
console.log(d3.version);
|
||||
```
|
||||
|
||||
3. **Open HTML in browser** or use Live Server extension in VS Code
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Bundler (Vite, Webpack)
|
||||
|
||||
**For production apps**:
|
||||
|
||||
1. **Install D3**:
|
||||
```bash
|
||||
npm install d3
|
||||
```
|
||||
|
||||
2. **Import modules**:
|
||||
```javascript
|
||||
// Import specific modules (recommended - smaller bundle)
|
||||
import { select, selectAll } from 'd3-selection';
|
||||
import { scaleLinear, scaleBand } from 'd3-scale';
|
||||
import { axisBottom, axisLeft } from 'd3-axis';
|
||||
|
||||
// Or import entire D3 namespace
|
||||
import * as d3 from 'd3';
|
||||
```
|
||||
|
||||
3. **Use in your code**:
|
||||
```javascript
|
||||
const svg = d3.select('svg');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 3: UMD Script Tag (Legacy)
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
// d3 available globally
|
||||
const svg = d3.select('svg');
|
||||
</script>
|
||||
```
|
||||
|
||||
**Note**: Modern approach uses ES modules (options 1-2).
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Concept 1: Selections
|
||||
|
||||
**What**: D3 selections are wrappers around DOM elements enabling method chaining for manipulation.
|
||||
|
||||
**Why**: Declarative syntax; modify multiple elements concisely.
|
||||
|
||||
**Basic Pattern**:
|
||||
```javascript
|
||||
// Select single element
|
||||
const svg = d3.select('svg');
|
||||
|
||||
// Select all matching elements
|
||||
const circles = d3.selectAll('circle');
|
||||
|
||||
// Modify attributes
|
||||
circles.attr('r', 10).attr('fill', 'steelblue');
|
||||
|
||||
// Add elements
|
||||
svg.append('circle').attr('cx', 50).attr('cy', 50).attr('r', 20);
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
- `.select(selector)`: First matching element
|
||||
- `.selectAll(selector)`: All matching elements
|
||||
- `.attr(name, value)`: Set attribute
|
||||
- `.style(name, value)`: Set CSS style
|
||||
- `.text(value)`: Set text content
|
||||
- `.append(type)`: Create and append child
|
||||
- `.remove()`: Delete elements
|
||||
|
||||
---
|
||||
|
||||
### Concept 2: Data Joins
|
||||
|
||||
**What**: Binding data arrays to DOM elements, establishing one-to-one correspondence.
|
||||
|
||||
**Why**: Enables data-driven visualizations; DOM automatically reflects data changes.
|
||||
|
||||
**Pattern**:
|
||||
```javascript
|
||||
const data = [10, 20, 30, 40, 50];
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(data) // Bind array to selection
|
||||
.join('circle') // Create/update/remove elements to match data
|
||||
.attr('cx', (d, i) => i * 50 + 25) // d = datum, i = index
|
||||
.attr('cy', 50)
|
||||
.attr('r', d => d); // Radius = data value
|
||||
```
|
||||
|
||||
**What `.join()` does**:
|
||||
- **Enter**: Creates new elements for array items without elements
|
||||
- **Update**: Updates existing elements
|
||||
- **Exit**: Removes elements without corresponding array items
|
||||
|
||||
---
|
||||
|
||||
### Concept 3: Scales
|
||||
|
||||
**What**: Functions mapping data domain (input range) to visual range (output values).
|
||||
|
||||
**Why**: Data values (e.g., 0-1000) don't directly map to pixels. Scales handle transformation.
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
const data = [{x: 0}, {x: 50}, {x: 100}];
|
||||
|
||||
// Create scale
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, 100]) // Data min/max
|
||||
.range([0, 500]); // Pixel min/max
|
||||
|
||||
// Use scale
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('cx', d => xScale(d.x)); // 0→0px, 50→250px, 100→500px
|
||||
```
|
||||
|
||||
**Common scales**:
|
||||
- `scaleLinear()`: Quantitative, proportional
|
||||
- `scaleBand()`: Categorical, for bars
|
||||
- `scaleTime()`: Temporal data
|
||||
- `scaleOrdinal()`: Categorical → categories (colors)
|
||||
|
||||
---
|
||||
|
||||
### Concept 4: Method Chaining
|
||||
|
||||
**What**: D3 methods return the selection, enabling sequential calls.
|
||||
|
||||
**Why**: Concise, readable code that mirrors workflow steps.
|
||||
|
||||
**Example**:
|
||||
```javascript
|
||||
d3.select('svg')
|
||||
.append('g')
|
||||
.attr('transform', 'translate(50, 50)')
|
||||
.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', (d, i) => i * 40)
|
||||
.attr('y', d => height - d.value)
|
||||
.attr('width', 30)
|
||||
.attr('height', d => d.value)
|
||||
.attr('fill', 'steelblue');
|
||||
```
|
||||
|
||||
Reads like: "Select SVG → append group → position group → select rects → bind data → create rects → set attributes"
|
||||
|
||||
---
|
||||
|
||||
## First Visualization: Simple Bar Chart
|
||||
|
||||
### Goal
|
||||
Create a vertical bar chart from array `[30, 80, 45, 60, 20, 90, 50]`.
|
||||
|
||||
### Setup SVG Container
|
||||
|
||||
```javascript
|
||||
// Dimensions
|
||||
const width = 500;
|
||||
const height = 300;
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 40};
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
|
||||
// Create SVG
|
||||
const svg = d3.select('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
// Create inner group for margins
|
||||
const g = svg.append('g')
|
||||
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
||||
```
|
||||
|
||||
**Why margins?** Leave space for axes outside the data area.
|
||||
|
||||
---
|
||||
|
||||
### Create Data and Scales
|
||||
|
||||
```javascript
|
||||
// Data
|
||||
const data = [30, 80, 45, 60, 20, 90, 50];
|
||||
|
||||
// X scale (categorical - bar positions)
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(d3.range(data.length)) // [0, 1, 2, 3, 4, 5, 6]
|
||||
.range([0, innerWidth])
|
||||
.padding(0.1); // 10% spacing between bars
|
||||
|
||||
// Y scale (quantitative - bar heights)
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(data)]) // 0 to 90
|
||||
.range([innerHeight, 0]); // Inverted (SVG y increases downward)
|
||||
```
|
||||
|
||||
**Why inverted y-range?** SVG y-axis increases downward; we want high values at top.
|
||||
|
||||
---
|
||||
|
||||
### Create Bars
|
||||
|
||||
```javascript
|
||||
g.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', (d, i) => xScale(i))
|
||||
.attr('y', d => yScale(d))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => innerHeight - yScale(d))
|
||||
.attr('fill', 'steelblue');
|
||||
```
|
||||
|
||||
**Breakdown**:
|
||||
- `x`: Bar position from xScale
|
||||
- `y`: Top of bar from yScale
|
||||
- `width`: Automatic from scaleBand
|
||||
- `height`: Distance from bar top to bottom (innerHeight - y)
|
||||
|
||||
---
|
||||
|
||||
### Add Axes
|
||||
|
||||
```javascript
|
||||
// X axis
|
||||
g.append('g')
|
||||
.attr('transform', `translate(0, ${innerHeight})`)
|
||||
.call(d3.axisBottom(xScale));
|
||||
|
||||
// Y axis
|
||||
g.append('g')
|
||||
.call(d3.axisLeft(yScale));
|
||||
```
|
||||
|
||||
**What `.call()` does**: Invokes function with selection as argument. `d3.axisBottom(xScale)` is a function that generates axis.
|
||||
|
||||
---
|
||||
|
||||
### Complete Code
|
||||
|
||||
```javascript
|
||||
import * as d3 from 'https://cdn.skypack.dev/d3@7';
|
||||
|
||||
const width = 500, height = 300;
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 40};
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
|
||||
const data = [30, 80, 45, 60, 20, 90, 50];
|
||||
|
||||
const svg = d3.select('svg').attr('width', width).attr('height', height);
|
||||
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
|
||||
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(d3.range(data.length))
|
||||
.range([0, innerWidth])
|
||||
.padding(0.1);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(data)])
|
||||
.range([innerHeight, 0]);
|
||||
|
||||
g.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', (d, i) => xScale(i))
|
||||
.attr('y', d => yScale(d))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => innerHeight - yScale(d))
|
||||
.attr('fill', 'steelblue');
|
||||
|
||||
g.append('g').attr('transform', `translate(0, ${innerHeight})`).call(d3.axisBottom(xScale));
|
||||
g.append('g').call(d3.axisLeft(yScale));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading Data
|
||||
|
||||
### CSV Files
|
||||
|
||||
**File**: `data.csv`
|
||||
```
|
||||
name,value
|
||||
A,30
|
||||
B,80
|
||||
C,45
|
||||
```
|
||||
|
||||
**Load and Parse**:
|
||||
```javascript
|
||||
d3.csv('data.csv').then(data => {
|
||||
// data = [{name: 'A', value: '30'}, ...] (values are strings!)
|
||||
|
||||
// Convert strings to numbers
|
||||
data.forEach(d => { d.value = +d.value; });
|
||||
|
||||
// Or use conversion function
|
||||
d3.csv('data.csv', d => ({
|
||||
name: d.name,
|
||||
value: +d.value
|
||||
})).then(data => {
|
||||
// Now d.value is number
|
||||
createVisualization(data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Returns Promise
|
||||
- Values are strings by default (CSV has no types)
|
||||
- Use `+` operator or `parseFloat()` for numbers
|
||||
- Use `d3.timeParse()` for dates
|
||||
|
||||
---
|
||||
|
||||
### JSON Files
|
||||
|
||||
**File**: `data.json`
|
||||
```json
|
||||
[
|
||||
{"name": "A", "value": 30},
|
||||
{"name": "B", "value": 80}
|
||||
]
|
||||
```
|
||||
|
||||
**Load**:
|
||||
```javascript
|
||||
d3.json('data.json').then(data => {
|
||||
// data already has correct types
|
||||
createVisualization(data);
|
||||
});
|
||||
```
|
||||
|
||||
**Advantage**: Types preserved (numbers are numbers, not strings).
|
||||
|
||||
---
|
||||
|
||||
### Async/Await Pattern
|
||||
|
||||
```javascript
|
||||
async function init() {
|
||||
const data = await d3.csv('data.csv', d => ({
|
||||
name: d.name,
|
||||
value: +d.value
|
||||
}));
|
||||
|
||||
createVisualization(data);
|
||||
}
|
||||
|
||||
init();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Forgetting Data Type Conversion
|
||||
|
||||
```javascript
|
||||
// WRONG - CSV values are strings
|
||||
d3.csv('data.csv').then(data => {
|
||||
const max = d3.max(data, d => d.value); // String comparison! '9' > '80'
|
||||
});
|
||||
|
||||
// CORRECT
|
||||
d3.csv('data.csv').then(data => {
|
||||
data.forEach(d => { d.value = +d.value; });
|
||||
const max = d3.max(data, d => d.value); // Numeric comparison
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Inverted Y-Axis
|
||||
|
||||
```javascript
|
||||
// WRONG - bars upside down
|
||||
const yScale = scaleLinear().domain([0, 100]).range([0, height]);
|
||||
|
||||
// CORRECT - high values at top
|
||||
const yScale = scaleLinear().domain([0, 100]).range([height, 0]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Missing Margins
|
||||
|
||||
```javascript
|
||||
// WRONG - axes overlap chart
|
||||
svg.selectAll('rect').attr('x', d => xScale(d.x))...
|
||||
|
||||
// CORRECT - use margin convention
|
||||
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
|
||||
g.selectAll('rect').attr('x', d => xScale(d.x))...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Not Using .call() for Axes
|
||||
|
||||
```javascript
|
||||
// WRONG - won't work
|
||||
g.append('g').axisBottom(xScale);
|
||||
|
||||
// CORRECT
|
||||
g.append('g').call(d3.axisBottom(xScale));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Understand data joins**: [Selections & Data Joins](selections-datajoins.md)
|
||||
- **Master scales**: [Scales & Axes](scales-axes.md)
|
||||
- **Build more charts**: [Workflows](workflows.md)
|
||||
- **Add interactions**: [Transitions & Interactions](transitions-interactions.md)
|
||||
497
skills/d3-visualization/resources/scales-axes.md
Normal file
497
skills/d3-visualization/resources/scales-axes.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# Scales & Axes
|
||||
|
||||
## Why Scales Matter
|
||||
|
||||
**The Problem**: Data values (0-1000, dates, categories) don't directly map to visual properties (pixels 0-500, colors, positions). Hardcoding transformations (`x = value * 0.5`) breaks when data changes.
|
||||
|
||||
**D3's Solution**: Scale functions encapsulate domain-to-range mapping. Change data → update domain → visualization adapts automatically.
|
||||
|
||||
**Key Principle**: "Separate data space from visual space." Scales are the bridge.
|
||||
|
||||
---
|
||||
|
||||
## Scale Fundamentals
|
||||
|
||||
### Domain and Range
|
||||
|
||||
```javascript
|
||||
const scale = d3.scaleLinear()
|
||||
.domain([0, 100]) // Data min/max (input)
|
||||
.range([0, 500]); // Visual min/max (output)
|
||||
|
||||
scale(0); // → 0px
|
||||
scale(50); // → 250px
|
||||
scale(100); // → 500px
|
||||
```
|
||||
|
||||
**Domain**: Input data extent
|
||||
**Range**: Output visual extent
|
||||
**Scale**: Function mapping domain → range
|
||||
|
||||
---
|
||||
|
||||
## Scale Types
|
||||
|
||||
### Continuous → Continuous
|
||||
|
||||
#### scaleLinear
|
||||
|
||||
**Use**: Quantitative data, proportional relationships
|
||||
|
||||
```javascript
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(data, d => d.value)])
|
||||
.range([0, width]);
|
||||
```
|
||||
|
||||
**Math**: Linear interpolation `y = mx + b`
|
||||
|
||||
---
|
||||
|
||||
#### scaleSqrt
|
||||
|
||||
**Use**: Sizing circles by area (not radius)
|
||||
|
||||
```javascript
|
||||
const radiusScale = d3.scaleSqrt()
|
||||
.domain([0, 1000000]) // Population
|
||||
.range([0, 50]); // Max radius
|
||||
|
||||
// Circle area ∝ population (perceptually accurate)
|
||||
```
|
||||
|
||||
**Why**: `Area = πr²`, so `r = √(Area)`. Sqrt scale makes area proportional to data.
|
||||
|
||||
---
|
||||
|
||||
#### scalePow
|
||||
|
||||
**Use**: Custom exponents
|
||||
|
||||
```javascript
|
||||
const scale = d3.scalePow()
|
||||
.exponent(2) // Quadratic
|
||||
.domain([0, 100])
|
||||
.range([0, 500]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### scaleLog
|
||||
|
||||
**Use**: Exponential data (orders of magnitude)
|
||||
|
||||
```javascript
|
||||
const scale = d3.scaleLog()
|
||||
.domain([1, 1000]) // Don't include 0!
|
||||
.range([0, 500]);
|
||||
|
||||
scale(1); // → 0
|
||||
scale(10); // → 167
|
||||
scale(100); // → 333
|
||||
scale(1000); // → 500
|
||||
```
|
||||
|
||||
**Note**: Log undefined at 0. Domain must be positive.
|
||||
|
||||
---
|
||||
|
||||
#### scaleTime
|
||||
|
||||
**Use**: Temporal data
|
||||
|
||||
```javascript
|
||||
const xScale = d3.scaleTime()
|
||||
.domain([new Date(2020, 0, 1), new Date(2020, 11, 31)])
|
||||
.range([0, width]);
|
||||
```
|
||||
|
||||
**Works with**: Date objects, timestamps
|
||||
|
||||
---
|
||||
|
||||
#### scaleSequential
|
||||
|
||||
**Use**: Continuous data → color gradients
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleSequential(d3.interpolateBlues)
|
||||
.domain([0, 100]);
|
||||
|
||||
colorScale(0); // → light blue
|
||||
colorScale(50); // → medium blue
|
||||
colorScale(100); // → dark blue
|
||||
```
|
||||
|
||||
**Interpolators**: `interpolateBlues`, `interpolateReds`, `interpolateViridis`, `interpolateRainbow` (avoid rainbow!)
|
||||
|
||||
---
|
||||
|
||||
### Continuous → Discrete
|
||||
|
||||
#### scaleQuantize
|
||||
|
||||
**Use**: Divide continuous domain into discrete bins
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleQuantize()
|
||||
.domain([0, 100])
|
||||
.range(['green', 'yellow', 'orange', 'red']);
|
||||
|
||||
colorScale(20); // → 'green' (0-25)
|
||||
colorScale(40); // → 'yellow' (25-50)
|
||||
colorScale(75); // → 'orange' (50-75)
|
||||
colorScale(95); // → 'red' (75-100)
|
||||
```
|
||||
|
||||
**Equal bins**: Domain divided evenly by range length.
|
||||
|
||||
---
|
||||
|
||||
#### scaleQuantile
|
||||
|
||||
**Use**: Divide data into quantiles (equal-sized groups)
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleQuantile()
|
||||
.domain(data.map(d => d.value)) // Actual data values
|
||||
.range(['green', 'yellow', 'orange', 'red']);
|
||||
|
||||
// Quartiles: 25% of data in each color
|
||||
```
|
||||
|
||||
**Difference from quantize**: Bins have equal data counts, not equal domain width.
|
||||
|
||||
---
|
||||
|
||||
#### scaleThreshold
|
||||
|
||||
**Use**: Explicit breakpoints
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleThreshold()
|
||||
.domain([40, 60, 80]) // Split points
|
||||
.range(['green', 'yellow', 'orange', 'red']);
|
||||
|
||||
colorScale(30); // → 'green' (<40)
|
||||
colorScale(50); // → 'yellow' (40-60)
|
||||
colorScale(70); // → 'orange' (60-80)
|
||||
colorScale(90); // → 'red' (≥80)
|
||||
```
|
||||
|
||||
**Use for**: Letter grades, risk levels, custom categories.
|
||||
|
||||
---
|
||||
|
||||
### Discrete → Discrete
|
||||
|
||||
#### scaleOrdinal
|
||||
|
||||
**Use**: Map categories to categories (often colors)
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleOrdinal()
|
||||
.domain(['A', 'B', 'C'])
|
||||
.range(['red', 'green', 'blue']);
|
||||
|
||||
colorScale('A'); // → 'red'
|
||||
colorScale('B'); // → 'green'
|
||||
```
|
||||
|
||||
**Built-in schemes**: `d3.schemeCategory10`, `d3.schemeTableau10`
|
||||
|
||||
```javascript
|
||||
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### scaleBand
|
||||
|
||||
**Use**: Position categorical bars
|
||||
|
||||
```javascript
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(['A', 'B', 'C', 'D'])
|
||||
.range([0, 500])
|
||||
.padding(0.1); // 10% spacing
|
||||
|
||||
xScale('A'); // → 0
|
||||
xScale('B'); // → 125
|
||||
xScale.bandwidth(); // → 112.5 (width of each band)
|
||||
```
|
||||
|
||||
**Key method**: `.bandwidth()` returns width for bars/columns.
|
||||
|
||||
---
|
||||
|
||||
#### scalePoint
|
||||
|
||||
**Use**: Position categorical points (scatter, line chart categories)
|
||||
|
||||
```javascript
|
||||
const xScale = d3.scalePoint()
|
||||
.domain(['A', 'B', 'C', 'D'])
|
||||
.range([0, 500])
|
||||
.padding(0.5);
|
||||
|
||||
xScale('A'); // → 50 (centered in first position)
|
||||
xScale('B'); // → 200
|
||||
```
|
||||
|
||||
**Difference from band**: Points (no width), bands (width for bars).
|
||||
|
||||
---
|
||||
|
||||
## Scale Selection Guide
|
||||
|
||||
**Quantitative linear**: scaleLinear | **Exponential**: scaleLog | **Circle sizing**: scaleSqrt
|
||||
**Temporal**: scaleTime | **Categorical bars**: scaleBand | **Categorical points**: scalePoint
|
||||
**Categorical colors**: scaleOrdinal | **Bins (equal domain)**: scaleQuantize | **Bins (equal data)**: scaleQuantile
|
||||
**Custom breakpoints**: scaleThreshold | **Color gradient**: scaleSequential
|
||||
|
||||
---
|
||||
|
||||
## Scale Methods
|
||||
|
||||
### Domain from Data
|
||||
|
||||
```javascript
|
||||
// Extent (min, max)
|
||||
const xScale = scaleLinear()
|
||||
.domain(d3.extent(data, d => d.x)) // [min, max]
|
||||
.range([0, width]);
|
||||
|
||||
// Max only (0 to max)
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, d3.max(data, d => d.y)])
|
||||
.range([height, 0]);
|
||||
|
||||
// Custom
|
||||
const yScale = scaleLinear()
|
||||
.domain([-100, 100]) // Symmetric around 0
|
||||
.range([height, 0]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Inversion
|
||||
|
||||
```javascript
|
||||
const xScale = scaleLinear()
|
||||
.domain([0, 100])
|
||||
.range([0, 500]);
|
||||
|
||||
xScale(50); // → 250 (data → visual)
|
||||
xScale.invert(250); // → 50 (visual → data)
|
||||
```
|
||||
|
||||
**Use**: Convert mouse position to data value.
|
||||
|
||||
---
|
||||
|
||||
### Clamping
|
||||
|
||||
```javascript
|
||||
const scale = scaleLinear()
|
||||
.domain([0, 100])
|
||||
.range([0, 500])
|
||||
.clamp(true);
|
||||
|
||||
scale(-10); // → 0 (clamped to range min)
|
||||
scale(150); // → 500 (clamped to range max)
|
||||
```
|
||||
|
||||
**Without clamp**: `scale(150)` → 750 (extrapolates beyond range).
|
||||
|
||||
---
|
||||
|
||||
### Nice Domains
|
||||
|
||||
```javascript
|
||||
const yScale = scaleLinear()
|
||||
.domain([0.201, 0.967])
|
||||
.range([height, 0])
|
||||
.nice();
|
||||
|
||||
yScale.domain(); // → [0.2, 1.0] (rounded)
|
||||
```
|
||||
|
||||
**Use**: Clean axis labels (0.2, 0.4, 0.6 instead of 0.201, 0.401...).
|
||||
|
||||
---
|
||||
|
||||
## Axes
|
||||
|
||||
### Creating Axes
|
||||
|
||||
```javascript
|
||||
// Scales first
|
||||
const xScale = scaleBand().domain(categories).range([0, width]);
|
||||
const yScale = scaleLinear().domain([0, max]).range([height, 0]);
|
||||
|
||||
// Axis generators
|
||||
const xAxis = d3.axisBottom(xScale);
|
||||
const yAxis = d3.axisLeft(yScale);
|
||||
|
||||
// Render axes
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0, ${height})`) // Position at bottom
|
||||
.call(xAxis);
|
||||
|
||||
svg.append('g')
|
||||
.call(yAxis);
|
||||
```
|
||||
|
||||
**Axis orientations**:
|
||||
- `axisBottom`: Ticks below, labels below
|
||||
- `axisTop`: Ticks above, labels above
|
||||
- `axisLeft`: Ticks left, labels left
|
||||
- `axisRight`: Ticks right, labels right
|
||||
|
||||
---
|
||||
|
||||
### Customizing Ticks
|
||||
|
||||
```javascript
|
||||
// Number of ticks (approximate)
|
||||
yAxis.ticks(5); // D3 chooses ~5 "nice" values
|
||||
|
||||
// Explicit tick values
|
||||
xAxis.tickValues([0, 25, 50, 75, 100]);
|
||||
|
||||
// Tick format
|
||||
yAxis.tickFormat(d => d + '%'); // Add percent sign
|
||||
yAxis.tickFormat(d3.format('.2f')); // 2 decimal places
|
||||
yAxis.tickFormat(d3.timeFormat('%b')); // Month abbreviations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tick Styling
|
||||
|
||||
```javascript
|
||||
yAxis.tickSize(10).tickPadding(5); // Length and spacing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Updating Axes
|
||||
|
||||
```javascript
|
||||
function update(newData) {
|
||||
// Update scale domain
|
||||
yScale.domain([0, d3.max(newData, d => d.value)]);
|
||||
|
||||
// Update axis with transition
|
||||
svg.select('.y-axis')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3.axisLeft(yScale));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Color Scales
|
||||
|
||||
### Categorical Colors
|
||||
|
||||
```javascript
|
||||
const color = d3.scaleOrdinal(d3.schemeCategory10); // Built-in scheme
|
||||
const color = d3.scaleOrdinal().domain(['low', 'medium', 'high']).range(['green', 'yellow', 'red']); // Custom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sequential & Diverging Colors
|
||||
|
||||
```javascript
|
||||
// Sequential
|
||||
const color = d3.scaleSequential(d3.interpolateBlues).domain([0, 100]);
|
||||
// Interpolators: Blues, Reds, Greens, Viridis, Plasma, Inferno (avoid Rainbow!)
|
||||
|
||||
// Diverging
|
||||
const color = d3.scaleDiverging(d3.interpolateRdYlGn).domain([-100, 0, 100]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
```javascript
|
||||
// Responsive: update range on resize
|
||||
const xScale = scaleLinear().domain([0, 100]).range([0, width]);
|
||||
|
||||
// Multi-scale chart
|
||||
const xScale = scaleTime().domain(dateExtent).range([0, width]);
|
||||
const colorScale = scaleOrdinal(schemeCategory10);
|
||||
const sizeScale = scaleSqrt().domain([0, maxPop]).range([0, 50]);
|
||||
|
||||
// Symmetric domain
|
||||
const max = d3.max(data, d => Math.abs(d.value));
|
||||
const yScale = scaleLinear().domain([-max, max]).range([height, 0]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Forgetting to Invert Y Range
|
||||
|
||||
```javascript
|
||||
// WRONG - high values at bottom
|
||||
const yScale = scaleLinear().domain([0, 100]).range([0, height]);
|
||||
|
||||
// CORRECT - high values at top
|
||||
const yScale = scaleLinear().domain([0, 100]).range([height, 0]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Log Scale with Zero
|
||||
|
||||
```javascript
|
||||
// WRONG - log(0) undefined
|
||||
const scale = scaleLog().domain([0, 1000]);
|
||||
|
||||
// CORRECT - start at small positive number
|
||||
const scale = scaleLog().domain([1, 1000]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Not Updating Domain
|
||||
|
||||
```javascript
|
||||
// WRONG - scale domain never changes
|
||||
const yScale = scaleLinear().domain([0, 100]).range([height, 0]);
|
||||
update(newData); // New max might be 200!
|
||||
|
||||
// CORRECT
|
||||
function update(newData) {
|
||||
yScale.domain([0, d3.max(newData, d => d.value)]);
|
||||
// Re-render with updated scale
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Using Band Scale for Quantitative Data
|
||||
|
||||
```javascript
|
||||
// WRONG
|
||||
const xScale = scaleBand().domain([0, 10, 20, 30]); // Numbers!
|
||||
|
||||
// CORRECT - use linear for numbers
|
||||
const xScale = scaleLinear().domain([0, 30]).range([0, width]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Use scales in charts: [Workflows](workflows.md)
|
||||
- Combine with shape generators: [Shapes & Layouts](shapes-layouts.md)
|
||||
- Add to interactive charts: [Transitions & Interactions](transitions-interactions.md)
|
||||
494
skills/d3-visualization/resources/selections-datajoins.md
Normal file
494
skills/d3-visualization/resources/selections-datajoins.md
Normal file
@@ -0,0 +1,494 @@
|
||||
# Selections & Data Joins
|
||||
|
||||
## Why Data Joins Matter
|
||||
|
||||
**The Problem**: Manually creating, updating, and removing DOM elements for dynamic data is error-prone. You must track which elements correspond to which data items, handle array length changes, and avoid orphaned elements.
|
||||
|
||||
**D3's Solution**: Data joins establish declarative one-to-one correspondence between data arrays and DOM elements. Specify desired end state; D3 handles lifecycle automatically.
|
||||
|
||||
**Key Principle**: "Join data to elements, then manipulate elements using data." Changes to data propagate to visuals through re-joining.
|
||||
|
||||
---
|
||||
|
||||
## Selections Fundamentals
|
||||
|
||||
### Creating Selections
|
||||
|
||||
```javascript
|
||||
// Select first matching element
|
||||
const svg = d3.select('svg');
|
||||
const firstCircle = d3.select('circle');
|
||||
|
||||
// Select all matching elements
|
||||
const allCircles = d3.selectAll('circle');
|
||||
const allRects = d3.selectAll('rect');
|
||||
|
||||
// CSS selectors supported
|
||||
const blueCircles = d3.selectAll('circle.blue');
|
||||
const chartGroup = d3.select('#chart');
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
- `select(selector)`: Returns selection with first match
|
||||
- `selectAll(selector)`: Returns selection with all matches
|
||||
- Empty selection if no matches (not null)
|
||||
|
||||
---
|
||||
|
||||
### Modifying Elements
|
||||
|
||||
```javascript
|
||||
// Set attributes
|
||||
selection.attr('r', 10); // Set radius to 10
|
||||
selection.attr('fill', 'steelblue'); // Set fill color
|
||||
|
||||
// Set styles
|
||||
selection.style('opacity', 0.7);
|
||||
selection.style('stroke-width', '2px');
|
||||
|
||||
// Set classes
|
||||
selection.classed('active', true); // Add class
|
||||
selection.classed('hidden', false); // Remove class
|
||||
|
||||
// Set text
|
||||
selection.text('Label');
|
||||
|
||||
// Set HTML
|
||||
selection.html('<strong>Bold</strong>');
|
||||
|
||||
// Set properties (DOM properties, not attributes)
|
||||
selection.property('checked', true); // For checkboxes, etc.
|
||||
```
|
||||
|
||||
**Attribute vs Style**:
|
||||
- Use `.attr()` for SVG attributes: `x`, `y`, `r`, `fill`, `stroke`
|
||||
- Use `.style()` for CSS properties: `opacity`, `font-size`, `color`
|
||||
|
||||
---
|
||||
|
||||
### Creating and Removing Elements
|
||||
|
||||
```javascript
|
||||
// Append child element
|
||||
const g = svg.append('g'); // Returns new selection
|
||||
g.attr('transform', 'translate(50, 50)');
|
||||
|
||||
// Insert before sibling
|
||||
svg.insert('rect', 'circle'); // Insert rect before first circle
|
||||
|
||||
// Remove elements
|
||||
selection.remove(); // Delete from DOM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Method Chaining
|
||||
|
||||
All modification methods return the selection, enabling chains:
|
||||
|
||||
```javascript
|
||||
d3.select('svg')
|
||||
.append('circle')
|
||||
.attr('cx', 100)
|
||||
.attr('cy', 100)
|
||||
.attr('r', 50)
|
||||
.attr('fill', 'steelblue')
|
||||
.style('opacity', 0.7)
|
||||
.on('click', handleClick);
|
||||
```
|
||||
|
||||
**Why Chaining Works**: Methods mutate selection and return it.
|
||||
|
||||
---
|
||||
|
||||
## Data Join Pattern
|
||||
|
||||
### Basic Join
|
||||
|
||||
```javascript
|
||||
const data = [10, 20, 30, 40, 50];
|
||||
|
||||
svg.selectAll('circle') // Select all circles (may be empty)
|
||||
.data(data) // Bind data array
|
||||
.join('circle') // Create/update/remove to match data
|
||||
.attr('r', d => d); // Set attributes using data
|
||||
```
|
||||
|
||||
**What `.join()` Does**:
|
||||
1. **Enter**: Creates `<circle>` elements for data items without elements (5 circles created if none exist)
|
||||
2. **Update**: Updates existing circles if some already exist
|
||||
3. **Exit**: Removes circles if data shrinks (e.g., data becomes `[10, 20]`, 3 circles removed)
|
||||
|
||||
---
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
Pass functions to `.attr()` and `.style()` receiving `(datum, index)` parameters:
|
||||
|
||||
```javascript
|
||||
.attr('cx', (d, i) => i * 50 + 25) // d = data value, i = index
|
||||
.attr('cy', 100) // Static value
|
||||
.attr('r', d => d) // Radius = data value
|
||||
```
|
||||
|
||||
**Pattern**: `(d, i) => expression`
|
||||
- `d`: Datum (current array element)
|
||||
- `i`: Index in array (0-based)
|
||||
|
||||
---
|
||||
|
||||
### Data with Objects
|
||||
|
||||
```javascript
|
||||
const data = [
|
||||
{name: 'A', value: 30, color: 'red'},
|
||||
{name: 'B', value: 80, color: 'blue'},
|
||||
{name: 'C', value: 45, color: 'green'}
|
||||
];
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('r', d => d.value) // Access object properties
|
||||
.attr('fill', d => d.color);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enter, Exit, Update Pattern
|
||||
|
||||
For fine-grained control (especially with custom transitions):
|
||||
|
||||
```javascript
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join(
|
||||
// Enter: new elements
|
||||
enter => enter.append('circle')
|
||||
.attr('r', 0) // Start small
|
||||
.call(enter => enter.transition().attr('r', d => d.value)),
|
||||
|
||||
// Update: existing elements
|
||||
update => update
|
||||
.call(update => update.transition().attr('r', d => d.value)),
|
||||
|
||||
// Exit: removing elements
|
||||
exit => exit
|
||||
.call(exit => exit.transition().attr('r', 0).remove())
|
||||
);
|
||||
```
|
||||
|
||||
**When to Use**:
|
||||
- Custom enter animations (fade in, slide in)
|
||||
- Custom exit animations (fade out, fall down)
|
||||
- Different behavior for enter vs update
|
||||
|
||||
**When NOT Needed**: Simple updates use `.join('circle').transition()...` after join.
|
||||
|
||||
---
|
||||
|
||||
## Key Functions (Object Constancy)
|
||||
|
||||
**Problem**: By default, data join matches by index. If data reorders, elements don't track items correctly.
|
||||
|
||||
```javascript
|
||||
// Without key function
|
||||
const data1 = [{id: 'A', value: 30}, {id: 'B', value: 80}];
|
||||
const data2 = [{id: 'B', value: 90}, {id: 'A', value: 40}]; // Reordered!
|
||||
|
||||
// Element 0 bound to A (30), then B (90) - wrong!
|
||||
// Element 1 bound to B (80), then A (40) - wrong!
|
||||
```
|
||||
|
||||
**Solution**: Key function returns unique identifier:
|
||||
|
||||
```javascript
|
||||
svg.selectAll('circle')
|
||||
.data(data, d => d.id) // Key function
|
||||
.join('circle')
|
||||
.attr('r', d => d.value);
|
||||
|
||||
// Now element tracks data item by ID, not position
|
||||
```
|
||||
|
||||
**Key Function Signature**: `(datum) => uniqueIdentifier`
|
||||
|
||||
**When to Use**:
|
||||
- Data array reorders
|
||||
- Data array filters (items removed/added)
|
||||
- Transitions where element identity matters
|
||||
|
||||
---
|
||||
|
||||
## Updating Scales
|
||||
|
||||
When data changes, update scale domains:
|
||||
|
||||
```javascript
|
||||
function update(newData) {
|
||||
// Recalculate domain
|
||||
yScale.domain([0, d3.max(newData, d => d.value)]);
|
||||
|
||||
// Update axis
|
||||
svg.select('.y-axis')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3.axisLeft(yScale));
|
||||
|
||||
// Update bars
|
||||
svg.selectAll('rect')
|
||||
.data(newData, d => d.id) // Key function
|
||||
.join('rect')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('height', d => height - yScale(d.value));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Handling
|
||||
|
||||
```javascript
|
||||
selection.on('click', function(event, d) {
|
||||
// `this` = DOM element
|
||||
// `event` = mouse event object
|
||||
// `d` = bound datum
|
||||
|
||||
console.log('Clicked', d);
|
||||
d3.select(this).attr('fill', 'red');
|
||||
});
|
||||
```
|
||||
|
||||
**Common Events**:
|
||||
- Mouse: `click`, `mouseover`, `mouseout`, `mousemove`
|
||||
- Touch: `touchstart`, `touchend`, `touchmove`
|
||||
- Drag: Use `d3.drag()` behavior instead
|
||||
- Zoom: Use `d3.zoom()` behavior instead
|
||||
|
||||
**Event Object Properties**:
|
||||
- `event.pageX`, `event.pageY`: Mouse position
|
||||
- `event.target`: DOM element
|
||||
- `event.preventDefault()`: Prevent default behavior
|
||||
|
||||
---
|
||||
|
||||
## Selection Iteration
|
||||
|
||||
```javascript
|
||||
// Apply function to each element
|
||||
selection.each(function(d, i) {
|
||||
// `this` = DOM element
|
||||
// `d` = datum
|
||||
// `i` = index
|
||||
console.log(d);
|
||||
});
|
||||
|
||||
// Call function with selection
|
||||
function styleCircles(selection) {
|
||||
selection
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
}
|
||||
|
||||
svg.selectAll('circle').call(styleCircles);
|
||||
```
|
||||
|
||||
**Use `.call()` for**: Reusable functions, axes, behaviors (drag, zoom).
|
||||
|
||||
---
|
||||
|
||||
## Filtering and Sorting
|
||||
|
||||
```javascript
|
||||
// Filter selection
|
||||
const largeCircles = svg.selectAll('circle')
|
||||
.filter(d => d.value > 50);
|
||||
|
||||
largeCircles.attr('fill', 'red');
|
||||
|
||||
// Sort elements in DOM
|
||||
svg.selectAll('circle')
|
||||
.sort((a, b) => a.value - b.value); // Ascending order
|
||||
```
|
||||
|
||||
**Note**: `.sort()` reorders DOM elements, affecting document flow and rendering order.
|
||||
|
||||
---
|
||||
|
||||
## Nested Selections
|
||||
|
||||
```javascript
|
||||
// Parent selection
|
||||
const groups = svg.selectAll('g')
|
||||
.data(data)
|
||||
.join('g');
|
||||
|
||||
// Child selection within each group
|
||||
groups.selectAll('circle')
|
||||
.data(d => [d, d, d]) // 3 circles per group
|
||||
.join('circle')
|
||||
.attr('r', 5);
|
||||
```
|
||||
|
||||
**Pattern**: Each parent element gets its own child selection.
|
||||
|
||||
---
|
||||
|
||||
## Selection vs Element
|
||||
|
||||
```javascript
|
||||
// Selection (D3 wrapper)
|
||||
const selection = d3.select('circle');
|
||||
selection.attr('r', 10); // D3 methods
|
||||
|
||||
// Raw DOM element
|
||||
const element = selection.node();
|
||||
element.setAttribute('r', 10); // Native DOM methods
|
||||
```
|
||||
|
||||
**Use `.node()` to**:
|
||||
- Access native DOM properties
|
||||
- Use with non-D3 libraries
|
||||
- Perform operations D3 doesn't support
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Update Function
|
||||
|
||||
```javascript
|
||||
function update(newData) {
|
||||
const circles = svg.selectAll('circle')
|
||||
.data(newData, d => d.id);
|
||||
|
||||
circles.join('circle')
|
||||
.attr('cx', d => xScale(d.x))
|
||||
.attr('cy', d => yScale(d.y))
|
||||
.attr('r', 5);
|
||||
}
|
||||
|
||||
// Call on data change
|
||||
update(data);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Conditional Styling
|
||||
|
||||
```javascript
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('r', 5)
|
||||
.attr('fill', d => d.value > 50 ? 'red' : 'steelblue');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Data-Driven Classes
|
||||
|
||||
```javascript
|
||||
svg.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.classed('high', d => d.value > 80)
|
||||
.classed('medium', d => d.value > 40 && d.value <= 80)
|
||||
.classed('low', d => d.value <= 40);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Multiple Attributes
|
||||
|
||||
```javascript
|
||||
selection.attr('stroke', 'black').attr('stroke-width', 2).attr('fill', 'none');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
```javascript
|
||||
console.log(selection.size()); // Number of elements
|
||||
console.log(selection.nodes()); // Array of DOM elements
|
||||
console.log(selection.data()); // Array of bound data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Forgetting to Return Accessor Value
|
||||
|
||||
```javascript
|
||||
// WRONG - undefined attribute
|
||||
.attr('r', d => { console.log(d); })
|
||||
|
||||
// CORRECT
|
||||
.attr('r', d => {
|
||||
console.log(d);
|
||||
return d.value; // Explicit return
|
||||
})
|
||||
|
||||
// Or use arrow function implicit return
|
||||
.attr('r', d => d.value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Not Using Key Function for Dynamic Data
|
||||
|
||||
```javascript
|
||||
// WRONG - elements don't track items
|
||||
.data(dynamicData)
|
||||
|
||||
// CORRECT
|
||||
.data(dynamicData, d => d.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Selecting Before Elements Exist
|
||||
|
||||
```javascript
|
||||
// WRONG - circles don't exist yet
|
||||
const circles = d3.selectAll('circle');
|
||||
svg.append('circle'); // This circle not in `circles` selection
|
||||
|
||||
// CORRECT - select after creating
|
||||
svg.append('circle');
|
||||
const circles = d3.selectAll('circle');
|
||||
|
||||
// Or use enter selection
|
||||
const circles = svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle'); // Creates circles, returns selection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Modifying Selection Doesn't Update Variable
|
||||
|
||||
```javascript
|
||||
const circles = svg.selectAll('circle');
|
||||
circles.attr('fill', 'red'); // OK - modifies elements
|
||||
|
||||
circles.data(newData).join('circle'); // Returns NEW selection
|
||||
// `circles` variable still references OLD selection
|
||||
|
||||
// CORRECT - reassign
|
||||
circles = svg.selectAll('circle')
|
||||
.data(newData)
|
||||
.join('circle');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn scale functions: [Scales & Axes](scales-axes.md)
|
||||
- See data join in action: [Workflows](workflows.md)
|
||||
- Add transitions: [Transitions & Interactions](transitions-interactions.md)
|
||||
490
skills/d3-visualization/resources/shapes-layouts.md
Normal file
490
skills/d3-visualization/resources/shapes-layouts.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# 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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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 monotonicity
|
||||
- `d3.curveCatmullRom`: Smooth, passes through points
|
||||
- `d3.curveStep`: Step function
|
||||
- `d3.curveBasis`: B-spline
|
||||
|
||||
---
|
||||
|
||||
### Missing Data
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
const arc = d3.arc()
|
||||
.innerRadius(50)
|
||||
.outerRadius(100)
|
||||
.cornerRadius(5); // Rounded edges
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Labels
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
const pie = d3.pie()
|
||||
.value(d => d.value)
|
||||
.sort((a, b) => b.value - a.value); // Descending
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Padding
|
||||
|
||||
```javascript
|
||||
const pie = d3.pie()
|
||||
.value(d => d.value)
|
||||
.padAngle(0.02); // Gap between slices
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stack Generator
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```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]);
|
||||
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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()
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// WRONG - degrees
|
||||
arc.startAngle(90).endAngle(180);
|
||||
|
||||
// CORRECT - radians
|
||||
arc.startAngle(Math.PI / 2).endAngle(Math.PI);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Apply to real data: [Workflows](workflows.md)
|
||||
- Add transitions: [Transitions & Interactions](transitions-interactions.md)
|
||||
- Combine with layouts: [Advanced Layouts](advanced-layouts.md)
|
||||
415
skills/d3-visualization/resources/transitions-interactions.md
Normal file
415
skills/d3-visualization/resources/transitions-interactions.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# Transitions & Interactions
|
||||
|
||||
## Transitions
|
||||
|
||||
### Why Transitions Matter
|
||||
|
||||
**The Problem**: Instant changes are jarring. Users miss updates or lose track of which element changed.
|
||||
|
||||
**D3's Solution**: Smooth interpolation between old and new states over time. Movement helps users track element identity (object constancy).
|
||||
|
||||
**When to Use**: Data updates, not initial render. Transitions show *change*, so there must be a previous state.
|
||||
|
||||
---
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
```javascript
|
||||
// Without transition (instant)
|
||||
selection.attr('r', 10);
|
||||
|
||||
// With transition (animated)
|
||||
selection.transition().duration(500).attr('r', 10);
|
||||
```
|
||||
|
||||
**Key**: Insert `.transition()` before `.attr()` or `.style()` calls you want animated.
|
||||
|
||||
---
|
||||
|
||||
### Duration and Delay
|
||||
|
||||
```javascript
|
||||
selection
|
||||
.transition()
|
||||
.duration(750) // Milliseconds
|
||||
.delay(100) // Wait before starting
|
||||
.attr('r', 20);
|
||||
|
||||
// Staggered (delay per element)
|
||||
selection
|
||||
.transition()
|
||||
.duration(500)
|
||||
.delay((d, i) => i * 50) // 0ms, 50ms, 100ms...
|
||||
.attr('opacity', 1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Easing Functions
|
||||
|
||||
```javascript
|
||||
selection
|
||||
.transition()
|
||||
.duration(500)
|
||||
.ease(d3.easeCubicOut) // Fast start, slow finish
|
||||
.attr('r', 20);
|
||||
```
|
||||
|
||||
**Common Easings**:
|
||||
- `d3.easeLinear`: Constant speed
|
||||
- `d3.easeCubicIn`: Slow start
|
||||
- `d3.easeCubicOut`: Slow finish
|
||||
- `d3.easeCubic`: Slow start and finish
|
||||
- `d3.easeBounceOut`: Bouncing effect
|
||||
- `d3.easeElasticOut`: Elastic spring
|
||||
|
||||
---
|
||||
|
||||
### Chained Transitions
|
||||
|
||||
```javascript
|
||||
selection
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('r', 20)
|
||||
.transition() // Second transition starts after first
|
||||
.duration(500)
|
||||
.attr('fill', 'red');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Named Transitions
|
||||
|
||||
```javascript
|
||||
// Interrupt previous transition with same name
|
||||
selection
|
||||
.transition('resize')
|
||||
.duration(500)
|
||||
.attr('r', 20);
|
||||
|
||||
// Later (cancels previous 'resize' transition)
|
||||
selection
|
||||
.transition('resize')
|
||||
.attr('r', 30);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Pattern with Transitions
|
||||
|
||||
```javascript
|
||||
function update(newData) {
|
||||
// Update scale
|
||||
yScale.domain([0, d3.max(newData, d => d.value)]);
|
||||
|
||||
// Update bars
|
||||
svg.selectAll('rect')
|
||||
.data(newData, d => d.id) // Key function
|
||||
.join('rect')
|
||||
.transition()
|
||||
.duration(750)
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('height', d => height - yScale(d.value));
|
||||
|
||||
// Update axis
|
||||
svg.select('.y-axis')
|
||||
.transition()
|
||||
.duration(750)
|
||||
.call(d3.axisLeft(yScale));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Enter/Exit Transitions
|
||||
|
||||
```javascript
|
||||
svg.selectAll('circle')
|
||||
.data(data, d => d.id)
|
||||
.join(
|
||||
enter => enter.append('circle')
|
||||
.attr('r', 0)
|
||||
.call(enter => enter.transition().attr('r', 5)),
|
||||
|
||||
update => update
|
||||
.call(update => update.transition().attr('fill', 'blue')),
|
||||
|
||||
exit => exit
|
||||
.call(exit => exit.transition().attr('r', 0).remove())
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactions
|
||||
|
||||
### Event Handling
|
||||
|
||||
```javascript
|
||||
selection.on('click', function(event, d) {
|
||||
console.log('Clicked', d);
|
||||
d3.select(this).attr('fill', 'red');
|
||||
});
|
||||
```
|
||||
|
||||
**Common Events**: `click`, `mouseover`, `mouseout`, `mousemove`, `dblclick`
|
||||
|
||||
---
|
||||
|
||||
### Tooltips
|
||||
|
||||
```javascript
|
||||
const tooltip = d3.select('body').append('div')
|
||||
.attr('class', 'tooltip')
|
||||
.style('position', 'absolute')
|
||||
.style('visibility', 'hidden')
|
||||
.style('background', '#fff')
|
||||
.style('padding', '5px')
|
||||
.style('border', '1px solid #ccc');
|
||||
|
||||
svg.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('r', 5)
|
||||
.on('mouseover', (event, d) => {
|
||||
tooltip.style('visibility', 'visible')
|
||||
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
|
||||
})
|
||||
.on('mousemove', (event) => {
|
||||
tooltip.style('top', (event.pageY - 10) + 'px')
|
||||
.style('left', (event.pageX + 10) + 'px');
|
||||
})
|
||||
.on('mouseout', () => {
|
||||
tooltip.style('visibility', 'hidden');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Hover Effects
|
||||
|
||||
```javascript
|
||||
svg.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('fill', 'steelblue')
|
||||
.on('mouseover', function() {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('fill', 'orange');
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('fill', 'steelblue');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Drag Behavior
|
||||
|
||||
### Basic Dragging
|
||||
|
||||
```javascript
|
||||
const drag = d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended);
|
||||
|
||||
function dragstarted(event, d) {
|
||||
d3.select(this).raise().attr('stroke', 'black');
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d3.select(this)
|
||||
.attr('cx', event.x)
|
||||
.attr('cy', event.y);
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
d3.select(this).attr('stroke', null);
|
||||
}
|
||||
|
||||
svg.selectAll('circle').call(drag);
|
||||
```
|
||||
|
||||
**Event Properties**:
|
||||
- `event.x`, `event.y`: New position
|
||||
- `event.dx`, `event.dy`: Change from last position
|
||||
- `event.subject`: Bound datum
|
||||
|
||||
---
|
||||
|
||||
### Drag with Constraints
|
||||
|
||||
```javascript
|
||||
function dragged(event, d) {
|
||||
d3.select(this)
|
||||
.attr('cx', Math.max(0, Math.min(width, event.x)))
|
||||
.attr('cy', Math.max(0, Math.min(height, event.y)));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zoom and Pan
|
||||
|
||||
### Basic Zoom
|
||||
|
||||
```javascript
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.5, 5]) // Min/max zoom
|
||||
.on('zoom', zoomed);
|
||||
|
||||
function zoomed(event) {
|
||||
g.attr('transform', event.transform);
|
||||
}
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Container for zoomable content
|
||||
const g = svg.append('g');
|
||||
g.selectAll('circle').data(data).join('circle')...
|
||||
```
|
||||
|
||||
**Key**: Apply transform to container group, not individual elements.
|
||||
|
||||
---
|
||||
|
||||
### Programmatic Zoom
|
||||
|
||||
```javascript
|
||||
// Zoom to specific level
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity.scale(2));
|
||||
|
||||
// Zoom to fit
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity.translate(100, 100).scale(1.5));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Zoom with Constraints
|
||||
|
||||
```javascript
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.5, 5])
|
||||
.translateExtent([[0, 0], [width, height]]) // Pan limits
|
||||
.on('zoom', zoomed);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brush Selection
|
||||
|
||||
### 2D Brush
|
||||
|
||||
```javascript
|
||||
const brush = d3.brush()
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on('end', brushended);
|
||||
|
||||
function brushended(event) {
|
||||
if (!event.selection) return; // No selection
|
||||
const [[x0, y0], [x1, y1]] = event.selection;
|
||||
|
||||
// Filter data
|
||||
const selected = data.filter(d => {
|
||||
const x = xScale(d.x), y = yScale(d.y);
|
||||
return x >= x0 && x <= x1 && y >= y0 && y <= y1;
|
||||
});
|
||||
|
||||
console.log('Selected', selected);
|
||||
}
|
||||
|
||||
svg.append('g').attr('class', 'brush').call(brush);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1D Brush
|
||||
|
||||
```javascript
|
||||
// X-axis only
|
||||
const brushX = d3.brushX()
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on('end', brushended);
|
||||
|
||||
// Y-axis only
|
||||
const brushY = d3.brushY()
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on('end', brushended);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Clear Brush
|
||||
|
||||
```javascript
|
||||
svg.select('.brush').call(brush.move, null); // Clear selection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Programmatic Brush
|
||||
|
||||
```javascript
|
||||
// Set selection programmatically
|
||||
svg.select('.brush').call(brush.move, [[100, 100], [200, 200]]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Transition on Initial Render
|
||||
|
||||
```javascript
|
||||
// WRONG - transition when no previous state
|
||||
svg.selectAll('circle').data(data).join('circle').transition().attr('r', 5);
|
||||
|
||||
// CORRECT - transition only on updates
|
||||
const circles = svg.selectAll('circle').data(data).join('circle').attr('r', 5);
|
||||
circles.transition().attr('r', 10); // Later, on update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Not Using Key Function
|
||||
|
||||
```javascript
|
||||
// WRONG - elements don't track data items
|
||||
.data(newData).join('circle').transition().attr('cx', d => xScale(d.x));
|
||||
|
||||
// CORRECT - use key function
|
||||
.data(newData, d => d.id).join('circle').transition().attr('cx', d => xScale(d.x));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Applying Zoom to Wrong Element
|
||||
|
||||
```javascript
|
||||
// WRONG - zoom affects individual elements
|
||||
svg.selectAll('circle').call(zoom);
|
||||
|
||||
// CORRECT - zoom affects container
|
||||
const g = svg.append('g');
|
||||
svg.call(zoom);
|
||||
g.selectAll('circle')... // Elements in zoomable container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Apply to workflows: [Workflows](workflows.md)
|
||||
- See complete examples: [Common Patterns](common-patterns.md)
|
||||
- Combine with layouts: [Advanced Layouts](advanced-layouts.md)
|
||||
499
skills/d3-visualization/resources/workflows.md
Normal file
499
skills/d3-visualization/resources/workflows.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user