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

652 lines
19 KiB
Markdown
Executable File

# ECharts Template Library
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
ECharts strengths: interactivity (tooltip/zoom/linking), big data (Canvas renders millions of points smoothly), strong Chinese community.
Output as HTML files — open directly in browser or export PNG via Playwright screenshot.
## HTML Universal Shell
Wrap all ECharts charts with this shell:
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{{TITLE}}</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: {{BG_COLOR}}; }
#chart { width: {{WIDTH}}px; height: {{HEIGHT}}px; margin: 40px auto; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
const chart = echarts.init(document.getElementById('chart'));
const option = { /* see templates below */ };
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
</script>
</body>
</html>
```
Default dimensions: `width=900, height=520`, white background `#FFFFFF`.
---
## Theme Configuration
### Light Theme (Default)
```javascript
const THEME = {
bg: '#FFFFFF',
text: '#111827',
textSub: '#6B7280',
textMuted: '#9CA3AF',
axis: '#E5E7EB',
grid: '#F3F4F6',
tooltip: { bg: '#1E293B', border: '#334155', text: '#F1F5F9' },
colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'],
};
```
### Dark Theme (Finance / Tech Dashboard)
```javascript
const DARK = {
bg: '#0F172A',
text: '#F1F5F9',
textSub: '#94A3B8',
textMuted: '#64748B',
axis: '#334155',
grid: '#1E293B',
tooltip: { bg: '#1E293B', border: '#475569', text: '#F1F5F9' },
colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#22C55E', '#EC4899'],
};
```
### Base Option Configuration
```javascript
function baseOption(theme, title, subtitle) {
return {
backgroundColor: theme.bg,
textStyle: { fontFamily: 'system-ui, SimHei, sans-serif', color: theme.text },
title: {
text: title, subtext: subtitle || '',
left: 24, top: 16,
textStyle: { fontSize: 16, fontWeight: 'bold', color: theme.text },
subtextStyle: { fontSize: 12, color: theme.textSub },
},
grid: { left: 60, right: 40, top: 80, bottom: 50, containLabel: true },
color: theme.colors,
tooltip: {
trigger: 'axis',
backgroundColor: theme.tooltip.bg,
borderColor: theme.tooltip.border,
borderWidth: 1,
textStyle: { color: theme.tooltip.text, fontSize: 12 },
},
animationDuration: 600,
animationEasing: 'cubicOut',
};
}
function cleanAxis(theme) {
return {
axisLine: { lineStyle: { color: theme.axis, width: 0.8 } },
axisTick: { show: false },
splitLine: { lineStyle: { color: theme.grid, width: 0.5 } },
axisLabel: { color: theme.textSub, fontSize: 10 },
};
}
```
---
## Template 1: Insight Bar Chart
```javascript
const option = {
...baseOption(THEME, 'Q3 收入环比增长 47%', '各季度收入对比(万元)'),
xAxis: { type: 'category', data: ['Q1','Q2','Q3','Q4'], ...cleanAxis(THEME) },
yAxis: { type: 'value', ...cleanAxis(THEME) },
series: [{
type: 'bar', barWidth: '50%',
itemStyle: { borderRadius: [4, 4, 0, 0] },
data: [
{ value: 120, itemStyle: { color: '#E5E7EB' } },
{ value: 145, itemStyle: { color: '#E5E7EB' } },
{ value: 213, itemStyle: { color: '#3B82F6' } },
{ value: 180, itemStyle: { color: '#E5E7EB' } },
],
label: {
show: true, position: 'top', fontSize: 11, color: '#6B7280',
formatter: (p) => p.dataIndex === 2
? '{hl|' + p.value + '}'
: p.value,
rich: { hl: { fontWeight: 'bold', fontSize: 13, color: '#111827' } },
},
}],
};
```
---
## Template 2: Multi-Line Trend
```javascript
const option = {
...baseOption(THEME, '2024 年增长持续加速'),
legend: { right: 40, top: 20, textStyle: { color: '#6B7280', fontSize: 10 } },
xAxis: { type: 'category', data: months, boundaryGap: false, ...cleanAxis(THEME) },
yAxis: { type: 'value', ...cleanAxis(THEME) },
series: [
{
name: '2024', type: 'line', data: thisYear,
lineStyle: { width: 2.5 },
symbol: 'circle', symbolSize: 6,
itemStyle: { color: '#3B82F6' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(59,130,246,0.12)' },
{ offset: 1, color: 'rgba(59,130,246,0)' },
]),
},
},
{
name: '2023', type: 'line', data: lastYear,
lineStyle: { width: 1.5, type: 'dashed', color: '#D1D5DB' },
symbol: 'none', itemStyle: { color: '#D1D5DB' },
},
],
};
```
---
## Template 3: Candlestick (Finance)
```javascript
// Dark theme
const option = {
...baseOption(DARK, 'BTC/USDT 日K线'),
xAxis: { type: 'category', data: dates, ...cleanAxis(DARK) },
yAxis: { type: 'value', scale: true, ...cleanAxis(DARK) },
dataZoom: [
{ type: 'inside', start: 70, end: 100 },
{ type: 'slider', start: 70, end: 100, height: 20, bottom: 10,
borderColor: DARK.axis, fillerColor: 'rgba(59,130,246,0.15)',
textStyle: { color: DARK.textSub } },
],
series: [{
type: 'candlestick',
data: ohlcData, // [[open,close,low,high], ...]
itemStyle: {
color: '#22C55E', // Bullish (close > open)
color0: '#EF4444', // Bearish
borderColor: '#16A34A',
borderColor0: '#DC2626',
},
}],
};
```
---
## Template 4: Dashboard (Multi-Chart Linking)
⚠️ **ECharts multi-chart dashboard anti-overlap rules** (highest priority):
1. Maximum 4 subplots per canvas; more must be split into multiple HTML files
2. Each subplot's `grid` area must not overlap; maintain ≥5% safety margin between adjacent grids
3. Pie chart `center` and `radius` must not intrude into other subplots' grid areas
4. Same applies to radar chart's `radar.center` and `radar.radius`
5. Place legend in top/bottom common area, not inside subplots
### Simple Dual Chart (Bar + Pie)
```javascript
const option = {
...baseOption(THEME, '产品线收入分布'),
grid: [{ left: 60, right: '55%', top: 80, bottom: 50 }],
xAxis: [{ type: 'value', gridIndex: 0, ...cleanAxis(THEME) }],
yAxis: [{ type: 'category', data: products, gridIndex: 0, ...cleanAxis(THEME) }],
series: [
{
type: 'bar', data: revenues,
itemStyle: { borderRadius: [0, 4, 4, 0] },
barWidth: '60%',
},
{
type: 'pie', center: ['78%', '50%'], radius: ['35%', '55%'],
data: products.map((name, i) => ({ name, value: revenues[i] })),
label: { formatter: '{b}\n{d}%', fontSize: 10 },
itemStyle: { borderColor: '#fff', borderWidth: 2 },
},
],
};
```
### Four-Chart Dashboard (Safe Layout Template)
```javascript
// ⚠️ Key: grid areas precisely defined, no overlap, maintain safety margins
const option = {
...baseOption(THEME, '数据全景仪表盘'),
grid: [
// Top-left: bar chart
{ left: '5%', right: '55%', top: '12%', bottom: '55%' },
// Top-right: line chart
{ left: '55%', right: '5%', top: '12%', bottom: '55%' },
// Bottom-left: scatter plot
{ left: '5%', right: '55%', top: '55%', bottom: '5%' },
// Bottom-right area reserved for pie chart (pie uses center + radius, not grid)
],
xAxis: [
{ type: 'category', gridIndex: 0, data: categories1, ...cleanAxis(THEME) },
{ type: 'category', gridIndex: 1, data: categories2, ...cleanAxis(THEME), boundaryGap: false },
{ type: 'value', gridIndex: 2, ...cleanAxis(THEME) },
],
yAxis: [
{ type: 'value', gridIndex: 0, ...cleanAxis(THEME) },
{ type: 'value', gridIndex: 1, ...cleanAxis(THEME) },
{ type: 'value', gridIndex: 2, ...cleanAxis(THEME) },
],
series: [
// Top-left: bar chart
{
type: 'bar', xAxisIndex: 0, yAxisIndex: 0,
data: barData,
itemStyle: { borderRadius: [4, 4, 0, 0] },
},
// Top-right: line chart
{
type: 'line', xAxisIndex: 1, yAxisIndex: 1,
data: lineData,
smooth: true,
areaStyle: { opacity: 0.08 },
},
// Bottom-left: scatter plot
{
type: 'scatter', xAxisIndex: 2, yAxisIndex: 2,
data: scatterData,
symbolSize: 8,
},
// Bottom-right: pie chart (positioned via center in bottom-right quadrant)
{
type: 'pie',
center: ['77%', '72%'], // Positioned at bottom-right area center
radius: ['15%', '25%'], // Radius stays within bottom-right quadrant
data: pieData,
label: { formatter: '{b}\n{d}%', fontSize: 10 },
itemStyle: { borderColor: '#fff', borderWidth: 2 },
},
],
};
```
### Grid Safety Margin Quick Reference
| Layout | Grid Config | Safety Margin |
|------|----------|---------|
| Left-right dual | Left `right:'55%'` Right `left:'55%'` | 10% center gap |
| Top-bottom dual | Top `bottom:'55%'` Bottom `top:'55%'` | 10% center gap |
| 2x2 quad | Each quadrant 45%, 10% center gap | 5% margin on all sides |
| With pie/radar | Pie center+radius must not intrude grid | Pie radius ≤ 40% of available area |
### What If More Than 4 Subplots?
```javascript
// ❌ Wrong: 8 charts crammed into one canvas — all labels will inevitably overlap
// ✅ Correct: split into 2 HTML files
// dashboard_overview.html — 4 overview charts
// dashboard_detail.html — 4 detailed analysis charts
// Or use tab switching (ECharts toolbox doesn't support this, need custom HTML tabs)
```
---
## Template 5: Radar Chart
```javascript
const option = {
...baseOption(THEME, '团队能力评估'),
radar: {
indicator: dims.map(d => ({ name: d, max: 100 })),
axisName: { color: '#6B7280', fontSize: 10 },
splitArea: { areaStyle: { color: ['#FAFAFA', '#F5F5F5'] } },
splitLine: { lineStyle: { color: '#E5E7EB' } },
axisLine: { lineStyle: { color: '#E5E7EB' } },
},
series: [{
type: 'radar',
data: teams.map((t, i) => ({
name: t.name, value: t.scores,
lineStyle: { width: 2 },
areaStyle: { opacity: 0.08 },
itemStyle: { color: THEME.colors[i] },
})),
}],
legend: { bottom: 10, textStyle: { color: '#6B7280' } },
};
```
---
## Export to PNG (Playwright)
```python
import asyncio
from playwright.async_api import async_playwright
async def echarts_to_png(html_path, png_path, width=900, height=520):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(viewport={'width': width, 'height': height})
await page.goto(f'file://{html_path}', wait_until='networkidle')
await page.wait_for_timeout(800) # Wait for animation to complete
await page.locator('#chart').screenshot(path=png_path)
await browser.close()
print(f'{png_path}')
# asyncio.run(echarts_to_png('./output/chart.html', './output/chart.png'))
```
---
## Template 5: Tree (Interactive Only)
**⚠️ For static PNG export, use Playwright+CSS (see `mindmap-css.md`). ECharts tree connector length and node spacing cannot be finely controlled — static output is not aesthetically satisfying.**
**ECharts tree is suitable for interactive scenarios** (click expand/collapse, hover tooltip, zoom/drag). For PNG/PDF static output, the CSS approach looks better.
### Basic Usage
```javascript
const option = {
tooltip: { trigger: 'item', triggerOn: 'mousemove' },
series: [{
type: 'tree',
data: [treeData], // Tree-structured JSON data
layout: 'orthogonal', // Orthogonal layout (right-angle connectors)
orient: 'LR', // Direction: LR(left→right) / RL / TB(top→bottom) / BT
// Node spacing control (key params to prevent crowding)
initialTreeDepth: -1, // -1=expand all, positive=initial expand depth
// Label style
label: {
position: 'left', // Leaf node label position
verticalAlign: 'middle',
fontSize: 13,
fontFamily: 'PingFang SC, SimHei, sans-serif',
},
leaves: {
label: { position: 'right' } // Leaf labels on right
},
// Connector style
lineStyle: {
color: '#94A3B8',
width: 1.5,
curveness: 0.5, // Curvature, 0=straight, 0.5=natural curve
},
// Node style
itemStyle: {
borderWidth: 1.5,
},
// Animation
animationDuration: 550,
animationDurationUpdate: 750,
}]
};
```
### Tree Data Format
```javascript
const treeData = {
name: '中心主题',
children: [
{
name: '分支A',
children: [
{ name: '叶子1' },
{ name: '叶子2' },
{ name: '叶子3', children: [{ name: '更深叶子' }] }
]
},
{
name: '分支B',
children: [
{ name: '叶子4' },
{ name: '叶子5' }
]
}
]
};
```
### Node Style Customization (by Level)
```javascript
// Root node highlight
function styleTreeData(node, depth) {
const styles = [
{ // Root node
itemStyle: { color: '#3B82F6', borderColor: '#2563EB', borderWidth: 2 },
label: { fontSize: 18, fontWeight: 'bold', color: '#fff',
backgroundColor: '#3B82F6', borderRadius: 6, padding: [8, 16] }
},
{ // Level 1 branches
itemStyle: { color: '#60A5FA', borderColor: '#3B82F6' },
label: { fontSize: 15, fontWeight: 600, color: '#1E40AF',
backgroundColor: '#EFF6FF', borderColor: '#3B82F6',
borderWidth: 1.5, borderRadius: 6, padding: [6, 14] }
},
{ // Level 2
itemStyle: { color: '#93C5FD', borderColor: '#60A5FA' },
label: { fontSize: 13, color: '#1E40AF',
backgroundColor: '#F0F7FF', borderColor: '#93C5FD',
borderWidth: 1, borderRadius: 4, padding: [4, 10] }
},
{ // Level 3+ leaves
itemStyle: { color: '#BFDBFE', borderColor: '#93C5FD' },
label: { fontSize: 12, color: '#475569', padding: [3, 8] }
}
];
const style = styles[Math.min(depth, styles.length - 1)];
Object.assign(node, style);
if (node.children) {
node.children.forEach(child => styleTreeData(child, depth + 1));
}
}
styleTreeData(treeData, 0);
```
### Left-Right Distribution (Large Tree Mode)
When branches ≥ 5, use two trees for left-right expansion:
```javascript
function splitTree(data) {
const children = data.children || [];
// Alternate assignment by subtree size
const sorted = children.map((c, i) => ({ c, w: countNodes(c), i }))
.sort((a, b) => b.w - a.w);
const left = [], right = [];
let lw = 0, rw = 0;
sorted.forEach(({ c }) => {
if (lw <= rw) { left.push(c); lw += countNodes(c); }
else { right.push(c); rw += countNodes(c); }
});
return {
left: { name: data.name, children: left },
right: { name: data.name, children: right }
};
}
function countNodes(node) {
if (!node.children) return 1;
return 1 + node.children.reduce((s, c) => s + countNodes(c), 0);
}
// Dual tree series
const { left, right } = splitTree(treeData);
const option = {
series: [
{ type: 'tree', data: [right], orient: 'LR', left: '50%', width: '45%', /* ... */ },
{ type: 'tree', data: [left], orient: 'RL', right: '50%', width: '45%', /* ... */ },
]
};
```
### Recommended Canvas Size
| Node Count | Width | Height |
|--------|------|------|
| ≤ 15 | 900px | 500px |
| 16-30 | 1200px | 600px |
| 31-60 | 1600px | 800px |
| 60+ | 2000px | 1000px |
---
## Template 6: Relationship / Force-Directed Graph
**ECharts graph suits process relationships, org charts, knowledge graphs** — nodes auto-repel to avoid overlap, connectors auto-bind, supports categorical coloring.
### Basic Usage
```javascript
const option = {
tooltip: {},
legend: [{ data: categories.map(c => c.name) }],
series: [{
type: 'graph',
layout: 'force', // Force-directed auto layout
// Force model params (controls repulsion and attraction)
force: {
repulsion: 300, // Repulsion force (higher = more spread out, recommended 200-500)
gravity: 0.1, // Gravity (prevents nodes from flying too far)
edgeLength: [100, 200], // Edge length range
layoutAnimation: true,
},
roam: true, // Allow drag and zoom
draggable: true, // Allow dragging nodes
// Nodes
data: nodes,
// Edges
links: links,
// Categories (for coloring)
categories: categories,
// Labels
label: {
show: true,
position: 'right',
fontSize: 12,
fontFamily: 'PingFang SC, SimHei, sans-serif',
},
// Connector style
lineStyle: {
color: 'source', // Edge color follows source node
curveness: 0.3, // Curvature
width: 1.5,
},
// Highlight effect
emphasis: {
focus: 'adjacency', // Highlight adjacent nodes on hover
lineStyle: { width: 3 },
},
}]
};
```
### Data Format
```javascript
const categories = [
{ name: '核心系统', itemStyle: { color: '#3B82F6' } },
{ name: '数据层', itemStyle: { color: '#10B981' } },
{ name: '应用层', itemStyle: { color: '#F59E0B' } },
];
const nodes = [
{ name: 'API Gateway', category: 0, symbolSize: 40 },
{ name: 'User Service', category: 0, symbolSize: 30 },
{ name: 'MySQL', category: 1, symbolSize: 35 },
{ name: 'Redis', category: 1, symbolSize: 28 },
{ name: 'Web App', category: 2, symbolSize: 32 },
];
const links = [
{ source: 'API Gateway', target: 'User Service' },
{ source: 'User Service', target: 'MySQL' },
{ source: 'User Service', target: 'Redis' },
{ source: 'Web App', target: 'API Gateway' },
];
```
### Flowchart Mode (Fixed Layout)
When you don't want force-directed auto-layout, fix node positions:
```javascript
const option = {
series: [{
type: 'graph',
layout: 'none', // Fixed layout, positions determined by x/y
data: [
{ name: '开始', x: 300, y: 50, symbolSize: 40,
itemStyle: { color: '#EFF6FF', borderColor: '#3B82F6', borderWidth: 2 } },
{ name: '处理', x: 300, y: 200, symbolSize: 35 },
{ name: '判断', x: 300, y: 350, symbolSize: 35,
symbol: 'diamond',
itemStyle: { color: '#FFF7ED', borderColor: '#F59E0B', borderWidth: 2 } },
{ name: '结束', x: 300, y: 500, symbolSize: 40 },
],
links: [
{ source: '开始', target: '处理' },
{ source: '处理', target: '判断' },
{ source: '判断', target: '结束', label: { show: true, formatter: '通过' } },
],
lineStyle: { color: '#94A3B8', width: 2, curveness: 0 },
edgeSymbol: ['', 'arrow'],
edgeSymbolSize: [0, 10],
}]
};
```
---
## ECharts vs Other Frameworks
| Capability | ECharts | Plotly | Chart.js |
|------|---------|--------|----------|
| Canvas rendering (big data) | ✅ Millions | ❌ SVG-based | ✅ But limited |
| Chinese docs | ✅ Official | ❌ English | ❌ English |
| Candlestick | ✅ Built-in | ❌ Plugin needed | ❌ None |
| Maps | ✅ Built-in China map | ✅ mapbox | ❌ None |
| 3D Charts | ✅ echarts-gl | ✅ Built-in | ❌ None |
| No Node.js needed | ✅ CDN import | ❌ Needs plotly.js | ✅ CDN |
| Server-side rendering | ✅ node-echarts | ✅ orca | ✅ chartjs-node |