- Add date_added to all 950+ skills for complete tracking - Update version to 6.5.0 in package.json and README - Regenerate all indexes and catalog - Sync all generated files Features from merged PR #150: - Stars/Upvotes system for community-driven discovery - Auto-update mechanism via START_APP.bat - Interactive Prompt Builder - Date tracking badges - Smart auto-categorization All skills validated and indexed. Made-with: Cursor
824 lines
21 KiB
Markdown
824 lines
21 KiB
Markdown
---
|
|
name: claude-d3js-skill
|
|
description: "Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visua..."
|
|
risk: unknown
|
|
source: community
|
|
date_added: "2026-02-27"
|
|
---
|
|
|
|
# D3.js Visualisation
|
|
|
|
## Overview
|
|
|
|
This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks.
|
|
|
|
## When to use d3.js
|
|
|
|
**Use d3.js for:**
|
|
- Custom visualisations requiring unique visual encodings or layouts
|
|
- Interactive explorations with complex pan, zoom, or brush behaviours
|
|
- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
|
|
- Geographic visualisations with custom projections
|
|
- Visualisations requiring smooth, choreographed transitions
|
|
- Publication-quality graphics with fine-grained styling control
|
|
- Novel chart types not available in standard libraries
|
|
|
|
**Consider alternatives for:**
|
|
- 3D visualisations - use Three.js instead
|
|
|
|
## Core workflow
|
|
|
|
### 1. Set up d3.js
|
|
|
|
Import d3 at the top of your script:
|
|
|
|
```javascript
|
|
import * as d3 from 'd3';
|
|
```
|
|
|
|
Or use the CDN version (7.x):
|
|
|
|
```html
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
```
|
|
|
|
All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.
|
|
|
|
### 2. Choose the integration pattern
|
|
|
|
**Pattern A: Direct DOM manipulation (recommended for most cases)**
|
|
Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:
|
|
|
|
```javascript
|
|
function drawChart(data) {
|
|
if (!data || data.length === 0) return;
|
|
|
|
const svg = d3.select('#chart'); // Select by ID, class, or DOM element
|
|
|
|
// Clear previous content
|
|
svg.selectAll("*").remove();
|
|
|
|
// Set up dimensions
|
|
const width = 800;
|
|
const height = 400;
|
|
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
|
|
|
|
// Create scales, axes, and draw visualisation
|
|
// ... d3 code here ...
|
|
}
|
|
|
|
// Call when data changes
|
|
drawChart(myData);
|
|
```
|
|
|
|
**Pattern B: Declarative rendering (for frameworks with templating)**
|
|
Use d3 for data calculations (scales, layouts) but render elements via your framework:
|
|
|
|
```javascript
|
|
function getChartElements(data) {
|
|
const xScale = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.value)])
|
|
.range([0, 400]);
|
|
|
|
return data.map((d, i) => ({
|
|
x: 50,
|
|
y: i * 30,
|
|
width: xScale(d.value),
|
|
height: 25
|
|
}));
|
|
}
|
|
|
|
// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
|
|
// In Vue: v-for directive over the returned array
|
|
// In vanilla JS: Create elements manually from the returned data
|
|
```
|
|
|
|
Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering.
|
|
|
|
### 3. Structure the visualisation code
|
|
|
|
Follow this standard structure in your drawing function:
|
|
|
|
```javascript
|
|
function drawVisualization(data) {
|
|
if (!data || data.length === 0) return;
|
|
|
|
const svg = d3.select('#chart'); // Or pass a selector/element
|
|
svg.selectAll("*").remove(); // Clear previous render
|
|
|
|
// 1. Define dimensions
|
|
const width = 800;
|
|
const height = 400;
|
|
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
|
|
const innerWidth = width - margin.left - margin.right;
|
|
const innerHeight = height - margin.top - margin.bottom;
|
|
|
|
// 2. Create main group with margins
|
|
const g = svg.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
// 3. Create scales
|
|
const xScale = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.x)])
|
|
.range([0, innerWidth]);
|
|
|
|
const yScale = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.y)])
|
|
.range([innerHeight, 0]); // Note: inverted for SVG coordinates
|
|
|
|
// 4. Create and append axes
|
|
const xAxis = d3.axisBottom(xScale);
|
|
const yAxis = d3.axisLeft(yScale);
|
|
|
|
g.append("g")
|
|
.attr("transform", `translate(0,${innerHeight})`)
|
|
.call(xAxis);
|
|
|
|
g.append("g")
|
|
.call(yAxis);
|
|
|
|
// 5. Bind data and create visual elements
|
|
g.selectAll("circle")
|
|
.data(data)
|
|
.join("circle")
|
|
.attr("cx", d => xScale(d.x))
|
|
.attr("cy", d => yScale(d.y))
|
|
.attr("r", 5)
|
|
.attr("fill", "steelblue");
|
|
}
|
|
|
|
// Call when data changes
|
|
drawVisualization(myData);
|
|
```
|
|
|
|
### 4. Implement responsive sizing
|
|
|
|
Make visualisations responsive to container size:
|
|
|
|
```javascript
|
|
function setupResponsiveChart(containerId, data) {
|
|
const container = document.getElementById(containerId);
|
|
const svg = d3.select(`#${containerId}`).append('svg');
|
|
|
|
function updateChart() {
|
|
const { width, height } = container.getBoundingClientRect();
|
|
svg.attr('width', width).attr('height', height);
|
|
|
|
// Redraw visualisation with new dimensions
|
|
drawChart(data, svg, width, height);
|
|
}
|
|
|
|
// Update on initial load
|
|
updateChart();
|
|
|
|
// Update on window resize
|
|
window.addEventListener('resize', updateChart);
|
|
|
|
// Return cleanup function
|
|
return () => window.removeEventListener('resize', updateChart);
|
|
}
|
|
|
|
// Usage:
|
|
// const cleanup = setupResponsiveChart('chart-container', myData);
|
|
// cleanup(); // Call when component unmounts or element removed
|
|
```
|
|
|
|
Or use ResizeObserver for more direct container monitoring:
|
|
|
|
```javascript
|
|
function setupResponsiveChartWithObserver(svgElement, data) {
|
|
const observer = new ResizeObserver(() => {
|
|
const { width, height } = svgElement.getBoundingClientRect();
|
|
d3.select(svgElement)
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
// Redraw visualisation
|
|
drawChart(data, d3.select(svgElement), width, height);
|
|
});
|
|
|
|
observer.observe(svgElement.parentElement);
|
|
return () => observer.disconnect();
|
|
}
|
|
```
|
|
|
|
## Common visualisation patterns
|
|
|
|
### Bar chart
|
|
|
|
```javascript
|
|
function drawBarChart(data, svgElement) {
|
|
if (!data || data.length === 0) return;
|
|
|
|
const svg = d3.select(svgElement);
|
|
svg.selectAll("*").remove();
|
|
|
|
const width = 800;
|
|
const height = 400;
|
|
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
|
|
const innerWidth = width - margin.left - margin.right;
|
|
const innerHeight = height - margin.top - margin.bottom;
|
|
|
|
const g = svg.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
const xScale = d3.scaleBand()
|
|
.domain(data.map(d => d.category))
|
|
.range([0, innerWidth])
|
|
.padding(0.1);
|
|
|
|
const yScale = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.value)])
|
|
.range([innerHeight, 0]);
|
|
|
|
g.append("g")
|
|
.attr("transform", `translate(0,${innerHeight})`)
|
|
.call(d3.axisBottom(xScale));
|
|
|
|
g.append("g")
|
|
.call(d3.axisLeft(yScale));
|
|
|
|
g.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 => innerHeight - yScale(d.value))
|
|
.attr("fill", "steelblue");
|
|
}
|
|
|
|
// Usage:
|
|
// drawBarChart(myData, document.getElementById('chart'));
|
|
```
|
|
|
|
### Line chart
|
|
|
|
```javascript
|
|
const line = d3.line()
|
|
.x(d => xScale(d.date))
|
|
.y(d => yScale(d.value))
|
|
.curve(d3.curveMonotoneX); // Smooth curve
|
|
|
|
g.append("path")
|
|
.datum(data)
|
|
.attr("fill", "none")
|
|
.attr("stroke", "steelblue")
|
|
.attr("stroke-width", 2)
|
|
.attr("d", line);
|
|
```
|
|
|
|
### Scatter plot
|
|
|
|
```javascript
|
|
g.selectAll("circle")
|
|
.data(data)
|
|
.join("circle")
|
|
.attr("cx", d => xScale(d.x))
|
|
.attr("cy", d => yScale(d.y))
|
|
.attr("r", d => sizeScale(d.size)) // Optional: size encoding
|
|
.attr("fill", d => colourScale(d.category)) // Optional: colour encoding
|
|
.attr("opacity", 0.7);
|
|
```
|
|
|
|
### Chord diagram
|
|
|
|
A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:
|
|
|
|
```javascript
|
|
function drawChordDiagram(data) {
|
|
// data format: array of objects with source, target, and value
|
|
// Example: [{ source: 'A', target: 'B', value: 10 }, ...]
|
|
|
|
if (!data || data.length === 0) return;
|
|
|
|
const svg = d3.select('#chart');
|
|
svg.selectAll("*").remove();
|
|
|
|
const width = 600;
|
|
const height = 600;
|
|
const innerRadius = Math.min(width, height) * 0.3;
|
|
const outerRadius = innerRadius + 30;
|
|
|
|
// Create matrix from data
|
|
const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
|
|
const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));
|
|
|
|
data.forEach(d => {
|
|
const i = nodes.indexOf(d.source);
|
|
const j = nodes.indexOf(d.target);
|
|
matrix[i][j] += d.value;
|
|
matrix[j][i] += d.value;
|
|
});
|
|
|
|
// Create chord layout
|
|
const chord = d3.chord()
|
|
.padAngle(0.05)
|
|
.sortSubgroups(d3.descending);
|
|
|
|
const arc = d3.arc()
|
|
.innerRadius(innerRadius)
|
|
.outerRadius(outerRadius);
|
|
|
|
const ribbon = d3.ribbon()
|
|
.source(d => d.source)
|
|
.target(d => d.target);
|
|
|
|
const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
|
|
.domain(nodes);
|
|
|
|
const g = svg.append("g")
|
|
.attr("transform", `translate(${width / 2},${height / 2})`);
|
|
|
|
const chords = chord(matrix);
|
|
|
|
// Draw ribbons
|
|
g.append("g")
|
|
.attr("fill-opacity", 0.67)
|
|
.selectAll("path")
|
|
.data(chords)
|
|
.join("path")
|
|
.attr("d", ribbon)
|
|
.attr("fill", d => colourScale(nodes[d.source.index]))
|
|
.attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());
|
|
|
|
// Draw groups (arcs)
|
|
const group = g.append("g")
|
|
.selectAll("g")
|
|
.data(chords.groups)
|
|
.join("g");
|
|
|
|
group.append("path")
|
|
.attr("d", arc)
|
|
.attr("fill", d => colourScale(nodes[d.index]))
|
|
.attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());
|
|
|
|
// Add labels
|
|
group.append("text")
|
|
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
|
|
.attr("dy", "0.31em")
|
|
.attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
|
|
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
|
|
.text((d, i) => nodes[i])
|
|
.style("font-size", "12px");
|
|
}
|
|
```
|
|
|
|
### Heatmap
|
|
|
|
A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:
|
|
|
|
```javascript
|
|
function drawHeatmap(data) {
|
|
// data format: array of objects with row, column, and value
|
|
// Example: [{ row: 'A', column: 'X', value: 10 }, ...]
|
|
|
|
if (!data || data.length === 0) return;
|
|
|
|
const svg = d3.select('#chart');
|
|
svg.selectAll("*").remove();
|
|
|
|
const width = 800;
|
|
const height = 600;
|
|
const margin = { top: 100, right: 30, bottom: 30, left: 100 };
|
|
const innerWidth = width - margin.left - margin.right;
|
|
const innerHeight = height - margin.top - margin.bottom;
|
|
|
|
// Get unique rows and columns
|
|
const rows = Array.from(new Set(data.map(d => d.row)));
|
|
const columns = Array.from(new Set(data.map(d => d.column)));
|
|
|
|
const g = svg.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
// Create scales
|
|
const xScale = d3.scaleBand()
|
|
.domain(columns)
|
|
.range([0, innerWidth])
|
|
.padding(0.01);
|
|
|
|
const yScale = d3.scaleBand()
|
|
.domain(rows)
|
|
.range([0, innerHeight])
|
|
.padding(0.01);
|
|
|
|
// Colour scale for values
|
|
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
|
|
.domain([0, d3.max(data, d => d.value)]);
|
|
|
|
// Draw rectangles
|
|
g.selectAll("rect")
|
|
.data(data)
|
|
.join("rect")
|
|
.attr("x", d => xScale(d.column))
|
|
.attr("y", d => yScale(d.row))
|
|
.attr("width", xScale.bandwidth())
|
|
.attr("height", yScale.bandwidth())
|
|
.attr("fill", d => colourScale(d.value));
|
|
|
|
// Add x-axis labels
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`)
|
|
.selectAll("text")
|
|
.data(columns)
|
|
.join("text")
|
|
.attr("x", d => xScale(d) + xScale.bandwidth() / 2)
|
|
.attr("y", -10)
|
|
.attr("text-anchor", "middle")
|
|
.text(d => d)
|
|
.style("font-size", "12px");
|
|
|
|
// Add y-axis labels
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`)
|
|
.selectAll("text")
|
|
.data(rows)
|
|
.join("text")
|
|
.attr("x", -10)
|
|
.attr("y", d => yScale(d) + yScale.bandwidth() / 2)
|
|
.attr("dy", "0.35em")
|
|
.attr("text-anchor", "end")
|
|
.text(d => d)
|
|
.style("font-size", "12px");
|
|
|
|
// Add colour legend
|
|
const legendWidth = 20;
|
|
const legendHeight = 200;
|
|
const legend = svg.append("g")
|
|
.attr("transform", `translate(${width - 60},${margin.top})`);
|
|
|
|
const legendScale = d3.scaleLinear()
|
|
.domain(colourScale.domain())
|
|
.range([legendHeight, 0]);
|
|
|
|
const legendAxis = d3.axisRight(legendScale)
|
|
.ticks(5);
|
|
|
|
// Draw colour gradient in legend
|
|
for (let i = 0; i < legendHeight; i++) {
|
|
legend.append("rect")
|
|
.attr("y", i)
|
|
.attr("width", legendWidth)
|
|
.attr("height", 1)
|
|
.attr("fill", colourScale(legendScale.invert(i)));
|
|
}
|
|
|
|
legend.append("g")
|
|
.attr("transform", `translate(${legendWidth},0)`)
|
|
.call(legendAxis);
|
|
}
|
|
```
|
|
|
|
### Pie chart
|
|
|
|
```javascript
|
|
const pie = d3.pie()
|
|
.value(d => d.value)
|
|
.sort(null);
|
|
|
|
const arc = d3.arc()
|
|
.innerRadius(0)
|
|
.outerRadius(Math.min(width, height) / 2 - 20);
|
|
|
|
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
|
|
|
|
const g = svg.append("g")
|
|
.attr("transform", `translate(${width / 2},${height / 2})`);
|
|
|
|
g.selectAll("path")
|
|
.data(pie(data))
|
|
.join("path")
|
|
.attr("d", arc)
|
|
.attr("fill", (d, i) => colourScale(i))
|
|
.attr("stroke", "white")
|
|
.attr("stroke-width", 2);
|
|
```
|
|
|
|
### Force-directed network
|
|
|
|
```javascript
|
|
const simulation = d3.forceSimulation(nodes)
|
|
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
|
|
.force("charge", d3.forceManyBody().strength(-300))
|
|
.force("center", d3.forceCenter(width / 2, height / 2));
|
|
|
|
const link = g.selectAll("line")
|
|
.data(links)
|
|
.join("line")
|
|
.attr("stroke", "#999")
|
|
.attr("stroke-width", 1);
|
|
|
|
const node = g.selectAll("circle")
|
|
.data(nodes)
|
|
.join("circle")
|
|
.attr("r", 8)
|
|
.attr("fill", "steelblue")
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended));
|
|
|
|
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);
|
|
});
|
|
|
|
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;
|
|
}
|
|
```
|
|
|
|
## Adding interactivity
|
|
|
|
### Tooltips
|
|
|
|
```javascript
|
|
// Create tooltip div (outside SVG)
|
|
const tooltip = d3.select("body").append("div")
|
|
.attr("class", "tooltip")
|
|
.style("position", "absolute")
|
|
.style("visibility", "hidden")
|
|
.style("background-color", "white")
|
|
.style("border", "1px solid #ddd")
|
|
.style("padding", "10px")
|
|
.style("border-radius", "4px")
|
|
.style("pointer-events", "none");
|
|
|
|
// Add to elements
|
|
circles
|
|
.on("mouseover", function(event, d) {
|
|
d3.select(this).attr("opacity", 1);
|
|
tooltip
|
|
.style("visibility", "visible")
|
|
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
|
|
})
|
|
.on("mousemove", function(event) {
|
|
tooltip
|
|
.style("top", (event.pageY - 10) + "px")
|
|
.style("left", (event.pageX + 10) + "px");
|
|
})
|
|
.on("mouseout", function() {
|
|
d3.select(this).attr("opacity", 0.7);
|
|
tooltip.style("visibility", "hidden");
|
|
});
|
|
```
|
|
|
|
### Zoom and pan
|
|
|
|
```javascript
|
|
const zoom = d3.zoom()
|
|
.scaleExtent([0.5, 10])
|
|
.on("zoom", (event) => {
|
|
g.attr("transform", event.transform);
|
|
});
|
|
|
|
svg.call(zoom);
|
|
```
|
|
|
|
### Click interactions
|
|
|
|
```javascript
|
|
circles
|
|
.on("click", function(event, d) {
|
|
// Handle click (dispatch event, update app state, etc.)
|
|
console.log("Clicked:", d);
|
|
|
|
// Visual feedback
|
|
d3.selectAll("circle").attr("fill", "steelblue");
|
|
d3.select(this).attr("fill", "orange");
|
|
|
|
// Optional: dispatch custom event for your framework/app to listen to
|
|
// window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
|
|
});
|
|
```
|
|
|
|
## Transitions and animations
|
|
|
|
Add smooth transitions to visual changes:
|
|
|
|
```javascript
|
|
// Basic transition
|
|
circles
|
|
.transition()
|
|
.duration(750)
|
|
.attr("r", 10);
|
|
|
|
// Chained transitions
|
|
circles
|
|
.transition()
|
|
.duration(500)
|
|
.attr("fill", "orange")
|
|
.transition()
|
|
.duration(500)
|
|
.attr("r", 15);
|
|
|
|
// Staggered transitions
|
|
circles
|
|
.transition()
|
|
.delay((d, i) => i * 50)
|
|
.duration(500)
|
|
.attr("cy", d => yScale(d.value));
|
|
|
|
// Custom easing
|
|
circles
|
|
.transition()
|
|
.duration(1000)
|
|
.ease(d3.easeBounceOut)
|
|
.attr("r", 10);
|
|
```
|
|
|
|
## Scales reference
|
|
|
|
### Quantitative scales
|
|
|
|
```javascript
|
|
// Linear scale
|
|
const xScale = d3.scaleLinear()
|
|
.domain([0, 100])
|
|
.range([0, 500]);
|
|
|
|
// Log scale (for exponential data)
|
|
const logScale = d3.scaleLog()
|
|
.domain([1, 1000])
|
|
.range([0, 500]);
|
|
|
|
// Power scale
|
|
const powScale = d3.scalePow()
|
|
.exponent(2)
|
|
.domain([0, 100])
|
|
.range([0, 500]);
|
|
|
|
// Time scale
|
|
const timeScale = d3.scaleTime()
|
|
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
|
|
.range([0, 500]);
|
|
```
|
|
|
|
### Ordinal scales
|
|
|
|
```javascript
|
|
// Band scale (for bar charts)
|
|
const bandScale = d3.scaleBand()
|
|
.domain(['A', 'B', 'C', 'D'])
|
|
.range([0, 400])
|
|
.padding(0.1);
|
|
|
|
// Point scale (for line/scatter categories)
|
|
const pointScale = d3.scalePoint()
|
|
.domain(['A', 'B', 'C', 'D'])
|
|
.range([0, 400]);
|
|
|
|
// Ordinal scale (for colours)
|
|
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
|
|
```
|
|
|
|
### Sequential scales
|
|
|
|
```javascript
|
|
// Sequential colour scale
|
|
const colourScale = d3.scaleSequential(d3.interpolateBlues)
|
|
.domain([0, 100]);
|
|
|
|
// Diverging colour scale
|
|
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
|
|
.domain([-10, 0, 10]);
|
|
```
|
|
|
|
## Best practices
|
|
|
|
### Data preparation
|
|
|
|
Always validate and prepare data before visualisation:
|
|
|
|
```javascript
|
|
// Filter invalid values
|
|
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
|
|
|
|
// Sort data if order matters
|
|
const sortedData = [...data].sort((a, b) => b.value - a.value);
|
|
|
|
// Parse dates
|
|
const parsedData = data.map(d => ({
|
|
...d,
|
|
date: d3.timeParse("%Y-%m-%d")(d.date)
|
|
}));
|
|
```
|
|
|
|
### Performance optimisation
|
|
|
|
For large datasets (>1000 elements):
|
|
|
|
```javascript
|
|
// Use canvas instead of SVG for many elements
|
|
// Use quadtree for collision detection
|
|
// Simplify paths with d3.line().curve(d3.curveStep)
|
|
// Implement virtual scrolling for large lists
|
|
// Use requestAnimationFrame for custom animations
|
|
```
|
|
|
|
### Accessibility
|
|
|
|
Make visualisations accessible:
|
|
|
|
```javascript
|
|
// Add ARIA labels
|
|
svg.attr("role", "img")
|
|
.attr("aria-label", "Bar chart showing quarterly revenue");
|
|
|
|
// Add title and description
|
|
svg.append("title").text("Quarterly Revenue 2024");
|
|
svg.append("desc").text("Bar chart showing revenue growth across four quarters");
|
|
|
|
// Ensure sufficient colour contrast
|
|
// Provide keyboard navigation for interactive elements
|
|
// Include data table alternative
|
|
```
|
|
|
|
### Styling
|
|
|
|
Use consistent, professional styling:
|
|
|
|
```javascript
|
|
// Define colour palettes upfront
|
|
const colours = {
|
|
primary: '#4A90E2',
|
|
secondary: '#7B68EE',
|
|
background: '#F5F7FA',
|
|
text: '#333333',
|
|
gridLines: '#E0E0E0'
|
|
};
|
|
|
|
// Apply consistent typography
|
|
svg.selectAll("text")
|
|
.style("font-family", "Inter, sans-serif")
|
|
.style("font-size", "12px");
|
|
|
|
// Use subtle grid lines
|
|
g.selectAll(".tick line")
|
|
.attr("stroke", colours.gridLines)
|
|
.attr("stroke-dasharray", "2,2");
|
|
```
|
|
|
|
## Common issues and solutions
|
|
|
|
**Issue**: Axes not appearing
|
|
- Ensure scales have valid domains (check for NaN values)
|
|
- Verify axis is appended to correct group
|
|
- Check transform translations are correct
|
|
|
|
**Issue**: Transitions not working
|
|
- Call `.transition()` before attribute changes
|
|
- Ensure elements have unique keys for proper data binding
|
|
- Check that useEffect dependencies include all changing data
|
|
|
|
**Issue**: Responsive sizing not working
|
|
- Use ResizeObserver or window resize listener
|
|
- Update dimensions in state to trigger re-render
|
|
- Ensure SVG has width/height attributes or viewBox
|
|
|
|
**Issue**: Performance problems
|
|
- Limit number of DOM elements (consider canvas for >1000 items)
|
|
- Debounce resize handlers
|
|
- Use `.join()` instead of separate enter/update/exit selections
|
|
- Avoid unnecessary re-renders by checking dependencies
|
|
|
|
## Resources
|
|
|
|
### references/
|
|
Contains detailed reference materials:
|
|
- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples
|
|
- `scale-reference.md` - Complete guide to d3 scales with examples
|
|
- `colour-schemes.md` - D3 colour schemes and palette recommendations
|
|
|
|
### assets/
|
|
|
|
Contains boilerplate templates:
|
|
|
|
- `chart-template.js` - Starter template for basic chart
|
|
- `interactive-template.js` - Template with tooltips, zoom, and interactions
|
|
- `sample-data.json` - Example datasets for testing
|
|
|
|
These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.
|
|
|
|
To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.
|