Files
2026-06-06 05:21:10 +00:00

6.1 KiB
Executable File
Raw Permalink Blame History

D3.js Template Library

⚠️ Before writing any code, read _rules.md — three non-negotiable rules on overlap, hierarchy, and color.

D3.js is the "ultimate weapon" in visualization — highest freedom, but steepest learning curve. Suitable for: visualizations requiring fully custom interactions, data journalism, art-grade infographics.

If your needs can be met by ECharts/matplotlib, don't use D3. D3's value lies in "charts others can't make".

HTML Universal Shell

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{{TITLE}}</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: #FFFFFF; font-family: system-ui, 'SimHei', sans-serif; }
  svg { display: block; margin: 40px auto; }
  
  /* Design tokens */
  :root {
    --text: #111827;
    --text-sub: #6B7280;
    --text-muted: #9CA3AF;
    --axis: #E5E7EB;
    --grid: #F3F4F6;
    --blue: #3B82F6;
    --cyan: #06B6D4;
    --purple: #8B5CF6;
    --amber: #F59E0B;
    --red: #EF4444;
    --green: #10B981;
  }
</style>
</head>
<body>
<svg id="chart"></svg>
<script>
// D3 code
</script>
</body>
</html>

Colors and Constants

const COLORS = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'];
const GRAY = { 200: '#E5E7EB', 300: '#D1D5DB', 400: '#9CA3AF', 500: '#6B7280', 900: '#111827' };

const margin = { top: 60, right: 40, bottom: 50, left: 60 };
const width = 900 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;

Template 1: Insight Bar Chart

const svg = d3.select('#chart')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  .append('g')
  .attr('transform', `translate(${margin.left},${margin.top})`);

// Title (insight-driven)
svg.append('text')
  .attr('x', 0).attr('y', -30)
  .attr('fill', GRAY[900])
  .attr('font-size', 16).attr('font-weight', 'bold')
  .text('产品C销售额领先达到89万');

// Scales
const x = d3.scaleBand().domain(data.map(d => d.name)).range([0, width]).padding(0.35);
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) * 1.15]).range([height, 0]);

// Y axis (minimal: no tick marks, light gray)
svg.append('g')
  .call(d3.axisLeft(y).ticks(5).tickSize(0).tickFormat(d3.format(',')))
  .call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8))
  .call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9));

// X axis
svg.append('g')
  .attr('transform', `translate(0,${height})`)
  .call(d3.axisBottom(x).tickSize(0))
  .call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8))
  .call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9));

// Bars (gray + highlight)
svg.selectAll('.bar')
  .data(data)
  .join('rect')
  .attr('x', d => x(d.name))
  .attr('width', x.bandwidth())
  .attr('y', d => y(d.value))
  .attr('height', d => height - y(d.value))
  .attr('fill', d => d.highlight ? '#3B82F6' : GRAY[200])
  .attr('rx', 3);

// Value labels
svg.selectAll('.label')
  .data(data)
  .join('text')
  .attr('x', d => x(d.name) + x.bandwidth()/2)
  .attr('y', d => y(d.value) - 6)
  .attr('text-anchor', 'middle')
  .attr('fill', d => d.highlight ? GRAY[900] : GRAY[400])
  .attr('font-size', d => d.highlight ? 12 : 10)
  .attr('font-weight', d => d.highlight ? 'bold' : 'normal')
  .text(d => d3.format(',')(d.value));

Template 2: Force-Directed Graph

This is D3's killer feature — other frameworks can hardly achieve the same effect.

const simulation = d3.forceSimulation(nodes)
  .force('link', d3.forceLink(links).id(d => d.id).distance(80))
  .force('charge', d3.forceManyBody().strength(-200))
  .force('center', d3.forceCenter(width/2, height/2))
  .force('collision', d3.forceCollide(20));

const link = svg.selectAll('.link')
  .data(links).join('line')
  .attr('stroke', GRAY[200]).attr('stroke-width', 1);

const node = svg.selectAll('.node')
  .data(nodes).join('circle')
  .attr('r', d => Math.sqrt(d.value) * 3)
  .attr('fill', (d, i) => COLORS[d.group % COLORS.length])
  .attr('stroke', '#fff').attr('stroke-width', 1.5)
  .call(d3.drag()
    .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
    .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
    .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
  );

// Labels
const labels = svg.selectAll('.label')
  .data(nodes).join('text')
  .text(d => d.name)
  .attr('font-size', 9).attr('fill', GRAY[500])
  .attr('text-anchor', 'middle').attr('dy', -15);

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);
  labels.attr('x', d => d.x).attr('y', d => d.y);
});

Template 3: Treemap

const root = d3.hierarchy(treeData).sum(d => d.value);
d3.treemap().size([width, height]).padding(2)(root);

svg.selectAll('.cell')
  .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, i) => COLORS[i % COLORS.length])
  .attr('rx', 2).attr('opacity', 0.85);

svg.selectAll('.cell-label')
  .data(root.leaves().filter(d => (d.x1-d.x0) > 40 && (d.y1-d.y0) > 20))
  .join('text')
  .attr('x', d => d.x0 + 4).attr('y', d => d.y0 + 14)
  .text(d => d.data.name)
  .attr('font-size', 10).attr('fill', 'white').attr('font-weight', 'bold');

D3 Use Cases (vs Overkill)

D3 Best Overkill
Force-directed relationship graph Regular bar chart (use matplotlib)
Custom geographic visualization Standard map (use ECharts)
Data-driven animation Static report chart (use matplotlib)
Treemap / Sunburst Standard pie chart (use matplotlib)
Complex interactions (brushing/linking/drill-down) Simple tooltip (use ECharts)
Data journalism/narrative visualization Dashboard (use ECharts)