Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# ⚠️ STRUCTURAL DIAGRAM IRON LAWS
These rules apply to Playwright+CSS structural diagrams (flowcharts, mind maps, radial grids, org charts). They are enforced by the template files. Violating any = task failure.
## 1. ZERO OVERLAP
No element may overlap another:
- Arrows/connectors must not cross over text boxes
- Connectors must not pass through node bodies
- Text boxes / nodes must not overlap each other
- Labels must not obscure any graphic element
**Post-generation**: verify every element has clear separation. If overlap exists, fix before delivery — enlarge canvas, increase spacing, or reduce content.
## 2. LAYOUT MUST HAVE HIERARCHY
Forbidden: all nodes same size, same level, mechanically tiled in a flat grid.
Required:
- Primary nodes visually larger/bolder than secondary nodes
- Annotation nodes clearly subordinate (smaller, muted color)
- Spacing between groups > spacing within groups
- Clear reading path (top→bottom, left→right, or center→outward)
**Squint test**: if every box looks identical, the layout has failed.
## 3. NODE BACKGROUND COLORS
**🚫 Forbidden as background (too saturated for large fills):**
| Color | Forbidden Hex |
|-------|--------------|
| Pure blue | `#3B82F6`, `#2563EB`, `#1D4ED8` |
| Pure green | `#10B981`, `#059669`, `#22C55E` |
| Pure red | `#EF4444`, `#DC2626`, `#F87171` |
| Pure purple | `#8B5CF6`, `#7C3AED`, `#A855F7` |
| Pure amber | `#F59E0B`, `#D97706`, `#FB923C` |
| Any color: R/G/B > 0xCC and saturation > 50% | — |
**✅ Allowed as background:**
| Color | Hex | Usage |
|-------|-----|-------|
| Ice blue | `#EFF6FF`, `#DBEAFE` | Normal step nodes |
| Mint green | `#F0FDF4`, `#D1FAE5` | Success/pass nodes |
| Light amber | `#FFF7ED`, `#FEF3C7` | Decision/warning nodes |
| Lavender | `#F5F3FF`, `#EDE9FE` | End/terminal nodes |
| Light gray | `#F8FAFC`, `#F1F5F9` | Group containers |
| White | `#FFFFFF` | Default canvas |
**Rule: Saturated colors go on BORDERS (2px) and TEXT only. Backgrounds stay pale.**

199
skills/charts/references/d3.md Executable file
View File

@@ -0,0 +1,199 @@
# D3.js Template Library
> **⚠️ Before writing any code, read [`_rules.md`](references/_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
```html
<!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
```javascript
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
```javascript
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.
```javascript
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
```javascript
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) |

View File

@@ -0,0 +1,651 @@
# 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 |

View File

@@ -0,0 +1,617 @@
# matplotlib Template Library
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
## Environment Initialization (Must Execute Before Each Plot)
```python
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
# ═══ Chinese Font Setup ═══
# SimHei is the default; other fonts available:
# SimSun.ttf (Songti, formal docs), SimKai.ttf (Kaiti, artistic),
# SarasaMonoSC-*.ttf (monospace CJK, code scenes)
# Run `fc-list :lang=zh` for system fonts (PingFang SC, Heiti TC, etc.)
# Font path: adjust for your system. Common locations:
# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf'
# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
# Custom: './fonts/SimHei.ttf'
import os
SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf')
matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH)
# ═══ Global Style ═══
plt.rcParams.update({
# Font
'font.sans-serif': ['SimHei'],
'axes.unicode_minus': False,
# Background
'figure.facecolor': '#FFFFFF',
'axes.facecolor': '#FFFFFF',
# Border: only keep left and bottom
'axes.edgecolor': '#E5E7EB',
'axes.linewidth': 0.8,
'axes.spines.top': False,
'axes.spines.right': False,
# Grid: off by default
'axes.grid': False,
# Ticks: hide tick marks
'xtick.major.size': 0,
'ytick.major.size': 0,
'xtick.labelsize': 9,
'ytick.labelsize': 9,
# Title
'axes.labelsize': 10,
'axes.titlesize': 16,
'axes.titleweight': 'bold',
'axes.titlepad': 16,
# Legend: no frame
'legend.frameon': False,
'legend.fontsize': 9,
# Export
'figure.dpi': 200,
'savefig.dpi': 200,
'savefig.bbox': 'tight',
'savefig.facecolor': '#FFFFFF',
'savefig.pad_inches': 0.3,
})
```
## Color Constants
```python
# ─── Cool Colors (Business/Tech) ───
C_BLUE = '#3B82F6'
C_CYAN = '#06B6D4'
C_PURPLE = '#8B5CF6'
C_AMBER = '#F59E0B'
C_RED = '#EF4444'
C_GREEN = '#10B981'
COOL = [C_BLUE, C_CYAN, C_PURPLE, C_AMBER, C_RED, C_GREEN]
# ─── Warm Colors (Warmth/Creative) ───
WARM = ['#F59E0B', '#EF4444', '#8B5CF6', '#3B82F6', '#10B981', '#EC4899']
# ─── Academic Grayscale ───
ACADEMIC = ['#111827', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6']
# ─── Colorblind-Safe (Paper Preferred) ───
CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']
# ─── Grayscale ───
G900, G700, G500, G400, G300, G200, G100, G50 = \
'#111827', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6', '#F9FAFB'
# ─── Gain/Loss ───
POS = '#22C55E'
NEG = '#EF4444'
```
## Helper Functions
```python
def clean_axis(ax, grid=True):
"""Clean axis: remove top and right borders, add faint grid"""
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
if grid:
ax.yaxis.grid(True, alpha=0.08, color=G300)
ax.set_axisbelow(True)
def add_value_labels(ax, bars, values, highlight_idx=None, fmt='{:,.0f}',
offset_ratio=0.02):
"""Add value labels on top of bars, highlight bar in bold.
⚠️ Automatically extends Y-axis upper limit to ensure labels don't overflow chart area."""
max_val = max(values)
for i, (bar, val) in enumerate(zip(bars, values)):
is_hl = (i == highlight_idx) if highlight_idx is not None else False
ax.text(bar.get_x() + bar.get_width()/2,
bar.get_height() + max_val * offset_ratio,
fmt.format(val), ha='center', va='bottom',
fontsize=10 if is_hl else 9,
color=G900 if is_hl else G400,
fontweight='bold' if is_hl else 'normal')
# ⚠️ Critical: extend Y-axis upper limit to leave enough space for labels (at least 15%)
ax.set_ylim(0, max_val * 1.18)
def save(fig, path, dpi=200):
"""Unified save — prefer constrained_layout, fallback to tight_layout"""
if not fig.get_constrained_layout():
try:
fig.tight_layout()
except Exception:
pass
fig.savefig(path, dpi=dpi, facecolor='white', bbox_inches='tight')
plt.close(fig)
import os
size_kb = os.path.getsize(path) / 1024
print(f'{path} ({size_kb:.0f}KB)')
```
### Label Text Avoidance (adjustText)
When charts have multiple label annotations (e.g., scatter plot labels, box plot annotations), **must use adjustText library** to prevent text overlap:
```python
# pip install adjustText
from adjustText import adjust_text
# Call after adding all annotations
texts = []
for i, (x, y, label) in enumerate(zip(x_data, y_data, labels)):
texts.append(ax.text(x, y, label, fontsize=9, color='#374151'))
# Auto-avoidance — pass all text objects, adjustText will move them to avoid overlap
adjust_text(texts, ax=ax,
arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8),
force_text=(0.5, 0.8), # Force to push text away
force_points=(0.3, 0.5), # Repulsion from data points
expand=(1.2, 1.4)) # Expansion factor for text bbox
```
---
## Template 1: Insight Bar Chart
**Scenario**: Emphasize outstanding performance of one data item. Gray out others, highlight the focus.
```python
def insight_bar(labels, values, highlight_idx, title,
highlight_color=C_BLUE, save_path='insight_bar.png'):
fig, ax = plt.subplots(figsize=(10, 6))
colors = [G200] * len(labels)
colors[highlight_idx] = highlight_color
bars = ax.bar(labels, values, color=colors, width=0.6,
zorder=3, edgecolor='white', linewidth=0.5)
add_value_labels(ax, bars, values, highlight_idx)
ax.set_title(title, loc='left')
# ⚠️ add_value_labels already sets ylim automatically, no need to repeat here
clean_axis(ax)
save(fig, save_path)
```
---
## Template 2: Trend Comparison Line Chart
**Scenario**: This year vs last year, actual vs target. Main line in colored solid, comparison line in gray dashed.
```python
def trend_compare(x, y_main, y_ref, label_main, label_ref, title,
color=C_BLUE, save_path='trend_compare.png'):
fig, ax = plt.subplots(figsize=(12, 6))
# Comparison line (gray dashed, at bottom layer)
ax.plot(x, y_ref, color=G300, linewidth=1.5, linestyle='--', zorder=2)
ax.text(len(x)-0.5, y_ref[-1], label_ref, color=G400, fontsize=9, va='center')
# Main line (colored solid + white-center dots)
ax.plot(x, y_main, color=color, linewidth=2.5, marker='o', markersize=5,
markerfacecolor='white', markeredgewidth=2, markeredgecolor=color, zorder=3)
ax.text(len(x)-0.5, y_main[-1], f'{label_main} {y_main[-1]:,.0f}',
color=color, fontsize=10, fontweight='bold', va='center')
# Difference area
ax.fill_between(range(len(x)), y_ref, y_main, alpha=0.06, color=color)
ax.set_title(title, loc='left')
ax.set_xticks(range(len(x)))
ax.set_xticklabels(x)
clean_axis(ax)
save(fig, save_path)
```
---
## Template 3: Grouped Bar Chart
**Scenario**: Comparison of multiple categories across multiple dimensions.
```python
def grouped_bar(labels, datasets, series_names, title,
colors=None, save_path='grouped_bar.png'):
if colors is None:
colors = COOL[:len(datasets)]
fig, ax = plt.subplots(figsize=(12, 6))
n = len(datasets)
width = 0.7 / n
x = np.arange(len(labels))
for i, (data, name, color) in enumerate(zip(datasets, series_names, colors)):
offset = (i - n/2 + 0.5) * width
ax.bar(x + offset, data, width=width*0.85, color=color,
label=name, zorder=3, edgecolor='white', linewidth=0.3)
ax.set_title(title, loc='left')
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.set_ylim(0, max(max(d) for d in datasets) * 1.20) # Leave 20% space for labels
ax.legend(loc='upper right', ncol=n)
clean_axis(ax)
save(fig, save_path)
```
---
## Template 4: Horizontal Ranking Chart
**Scenario**: Rankings / Top N. Progressive highlight for top items.
```python
def ranking_bar(labels, values, title, top_n=3,
color=C_BLUE, save_path='ranking.png'):
from matplotlib.colors import to_rgba
sorted_pairs = sorted(zip(labels, values), key=lambda x: x[1])
labels_s, values_s = zip(*sorted_pairs)
fig, ax = plt.subplots(figsize=(10, max(6, len(labels)*0.45)))
bar_colors = [G200] * len(labels_s)
for i in range(len(labels_s) - top_n, len(labels_s)):
progress = (i - (len(labels_s) - top_n)) / max(top_n - 1, 1)
bar_colors[i] = to_rgba(color, 0.35 + 0.65 * progress)
bars = ax.barh(range(len(labels_s)), values_s, color=bar_colors,
height=0.6, zorder=3, edgecolor='white', linewidth=0.3)
for i, (bar, val) in enumerate(zip(bars, values_s)):
is_top = i >= len(labels_s) - top_n
ax.text(bar.get_width() + max(values_s)*0.01,
bar.get_y() + bar.get_height()/2,
f'{val:,.0f}', va='center', fontsize=9,
color=G900 if is_top else G400)
ax.set_yticks(range(len(labels_s)))
ax.set_yticklabels(labels_s)
ax.set_title(title, loc='left')
ax.spines['bottom'].set_visible(False)
ax.xaxis.set_visible(False)
save(fig, save_path)
```
---
## Template 5: Donut Chart
**Scenario**: Proportion distribution (max 5 slices, avoid if possible — bar charts are usually better).
```python
def donut(labels, values, title, center_text=None,
colors=None, save_path='donut.png'):
if colors is None:
colors = COOL[:len(labels)]
fig, ax = plt.subplots(figsize=(8, 8))
wedges, _, autotexts = ax.pie(
values, labels=None, colors=colors, autopct='%1.0f%%',
startangle=90, pctdistance=0.78,
wedgeprops=dict(width=0.35, edgecolor='white', linewidth=2))
for t in autotexts:
t.set_fontsize(10)
t.set_fontweight('bold')
if center_text:
ax.text(0, 0.06, str(center_text), ha='center', va='center',
fontsize=28, fontweight='bold', color=G900)
ax.text(0, -0.1, '总计', ha='center', va='center', fontsize=11, color=G500)
ax.legend(wedges, labels, loc='center left',
bbox_to_anchor=(1, 0.5), fontsize=10)
ax.set_title(title, loc='center', pad=20)
save(fig, save_path)
```
---
## Template 6: Scatter Plot + Trend Line
**Scenario**: Two-variable correlation analysis.
```python
def scatter_trend(x, y, title, xlabel, ylabel,
color=C_BLUE, save_path='scatter.png'):
fig, ax = plt.subplots(figsize=(10, 7))
ax.scatter(x, y, c=color, s=50, alpha=0.6,
edgecolors='white', linewidth=1, zorder=3)
# Trend line
z = np.polyfit(x, y, 1)
p = np.poly1d(z)
x_line = np.linspace(min(x), max(x), 100)
ax.plot(x_line, p(x_line), color=G400, linewidth=1.5,
linestyle='--', zorder=2, alpha=0.7)
ax.set_title(title, loc='left')
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
clean_axis(ax, grid=True)
ax.xaxis.grid(True, alpha=0.08, color=G300)
save(fig, save_path)
```
---
## Template 7: Heatmap
**Scenario**: Matrix data, correlations, time × category.
```python
def heatmap(data, row_labels, col_labels, title,
cmap_color=C_BLUE, save_path='heatmap.png'):
from matplotlib.colors import LinearSegmentedColormap
cmap = LinearSegmentedColormap.from_list('bc', ['#FFFFFF', cmap_color])
fig, ax = plt.subplots(
figsize=(max(8, len(col_labels)*1.2), max(6, len(row_labels)*0.6)))
arr = np.array(data)
im = ax.imshow(arr, cmap=cmap, aspect='auto')
vmax = arr.max()
for i in range(len(row_labels)):
for j in range(len(col_labels)):
val = arr[i][j]
color = 'white' if val > vmax * 0.6 else G700
ax.text(j, i, f'{val:.1f}', ha='center', va='center',
fontsize=9, color=color)
ax.set_xticks(range(len(col_labels)))
ax.set_yticks(range(len(row_labels)))
ax.set_xticklabels(col_labels)
ax.set_yticklabels(row_labels)
ax.set_title(title, loc='left', pad=16)
cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.08, shrink=0.8)
cbar.outline.set_visible(False)
cbar.ax.tick_params(labelsize=8) # colorbar ticks should not be too large
for spine in ax.spines.values():
spine.set_visible(True)
spine.set_color(G200)
save(fig, save_path)
```
---
## Template 8: KPI Metric Cards
**Scenario**: Dashboard top key number display.
```python
def kpi_cards(metrics, save_path='kpi.png'):
"""
metrics: [{'label': '总收入', 'value': '12.8M',
'change': '+23%', 'positive': True}, ...]
"""
from matplotlib.patches import FancyBboxPatch
n = len(metrics)
fig, axes = plt.subplots(1, n, figsize=(3.8*n, 2.8))
if n == 1: axes = [axes]
for ax, m in zip(axes, metrics):
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off')
bg = FancyBboxPatch((0.05, 0.05), 0.9, 0.9,
boxstyle='round,pad=0.05', facecolor=G50, edgecolor=G200, linewidth=0.8)
ax.add_patch(bg)
ax.text(0.5, 0.75, m['label'], ha='center', va='center',
fontsize=10, color=G500)
ax.text(0.5, 0.45, m['value'], ha='center', va='center',
fontsize=24, fontweight='bold', color=G900)
if 'change' in m:
is_pos = m.get('positive', True)
ax.text(0.5, 0.18,
f'{"" if is_pos else ""} {m["change"]}',
ha='center', va='center', fontsize=11,
color=POS if is_pos else NEG, fontweight='bold')
save(fig, save_path)
```
---
## Template 9: Radar Chart
**Scenario**: Multi-dimensional capability comparison (max 8 dimensions, max 3 groups).
```python
def radar(categories, datasets, series_names, title,
colors=None, save_path='radar.png'):
if colors is None:
colors = COOL[:len(datasets)]
N = len(categories)
angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
angles += angles[:1] # Close the polygon
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, fontsize=10)
ax.yaxis.set_visible(False)
# Grid beautification
ax.spines['polar'].set_color(G200)
ax.grid(color=G200, linewidth=0.5, alpha=0.5)
for data, name, color in zip(datasets, series_names, colors):
vals = data + data[:1] # Close the polygon
ax.plot(angles, vals, color=color, linewidth=2, label=name)
ax.fill(angles, vals, color=color, alpha=0.08)
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
ax.set_title(title, pad=30, fontsize=16, fontweight='bold')
save(fig, save_path)
```
---
## Template 10: Waterfall Chart
**Scenario**: Show incremental changes from start to end value.
```python
def waterfall(labels, values, title, save_path='waterfall.png'):
"""labels and values correspond, positive=increase negative=decrease, last item auto-treated as total"""
fig, ax = plt.subplots(figsize=(12, 6))
cumulative = [0]
for v in values[:-1]:
cumulative.append(cumulative[-1] + v)
bar_colors = []
for i, v in enumerate(values):
if i == len(values) - 1:
bar_colors.append(C_BLUE) # Total
elif v >= 0:
bar_colors.append(POS) # Increase
else:
bar_colors.append(NEG) # Decrease
bottoms = []
for i, v in enumerate(values):
if i == len(values) - 1:
bottoms.append(0) # Total starts from 0
elif v >= 0:
bottoms.append(cumulative[i])
else:
bottoms.append(cumulative[i] + v)
bars = ax.bar(labels, [abs(v) for v in values], bottom=bottoms,
color=bar_colors, width=0.6, edgecolor='white', linewidth=0.5, zorder=3)
# Connecting lines
for i in range(len(values) - 2):
y = cumulative[i+1]
ax.plot([i+0.3, i+0.7], [y, y], color=G300, linewidth=0.8, zorder=2)
# Value labels
for i, (bar, val) in enumerate(zip(bars, values)):
y_pos = bar.get_y() + bar.get_height() + max(abs(v) for v in values)*0.01
prefix = '+' if val > 0 and i < len(values)-1 else ''
ax.text(bar.get_x()+bar.get_width()/2, y_pos,
f'{prefix}{val:,.0f}', ha='center', va='bottom',
fontsize=9, color=G700)
ax.set_title(title, loc='left')
clean_axis(ax)
save(fig, save_path)
```
---
## Template 11: Multi-Subplot Dashboard (GridSpec Precision Layout)
**Scenario**: Combine multiple subplots into a dashboard. ⚠️ Max 4 subplots per canvas, split if exceeded.
**Core Principle**: Use `GridSpec` for precise control of each subplot's position and spacing, don't rely on `tight_layout()`.
```python
import matplotlib.gridspec as gridspec
def dashboard(data_dict, title, save_path='dashboard.png'):
"""
2x2 dashboard layout example.
data_dict contains data needed for each subplot.
"""
# ⚠️ Use constrained_layout instead of tight_layout
fig = plt.figure(figsize=(16, 12), constrained_layout=True)
fig.suptitle(title, fontsize=20, fontweight='bold', y=0.98)
# GridSpec: precise spacing control
gs = gridspec.GridSpec(2, 2, figure=fig,
wspace=0.35, # Column spacing (at least 0.3)
hspace=0.35, # Row spacing (at least 0.3)
left=0.08, right=0.92,
top=0.92, bottom=0.08)
# ─── Top-left: bar chart ───
ax1 = fig.add_subplot(gs[0, 0])
ax1.set_title('Quarterly Revenue', loc='left', fontsize=13, fontweight='bold')
# ... binddata ...
clean_axis(ax1)
# ─── Top-right: line chart ───
ax2 = fig.add_subplot(gs[0, 1])
ax2.set_title('Monthly Trend', loc='left', fontsize=13, fontweight='bold')
# ... bind data ...
clean_axis(ax2)
# ─── Bottom-left: pie chart ───
ax3 = fig.add_subplot(gs[1, 0])
ax3.set_title('Category Share', loc='left', fontsize=13, fontweight='bold')
# ... bind data ...
# ─── Bottom-right: scatter plot ───
ax4 = fig.add_subplot(gs[1, 1])
ax4.set_title('Conversion Analysis', loc='left', fontsize=13, fontweight='bold')
# ... bind data ...
clean_axis(ax4)
fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close(fig)
```
### Dashboard Layout Golden Rules
| Rule | Description |
|------|-------------|
| **Max 4 subplots** | Split into multiple charts if exceeded |
| **Use `constrained_layout=True`** | Smarter than `tight_layout()`, auto-avoids labels |
| **`wspace/hspace ≥ 0.3`** | Subplot spacing too small causes overlap |
| **Independent title per subplot** | Use `ax.set_title()` instead of `fig.suptitle()` |
| **Consistent font sizes** | Subtitle 13px, axis labels 10px, data labels 9px |
| **Colorbar in separate column** | If a subplot needs colorbar, allocate `gs[0, 2]` separately |
| **Don't mix legend and direct labels** | Use either all legends or all direct labels in a dashboard |
### Safe Layout with Colorbar
```python
# When a subplot needs a colorbar, use 3-column layout, rightmost column for colorbar
gs = gridspec.GridSpec(2, 3, figure=fig,
width_ratios=[1, 1, 0.05], # Third column very narrow, dedicated to colorbar
wspace=0.4, hspace=0.35)
ax_heat = fig.add_subplot(gs[0, 1])
im = ax_heat.imshow(data, cmap='Blues')
# Colorbar placed in its own subplot position, won't obscure any content
cbar_ax = fig.add_subplot(gs[0, 2])
fig.colorbar(im, cax=cbar_ax)
cbar_ax.set_ylabel('Value', fontsize=10)
```
### Split Strategy for More Than 4 Subplots
```python
# ❌ Wrong: 8 subplots crammed into one canvas
fig, axes = plt.subplots(2, 4, figsize=(20, 10)) # Everything becomes unreadable
# ✅ Correct: split into 2 figures, 4 subplots each
# Figure 1: overview metrics
fig1 = plt.figure(figsize=(16, 12), constrained_layout=True)
# ... 4 subplots ...
fig1.savefig('dashboard_overview.png', dpi=200)
# Figure 2: detailed analysis
fig2 = plt.figure(figsize=(16, 12), constrained_layout=True)
# ... 4 subplots ...
fig2.savefig('dashboard_detail.png', dpi=200)
```

View File

@@ -0,0 +1,797 @@
# Mermaid Template Library
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
Mermaid is the best "text-as-diagram" solution — write structural diagrams with Markdown-like syntax, zero design skills needed.
Best for: flowcharts, sequence diagrams, architecture diagrams, Gantt charts, class diagrams, ER diagrams, mind maps, state diagrams, pie charts, Git branch graphs.
**Core advantages**: text is version-controllable, minimal maintenance cost, high rendering consistency, CJK support.
## ⚠️ Flowchart Quality Rules (Highest Priority)
### Font Size Control
Mermaid font sizes are controlled via `themeVariables` and CSS:
- `fontSize`: Global font size, recommended `14px`-`16px`, **no less than 12px**
- Node text is controlled via `fontSize` or `%%{init:}%%` directive
- Annotations/footnotes use subgraph titles or separate nodes, font size no less than `11px`
### Connectors & Spacing
```javascript
flowchart: {
padding: 32, // Node padding (CJK needs more space)
nodeSpacing: 80, // Horizontal spacing between nodes (default 50 too tight, 60 still not enough)
rankSpacing: 80, // Vertical spacing between ranks
curve: 'basis', // Connection curve style, consistent across the chart
}
```
**Connector style must be consistent throughout the chart**: do not mix straight, curved, and polylines in the same diagram. Mermaid controls this globally via the `curve` parameter.
### ⚠️ Mermaid Flowchart Hard Constraints (MANDATORY)
The following constraints are enforced **when generating Mermaid flowchart code**, not as post-checks:
1. **Node text must be wrapped in quotes**: `A["用户登录"]` ✅ / `A[用户登录]` ❌ — quotes prevent CJK special characters from causing parse errors
2. **Max 10 CJK characters per line in node text**: exceed → use `<br>` to break → `A["用户身份<br>验证模块"]`
3. **Max 5 nodes per subgraph**: exceed → split into multiple subgraphs or switch to CSS approach
4. **Max 10 total nodes**: exceed → switch to CSS flowchart template in `references/playwright-css.md`
5. **Max 6 CJK characters in connector labels**: `-->|验证通过|` ✅ / `-->|用户身份验证通过后跳转|`
6. **Config params must use enlarged values**: `padding: 32, nodeSpacing: 80, rankSpacing: 80`
## Rendering Methods
### Method 1: Playwright + HTML (Recommended, export PNG/SVG/PDF)
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
display: flex;
justify-content: center;
padding: 48px;
}
#diagram { width: fit-content; min-width: 800px; }
</style>
</head>
<body>
<div id="diagram">
<pre class="mermaid">
<!-- Mermaid code goes here -->
</pre>
</div>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
// See "Theme configuration" below
},
flowchart: {
curve: 'basis',
padding: 32, // Node padding (CJK chars 50% wider than Latin, need more space)
nodeSpacing: 80, // Horizontal spacing (prevents CJK node overlap)
rankSpacing: 80, // Vertical spacing between ranks (prevents overlap between levels)
htmlLabels: true, // Enable HTML label rendering, supports line breaks
wrappingWidth: 160, // Max text width before auto-wrap (160 wraps earlier than 200, prevents overly wide nodes)
},
sequence: { mirrorActors: false, messageAlign: 'center' },
gantt: { titleTopMargin: 25, barHeight: 24, barGap: 6 },
});
</script>
</body>
</html>
```
**Python screenshot script**:
```python
import asyncio
from playwright.async_api import async_playwright
async def mermaid_to_png(html_path, png_path, width=1400, scale=2):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
viewport={'width': width, 'height': 800},
device_scale_factor=scale
)
await page.goto(f'file://{html_path}', wait_until='load', timeout=30000)
# Wait for Mermaid SVG to render
await page.wait_for_selector('#diagram svg', timeout=15000)
await page.wait_for_timeout(1000)
# ⚠️ Read SVG's ACTUAL rendered size (not CSS box model!)
# Mermaid SVGs often overflow their CSS container — getBBox/clientRect
# returns the true size, while CSS bounding_box() returns the clipped box.
svg_size = await page.evaluate('''() => {
const svg = document.querySelector('#diagram svg');
if (!svg) return null;
const r = svg.getBoundingClientRect();
return { width: r.width, height: r.height };
}''')
el = page.locator('#diagram')
css_bbox = await el.bounding_box()
svg_w = svg_size['width'] if svg_size else width
svg_h = svg_size['height'] if svg_size else 800
css_w = css_bbox['width'] if css_bbox else width
css_h = css_bbox['height'] if css_bbox else 800
# Use the LARGER of CSS box and SVG actual size
fit_w = max(width, int(max(svg_w, css_w) + 200))
fit_h = int(max(svg_h, css_h) + 200)
await page.set_viewport_size({'width': fit_w, 'height': fit_h})
await page.wait_for_timeout(500)
await el.screenshot(path=png_path)
await browser.close()
import os
print(f'{png_path} ({os.path.getsize(png_path)/1024:.0f}KB)')
# asyncio.run(mermaid_to_png('./output/diagram.html', './output/diagram.png'))
```
> **⚠️ CRITICAL: CSS `bounding_box()` vs SVG actual size**
>
> Mermaid generates SVGs that can be wider/taller than their CSS container. `bounding_box()` (Playwright) and `getBoundingClientRect()` on the container return **CSS box model size**, which may be smaller than the SVG's viewBox.
>
> **Always read the SVG element's own `getBoundingClientRect()`** via `page.evaluate()` and use `max(css_size, svg_size)` for viewport dimensions. This is the root cause of the "right side clipped" bug.
>
> Also: `wait_until='load'` is preferred over `'networkidle'` because Mermaid initializes on DOM load. `'networkidle'` can timeout if CDN is slow.
### Method 2: Mermaid CLI (mmdc, command line)
```bash
# Installation
npm install -g @mermaid-js/mermaid-cli
# Usage: .mmd file → PNG/SVG/PDF
mmdc -i diagram.mmd -o diagram.png -w 1200 -b transparent
mmdc -i diagram.mmd -o diagram.svg
mmdc -i diagram.mmd -o diagram.pdf
# Specify theme configuration
mmdc -i diagram.mmd -o diagram.png --configFile mermaid-config.json
```
`mermaid-config.json` example:
```json
{
"theme": "base",
"themeVariables": {
"primaryColor": "#EFF6FF",
"primaryBorderColor": "#3B82F6",
"primaryTextColor": "#1E293B",
"lineColor": "#94A3B8",
"secondaryColor": "#F0FDF4",
"tertiaryColor": "#FFF7ED"
}
}
```
### Method 3: Online Preview
Paste code at [mermaid.live](https://mermaid.live) for instant preview and export.
---
## Theme Configuration (Design System Integration)
Mermaid uses `theme: 'base'` + `themeVariables` for fully custom colors.
The following themes align with the charts skill mood color system:
### Business Professional (Default)
```javascript
themeVariables: {
primaryColor: '#EFF6FF', // Node background (very light blue)
primaryBorderColor: '#3B82F6', // Node border (blue)
primaryTextColor: '#1E293B', // Node text (dark gray-blue)
lineColor: '#94A3B8', // Connectors (gray)
secondaryColor: '#F0FDF4', // Secondary nodes (very light green)
secondaryBorderColor: '#10B981',
secondaryTextColor: '#1E293B',
tertiaryColor: '#FFF7ED', // Tertiary nodes (very light amber)
tertiaryBorderColor: '#F59E0B',
tertiaryTextColor: '#1E293B',
noteBkgColor: '#F8FAFC', // Note background
noteTextColor: '#6B7280',
noteBorderColor: '#E2E8F0',
fontSize: '14px',
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
}
```
### Tech Dark
```javascript
themeVariables: {
primaryColor: '#1E293B',
primaryBorderColor: '#3B82F6',
primaryTextColor: '#F1F5F9',
lineColor: '#475569',
secondaryColor: '#0F2E1F',
secondaryBorderColor: '#10B981',
secondaryTextColor: '#F1F5F9',
tertiaryColor: '#1A1625',
tertiaryBorderColor: '#8B5CF6',
tertiaryTextColor: '#F1F5F9',
noteBkgColor: '#0F172A',
noteTextColor: '#94A3B8',
noteBorderColor: '#334155',
fontSize: '14px',
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
background: '#0F172A',
}
```
---
## Template 1: Flowchart
The most common chart type. Supports directions: `TB` (top→bottom), `LR` (left→right), `BT`, `RL`.
```mermaid
flowchart TB
A[开始] --> B{条件判断}
B -->|是| C[执行操作A]
B -->|否| D[执行操作B]
C --> E[结束]
D --> E
style A fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
style B fill:#FFF7ED,stroke:#F59E0B,stroke-width:2px,color:#1E293B
style C fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B
style D fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B
style E fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
```
### Node Shape Quick Reference
| Syntax | Shape | Use For |
|------|------|--------|
| `A[text]` | Rectangle | Steps/Actions |
| `A(text)` | Rounded rect | General nodes |
| `A([text])` | Stadium | Start/End |
| `A{text}` | Diamond | Decision |
| `A{{text}}` | Hexagon | Preparation |
| `A[/text/]` | Parallelogram | Input/Output |
| `A((text))` | Circle | Connector |
| `A>text]` | Flag | Event/Signal |
### Subgraphs (Grouping)
```mermaid
flowchart LR
subgraph 前端["🖥️ 前端"]
A[React App] --> B[API 调用]
end
subgraph 后端["⚙️ 后端"]
C[FastAPI] --> D[(PostgreSQL)]
end
B --> C
style 前端 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1px
style 后端 fill:#F0FDF4,stroke:#10B981,stroke-width:1px
```
---
## Template 2: Sequence Diagram
Shows interaction sequence between systems/actors.
```mermaid
sequenceDiagram
actor 用户
participant 前端 as 🖥️ 前端
participant API as ⚙️ API 网关
participant DB as 🗄️ 数据库
用户->>前端: 点击登录
前端->>API: POST /auth/login
API->>DB: 查询用户
DB-->>API: 用户信息
alt 验证成功
API-->>前端: 200 + JWT Token
前端-->>用户: 跳转首页
else 验证失败
API-->>前端: 401 未授权
前端-->>用户: 显示错误提示
end
Note over 前端,API: Token 有效期 24 小时
```
### Arrow Types
| Syntax | Meaning |
|------|------|
| `->>` | Solid arrow (synchronous call) |
| `-->>` | Dashed arrow (return/response) |
| `-x` | Solid with x (failure/rejection) |
| `-)` | Async message |
---
## Template 3: Architecture Diagram (C4 Style via Subgraphs)
```mermaid
flowchart TB
subgraph 用户层["👤 用户层"]
U1[Web 浏览器]
U2[移动 App]
end
subgraph 接入层["🌐 接入层"]
GW[API Gateway<br><small>Nginx + Rate Limit</small>]
LB[负载均衡<br><small>Round Robin</small>]
end
subgraph 服务层["⚙️ 微服务"]
S1[用户服务<br><small>FastAPI</small>]
S2[内容服务<br><small>FastAPI</small>]
S3[推荐服务<br><small>PyTorch</small>]
end
subgraph 数据层["🗄️ 数据层"]
DB[(PostgreSQL)]
RD[(Redis Cache)]
ES[(Elasticsearch)]
end
U1 & U2 --> GW
GW --> LB
LB --> S1 & S2 & S3
S1 --> DB & RD
S2 --> DB & ES
S3 --> RD & ES
style 用户层 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px
style 接入层 fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px
style 服务层 fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px
style 数据层 fill:#F5F3FF,stroke:#8B5CF6,stroke-width:1.5px
```
---
## Template 4: Gantt Chart
```mermaid
gantt
title 项目里程碑计划
dateFormat YYYY-MM-DD
axisFormat %m/%d
section 需求阶段
需求调研 :done, req1, 2024-01-01, 14d
需求评审 :done, req2, after req1, 3d
section 开发阶段
后端开发 :active, dev1, after req2, 21d
前端开发 :active, dev2, after req2, 18d
联调测试 : dev3, after dev1, 7d
section 上线阶段
灰度发布 : rel1, after dev3, 3d
全量上线 :milestone, rel2, after rel1, 0d
```
---
## Template 5: Class Diagram
```mermaid
classDiagram
class User {
+String name
+String email
+login()
+logout()
}
class Order {
+int id
+Date created_at
+float total
+submit()
+cancel()
}
class Product {
+String name
+float price
+int stock
}
User "1" --> "*" Order : 下单
Order "*" --> "*" Product : 包含
```
---
## Template 6: ER Diagram
```mermaid
erDiagram
USER {
int id PK
string name
string email UK
datetime created_at
}
ORDER {
int id PK
int user_id FK
float total
string status
datetime created_at
}
ORDER_ITEM {
int id PK
int order_id FK
int product_id FK
int quantity
float price
}
PRODUCT {
int id PK
string name
float price
int stock
}
USER ||--o{ ORDER : "下单"
ORDER ||--|{ ORDER_ITEM : "包含"
PRODUCT ||--o{ ORDER_ITEM : "被购买"
```
---
## Template 7: Mind Map
> ⚠️ Mermaid mindmap has limited layout capabilities. **For high-quality mind maps**, prefer `references/mindmap-css.md`.
> The following approach is for **quick drafts** or embedding in Markdown documents, with CSS injection to optimize visual quality.
### Optimized HTML Shell (Important! Use this, not the default template)
Mermaid mindmap doesn't support `style`/`classDef`, but you can greatly improve results with **CSS overriding SVG styles** + **themeVariables**:
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
display: flex;
justify-content: center;
padding: 48px;
}
#diagram { min-width: 900px; }
/* ─── CSS injection to optimize Mermaid mindmap rendering ─── */
/*
* Actual SVG class names in Mermaid v11 mindmap:
* - .section-root = root node
* - .section-0 ~ .section-N = first-level branches (in order)
* - .section-edge-0 ~ .section-edge-N = connectors for corresponding branches
* - .node-bkg = node background path
* - .node-line- = node bottom decoration line
* - .nodeLabel = text label
* - .edge = connector
* - .edge-depth-1/5 = connector depth level
*/
/* 1. All connectors: rounded, soft */
.edge { stroke-width: 2px !important; stroke-linecap: round !important; }
/* 2. Remove node bottom decoration line (ugly by default) */
.node-line- { stroke: transparent !important; }
/* 3. Root node: deep blue circle + shadow */
.section-root circle,
.section-root ellipse {
fill: #1E40AF !important;
stroke: #1E3A8A !important;
stroke-width: 3px !important;
filter: drop-shadow(0 4px 12px rgba(30,64,175,0.35));
}
.section-root .nodeLabel { color: #FFFFFF !important; font-size: 17px !important; font-weight: 700 !important; }
/* 4. First-level branches colored in order (supports up to 8-color cycle) */
/* Blue */
.section-0 .node-bkg { fill: #DBEAFE !important; stroke: #3B82F6 !important; stroke-width: 2px !important; }
.section-0 .nodeLabel { color: #1E40AF !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-0 { stroke: #93C5FD !important; }
/* Green */
.section-1 .node-bkg { fill: #D1FAE5 !important; stroke: #10B981 !important; stroke-width: 2px !important; }
.section-1 .nodeLabel { color: #065F46 !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-1 { stroke: #6EE7B7 !important; }
/* Amber */
.section-2 .node-bkg { fill: #FEF3C7 !important; stroke: #F59E0B !important; stroke-width: 2px !important; }
.section-2 .nodeLabel { color: #92400E !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-2 { stroke: #FCD34D !important; }
/* Purple */
.section-3 .node-bkg { fill: #EDE9FE !important; stroke: #8B5CF6 !important; stroke-width: 2px !important; }
.section-3 .nodeLabel { color: #5B21B6 !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-3 { stroke: #C4B5FD !important; }
/* Red */
.section-4 .node-bkg { fill: #FEE2E2 !important; stroke: #EF4444 !important; stroke-width: 2px !important; }
.section-4 .nodeLabel { color: #991B1B !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-4 { stroke: #FCA5A5 !important; }
/* Cyan */
.section-5 .node-bkg { fill: #CFFAFE !important; stroke: #06B6D4 !important; stroke-width: 2px !important; }
.section-5 .nodeLabel { color: #155E75 !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-5 { stroke: #67E8F9 !important; }
/* Pink */
.section-6 .node-bkg { fill: #FCE7F3 !important; stroke: #EC4899 !important; stroke-width: 2px !important; }
.section-6 .nodeLabel { color: #9D174D !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-6 { stroke: #F9A8D4 !important; }
/* Gray-green */
.section-7 .node-bkg { fill: #D1FAE5 !important; stroke: #059669 !important; stroke-width: 2px !important; }
.section-7 .nodeLabel { color: #064E3B !important; font-weight: 600 !important; font-size: 14px !important; }
.section-edge-7 { stroke: #6EE7B7 !important; }
/* 5. Deeper connectors are lighter */
.edge-depth-5 { stroke-width: 1.5px !important; opacity: 0.6; }
/* 6. Light gray background */
body { background: #FAFBFE; }
</style>
</head>
<body>
<div id="diagram">
<pre class="mermaid">
mindmap
root((你的主题))
一级分支1
二级内容A
二级内容B
一级分支2
二级内容C
</pre>
</div>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
primaryColor: '#EFF6FF',
primaryBorderColor: '#3B82F6',
primaryTextColor: '#1E293B',
lineColor: '#CBD5E1',
fontSize: '13px',
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
},
mindmap: {
padding: 20,
useMaxWidth: false,
}
});
</script>
</body>
</html>
```
### Auto-Upgrade Rules (Important!)
> ⚠️ **Never trim user content just to fit Mermaid!**
> Content comes first; tools serve content, not the other way around.
When content complexity exceeds Mermaid mindmap's comfort zone, **auto-switch to CSS approach** (`references/mindmap-css.md`):
| Trigger Condition (any one met) | Action |
|----------------------|------|
| More than 7 L1 branches | → Switch to CSS |
| Any branch has >8 child nodes | → Switch to CSS |
| Nesting exceeds 3 levels | → Switch to CSS |
| Single node text >15 chars | → Switch to CSS |
| Total nodes >40 | → Switch to CSS |
**When none of the above triggers**, Mermaid mindmap is adequate. The following suggestions help optimize rendering (recommendations, not hard limits):
| Suggestion | Notes |
|------|------|
| Keep node text concise | Use spaces to segment long text, avoid punctuation |
| Use emoji prefixes per branch | Higher visual distinctiveness |
| Use CSS injection for coloring | Use the optimized HTML shell above |
### Example (Optimized)
```mermaid
mindmap
root((AI 内容运营))
选题策划
热点扫描
竞品分析
用户调研
内容生产
长文
短文
视频
渠道分发
微信生态
小红书
B站
数据运营
数据分析
评论互动
持续优化
```
### Known Limitations of Mermaid Mindmap
- ❌ No `style` / `classDef` support for direct node coloring (CSS injection of SVG styles only)
- ❌ Line thickness/curvature cannot be controlled from Mermaid syntax (CSS override `.mindmap-edge`)
- ❌ Node spacing calculated by algorithm, cannot be manually specified
- ❌ Long CJK text easily overlaps with connectors (strict character count control needed)
- ⚠️ CSS injection depends on Mermaid internal class naming, may break on version upgrades
**Conclusion**: For quick drafts use Mermaid + the CSS-optimized shell above; for production output use CSS mind map → `references/mindmap-css.md`
---
## Template 8: State Diagram
```mermaid
stateDiagram-v2
[*] --> 草稿
草稿 --> 审核中 : 提交审核
审核中 --> 已发布 : 审核通过
审核中 --> 草稿 : 退回修改
已发布 --> 已下架 : 违规/过期
已下架 --> 草稿 : 重新编辑
已发布 --> [*] : 永久删除
```
---
## Template 9: Git Branch Graph
```mermaid
gitGraph
commit id: "init"
branch develop
checkout develop
commit id: "feat: 用户模块"
commit id: "feat: 订单模块"
branch feature/payment
checkout feature/payment
commit id: "feat: 支付接入"
commit id: "fix: 金额精度"
checkout develop
merge feature/payment id: "merge: 支付"
checkout main
merge develop id: "release: v1.0"
commit id: "hotfix: 安全补丁" type: REVERSE
```
---
## Styling Tips
### Single Node Style
```mermaid
style 节点ID fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
```
### Batch Styles (classDef)
```mermaid
flowchart LR
classDef blue fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px,color:#1E293B
classDef green fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px,color:#1E293B
classDef amber fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px,color:#1E293B
A[步骤1]:::blue --> B{判断}:::amber
B -->|是| C[结果A]:::green
B -->|否| D[结果B]:::green
```
### Connector Styles
```mermaid
%% Style for the N-th connector (0-indexed)
linkStyle 0 stroke:#3B82F6,stroke-width:2px
linkStyle 1 stroke:#10B981,stroke-width:2px,stroke-dasharray: 5 5
```
---
## Mermaid vs Other Approaches
| Capability | Mermaid | Playwright+CSS | draw.io |
|------|---------|---------------|---------|
| Learning curve | ✅ Very low (Markdown-like) | Medium (HTML/CSS) | ✅ Very low (drag&drop) |
| Version control friendly | ✅ Plain text | ✅ Plain text | ❌ XML binary |
| Flowcharts | ✅ Built-in | ⚠️ Manual layout | ✅ Drag&drop |
| Sequence diagrams | ✅ Built-in | ❌ Very complex | ✅ Templates |
| Gantt charts | ✅ Built-in | ❌ Build from scratch | ⚠️ Limited |
| Class/ER diagrams | ✅ Built-in | ❌ Not suited | ✅ Templates |
| Visual freedom | ⚠️ Limited | ✅ Full freedom | ✅ Free |
| PNG export | ✅ mmdc/Playwright | ✅ Playwright | ✅ Built-in |
| CJK support | ✅ Native | ✅ Font config | ✅ Native |
| Auto layout | ✅ Automatic | ❌ Manual | ⚠️ Semi-auto |
**Principle: Use Mermaid for structural/relationship diagrams, Playwright+CSS for creative design diagrams.**
---
## FAQ
### Q: CJK node names cause layout issues?
Ensure `fontFamily` includes a CJK font:
```javascript
themeVariables: {
fontFamily: '-apple-system, PingFang SC, SimHei, sans-serif'
}
```
### Q: How to control node spacing?
Mermaid auto-layouts; spacing is adjusted via config:
```javascript
flowchart: { padding: 16, nodeSpacing: 50, rankSpacing: 60 }
```
### Q: Chart too large?
- Split into subgraphs
- Change direction (`TB` too tall → switch to `LR`)
- Use `mmdc -w 1600` to increase canvas width
### Q: How to add line breaks in nodes?
Use `<br>` tags:
```mermaid
A[第一行<br>第二行<br><small>小字注释</small>]
```
### Q: Flowchart node text truncated or overlapping?
**Common causes and fixes**:
1. **Insufficient node padding**: ensure `flowchart.padding` is at least `24` (CJK chars are ~50% wider than Latin)
2. **Text too long**: use `<br>` for manual line breaks, or shorten text
3. **Canvas too narrow**: use `width: fit-content` on `#diagram` container (🚫 NEVER use `max-width` — Mermaid SVG width is unpredictable)
4. **Node spacing too small**: increase `nodeSpacing` and `rankSpacing` (recommended 60+)
5. **When using classDef**: ensure `font-size` isn't too large, 12-14px is ideal
**Correct approach for long-text nodes**:
```mermaid
flowchart LR
A["这是一段比较长的<br>需要换行的文字"]
B["用引号包裹节点文字<br>可以使用 HTML 标签"]
```
**Key configuration**:
```javascript
mermaid.initialize({
flowchart: {
padding: 32,
nodeSpacing: 80,
rankSpacing: 80,
htmlLabels: true,
wrappingWidth: 160,
}
});
```

View File

@@ -0,0 +1,911 @@
# CSS Mind Map Rendering Engine
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
**Core principle: Content-driven, not template-driven. First analyze the content structure, then decide on the layout, and finally render.**
---
## Step 1: Content Analysis
After receiving a mind map requirement, **don't write any HTML/CSS yet**. First parse the content into a tree structure, then calculate key metrics.
### 1.1 Build Tree JSON
```
Input: "How to work efficiently from home", branches include "Environment Setup", "Time Management", "Tool Selection"...
Output:
{
"root": "在家如何高效办公",
"branches": [
{
"label": "环境准备",
"children": ["独立工作区", "降噪耳机", "人体工学椅"]
},
{
"label": "时间管理",
"children": [
{ "label": "番茄工作法", "children": ["25分钟专注", "5分钟休息"] },
"日程规划",
"避免多任务"
]
},
...
]
}
```
### 1.2 Key Metrics
| Metric | Calculation Method | What It Affects |
|------|---------|---------|
| `branchCount` | Number of first-level branches | Choose single-sided/dual-sided expansion |
| `maxDepth` | Maximum nesting depth | Canvas width, whether sub-branch is needed |
| `maxChildren` | Maximum children per node | Vertical height of that branch |
| `totalNodes` | Total number of all nodes | Overall canvas size |
| `maxTextLen` | Longest text character count | Node width, whether line breaks are needed |
| `branchWeights[]` | Total descendants per first-level branch | Left-right balance allocation |
### 1.3 Example Analysis
```
Content: "产品经理核心技能", 6 L1 branches, max depth 3, total 35 nodes, max text length 12 chars
→ branchCount=6, maxDepth=3, totalNodes=35, maxTextLen=12
→ branchWeights=[8, 5, 7, 4, 6, 5]
```
---
## Step 2: Layout Decision
Based on the metrics from Step 1, choose a layout scheme. The following are reference suggestions, not hard rules—adjust flexibly according to actual content.
### 2.1 Layout Selection Guide
**Core principle: Use simple layouts for less content, dual-sided layouts for more content, avoid one side becoming too long.**
```
Few branches (roughly ≤4), simple content?
→ Style A: Right-expanding tree (single-sided, compact)
Many branches (roughly ≥5), or single side would be too long?
→ Style B: Left-right expanding tree (dual-sided balanced, most common)
User explicitly requests a specific form?
→ "cards"/"modules" → Style C: Card grid
→ "fishbone"/"root cause analysis" → Style D: Fishbone diagram
```
**Why not use radial layout?** Radial layout (spreading from center outward) almost inevitably causes node overlap when there are more than 3-4 branches, and is extremely difficult to debug. Not recommended.
### 2.2 Canvas Size Calculation
Don't use fixed sizes. Calculate based on content:
```python
def calc_canvas(branch_count, max_depth, total_nodes, max_text_len, layout):
# Width: depends on depth and text length
node_width = max(120, max_text_len * 14) # ~14px per CJK char (including padding)
if layout == 'left-right':
width = node_width * max_depth * 2 + 200 # Dual-sided, 200px in center for root node
else:
width = node_width * max_depth + 200 # Single-sided
# Height: depends on max branch vertical expansion
if layout == 'left-right':
max_side_nodes = total_nodes // 2 + 2 # rough estimate of single-side nodes
else:
max_side_nodes = total_nodes
height = max_side_nodes * 32 + 200 # ~32px per leaf (including gap)
# Lower bounds
width = max(width, 1200)
height = max(height, 600)
return width, height
```
### 2.3 Left-Right Branch Allocation (Style B)
Goal: Achieve similar visual weight on both sides.
```python
def balance_branches(branch_weights):
"""Greedy bin-packing: sort by weight descending, alternate left/right"""
indexed = sorted(enumerate(branch_weights), key=lambda x: -x[1])
left, right = [], []
left_sum, right_sum = 0, 0
for idx, weight in indexed:
if left_sum <= right_sum:
left.append(idx)
left_sum += weight
else:
right.append(idx)
right_sum += weight
return left, right
# e.g.: weights=[8,5,7,4,6,5] → left=[0,3,5] right=[2,4,1] → 17 vs 18
```
### 2.4 Node Styling Decision
**Principle: The deeper the level, the lighter the visual weight.** This way readers can distinguish main branches from details at a glance.
```
Root node → most prominent: dark solid background, large font (~20px)
L1 branches → next prominent: light fill + colored border, medium font (~15px)
L2 nodes → lighter still: paler fill + thin border, medium-small font (~13px)
Leaf nodes → lightest: capsule frame or plain text, small font (~12-13px)
```
**Key: Leaves should not have the same visual weight as first-level branches.** Leaves' padding, gap, and border thickness should all be significantly smaller than their parent. Otherwise the chart will be vertically too long and lack hierarchy.
---
## Step 3: Rendering
Based on the decisions from Step 2, generate HTML + CSS + JS.
### 3.1 Playwright Screenshot (Universal)
```python
import asyncio
from playwright.async_api import async_playwright
async def mindmap_to_png(html_path, png_path, width=1600):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(viewport={'width': width, 'height': 1200}, device_scale_factor=2)
await page.goto(f'file://{html_path}', wait_until='networkidle')
await page.wait_for_timeout(500)
el = page.locator('#mindmap')
bbox = await el.bounding_box()
# First expansion: ensure content is not clipped
expand_w = max(width, int(bbox['width'] + 100))
expand_h = int(bbox['height'] + 100)
await page.set_viewport_size({'width': expand_w, 'height': expand_h})
await page.wait_for_timeout(200)
# Call connector script
await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()')
await page.wait_for_timeout(200)
# Second contraction: measure actual content right edge, trim right-side blank space
trim = await page.evaluate('''() => {
const map = document.getElementById('mindmap');
const nodes = map.querySelectorAll('.root-node,.branch-node,.sub-node,.leaf,.deep-node');
const mapRect = map.getBoundingClientRect();
let maxR = 0, maxB = 0;
nodes.forEach(n => {
const r = n.getBoundingClientRect();
maxR = Math.max(maxR, r.right - mapRect.left);
maxB = Math.max(maxB, r.bottom - mapRect.top);
});
return { contentW: Math.ceil(maxR) + 80, contentH: Math.ceil(maxB) + 80 };
}''')
await page.set_viewport_size({'width': trim['contentW'], 'height': trim['contentH']})
await page.wait_for_timeout(200)
# Redraw connectors (viewport changed so coordinates change)
await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()')
await page.wait_for_timeout(200)
await el.screenshot(path=png_path)
await browser.close()
import os
print(f'{png_path} ({os.path.getsize(png_path)/1024:.0f}KB)')
```
### 3.2 Universal Recursive Connector Script v4-fix (All Styles)
This script automatically handles tree connectors of **any depth** (3, 4, 5 levels... all work). HTML for styles A and B should include this script at the end.
**⚠️ DOM Structure Convention (connector script depends on this structure):**
```
#mindmap
.tree-layout ← flex container (for left-right tree)
.left-side ← left branch area
.branch.c-{color} ← L1 branch (must have color class)
.branch-node ← L1 node
.children ← L2 container
div ← child wrapper
.sub-node ← L2 node
.leaf-group ← L3 container
div / .leaf ← leaf or wrapper with deeper levels
.leaf ← L3 leaf
.deep-group ← L4 container (recursive, same as above)
.deep-node ← L4/L5 node
.center-root
.root-node ← root node
.right-side ← right branches (same structure as .left-side)
```
**Also supports legacy structure (.branches/.right-branches/.left-branches), backward compatible.**
**⚠️ CSS Indentation Rules (connectors depend on child nodes having offset relative to parent, no indentation = broken connectors):**
```css
/* .tree-layout structure (new version) must include these paddings */
.left-side .children, .left-side .leaf-group, .left-side .deep-group {
align-items: flex-end; padding-right: 16px;
}
.right-side .children, .right-side .leaf-group, .right-side .deep-group {
align-items: flex-start; padding-left: 16px;
}
```
**Common causes of missing connectors/layout errors:**
- **⚠️ `.sub-branch` missing `display: flex`** (most critical! Without flex, `flex-direction: row-reverse` doesn't work, left-side leaves won't expand left, instead all pile up on the right)
- **⚠️ Child node containers missing padding-left/padding-right** (without indentation, child nodes align with parent, midX is outside child nodes, connectors break)
- **⚠️ `.lr-tree` / `.tree` should not have `z-index`** (creates stacking context, covers SVG connectors)
- **⚠️ Leaf nodes must NOT stretch to equal width** — each leaf should size to its own text content (`white-space: nowrap` or `width: fit-content`). Never add `width: 100%`, `flex-grow: 1`, or `align-items: stretch` to leaf containers. Leaves with shorter text should be narrower than leaves with longer text.
- Leaf container not named `.leaf-group` (using `.child-list`, `.sub-items`, etc.)
- Deep-level container not named `.deep-group`
- `#mindmap` missing `position: relative`
**Key improvements (v4-fix vs v2):**
- **No transparency**—use solid color blend for fading (`color + '80'` is almost invisible on white background)
- **Recursively process `.children`, `.leaf-group`, `.deep-group`**—no longer limited to 3 levels
- **Vertical line draws complete range**—one line from `min(Y)` to `max(Y)`, instead of drawing per child node
```javascript
/**
* Universal recursive connector script v4-fix
* - Supports any nesting depth (recursively processes .children + .leaf-group + .deep-group)
* - Unified gray-tone connectors (#64748B → #94A3B8 → #A8B4C2), visually clean
* - Direction logic:
* dir='left': startX=parent.left → midX(left-biased) → endX=child.right
* dir='right': startX=parent.right → midX(right-biased) → endX=child.left
*/
function drawAllLines() {
const map = document.getElementById('mindmap');
if (!map) { console.error('❌ #mindmap not found'); return; }
const cRect = map.getBoundingClientRect();
const old = map.querySelector('svg.lines');
if (old) old.remove();
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('lines');
svg.setAttribute('width', cRect.width);
svg.setAttribute('height', cRect.height);
svg.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:1;';
let lineCount = 0;
function rel(el) {
const r = el.getBoundingClientRect();
return {
cx: r.left - cRect.left + r.width/2,
cy: r.top - cRect.top + r.height/2,
left: r.left - cRect.left,
right: r.right - cRect.left,
};
}
function drawLine(x1, y1, x2, y2, color, width) {
const l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
// Round to nearest pixel to prevent sub-pixel misalignment (visual kinks)
l.setAttribute('x1', Math.round(x1)); l.setAttribute('y1', Math.round(y1));
l.setAttribute('x2', Math.round(x2)); l.setAttribute('y2', Math.round(y2));
l.setAttribute('stroke', color); l.setAttribute('stroke-width', width);
l.setAttribute('stroke-linecap', 'round');
svg.appendChild(l);
lineCount++;
}
// ─── Unified gray connectors (decreasing by depth, visually clean) ───
const lineStyles = [
{ color: '#64748B', width: 2.5 }, // root → L1
{ color: '#94A3B8', width: 2 }, // L1 → L2
{ color: '#A8B4C2', width: 1.5 }, // L2 → L3
{ color: '#B8C2CC', width: 1.2 }, // L3 → L4
{ color: '#CBD5E1', width: 1 }, // L4 → L5
];
function getLineStyle(branchColor, depth) {
const s = lineStyles[Math.min(depth, lineStyles.length - 1)];
return { color: s.color, width: s.width };
}
// ─── Connector direction ───
// dir='left': parent.left → midX → child.right (each child gets its own polyline)
// dir='right': parent.right → midX → child.left
//
// [Principle] midX is the X coordinate of the vertical line; it must be in the gap between parent and children.
// Use a fraction of the parent-to-nearest-child distance as offset, so:
// - Large node spacing → large offset, lines spread out
// - Small node spacing → small offset, lines compact without crossing text
// All children in the same connect() call share one midX, ensuring vertical line alignment.
function connect(parentEl, childEls, color, width, dir) {
if (!childEls.length) return;
const p = rel(parentEl);
const startX = dir === 'left' ? p.left : p.right;
const startY = p.cy;
// Special case: single child — draw one straight horizontal line, no vertical spine
if (childEls.length === 1) {
const c = rel(childEls[0]);
const endX = dir === 'left' ? c.right : c.left;
const midY = Math.round((startY + c.cy) / 2);
drawLine(startX, midY, endX, midY, color, width);
return;
}
// Find the closest child edge to calculate available space
let closestEdge;
if (dir === 'left') {
closestEdge = Math.max(...childEls.map(ch => rel(ch).right));
} else {
closestEdge = Math.min(...childEls.map(ch => rel(ch).left));
}
// Place midX at the midpoint between parent edge and closest child edge,
// but guarantee at least 16px clearance from child nodes
const childClearance = 16;
const midpoint = startX + (closestEdge - startX) / 2;
const midFromChild = dir === 'left' ? closestEdge + childClearance : closestEdge - childClearance;
// Use the position that's further from children (safer)
const midX = dir === 'left'
? Math.min(midpoint, midFromChild)
: Math.max(midpoint, midFromChild);
drawLine(startX, startY, midX, startY, color, width);
// Draw ONE continuous vertical line spanning from parent to the last child
const allCYs = childEls.map(ch => rel(ch).cy);
const minY = Math.min(startY, ...allCYs);
const maxY = Math.max(startY, ...allCYs);
drawLine(midX, minY, midX, maxY, color, width);
// Then draw horizontal lines from the vertical spine to each child
childEls.forEach(ch => {
const c = rel(ch);
const endX = dir === 'left' ? c.right : c.left;
const endY = c.cy;
drawLine(midX, endY, endX, endY, color, width);
});
}
// ─── Recursively process subtree (supports .children + .leaf-group + .deep-group) ───
const NODE_SEL = '.branch-node, .sub-node, .leaf, .deep-node';
const CONTAINER_SEL = ':scope > .children, :scope > .leaf-group, :scope > .deep-group';
function processChildren(parentNodeEl, containerEl, branchColor, depth, dir) {
if (!containerEl) return;
const childNodeEls = [];
for (const wrapper of containerEl.children) {
const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL);
if (nodeEl) childNodeEls.push(nodeEl);
}
if (!childNodeEls.length) return;
const style = getLineStyle(branchColor, depth);
connect(parentNodeEl, childNodeEls, style.color, style.width, dir);
for (const wrapper of containerEl.children) {
const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL);
if (!nodeEl) continue;
wrapper.querySelectorAll(CONTAINER_SEL).forEach(nc =>
processChildren(nodeEl, nc, branchColor, depth + 1, dir)
);
}
}
// ─── Main flow ───
const rootNode = map.querySelector('.root-node');
if (!rootNode) { console.error('❌ .root-node not found'); return; }
const rp = rel(rootNode);
// Left side: collect all L1 branch-nodes, draw polylines with connect() (with vertical spine)
const leftSel = '.left-side > .branch, .left-branches > .left-branch, .left-branches > div';
const leftBranches = map.querySelectorAll(leftSel);
const leftBranchNodes = [];
leftBranches.forEach(branch => {
const bNode = branch.querySelector('.branch-node');
if (bNode) leftBranchNodes.push(bNode);
});
if (leftBranchNodes.length) {
connect(rootNode, leftBranchNodes, lineStyles[0].color, lineStyles[0].width, 'left');
}
leftBranches.forEach(branch => {
const bNode = branch.querySelector('.branch-node');
if (!bNode) return;
processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'left');
});
// Right side: same as above
const rightSel = '.right-side > .branch, .branches > .branch, .right-branches > .right-branch';
const rightBranches = map.querySelectorAll(rightSel);
const rightBranchNodes = [];
rightBranches.forEach(branch => {
const bNode = branch.querySelector('.branch-node');
if (bNode) rightBranchNodes.push(bNode);
});
if (rightBranchNodes.length) {
connect(rootNode, rightBranchNodes, lineStyles[0].color, lineStyles[0].width, 'right');
}
rightBranches.forEach(branch => {
const bNode = branch.querySelector('.branch-node');
if (!bNode) return;
processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'right');
});
map.insertBefore(svg, map.firstChild);
console.log(`✅ Drew ${lineCount} lines`);
}
```
**How to call (at end of HTML):**
```html
<script>
/* ← paste the drawAllLines function above */
window.addEventListener('load', () => setTimeout(drawAllLines, 300));
</script>
```
### 3.3 Universal CSS Base (Shared by All Styles)
```css
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
}
#mindmap { padding: 60px; display: inline-block; min-width: 100%; position: relative; }
#mindmap > svg.lines {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 0;
}
/* ─── Root Node ─── */
/* ─── Topic Intent Color System ─── */
/* Model picks ONE intent based on content semantics. Add data-intent="xxx" to #mindmap.
DO NOT manually write hex colors for root node — use intent system only. */
/* Intent → Root Node + Branch Palette mapping */
#mindmap[data-intent="professional"] .root-node { background: linear-gradient(135deg, #1E3A5F, #2D4A6F); box-shadow: 0 4px 12px rgba(30,58,95,0.25); }
#mindmap[data-intent="technical"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); }
#mindmap[data-intent="medical"] .root-node { background: linear-gradient(135deg, #0F766E, #0D9488); box-shadow: 0 4px 12px rgba(15,118,110,0.25); }
#mindmap[data-intent="education"] .root-node { background: linear-gradient(135deg, #9A3412, #B45309); box-shadow: 0 4px 12px rgba(154,52,18,0.25); }
#mindmap[data-intent="creative"] .root-node { background: linear-gradient(135deg, #7C3AED, #8B5CF6); box-shadow: 0 4px 12px rgba(124,58,237,0.25); }
#mindmap[data-intent="finance"] .root-node { background: linear-gradient(135deg, #1E3A5F, #1E40AF); box-shadow: 0 4px 12px rgba(30,58,95,0.25); }
#mindmap[data-intent="nature"] .root-node { background: linear-gradient(135deg, #166534, #15803D); box-shadow: 0 4px 12px rgba(22,101,52,0.25); }
#mindmap[data-intent="warning"] .root-node { background: linear-gradient(135deg, #991B1B, #B91C1C); box-shadow: 0 4px 12px rgba(153,27,27,0.25); }
#mindmap[data-intent="neutral"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); }
/*
Intent Selection Guide (for model):
professional → corporate reports, strategy, management, business plans
technical → software, engineering, architecture, systems, AI/ML
medical → healthcare, clinical, pharmaceutical, nursing, anatomy
education → teaching, learning, curriculum, training, academic
creative → design, art, marketing, branding, media
finance → banking, investment, accounting, economics, trading
nature → environment, ecology, agriculture, biology, geography
warning → risk analysis, safety, incident review, compliance, audit
neutral → general topics, mixed content, unclear domain
Recommended branch color combos per intent:
professional → [blue, teal, cyan]
technical → [blue, purple, cyan]
medical → [teal, green, blue]
education → [amber, green, blue]
creative → [purple, amber, cyan]
finance → [blue, green, cyan]
nature → [green, teal, amber]
warning → [red, amber, cyan]
neutral → [blue, green, purple]
*/
.root-node {
color: white; font-size: 20px; font-weight: 700;
padding: 18px 28px; border-radius: 12px;
white-space: nowrap; flex-shrink: 0; align-self: center;
}
/* Fallback if no intent specified — defaults to professional blue */
#mindmap:not([data-intent]) .root-node {
background: linear-gradient(135deg, #1E3A5F, #2D4A6F);
box-shadow: 0 4px 12px rgba(30,58,95,0.25);
}
/* ─── Container Layout ─── */
.tree { display: flex; align-items: flex-start; gap: 0; position: relative; }
.branches { display: flex; flex-direction: column; gap: 16px; margin-left: 60px; }
.branch, .sub-branch { display: flex; align-items: flex-start; gap: 0; }
/* ─── First-Level Branch Node ─── */
.branch-node {
font-size: 15px; font-weight: 600;
padding: 10px 20px; border-radius: 8px;
white-space: nowrap; flex-shrink: 0; border: 2px solid;
}
/* ─── Second-Level Sub-Node (when has children) ─── */
.sub-node {
font-size: 13px; font-weight: 500;
padding: 7px 14px; border-radius: 6px;
white-space: nowrap; flex-shrink: 0; border: 1.5px solid;
background: #F8FAFC;
}
/* ─── Leaf Node ─── */
/* Default: lightweight capsule frame */
.leaf {
font-size: 13px; font-weight: 400; color: #475569;
padding: 4px 10px; background: #FAFAFA;
border-radius: 12px; border: 1px solid #E5E7EB;
white-space: nowrap; line-height: 1.5;
}
/* Text-only mode (more compact, add class="leaf text-only") */
.leaf.text-only {
background: none; border: none; padding: 2px 0; border-radius: 0;
}
/* Leaf supplementary description */
.leaf-desc {
font-size: 12px; color: #94A3B8; font-weight: 400; margin-left: 8px;
}
.leaf-desc::before { content: '— '; color: #CBD5E1; }
/* ─── Child Node Container ─── */
/* Principle: The deeper the level, the smaller the gap, but not so small that connectors and text overlap */
.children { display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start; }
.children:has(.sub-branch) { gap: 10px; } /* sub-branch is larger than leaf, needs more spacing */
.sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; }
.sub-branch .sub-branch .children { gap: 4px; margin-left: 36px; align-items: flex-start; }
/* Tip: If leaf text and connectors overlap, prioritize increasing gap */
/* ─── Color System (for distinguishing different branches) ─── */
.c-blue { background: #EFF6FF; border-color: #3B82F6; color: #1E40AF; }
.c-green { background: #F0FDF4; border-color: #10B981; color: #065F46; }
.c-amber { background: #FFF7ED; border-color: #F59E0B; color: #92400E; }
.c-purple { background: #F5F3FF; border-color: #8B5CF6; color: #5B21B6; }
.c-red { background: #FEF2F2; border-color: #EF4444; color: #991B1B; }
.c-cyan { background: #ECFEFF; border-color: #06B6D4; color: #155E75; }
.c-teal { background: #F0FDFA; border-color: #14B8A6; color: #134E4A; }
/* Second-level sub-node inherits branch color scheme (lighter) */
.c-blue .sub-node { background: #F0F7FF; border-color: #93C5FD; color: #1E40AF; }
.c-green .sub-node { background: #F2FDF6; border-color: #6EE7B7; color: #065F46; }
.c-amber .sub-node { background: #FFF9F0; border-color: #FCD34D; color: #92400E; }
.c-purple .sub-node { background: #F8F6FF; border-color: #C4B5FD; color: #5B21B6; }
.c-red .sub-node { background: #FFF5F5; border-color: #FCA5A5; color: #991B1B; }
.c-cyan .sub-node { background: #F0FEFF; border-color: #67E8F9; color: #155E75; }
.c-teal .sub-node { background: #F2FDFA; border-color: #5EEAD4; color: #134E4A; }
```
---
## Style A: Right-Expanding Tree (Simple scenarios with branchCount ≤ 4 or maxDepth ≤ 2)
### HTML Structure
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
/* ← paste the "Universal CSS Base" above */
</style>
</head>
<body>
<div id="mindmap" data-intent="professional">
<div class="tree">
<div class="root-node">中心主题</div>
<div class="branches">
<div class="branch">
<div class="branch-node c-blue">一级分支A</div>
<div class="children">
<div class="leaf">叶子1</div>
<div class="leaf">叶子2</div>
</div>
</div>
<div class="branch">
<div class="branch-node c-green">一级分支B</div>
<div class="children">
<div class="sub-branch">
<div class="sub-node c-green">二级有下级</div>
<div class="children">
<div class="leaf">三级叶子1</div>
<div class="leaf">三级叶子2</div>
</div>
</div>
<div class="leaf">二级叶子</div>
</div>
</div>
</div>
</div>
</div>
<script>
/* ← paste the "Universal Recursive Connector Script" */
drawAllLines();
</script>
</body>
</html>
```
---
## Style B: Left-Right Expanding Tree (Standard scenario with branchCount ≥ 5, most common)
### Additional CSS (append after universal base)
```css
/* ─── Left-Right Expanding Layout ─── */
.lr-tree { display: flex; align-items: center; gap: 0; position: relative; }
.lr-tree .root-node {
padding: 20px 32px; border-radius: 14px;
margin: 0 60px; text-align: center;
}
/* ⚠️ sub-branch must be flex, otherwise flex-direction: row-reverse won't work, leaves won't expand left */
.sub-branch { display: flex; align-items: flex-start; gap: 0; }
.left-branches { display: flex; flex-direction: column; gap: 16px; }
.left-branch { display: flex; align-items: flex-start; flex-direction: row-reverse; gap: 0; }
.left-branch .children {
display: flex; flex-direction: column; gap: 6px;
margin-right: 48px; align-items: flex-end;
}
.left-branch .children:has(.sub-branch) { gap: 10px; }
.left-branch .sub-branch .children { gap: 5px; margin-right: 40px; margin-left: 0; align-items: flex-end; }
.left-branch .sub-branch { flex-direction: row-reverse; }
.right-branches { display: flex; flex-direction: column; gap: 16px; }
.right-branch { display: flex; align-items: flex-start; gap: 0; }
.right-branch .children {
display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start;
}
.right-branch .children:has(.sub-branch) { gap: 10px; }
.right-branch .sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; }
```
### HTML Structure
```html
<div id="mindmap" data-intent="professional">
<div class="lr-tree">
<!-- Left branches (allocated by balance_branches) -->
<div class="left-branches">
<div class="left-branch">
<div class="branch-node c-purple">分支D放左边</div>
<div class="children">
<div class="leaf">叶子1</div>
<div class="leaf">叶子2</div>
</div>
</div>
<!-- More left branches... -->
</div>
<div class="root-node">中心主题</div>
<!-- Right branches -->
<div class="right-branches">
<div class="right-branch">
<div class="branch-node c-blue">分支A放右边</div>
<div class="children">
<div class="leaf">叶子1</div>
<div class="leaf">叶子2</div>
</div>
</div>
<!-- More right branches... -->
</div>
</div>
</div>
<script>
/* ← paste the "Universal Recursive Connector Script" */
drawAllLines(); // v4: auto-handles both left and right sides
</script>
```
---
## Style C: Card Grid (when user explicitly requests "cards" / "modules")
> ⚠️ This is not a mind map — it's a modular display. No connectors; use cards + color coding to show relationships.
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
}
#mindmap { padding: 48px 64px; }
.map-title {
font-size: 24px; font-weight: 700; color: #1E293B; text-align: center;
margin-bottom: 8px;
}
.map-subtitle {
font-size: 14px; color: #64748B; text-align: center;
margin-bottom: 36px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px; max-width: 1000px; margin: 0 auto;
}
.card {
background: #FFFFFF; border-radius: 12px;
padding: 20px; border: 1px solid #E2E8F0;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid; }
.card-title { font-size: 15px; font-weight: 600; }
.card-icon {
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
font-size: 16px; border-radius: 50%; flex-shrink: 0;
}
.card-items { list-style: none; display: flex; flex-direction: column; gap: 6px; }
.card-items li {
font-size: 13px; color: #475569; padding-left: 16px; position: relative;
}
.card-items li::before {
content: ''; position: absolute; left: 0; top: 8px;
width: 6px; height: 6px; border-radius: 50%;
}
/* Color variants */
.card.blue .card-header { border-color: #3B82F6; }
.card.blue .card-icon { background: #EFF6FF; }
.card.blue .card-title { color: #1E40AF; }
.card.blue li::before { background: #3B82F6; }
.card.green .card-header { border-color: #10B981; }
.card.green .card-icon { background: #F0FDF4; }
.card.green .card-title { color: #065F46; }
.card.green li::before { background: #10B981; }
.card.amber .card-header { border-color: #F59E0B; }
.card.amber .card-icon { background: #FFF7ED; }
.card.amber .card-title { color: #92400E; }
.card.amber li::before { background: #F59E0B; }
.card.purple .card-header { border-color: #8B5CF6; }
.card.purple .card-icon { background: #F5F3FF; }
.card.purple .card-title { color: #5B21B6; }
.card.purple li::before { background: #8B5CF6; }
</style>
</head>
<body>
<div id="mindmap" data-intent="professional">
<div class="map-title">标题</div>
<div class="map-subtitle">副标题</div>
<div class="card-grid">
<div class="card blue">
<div class="card-header">
<div class="card-icon">📋</div>
<div class="card-title">模块A</div>
</div>
<ul class="card-items">
<li>条目1</li>
<li>条目2</li>
</ul>
</div>
<!-- More cards... -->
</div>
</div>
</body>
</html>
```
---
## Style D: Fishbone Diagram (Problem Analysis / Root Cause)
Best for problem analysis, root cause tracing, quality management (Ishikawa diagram).
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #FFFFFF;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
}
#mindmap { padding: 48px 64px; }
.fishbone { position: relative; }
.spine { display: flex; align-items: center; gap: 0; }
.spine-line { flex: 1; height: 3px; background: #1E293B; }
.spine-head {
width: 0; height: 0;
border-left: 16px solid #1E293B;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}
.result-node {
background: #1E293B; color: white;
font-size: 16px; font-weight: 700;
padding: 12px 24px; border-radius: 8px;
white-space: nowrap; margin-left: 4px;
}
.bone-branches {
position: absolute; left: 80px; right: 200px; top: 50%;
display: flex; justify-content: space-around;
}
.bone { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.bone.up { transform: translateY(calc(-100% - 20px)); }
.bone.down { transform: translateY(20px); }
.bone-title {
font-size: 14px; font-weight: 600;
padding: 8px 16px; border-radius: 8px; white-space: nowrap;
}
.bone-items { display: flex; flex-direction: column; gap: 4px; align-items: center; }
.bone-item {
font-size: 12px; color: #64748B;
padding: 3px 10px; background: #F8FAFC;
border-radius: 4px; border: 1px solid #E2E8F0; white-space: nowrap;
}
.bone-line { width: 2px; height: 24px; background: #CBD5E1; }
</style>
</head>
<body>
<div id="mindmap" data-intent="professional">
<div class="fishbone">
<div class="spine">
<div class="spine-line"></div>
<div class="spine-head"></div>
<div class="result-node">问题/结果</div>
</div>
<!-- Upper causes -->
<div class="bone-branches" style="transform: translateY(calc(-50% - 20px));">
<div class="bone">
<div class="bone-items">
<div class="bone-item">子原因1</div>
<div class="bone-item">子原因2</div>
</div>
<div class="bone-line"></div>
<div class="bone-title" style="background:#EFF6FF;color:#1E40AF;border:1.5px solid #3B82F6;">原因类别A</div>
</div>
</div>
<!-- Lower causes -->
<div class="bone-branches" style="transform: translateY(calc(-50% + 20px));">
<div class="bone">
<div class="bone-title" style="background:#F0FDF4;color:#065F46;border:1.5px solid #10B981;">原因类别B</div>
<div class="bone-line"></div>
<div class="bone-items">
<div class="bone-item">子原因1</div>
<div class="bone-item">子原因2</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
```
---
## Quality Checklist
After rendering, verify against this checklist:
1. **Content complete** — Every node from the original requirement is in the map, nothing missing
2. **Clear hierarchy** — L1 branches and leaves are instantly distinguishable (different bg/border/font-weight)
3. **No overlap** — No boxes covering boxes, no lines through text
4. **Connectors visible** — Every parent-child pair has a connector, no orphan leaves. Connector color ≥ `#94A3B8` (too light = invisible)
5. **Connector direction correct** — Left-side leaves extend left, right-side extends right
6. **Proportions reasonable** — Map is not extremely narrow/tall (target aspect ratio 1:1 to 3:1), visually comfortable
7. **Text readable** — At final output size, smallest text is legible. Reference: root 18px+, L1 15px+, L2 13px+, leaves 12px+
8. **Roughly balanced** (Style B) — Visual weight approximately equal on both sides, doesn't need to be symmetric. Target ≤30% difference
9. **Canvas large enough** — No nodes clipped, sufficient padding on all sides (reference 60px+)
10. **No large blank areas on right** — Screenshot trimmed to content edge
---
## ⛔ Radial Layout Warning
**Radial layout is strongly discouraged.** Using `position: absolute` for fixed branch positions leads to overlap when branches increase, and deep levels cannot be handled.
If content is very simple (reference: ≤3 branches, ≤3 children per branch, text ≤6 chars, depth ≤2, total ≤12 nodes), it's technically possible, but tree layout is always the safer choice.
**No code template provided.** If conditions are met, manually adjust based on Style A.

View File

@@ -0,0 +1,801 @@
# Playwright + CSS Rendering Engine
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
**Core principle: Content-driven, not template-driven. Analyze content structure first, then decide layout, then render.**
Applicable to: flowcharts, infographics, KPI cards, data posters — any visualization requiring full CSS power (gradients, shadows, rounded corners, Grid/Flexbox layout).
---
## Step 1: Content Analysis
When you receive a flowchart/infographic requirement, **don't write HTML/CSS first**. Analyze the content structure.
### 1.1 Flowchart Content Analysis
```
Input: "Honey production process", 8 steps, linear without branches
{
"type": "flowchart",
"nodes": [
{ "id": "1", "label": "花粉采集", "desc": "蜜蜂从花朵采集花蜜" },
{ "id": "2", "label": "酿造", "desc": "蜜蜂在蜂巢中反复吞吐" },
...
],
"edges": [["1","2"], ["2","3"], ...],
"nodeTypes": { "start": ["1"], "end": ["8"], "decision": [], "normal": ["2","3","4","5","6","7"] }
}
```
### 1.2 Key Metrics
| Metric | Calculation | Affects |
|------|---------|---------|
| `nodeCount` | Total node count | Mermaid vs CSS flowchart |
| `maxTextLen` | Longest text char count | Node width |
| `hasDecision` | Has decision branches | Layout complexity |
| `hasBranch` | Has parallel/branches | Column count |
| `parallelCount` | Max parallel branches | Column count |
| `phaseCount` | Phase/group count | Need for phased containers |
| `hasRoles` | Has roles/swimlanes | Need for dual-panel |
| `isLinear` | Linear without branches | Can use snake layout |
### 1.3 Infographic Content Analysis
Infographics (KPI cards, data posters) are simpler to analyze:
- How many data metrics? → Determines grid columns
- Any trend data? → Need for sparklines
- Title area? → Need for hero header
- Comparison data? → Need for bar chart
---
## Step 2: Layout Decision
### 2.1 Flowchart Layout Decision Tree
```
⚠️ DEFAULT RULE: When user asks "generate/create XXX 流程图" without specifying format,
DEFAULT to Layout C (Phased Vertical). Almost all real-world processes have phases.
User specified Mermaid/markdown?
└─ Yes → Follow user choice (Format Constraint Rule)
└─ No →
nodeCount ≤ 6 AND no phases AND maxTextLen ≤ 8 (CJK)?
└─ Yes → Mermaid (minimal flowchart)
└─ No →
phaseCount > 0 OR nodeCount ≥ 5?
└─ Yes → ⭐ Layout C: CSS Phased Vertical flowchart (DEFAULT)
└─ No →
hasRoles?
└─ Yes → Layout D: CSS Dual-panel/Swimlane flowchart
└─ No → Layout C: CSS Phased Vertical flowchart (fallback also uses C)
```
**⚠️ Layout A (Grid) and Layout B (Snake) are only for very special scenarios (e.g., flat comparison, unordered parallel items). Flowcharts default to Layout C.**
### 🚫 Flowchart Anti-Patterns (FORBIDDEN)
| ❌ Bad Pattern | ✅ Correct Pattern |
|---|---|
| Phase titles (一、二、三...) as isolated left-side text labels | Phase titles as colored title bars, wrapped inside group cards |
| All nodes flat-laid in Grid without group containers | Each phase wrapped in a `.phase-group` card containing its steps |
| Role labels scattered above nodes | Role info displayed uniformly at the top of the flowchart, or as phase card labels |
| Nodes connected with loose diverging lines | Phases connected with arrows (↓), steps within phases use numbering |
| Inconsistent node sizes, uneven spacing, misaligned | Same-phase nodes share uniform style, overall alignment consistent |
| Using Layout A Grid for a phased flowchart | Has phases → must use Layout C |
### 2.2 Canvas Size Calculation
```python
def calc_flowchart_canvas(node_count, max_text_len, parallel_count, has_roles, layout):
node_width = max(180, max_text_len * 16) # ~16px per CJK char (including padding)
if layout == 'snake':
cols = min(4, node_count)
rows = (node_count + cols - 1) // cols
width = cols * (node_width + 60) + 120 # 60=gap, 120=padding
height = rows * 120 + 200 # 120=row height
elif layout == 'dual-panel':
width = max(1600, node_width * 2 + 400) # left panel + right flow
height = node_count * 100 + 200
elif layout == 'phased-vertical':
width = max(800, node_width + 200)
height = node_count * 80 + 200
else: # grid
cols = max(parallel_count, 2)
width = cols * (node_width + 60) + 120
rows = (node_count + cols - 1) // cols
height = rows * 120 + 200
return max(width, 800), max(height, 600)
```
### 2.3 Color Decision
**Iron rule: Node background = low-saturation light color, border = high-saturation color. Large high-saturation areas = children's drawing.**
**⚠️ Text contrast iron rule: Dark/accent background nodes must use light text (white or near-white) for title and description.**
Light background → dark text (`#1F2937`), dark background → light text (`#FFFFFF` or `#FFF7ED`).
Common mistake: Endpoint/highlight node switched to dark background, but description text remains dark gray, making it completely unreadable.
```
Node type → color:
Start/end → bg: #EFF6FF, border: #3B82F6 (blue), text: #1E40AF
Normal step → bg: #F8FAFC, border: #94A3B8 (gray-blue), text: #374151 or colored by phase
Decision node → bg: #FFF7ED, border: #F59E0B (amber), text: #92400E
Success/pass → bg: #F0FDF4, border: #10B981 (green), text: #065F46
End/failure → bg: #F5F3FF, border: #8B5CF6 (purple), text: #5B21B6
Emphasis/endpoint (dark bg) → bg: #92400E, border: #F59E0B, text: #FFFFFF, desc: #FFF7ED
Max 3-4 background colors for nodes in the same flowchart.
Overall chart background = white #FFFFFF.
```
**Phase bar colors**: Same-hue gradient (blue-gray family), **never use different hues per phase**.
```css
/* ✅ Same-hue blue-gray progression */
.phase-1 { background: #F0F4F8; border-left: 4px solid #64748B; }
.phase-2 { background: #E8EDF2; border-left: 4px solid #5B7A99; }
/* ❌ Different hue per phase (rainbow effect) */
.phase-1 { background: #EFF6FF; border-left: 4px solid #3B82F6; } /* blue */
.phase-2 { background: #F0FDF4; border-left: 4px solid #10B981; } /* green */
.phase-3 { background: #FFF7ED; border-left: 4px solid #F59E0B; } /* amber */
```
---
## Step 3: Rendering
### 3.1 Playwright Screenshot (Universal)
```python
import asyncio
from playwright.async_api import async_playwright
async def html_to_image(html_path, output_path, selector='#root',
width=1200, height=None, scale=2):
"""HTML → PNG/PDF
scale: 2 (default crisp), 1.5 (large canvas 3000px+), 3 (print).
Width must accommodate ALL content. After first render, auto-resize viewport to fit.
"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
viewport={'width': width, 'height': height or 800},
device_scale_factor=scale
)
await page.goto(f'file://{html_path}', wait_until='networkidle')
await page.wait_for_timeout(500)
if output_path.endswith('.pdf'):
await page.pdf(path=output_path, print_background=True)
else:
el = page.locator(selector)
bbox = await el.bounding_box()
if bbox:
fit_w = max(width, int(bbox['width'] + 100))
fit_h = int(bbox['height'] + 100)
await page.set_viewport_size({'width': fit_w, 'height': fit_h})
await page.wait_for_timeout(200)
await el.screenshot(path=output_path)
await browser.close()
import os
print(f'{output_path} ({os.path.getsize(output_path)/1024:.0f}KB)')
```
### 3.2 HTML Universal Shell
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--text: #111827;
--text-sub: #6B7280;
--text-muted: #9CA3AF;
--bg: #FFFFFF;
--surface: #F9FAFB;
--border: #E5E7EB;
--blue: #3B82F6;
--cyan: #06B6D4;
--purple: #8B5CF6;
--amber: #F59E0B;
--red: #EF4444;
--green: #10B981;
--positive: #22C55E;
--negative: #EF4444;
--connector: #94A3B8;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
}
#root { width: fit-content; min-width: 800px; margin: 0 auto; padding: 48px 40px; }
</style>
</head>
<body>
<div id="root">
<!-- Content -->
</div>
</body>
</html>
```
### 3.3 CSS Variables: Node Color System
```css
:root {
/* Node types — low-saturation background + high-saturation border */
--node-bg: #EFF6FF; --node-border: #3B82F6; /* Normal step (blue) */
--node-decision-bg: #FFF7ED; --node-decision-border: #F59E0B; /* Decision (amber) */
--node-success-bg: #F0FDF4; --node-success-border: #10B981; /* Success (green) */
--node-end-bg: #F5F3FF; --node-end-border: #8B5CF6; /* End (purple) */
--group-bg: #F8FAFC; --group-border: #E2E8F0; /* Group container */
}
```
---
## Layout A: CSS Grid Flowchart (Universal, Most Common)
For: Flowcharts with branches/decisions, >10 nodes or long CJK text.
### Core CSS
```css
.flow-title {
font-size: 22px; font-weight: 700; color: var(--text);
text-align: center; margin-bottom: 40px;
}
.flow-subtitle {
font-size: 14px; color: var(--text-sub);
text-align: center; margin-top: 8px;
}
/* Grid container */
.flow-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 220px));
gap: 40px 60px;
justify-content: center;
position: relative;
}
/* Node */
.flow-node {
background: var(--node-bg); border: 2px solid var(--node-border);
border-radius: 10px; padding: 16px 20px;
text-align: center; position: relative; z-index: 1;
min-height: 56px; display: flex; flex-direction: column;
justify-content: center; align-items: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
max-width: 260px; /* Prevent single node from being too wide and crowding parallel nodes */
word-break: break-word; /* Force line break for overly long text */
white-space: normal; /* Allow line breaks (override possible nowrap) */
}
.flow-node .node-title { font-size: 15px; font-weight: 600; line-height: 1.4; }
.flow-node .node-desc { font-size: 12px; color: var(--text-sub); margin-top: 4px; }
/* Node variants */
.flow-node.start { border-radius: 24px; }
.flow-node.decision { background: var(--node-decision-bg); border-color: var(--node-decision-border); }
.flow-node.success { background: var(--node-success-bg); border-color: var(--node-success-border); }
.flow-node.end { background: var(--node-end-bg); border-color: var(--node-end-border); }
/* Group box */
.flow-group {
background: var(--group-bg); border: 1.5px dashed var(--group-border);
border-radius: 12px; padding: 24px 20px 20px; position: relative;
}
.flow-group .group-label {
position: absolute; top: -10px; left: 16px;
background: var(--bg); padding: 0 8px;
font-size: 12px; font-weight: 600; color: var(--text-sub);
}
/* ─── Parallel branch constraints (prevent overlap when multiple nodes in same row) ─── */
/*
⚠️ Parallel branch iron rules:
1. Gap between parallel nodes in same row ≥ 40px (guaranteed by .flow-grid gap)
2. Each node max-width: 260px + word-break: break-word (set in .flow-node)
3. If parallel nodes > 3 → switch to vertical branch layout (don't force-squeeze into one row)
4. Text over 15 CJK characters → must line-break, don't expand node width
5. When using flex instead of manual grid-column for parallel areas, add flex-wrap: wrap as fallback
*/
.parallel-group {
display: flex; gap: 40px; justify-content: center;
flex-wrap: wrap; /* Fallback: auto-wrap when exceeding width */
}
.parallel-group .flow-node {
flex: 0 1 220px; /* Max 220px, can shrink, won't grow infinitely */
}
/* SVG connector layer */
.flow-connectors {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 0;
}
.flow-connectors line, .flow-connectors path {
stroke: var(--connector); stroke-width: 2; fill: none;
marker-end: url(#arrowhead);
}
.connector-label {
font-size: 12px; fill: var(--text-sub); text-anchor: middle;
font-family: -apple-system, 'PingFang SC', 'SimHei', sans-serif;
}
/* Legend — must be independent container, not inside flow-grid */
.flow-legend {
display: flex; gap: 24px; justify-content: center;
margin-top: 40px; padding: 16px 24px;
background: #F9FAFB; border-radius: 8px; border: 1px solid #E5E7EB;
}
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #4B5563; }
.legend-dot { width: 12px; height: 12px; border-radius: 3px; border: 2px solid; }
```
### Auto Connector Script
```javascript
// connections = [['sourceID', 'targetID', 'label'], ...]
function drawConnectors(connections) {
const svg = document.getElementById('connectorSvg');
const container = svg.parentElement;
const cRect = container.getBoundingClientRect();
svg.setAttribute('width', cRect.width);
svg.setAttribute('height', cRect.height);
svg.setAttribute('viewBox', `0 0 ${cRect.width} ${cRect.height}`);
// Clear old connectors (keep defs)
svg.querySelectorAll('line, path, text.connector-label').forEach(el => el.remove());
connections.forEach(([fromId, toId, label]) => {
const fromEl = document.querySelector(`[data-id="${fromId}"]`);
const toEl = document.querySelector(`[data-id="${toId}"]`);
if (!fromEl || !toEl) return;
const f = fromEl.getBoundingClientRect();
const t = toEl.getBoundingClientRect();
const x1 = f.left + f.width/2 - cRect.left;
const y1 = f.bottom - cRect.top;
const x2 = t.left + t.width/2 - cRect.left;
const y2 = t.top - cRect.top;
if (Math.abs(x1 - x2) < 10) {
// Same column → straight line
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
svg.appendChild(line);
} else {
// Different column → bent line
const midY = (y1 + y2) / 2;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`);
svg.appendChild(path);
}
if (label) {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', (x1 + x2) / 2);
text.setAttribute('y', (y1 + y2) / 2 - 6);
text.setAttribute('class', 'connector-label');
text.textContent = label;
svg.appendChild(text);
}
});
}
// SVG defs (arrow definitions)
function ensureArrowDef(svg) {
if (svg.querySelector('#arrowhead')) return;
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.innerHTML = '<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#94A3B8" /></marker>';
svg.appendChild(defs);
}
```
### HTML Structure Example
```html
<div id="root">
<div class="flow-title">流程图标题</div>
<div style="position: relative;">
<svg class="flow-connectors" id="connectorSvg"></svg>
<div class="flow-grid" id="flowGrid">
<div class="flow-node start" data-id="start" style="grid-column: 2;">
<div class="node-title">开始</div>
</div>
<div class="flow-node" data-id="step1" style="grid-column: 1;">
<div class="node-title">步骤一</div>
<div class="node-desc">详细说明</div>
</div>
<div class="flow-node decision" data-id="decide" style="grid-column: 2;">
<div class="node-title">判断条件?</div>
</div>
<div class="flow-node end" data-id="end" style="grid-column: 2;">
<div class="node-title">结束</div>
</div>
</div>
</div>
<div class="flow-legend">
<div class="legend-item"><div class="legend-dot" style="border-color:#3B82F6;background:#EFF6FF;"></div>步骤</div>
<div class="legend-item"><div class="legend-dot" style="border-color:#F59E0B;background:#FFF7ED;"></div>判断</div>
</div>
</div>
<script>
const connections = [
['start', 'step1', ''], ['step1', 'decide', ''],
['decide', 'end', '通过']
];
window.addEventListener('load', () => {
ensureArrowDef(document.getElementById('connectorSvg'));
setTimeout(() => drawConnectors(connections), 100);
});
</script>
```
---
## Layout B: Snake Flowchart (>4 Linear Steps)
For: Linear non-branching processes with many steps (5-20).
### Key Rules
- Max 4 nodes per row
- First row left→right, second row right→left (snake pattern)
- End-of-row to next-row turn connectors use bend lines, with ≥60px clearance at turns
- Node positions in grid are auto-calculated by JS (no manual grid-column)
### Snake Layout Generation Script
```javascript
function layoutSnake(nodeIds, cols) {
cols = cols || 4;
nodeIds.forEach((id, i) => {
const row = Math.floor(i / cols);
const colInRow = i % cols;
const col = row % 2 === 0 ? colInRow + 1 : cols - colInRow; // Even rows L→R, odd rows R→L
const el = document.querySelector(`[data-id="${id}"]`);
if (el) {
el.style.gridRow = row + 1;
el.style.gridColumn = col;
}
});
}
```
---
## Layout C: Phased Vertical Flowchart (⭐ DEFAULT for all flowcharts)
**This is the DEFAULT layout for flowcharts.** When the user asks for any flowchart without specifying format, use this layout.
For: Any process with phases/stages, which is nearly ALL real-world processes (manufacturing, legal, project management, business operations, etc.). Also the safe fallback when unsure.
**Why this is the default:** Layout C produces consistently professional, readable results. Even if the process has only 2 "phases", the card-based grouping still looks clean. In contrast, Layout A (Grid) without proper grouping produces scattered, unreadable results.
### Key Design
**Phase titles vs sub-steps must have clear visual distinction**:
- Phase titles: colored background, font-size ≥ 16px, font-weight: 700
- Sub-steps: white/light gray background, font-size 14-15px, font-weight: 400-500
**No arrows between sub-steps**. Arrows only connect phase-to-phase. Sub-steps use indent + numbering for sequence.
### Phase Connector Direction Rule
Phase-to-phase connector arrows MUST match the logical flow direction. If the flow goes top → bottom, arrows point ↓. If bottom → top, arrows point ↑. If left → right, arrows point →. **Never draw arrows opposing the flow direction.**
### Additional CSS
```css
.phase-group {
background: #F8FAFC; border-radius: 12px; padding: 20px 24px;
margin-bottom: 24px;
}
.phase-title {
font-size: 16px; font-weight: 700; padding: 10px 16px;
border-radius: 8px; margin-bottom: 16px;
}
.phase-steps { display: flex; flex-direction: column; gap: 8px; padding-left: 12px; }
.phase-step {
font-size: 14px; font-weight: 400; color: var(--text);
padding: 8px 14px; background: white; border-radius: 6px;
border: 1px solid var(--border);
}
.phase-step .step-num {
display: inline-block; width: 22px; height: 22px; line-height: 22px;
text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;
margin-right: 8px;
}
/* Phase colors — same-hue blue-gray gradient (low saturation, easy on the eyes)
All phases share the blue-gray color family, distinguished by brightness progression.
🚫 FORBIDDEN: Different hue per phase (blue→green→amber→purple) — becomes rainbow with many phases.
✅ CORRECT: Progress within same hue family (light→dark), or pure grayscale + single-color accent.
Two schemes provided below; model selects based on phase count:
- ≤4 phases: Scheme A (blue-gray progression)
- 5-7 phases: Scheme B (neutral gray base + blue accent progression)
- >7 phases: all use same gray base, distinguish only by numbering
*/
/* Scheme A: Blue-gray progression (≤4 phases) */
.phase-1 .phase-title { background: #F0F4F8; color: #334155; border-left: 4px solid #64748B; }
.phase-1 .step-num { background: #E2E8F0; color: #475569; }
.phase-2 .phase-title { background: #E8EDF2; color: #1E3A5F; border-left: 4px solid #5B7A99; }
.phase-2 .step-num { background: #DBEAFE; color: #1E3A5F; }
.phase-3 .phase-title { background: #E0E7EF; color: #1E3050; border-left: 4px solid #4A6B8A; }
.phase-3 .step-num { background: #D0D9E4; color: #1E3050; }
.phase-4 .phase-title { background: #D8E0EA; color: #172540; border-left: 4px solid #3A5C7A; }
.phase-4 .step-num { background: #C7D2E0; color: #172540; }
/* Scheme B: Neutral gray base + blue accent progression (5-7 phases) */
/*
.phase-1 .phase-title { background: #F8FAFC; color: #334155; border-left: 4px solid #94A3B8; }
.phase-1 .step-num { background: #F1F5F9; color: #475569; }
.phase-2 .phase-title { background: #F1F5F9; color: #334155; border-left: 4px solid #7B8FA3; }
.phase-2 .step-num { background: #E2E8F0; color: #475569; }
.phase-3 .phase-title { background: #E8EDF3; color: #2D4156; border-left: 4px solid #6B809A; }
.phase-3 .step-num { background: #DBEAFE; color: #2D4156; }
.phase-4 .phase-title { background: #E2E8F0; color: #283C52; border-left: 4px solid #5B7590; }
.phase-4 .step-num { background: #D0D9E4; color: #283C52; }
.phase-5 .phase-title { background: #DAE1EB; color: #23364D; border-left: 4px solid #4B6A87; }
.phase-5 .step-num { background: #C7D2E0; color: #23364D; }
.phase-6 .phase-title { background: #D2DAE5; color: #1E3048; border-left: 4px solid #3B5F7D; }
.phase-6 .step-num { background: #BFC9D8; color: #1E3048; }
.phase-7 .phase-title { background: #CAD3E0; color: #192A3E; border-left: 4px solid #2B5473; }
.phase-7 .step-num { background: #B7C2D2; color: #192A3E; }
*/
```
### HTML Structure
```html
<div id="root">
<div class="flow-title">项目流程</div>
<div class="phase-group phase-1">
<div class="phase-title">第一阶段:需求分析</div>
<div class="phase-steps">
<div class="phase-step"><span class="step-num">1</span>需求收集与整理</div>
<div class="phase-step"><span class="step-num">2</span>可行性评估</div>
<div class="phase-step"><span class="step-num">3</span>需求优先级排序</div>
</div>
</div>
<!-- Phase-to-phase arrow (use SVG or simple centered down arrow) -->
<div style="text-align:center; color:#94A3B8; font-size:24px; margin: 8px 0;"></div>
<div class="phase-group phase-2">
<div class="phase-title">第二阶段:设计开发</div>
<div class="phase-steps">
<div class="phase-step"><span class="step-num">4</span>UI/UX 设计</div>
<div class="phase-step"><span class="step-num">5</span>前后端开发</div>
</div>
</div>
</div>
```
---
## Layout D: Dual-Panel / Swimlane Flowchart
For: Processes involving multiple roles or departments.
### Key Rules
- Canvas width ≥ 1600px
- Left panel for role/swimlane labels, right panel for flow nodes
- Role labels font-size ≥ 12px, solid background, right edge ≥ 40px from canvas
- `overflow: hidden` is forbidden
### Additional CSS
```css
.dual-layout { display: flex; gap: 40px; }
.role-panel {
flex-shrink: 0; width: 160px;
display: flex; flex-direction: column; gap: 12px;
}
.role-tag {
font-size: 13px; font-weight: 600; padding: 8px 12px;
border-radius: 6px; text-align: center;
}
.flow-panel { flex: 1; min-width: 0; }
```
---
## Infographic Templates
The following templates are for non-flowchart information visualization.
### Template: KPI Dashboard Cards
```css
.kpi-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 20px; margin-bottom: 32px;
}
.kpi-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 24px; text-align: center;
}
.kpi-label { font-size: 13px; color: var(--text-sub); margin-bottom: 8px; }
.kpi-value { font-size: 32px; font-weight: 700; }
.kpi-change { font-size: 14px; font-weight: 600; margin-top: 8px; }
.kpi-change.up { color: var(--positive); }
.kpi-change.down { color: var(--negative); }
```
### Template: CSS Bar Chart (Pure CSS, No JS)
```css
.bar-chart {
display: flex; align-items: flex-end; gap: 16px;
height: 300px; padding: 0 20px; border-bottom: 1px solid var(--border);
}
.bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.bar { width: 100%; max-width: 60px; border-radius: 6px 6px 0 0; background: var(--border); }
.bar.highlight { background: var(--blue); }
.bar-label { font-size: 12px; color: var(--text-sub); }
.bar-value { font-size: 11px; color: var(--text-muted); font-weight: 600; }
```
### Template: Gradient Background Infographic Header
```css
.hero-header {
background: linear-gradient(135deg, #1E293B 0%, #0F172A 100%);
border-radius: 16px; padding: 48px; color: white; margin-bottom: 32px;
}
.hero-header h1 { font-size: 28px; font-weight: 700; margin-bottom: 12px; }
.hero-header p { font-size: 15px; color: #94A3B8; max-width: 600px; line-height: 1.6; }
.hero-badge {
display: inline-block; background: rgba(59,130,246,0.15);
color: #60A5FA; font-size: 12px; font-weight: 600;
padding: 4px 12px; border-radius: 20px; margin-bottom: 16px;
}
```
### Template: Data Card + Mini Sparkline
```html
<div class="metric-card">
<div class="metric-info">
<div class="metric-title">月活用户</div>
<div class="metric-value">34,521</div>
<div class="metric-change" style="color: var(--positive)">↑ +18.2%</div>
</div>
<div class="metric-sparkline">
<svg width="120" height="40" viewBox="0 0 120 40">
<polyline fill="none" stroke="var(--blue)" stroke-width="2"
points="0,35 15,30 30,25 45,28 60,20 75,15 90,12 105,8 120,5" />
</svg>
</div>
</div>
```
```css
.metric-card {
background: white; border: 1px solid var(--border);
border-radius: 16px; padding: 28px;
display: flex; justify-content: space-between; align-items: center;
}
.metric-title { font-size: 13px; color: var(--text-sub); }
.metric-value { font-size: 36px; font-weight: 700; margin: 4px 0; }
.metric-change { font-size: 14px; font-weight: 600; }
```
---
## Advanced Connector Rules
### Many-to-One Convergence
When multiple lines converge into one node, use the "merge first, then enter" pattern:
```
[A] ──┐
[B] ──┤── → [目标]
[C] ──┘
```
Implementation: Source lines reach a relay x-coordinate, merge into one vertical line, then a single line enters the target.
### Cross-Layer Connector Avoidance
If other nodes block the path between two nodes, **do not draw a straight line through them**.
Priority:
1. **Redesign hierarchy** (best) — Most cross-layer lines indicate hierarchy design issues; connect adjacent layers only
2. **Detour line** — Route around middle nodes via canvas edge (offset 40px right)
3. **Thread through gap** — If middle-layer node gap ≥ 40px, route through the gap
### Connector Alignment
- Multiple lines from the same node start at a consistent position on the node border
- Use only one connector style per chart (right-angle/curved/straight), no mixing
- Connector label positions must be consistent (all above line or all centered)
---
## Font Size Rules
| Element | Recommended | Minimum |
|------|------|------|
| Flowchart main node title | 16-18px | 14px |
| Node description / subtext | 13-15px | 12px |
| Connector labels | 12-14px | 11px |
| Legend text | 13-14px | 12px |
| Footnotes / watermark | 11-13px | 10px |
| Phase titles | 16-18px | 16px |
| Role labels | 13-14px | 12px |
**Not enough space → Enlarge canvas, don't shrink fonts.**
---
## Overflow Protection
1. **`#root { width: fit-content; min-width: 800px; }`** — Expands with content automatically
2. **`overflow: hidden` / `overflow: clip` are forbidden**
3. **Auto-resize viewport before Playwright screenshot** (see 3.1 screenshot script)
4. **Canvas minimum width**:
| Layout Type | Minimum Width | Recommended |
|---------|---------|------|
| Single-column flowchart | 800px | 1000px |
| Dual-panel / swimlane | 1400px | 1600-1800px |
| Three-column / multi-panel | 1800px | 2000-2400px |
---
## Quality Checklist
1. **Layout C used by default** — If this is a flowchart, verify you're using Layout C (Phased Vertical) unless there's a specific reason not to
2. **Content complete** — Every node/step from the requirement is in the chart
3. **No overlap** — No boxes covering boxes, no lines through boxes
4. **Clear hierarchy** — Phase titles and sub-steps are instantly distinguishable
5. **Colors reasonable** — Node backgrounds low-saturation, no children's-drawing palette
6. **Connectors visible** — Connector color ≥ `#94A3B8`, arrow direction correct
7. **Font sizes meet standards** — Check against font size table, nothing below minimum
8. **Legend independent** — Not inside flow-grid, not obscured by any node
9. **No clipping** — Padding ≥ 40px on all sides, all nodes and labels fully visible
10. **Phase colors consistent** — Same hue family, no blue/brown/green/purple mix
11. **Connectors don't pass through nodes** — Cross-layer lines use detour or redesign hierarchy
12. **No scattered layout** — Phase titles MUST be inside group cards, NOT floating as isolated labels
---
## Playwright+CSS vs Other Approaches
| Capability | Playwright+CSS | matplotlib | ECharts |
|------|---------------|------------|---------|
| Gradients/shadows/rounded | ✅ Full CSS power | ❌ Limited | ⚠️ Partial |
| Responsive layout | ✅ Flexbox/Grid | ❌ Fixed size | ⚠️ resize |
| PNG/PDF export | ✅ Native | ✅ savefig | ⚠️ Needs Playwright |
| Precise data charts | ⚠️ Manual | ✅ Built-in | ✅ Built-in |
**Best practice: CSS for layout and visual design, embed ECharts/SVG for precise charts.**

View File

@@ -0,0 +1,576 @@
# CSS Radial Grid Layout (Center-Outward Diagrams)
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
**For: SWOT analysis, Balanced Scorecard (BSC), Porter's Five Forces, PEST analysis, and any "center + 4-6 surrounding dimensions" diagram.**
**Core principle: Use flex rows to lock positions — model never calculates coordinates. Connectors are drawn by script reading bounding boxes.**
---
## When to Use This Template
| Diagram Type | Dimensions | Use This? |
|-------------|-----------|-----------|
| SWOT (Strengths/Weaknesses/Opportunities/Threats) | 4 quadrants | ✅ Layout B (2×2 Grid) |
| Balanced Scorecard (Financial/Customer/Internal/Learning) | 4 dimensions | ✅ Layout A (Cross) |
| Porter's Five Forces | 5 forces | ✅ Layout A (Cross + extra row) |
| PEST (Political/Economic/Social/Technological) | 4 dimensions | ✅ Layout B (2×2 Grid) |
| Competency wheel / capability map | 5-8 dimensions | ✅ Layout A with extra rows |
| Anything with center + surrounding elements | 3-8 | ✅ |
---
## Layout A: Cross Layout (4-6 Dimensions)
Best for: BSC, Porter's Five Forces, any "center with surrounding dimensions" structure.
### 🚫 FORBIDDEN: 3×3 CSS Grid Cross
**Do NOT use `grid-template-columns: Xpx Ypx Xpx` to place cards in a cross pattern.** The top/bottom cards in the center column will overflow into the side columns and overlap with left/right cards when content is longer than expected.
### ✅ REQUIRED: Three-Row Flex Layout
**Each row is an independent flex container. Rows cannot overlap each other — physically impossible.**
```
Row 1 (top): [top dimension card] ← independent flex row, centered
Row 2 (middle): [left card] [center] [right card] ← independent flex row, horizontal
Row 3 (bottom): [bottom dimension card] ← independent flex row, centered
```
### HTML + CSS
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
background: #FFFFFF;
}
#root { width: fit-content; min-width: 900px; padding: 48px 60px; }
.diagram-title {
font-size: 22px; font-weight: 700; color: #1F2937;
text-align: center; margin-bottom: 6px;
}
.diagram-subtitle {
font-size: 14px; color: #6B7280;
text-align: center; margin-bottom: 40px;
}
/* === Three-row flex layout: rows CANNOT overlap === */
.cross-layout {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
position: relative;
}
/* Middle row: left card + center + right card */
.middle-row {
display: flex;
align-items: center;
gap: 40px;
}
/* Center node */
.center-node {
background: linear-gradient(135deg, #1E293B, #334155);
color: white; font-size: 18px; font-weight: 700;
padding: 24px 32px; border-radius: 14px;
text-align: center; z-index: 2;
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
flex-shrink: 0;
}
.center-sub {
font-size: 12px; font-weight: 400; color: #94A3B8;
margin-top: 4px;
}
/* Dimension cards */
.dim-card {
background: #FFFFFF; border-radius: 12px;
padding: 20px; border: 2px solid;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
z-index: 2;
width: 260px;
flex-shrink: 0;
}
.dim-card .dim-title {
font-size: 15px; font-weight: 700; margin-bottom: 10px;
padding-bottom: 8px; border-bottom: 2px solid;
display: flex; align-items: center; gap: 8px;
}
.dim-card .dim-icon {
width: 26px; height: 26px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.dim-card .dim-items { list-style: none; }
.dim-card .dim-items li {
font-size: 13px; color: #475569; padding: 4px 0 4px 18px;
position: relative; line-height: 1.5;
}
.dim-card .dim-items li::before {
content: ''; position: absolute; left: 0; top: 10px;
width: 6px; height: 6px; border-radius: 50%;
}
/* Color variants — border + title border + bullet */
.dim-blue { border-color: #3B82F6; }
.dim-blue .dim-title { color: #1E40AF; border-color: #3B82F6; }
.dim-blue .dim-icon { background: #DBEAFE; color: #1E40AF; }
.dim-blue li::before { background: #3B82F6; }
.dim-green { border-color: #10B981; }
.dim-green .dim-title { color: #065F46; border-color: #10B981; }
.dim-green .dim-icon { background: #D1FAE5; color: #065F46; }
.dim-green li::before { background: #10B981; }
.dim-amber { border-color: #F59E0B; }
.dim-amber .dim-title { color: #92400E; border-color: #F59E0B; }
.dim-amber .dim-icon { background: #FEF3C7; color: #92400E; }
.dim-amber li::before { background: #F59E0B; }
.dim-purple { border-color: #8B5CF6; }
.dim-purple .dim-title { color: #5B21B6; border-color: #8B5CF6; }
.dim-purple .dim-icon { background: #EDE9FE; color: #5B21B6; }
.dim-purple li::before { background: #8B5CF6; }
.dim-red { border-color: #EF4444; }
.dim-red .dim-title { color: #991B1B; border-color: #EF4444; }
.dim-red .dim-icon { background: #FEE2E2; color: #991B1B; }
.dim-red li::before { background: #EF4444; }
.dim-cyan { border-color: #06B6D4; }
.dim-cyan .dim-title { color: #155E75; border-color: #06B6D4; }
.dim-cyan .dim-icon { background: #CFFAFE; color: #155E75; }
.dim-cyan li::before { background: #06B6D4; }
/* SVG connector layer */
.cross-connectors {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 1;
}
</style>
</head>
<body>
<div id="root">
<div class="diagram-title">平衡计分卡四维评价体系</div>
<div class="diagram-subtitle">基于战略目标的绩效管理框架</div>
<div class="cross-layout" id="crossLayout">
<!-- SVG connectors drawn by script -->
<svg class="cross-connectors" id="connSvg"></svg>
<!-- Row 1: top dimension -->
<div class="dim-card dim-blue" data-pos="top">
<div class="dim-title"><div class="dim-icon">F</div> 财务维度</div>
<ul class="dim-items">
<li>营收增长率</li>
<li>利润率</li>
<li>ROI</li>
</ul>
</div>
<!-- Row 2: left + center + right -->
<div class="middle-row">
<div class="dim-card dim-green" data-pos="left">
<div class="dim-title"><div class="dim-icon">I</div> 内部流程</div>
<ul class="dim-items">
<li>流程效率</li>
<li>质量管控</li>
<li>创新能力</li>
</ul>
</div>
<div class="center-node">战略目标</div>
<div class="dim-card dim-amber" data-pos="right">
<div class="dim-title"><div class="dim-icon">C</div> 客户维度</div>
<ul class="dim-items">
<li>客户满意度</li>
<li>市场份额</li>
<li>客户留存率</li>
</ul>
</div>
</div>
<!-- Row 3: bottom dimension -->
<div class="dim-card dim-purple" data-pos="bottom">
<div class="dim-title"><div class="dim-icon">L</div> 学习与成长</div>
<ul class="dim-items">
<li>员工能力提升</li>
<li>信息系统建设</li>
<li>组织文化</li>
</ul>
</div>
</div>
</div>
<script>
function drawCrossConnectors() {
const layout = document.getElementById('crossLayout');
const svg = document.getElementById('connSvg');
const gRect = layout.getBoundingClientRect();
svg.setAttribute('width', gRect.width);
svg.setAttribute('height', gRect.height);
svg.setAttribute('viewBox', `0 0 ${gRect.width} ${gRect.height}`);
svg.innerHTML = '';
// Bidirectional arrow markers — eliminates direction ambiguity
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.innerHTML = `
<marker id="arrowEnd" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#94A3B8" />
</marker>
<marker id="arrowStart" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto">
<polygon points="8 0, 0 3, 8 6" fill="#94A3B8" />
</marker>
`;
svg.appendChild(defs);
const center = layout.querySelector('.center-node');
const cR = center.getBoundingClientRect();
const cx = cR.left - gRect.left + cR.width / 2;
const cy = cR.top - gRect.top + cR.height / 2;
// Draw connector from center edge to each card edge
const cards = layout.querySelectorAll('.dim-card');
cards.forEach(card => {
const pos = card.dataset.pos;
const r = card.getBoundingClientRect();
const cardCx = r.left - gRect.left + r.width / 2;
const cardCy = r.top - gRect.top + r.height / 2;
let x1, y1, x2, y2;
switch (pos) {
case 'top':
x1 = cx; y1 = cR.top - gRect.top; // center top edge midpoint
x2 = cardCx; y2 = r.bottom - gRect.top; // card bottom edge midpoint
break;
case 'bottom':
x1 = cx; y1 = cR.bottom - gRect.top; // center bottom edge midpoint
x2 = cardCx; y2 = r.top - gRect.top; // card top edge midpoint
break;
case 'left':
x1 = cR.left - gRect.left; y1 = cy; // center left edge midpoint
x2 = r.right - gRect.left; y2 = cardCy; // card right edge midpoint
break;
case 'right':
x1 = cR.right - gRect.left; y1 = cy; // center right edge midpoint
x2 = r.left - gRect.left; y2 = cardCy; // card left edge midpoint
break;
// --- 5+ dimension: bottom row with two cards side by side ---
case 'bottom-left':
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT (not corner!)
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
break;
case 'bottom-right':
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT (not corner!)
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
break;
}
// 🚫 FORBIDDEN: drawing lines from center CORNERS (e.g. cR.left + cR.bottom)
// All lines MUST originate from center EDGE MIDPOINTS (cx or cy)
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', '#94A3B8');
line.setAttribute('stroke-width', '2');
line.setAttribute('stroke-dasharray', '6,4');
line.setAttribute('marker-start', 'url(#arrowStart)');
line.setAttribute('marker-end', 'url(#arrowEnd)');
svg.appendChild(line);
});
}
window.addEventListener('load', () => setTimeout(drawCrossConnectors, 300));
</script>
</body>
</html>
```
**Key design decisions:**
- **Three-row flex** instead of 3×3 Grid — rows physically cannot overlap
- **Fixed card width (260px)** — prevents content-driven overflow
- **`gap: 24px`** between rows, **`gap: 40px`** within middle row — generous spacing
- **Center node uses `flex-shrink: 0`** — never collapses under pressure
### Adapting for 5+ Dimensions
For Porter's Five Forces (5 dimensions) or more:
```html
<!-- Row 1: top -->
<div class="dim-card dim-blue" data-pos="top">...</div>
<!-- Row 2: left + center + right -->
<div class="middle-row">
<div class="dim-card dim-green" data-pos="left">...</div>
<div class="center-node">...</div>
<div class="dim-card dim-amber" data-pos="right">...</div>
</div>
<!-- Row 3: bottom-left + bottom-right (two cards side by side) -->
<div class="middle-row" style="gap: 40px;">
<div class="dim-card dim-purple" data-pos="bottom-left">...</div>
<div class="dim-card dim-red" data-pos="bottom-right">...</div>
</div>
```
For the connector script, add cases for `bottom-left` and `bottom-right`:
```javascript
// 🚫 FORBIDDEN: using cR.left/cR.right as x1 — that draws from center CORNER, angle is ugly
// ✅ CORRECT: always use cx (center bottom midpoint) as x1
case 'bottom-left':
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
break;
case 'bottom-right':
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
break;
```
### Adapting for 3 Dimensions
Simply remove one side card from the middle row:
```html
<!-- Row 1: top -->
<div class="dim-card dim-blue" data-pos="top">...</div>
<!-- Row 2: center + right only -->
<div class="middle-row">
<div class="center-node">...</div>
<div class="dim-card dim-amber" data-pos="right">...</div>
</div>
<!-- Row 3: bottom -->
<div class="dim-card dim-purple" data-pos="bottom">...</div>
```
---
## Layout B: 2×2 Quadrant Grid (SWOT / PEST)
Best for: exactly 4 dimensions arranged as equal quadrants (no center node needed).
**This layout has NO center node — the four quadrants themselves tell the story.**
### HTML + CSS
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
background: #FFFFFF;
}
#root { width: fit-content; min-width: 900px; padding: 48px 60px; }
.diagram-title {
font-size: 22px; font-weight: 700; color: #1F2937;
text-align: center; margin-bottom: 12px;
}
.diagram-subtitle {
font-size: 14px; color: #6B7280;
text-align: center; margin-bottom: 36px;
}
.quadrant-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
max-width: 800px;
margin: 0 auto;
}
.quadrant {
border-radius: 12px; padding: 24px;
border: 1.5px solid;
min-height: 200px;
}
.quadrant .q-title {
font-size: 16px; font-weight: 700; margin-bottom: 14px;
display: flex; align-items: center; gap: 8px;
}
.quadrant .q-icon {
width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700;
}
.quadrant .q-items { list-style: none; }
.quadrant .q-items li {
font-size: 13px; padding: 5px 0 5px 18px;
position: relative; line-height: 1.5;
}
.quadrant .q-items li::before {
content: ''; position: absolute; left: 0; top: 11px;
width: 6px; height: 6px; border-radius: 50%;
}
/* SWOT colors — low-sat backgrounds, sat borders */
.q-strengths { background: #F0FDF4; border-color: #86EFAC; }
.q-strengths .q-title { color: #065F46; }
.q-strengths .q-icon { background: #D1FAE5; color: #065F46; }
.q-strengths li { color: #1F2937; }
.q-strengths li::before { background: #10B981; }
.q-weaknesses { background: #FEF2F2; border-color: #FECACA; }
.q-weaknesses .q-title { color: #991B1B; }
.q-weaknesses .q-icon { background: #FEE2E2; color: #991B1B; }
.q-weaknesses li { color: #1F2937; }
.q-weaknesses li::before { background: #EF4444; }
.q-opportunities { background: #EFF6FF; border-color: #93C5FD; }
.q-opportunities .q-title { color: #1E40AF; }
.q-opportunities .q-icon { background: #DBEAFE; color: #1E40AF; }
.q-opportunities li { color: #1F2937; }
.q-opportunities li::before { background: #3B82F6; }
.q-threats { background: #FFF7ED; border-color: #FDE68A; }
.q-threats .q-title { color: #92400E; }
.q-threats .q-icon { background: #FEF3C7; color: #92400E; }
.q-threats li { color: #1F2937; }
.q-threats li::before { background: #F59E0B; }
</style>
</head>
<body>
<div id="root">
<div class="diagram-title">SWOT 分析</div>
<div class="diagram-subtitle">企业战略定位评估</div>
<div class="quadrant-grid">
<div class="quadrant q-strengths">
<div class="q-title"><div class="q-icon">S</div> 优势 Strengths</div>
<ul class="q-items">
<li>核心技术领先</li>
<li>品牌知名度高</li>
<li>供应链成熟</li>
</ul>
</div>
<div class="quadrant q-weaknesses">
<div class="q-title"><div class="q-icon">W</div> 劣势 Weaknesses</div>
<ul class="q-items">
<li>国际化经验不足</li>
<li>产品线单一</li>
<li>人才储备有限</li>
</ul>
</div>
<div class="quadrant q-opportunities">
<div class="q-title"><div class="q-icon">O</div> 机会 Opportunities</div>
<ul class="q-items">
<li>新兴市场需求增长</li>
<li>政策利好</li>
<li>技术融合趋势</li>
</ul>
</div>
<div class="quadrant q-threats">
<div class="q-title"><div class="q-icon">T</div> 威胁 Threats</div>
<ul class="q-items">
<li>竞争加剧</li>
<li>原材料价格波动</li>
<li>法规变化风险</li>
</ul>
</div>
</div>
</div>
</body>
</html>
```
**No connectors needed** — the 2×2 grid itself communicates the four-quadrant relationship. Adding arrows would be visual noise.
---
## Connector Rules
1. **Connectors are ALWAYS drawn by script reading bounding boxes** — never hardcode x/y values in HTML/CSS
2. **Straight lines only** (horizontal or vertical) — no diagonal lines unless top/bottom cards are offset from center
3. **Dashed lines** (`stroke-dasharray: 6,4`) for conceptual relationships
4. **Solid lines** for causal/sequential relationships
5. **Arrow direction**: model chooses based on diagram semantics — pick ONE style per diagram, don't mix:
- **Outward** (`marker-end` only): center influences/drives dimensions (e.g. BSC: strategy → dimensions)
- **Inward** (`marker-start` only): dimensions report/pressure center (e.g. Porter: forces → competition)
- **Bidirectional** (`marker-start` + `marker-end`): mutual influence (e.g. feedback loops)
6. **🚫 All lines MUST originate from center EDGE MIDPOINTS** (cx or cy) — never from center corners
7. **Line color**: `#94A3B8` (gray-blue) — never use dimension-specific colors for connectors (visual chaos)
### SVG Arrow Markers (copy-paste into defs)
```javascript
// Include both markers; use only what the diagram needs
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.innerHTML = `
<marker id="arrowEnd" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#94A3B8" />
</marker>
<marker id="arrowStart" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto">
<polygon points="8 0, 0 3, 8 6" fill="#94A3B8" />
</marker>
`;
svg.appendChild(defs);
// Outward (center → card):
line.setAttribute('marker-end', 'url(#arrowEnd)');
// Inward (card → center):
line.setAttribute('marker-start', 'url(#arrowStart)');
// Bidirectional:
line.setAttribute('marker-start', 'url(#arrowStart)');
line.setAttribute('marker-end', 'url(#arrowEnd)');
```
---
## Content Rules
1. **Each dimension card: max 6 bullet items** — more than 6 → group into sub-categories or use a separate detail table
2. **Bullet text: max 15 Chinese characters per line** — longer text wraps naturally (word-break: break-word)
3. **Center node text: max 8 Chinese characters** — keep it a short label, not a sentence
4. **Dimension title: max 10 Chinese characters** — concise category name
---
## Font Size Rules
| Element | Size | Weight |
|---------|------|--------|
| Diagram title | 22px | 700 |
| Center node | 18px | 700 |
| Dimension title | 15-16px | 700 |
| Bullet items | 13px | 400 |
| Subtitle/footnote | 14px | 400 |
---
## Quality Checklist
1. **No overlap** — all dimension cards fully visible, none clipped or covering another
2. **Connectors point to card edges** — not to random points in space
3. **Center node visually dominant** — largest, darkest, or most contrast
4. **Dimension cards visually equal** — similar size, similar padding, no one card 3× larger
5. **Colors follow scheme** — each dimension has its own color (border + title), backgrounds stay pale (white or near-white)
6. **Layout is centered** — equal margins on all sides
7. **Canvas large enough** — min 900px wide for cross layout, min 800px for 2×2 quadrant

View File

@@ -0,0 +1,324 @@
# Seaborn Template Library
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
Seaborn is built on top of matplotlib, specializing in **statistical visualization** — distributions, regression, categorical comparisons, etc.
Its default styles are already much better looking than matplotlib, but still need tuning to reach professional standards.
## Environment Setup
```python
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
# ═══ Chinese Font Setup ═══
# Font path: adjust for your system. Common locations:
# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf'
# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
# Custom: './fonts/SimHei.ttf'
import os
SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf')
matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH)
# Seaborn theme + custom overrides
sns.set_theme(style='whitegrid', font='SimHei', rc={
'axes.unicode_minus': False,
'figure.facecolor': '#FFFFFF',
'axes.facecolor': '#FFFFFF',
'axes.edgecolor': '#E5E7EB',
'axes.linewidth': 0.8,
'axes.spines.top': False,
'axes.spines.right': False,
'grid.color': '#F3F4F6',
'grid.alpha': 0.5,
'grid.linewidth': 0.5,
'xtick.major.size': 0,
'ytick.major.size': 0,
'axes.titlesize': 16,
'axes.titleweight': 'bold',
'axes.titlepad': 16,
'legend.frameon': False,
'figure.dpi': 200,
'savefig.dpi': 200,
'savefig.bbox': 'tight',
})
# Colors (consistent with matplotlib.md)
COOL = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981']
CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']
```
## Seaborn Palette Setup
```python
# Method 1: use hex list directly
sns.set_palette(COOL)
# Method 2: register custom palette
from matplotlib.colors import ListedColormap
bc_palette = ListedColormap(COOL, name='charts')
```
---
## Template 1: Distribution Plot (Histogram + KDE)
**Scenario**: View data distribution shape, detect skewness and outliers.
```python
def dist_plot(data, title, xlabel, color='#3B82F6', save_path='dist.png'):
fig, ax = plt.subplots(figsize=(10, 6))
sns.histplot(data, kde=True, color=color, edgecolor='white',
linewidth=0.5, alpha=0.7, ax=ax)
# Bold the KDE line
for line in ax.get_lines():
line.set_linewidth(2.5)
ax.set_title(title, loc='left')
ax.set_xlabel(xlabel)
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 2: Box Plot (Categorical Comparison)
**Scenario**: Compare distribution characteristics across groups (median, quartiles, outliers).
```python
def box_compare(df, x_col, y_col, title, palette=None, save_path='box.png'):
if palette is None:
palette = COOL
fig, ax = plt.subplots(figsize=(10, 6))
sns.boxplot(data=df, x=x_col, y=y_col, palette=palette,
width=0.5, linewidth=1.2, fliersize=4,
boxprops=dict(edgecolor='white'),
medianprops=dict(color='white', linewidth=2),
ax=ax)
ax.set_title(title, loc='left')
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
### Box Plot + Label Avoidance (For Complex Scenarios)
When you need to annotate outliers, specific data points, or group names on a box plot, **label avoidance is required**:
```python
def box_annotated(df, x_col, y_col, title, annotations=None,
palette=None, save_path='box_annotated.png'):
"""
annotations: [{'x': 0, 'y': 45, 'text': '产线A 异常'}, ...]
"""
from adjustText import adjust_text
if palette is None:
palette = COOL
fig, ax = plt.subplots(figsize=(12, 7)) # Slightly larger canvas, leave room for annotations
sns.boxplot(data=df, x=x_col, y=y_col, palette=palette,
width=0.5, linewidth=1.2, fliersize=4,
boxprops=dict(edgecolor='white'),
medianprops=dict(color='white', linewidth=2),
ax=ax)
# Annotation text — use adjustText for auto-avoidance
if annotations:
texts = []
for ann in annotations:
t = ax.text(ann['x'], ann['y'], ann['text'],
fontsize=9, color='#374151',
bbox=dict(boxstyle='round,pad=0.3',
facecolor='#FFF7ED', edgecolor='#F59E0B',
alpha=0.9))
texts.append(t)
adjust_text(texts, ax=ax,
arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8),
force_text=(0.8, 1.0),
force_points=(0.5, 0.8))
ax.set_title(title, loc='left')
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
### Box Plot + Colorbar (e.g., MTTR Analysis)
When a box plot needs to work with a colorbar (e.g., colored by dimension), you must leave enough space for the colorbar:
```python
def box_with_colorbar(df, x_col, y_col, color_col, title,
save_path='box_cbar.png'):
import matplotlib.gridspec as gridspec
from matplotlib.colors import Normalize
from matplotlib.cm import ScalarMappable
fig = plt.figure(figsize=(14, 7), constrained_layout=True)
gs = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 0.03], wspace=0.05)
ax = fig.add_subplot(gs[0, 0])
cbar_ax = fig.add_subplot(gs[0, 1])
sns.boxplot(data=df, x=x_col, y=y_col, ax=ax,
width=0.5, linewidth=1.2)
# Colorbar in its own subplot, won't obscure box plot
norm = Normalize(vmin=df[color_col].min(), vmax=df[color_col].max())
sm = ScalarMappable(norm=norm, cmap='Blues')
fig.colorbar(sm, cax=cbar_ax, label=color_col)
ax.set_title(title, loc='left')
fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 3: Violin Plot
**Scenario**: Enhanced version of box plot, showing distribution density simultaneously.
```python
def violin_plot(df, x_col, y_col, title, palette=None, save_path='violin.png'):
if palette is None:
palette = COOL
fig, ax = plt.subplots(figsize=(10, 6))
sns.violinplot(data=df, x=x_col, y=y_col, palette=palette,
inner='box', linewidth=1, ax=ax)
ax.set_title(title, loc='left')
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 4: Regression Scatter Plot
**Scenario**: Two-variable relationship + linear regression + confidence interval.
```python
def reg_plot(df, x_col, y_col, title, color='#3B82F6', save_path='reg.png'):
fig, ax = plt.subplots(figsize=(10, 7))
sns.regplot(data=df, x=x_col, y=y_col, color=color,
scatter_kws={'s': 50, 'alpha': 0.6, 'edgecolor': 'white', 'linewidth': 1},
line_kws={'linewidth': 2},
ax=ax)
ax.set_title(title, loc='left')
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 5: Correlation Heatmap
**Scenario**: Correlation coefficient matrix among multiple variables.
```python
def corr_heatmap(df, title, cmap='RdBu_r', save_path='corr.png'):
corr = df.corr()
fig, ax = plt.subplots(figsize=(10, 8))
# Show only lower triangle
mask = np.triu(np.ones_like(corr, dtype=bool))
sns.heatmap(corr, mask=mask, cmap=cmap, center=0,
vmin=-1, vmax=1, annot=True, fmt='.2f',
square=True, linewidths=1, linecolor='white',
cbar_kws={'shrink': 0.8, 'label': 'Correlation'},
annot_kws={'size': 9},
ax=ax)
ax.set_title(title, loc='left', pad=16)
plt.tight_layout()
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 6: Pair Plot
**Scenario**: Overview of pairwise relationships among multiple variables (distribution on diagonal, scatter plots elsewhere).
```python
def pair_plot(df, hue_col=None, palette=None, save_path='pair.png'):
if palette is None:
palette = COOL
g = sns.pairplot(df, hue=hue_col, palette=palette,
diag_kind='kde', plot_kws={'alpha': 0.6, 's': 30},
height=2.5, aspect=1)
g.figure.suptitle('Pairwise Variable Relationships', y=1.02, fontsize=16, fontweight='bold')
g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Template 7: Facet Grid (FacetGrid)
**Scenario**: Split into multiple subplots by categorical variable for comparison.
```python
def facet_hist(df, col_var, value_col, title, color='#3B82F6', save_path='facet.png'):
g = sns.FacetGrid(df, col=col_var, col_wrap=3, height=3.5, aspect=1.3)
g.map(sns.histplot, value_col, color=color, edgecolor='white',
linewidth=0.5, alpha=0.7, kde=True)
g.set_titles('{col_name}', fontsize=12, fontweight='bold')
g.figure.suptitle(title, y=1.02, fontsize=16, fontweight='bold')
g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
plt.close()
```
---
## Seaborn vs matplotlib Selection Guide
| What to Plot | Use Seaborn | Use matplotlib |
|---------|-----------|--------------|
| Histogram/KDE distribution | ✅ `histplot` / `kdeplot` | Works, but more code |
| Box plot/Violin plot | ✅ One-liner | Works, but rougher style |
| Regression scatter + confidence interval | ✅ `regplot` auto-calculates | Manual fitting + plotting |
| Correlation heatmap | ✅ `heatmap` + mask | Manual `imshow` tedious |
| Pairwise relationship matrix | ✅ `pairplot` unique | No equivalent |
| Facet grid | ✅ `FacetGrid` | `plt.subplots` manual loop |
| Regular bar chart | Less flexible than matplotlib | ✅ More control |
| Line trend chart | Less than matplotlib | ✅ More control |
| Custom annotations/arrows | Not suitable | ✅ `ax.annotate` |
**Principle: Use Seaborn for statistical charts, matplotlib for customized charts.**