D3 Viz

calesthio/OpenMontage/.claude/skills/d3-viz

作者 calesthio9327439db69021ab4b0e2776729bf3b58fdb5a87無授權條款65K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫5 天前更新

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 visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment.

AI 產生的概覽

指導使用 d3.js 建立自訂互動式資料視覺化,涵蓋圖表、網路圖、地圖與轉場動畫。

功能
此技能提供使用 d3.js 建構客製化互動式資料視覺化的說明與程式碼模式。內容涵蓋環境設定、在原生 JavaScript 與各類框架中的整合方式、回應式尺寸調整,以及長條圖、折線圖、散佈圖、和弦圖、熱圖、圓餅圖和力導向網路等常見圖表類型,並包含互動、轉場動畫、比例尺、無障礙與效能方面的指引。它也會指向關於 d3 模式、比例尺和配色方案的參考文件,以及範本與範例資料。
適用情境
當任務需要自訂圖表、圖形、網路圖或地理視覺化,或任何需要對元素、轉場或互動進行細緻控制的 SVG 視覺化時使用。它適合超出標準圖表函式庫能力的客製化視覺化,可在 React、Vue、Svelte、原生 JavaScript 或類似環境中使用。
執行需求
需要 d3.js(以模組方式匯入或從 7.x CDN 載入),以及能繪製 SVG 的 JavaScript 環境。此技能附帶參考文件與範本資源,但不含可執行的指令碼;只有在選用 CDN 版本時才需要網路連線。

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 changesdrawChart(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 changesdrawVisualization(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 elementscircles  .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 transitioncircles  .transition()  .duration(750)  .attr("r", 10);
// Chained transitionscircles  .transition()  .duration(500)  .attr("fill", "orange")  .transition()  .duration(500)  .attr("r", 15);
// Staggered transitionscircles  .transition()  .delay((d, i) => i * 50)  .duration(500)  .attr("cy", d => yScale(d.value));
// Custom easingcircles  .transition()  .duration(1000)  .ease(d3.easeBounceOut)  .attr("r", 10);

Scales reference

Quantitative scales

javascript
// Linear scaleconst 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 scaleconst powScale = d3.scalePow()  .exponent(2)  .domain([0, 100])  .range([0, 500]);
// Time scaleconst 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 scaleconst colourScale = d3.scaleSequential(d3.interpolateBlues)  .domain([0, 100]);
// Diverging colour scaleconst divScale = d3.scaleDiverging(d3.interpolateRdBu)  .domain([-10, 0, 10]);

Best practices

Data preparation

Always validate and prepare data before visualisation:

javascript
// Filter invalid valuesconst cleanData = data.filter(d => d.value != null && !isNaN(d.value));
// Sort data if order mattersconst sortedData = [...data].sort((a, b) => b.value - a.value);
// Parse datesconst 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 labelssvg.attr("role", "img")   .attr("aria-label", "Bar chart showing quarterly revenue");
// Add title and descriptionsvg.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 upfrontconst colours = {  primary: '#4A90E2',  secondary: '#7B68EE',  background: '#F5F7FA',  text: '#333333',  gridLines: '#E0E0E0'};
// Apply consistent typographysvg.selectAll("text")  .style("font-family", "Inter, sans-serif")  .style("font-size", "12px");
// Use subtle grid linesg.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.

來源與署名

來源:calesthio/OpenMontage位於.claude/skills/d3-viz提交9327439

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架