Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:38:26 +08:00
commit 41d9f6b189
304 changed files with 98322 additions and 0 deletions

View 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)

View 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'));
```

View 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"
}
}
]
}

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)