6.1 KiB
Executable File
6.1 KiB
Executable File
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) |