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

13
skills/charts/LICENSE.txt Executable file
View File

@@ -0,0 +1,13 @@
Copyright (c) 2026 Z.ai All rights reserved.
Permission is granted for personal, educational, and non-commercial use only.
Commercial use is strictly prohibited without prior written permission from the author.
Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited.
The author reserves the right to make the final determination of what constitutes "commercial use".
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.

427
skills/charts/SKILL.md Executable file
View File

@@ -0,0 +1,427 @@
---
name: charts
metadata:
author: Z.AI
version: "1.0"
description: >
Professional chart and diagram creation skill. Covers all types of visual data
representation and structural diagrams:
- **Data charts**: bar charts, line charts, pie charts, scatter plots, heatmaps,
radar charts, candlestick charts, boxplots, histograms, area charts, waterfall charts,
regression plots, distribution plots, and statistical visualizations.
- **Structural diagrams**: flowcharts, mind maps, tree diagrams, org charts,
architecture diagrams, network/relationship graphs, ER diagrams, class diagrams,
Gantt charts, swimlane diagrams, and sequence diagrams.
- **Dashboards**: data dashboards, KPI panels, multi-chart compositions,
and interactive visualizations.
- **Design quality**: professional color systems, anti-overlap rules, layout optimization,
scene-based framework routing (matplotlib, seaborn, ECharts, D3.js, Mermaid, Playwright+CSS),
and publication-ready output.
Applies when the user wants to create, generate, draw, plot, visualize, or improve
any chart, graph, diagram, or dashboard. Also applies when the user asks for something
more polished, cleaner, or publication-ready.
NOT for: PDF document layout (use pdf skill), slide decks (use slides skill),
spreadsheets with embedded charts (use xlsx skill), AI image generation (use image_gen),
posters / infographics / creative cards (use pdf skill Creative pipeline).
FORBIDDEN: Using matplotlib/seaborn to draw mind maps, tree diagrams, org charts,
flowcharts, or any structural diagram. These MUST use Playwright+CSS.
license: Proprietary. LICENSE.txt has complete terms
---
# Beautiful Charts
## Quick Setup
```bash
bash "$SKILL_DIR/setup.sh" # Interactive environment check + install
```
Make every chart and diagram look professionally designed, not AI-generated.
## Architecture
| Module | File | When to Load |
|--------|------|-------------|
| **Routing + Core Rules** | This file | Always read first |
| **Framework Templates** | `references/` by framework | After choosing framework, read the corresponding file |
**Loading order: Read this file → choose framework → read template file → start coding.**
Each template file contains its own framework-specific rules (spacing, connectors, color details). This file contains only routing decisions and universal rules that apply to ALL charts.
---
# Part 1: Routing
## ⚠️ Format Constraint Rule (HIGHEST PRIORITY)
**When the user specifies an output format/tool, you MUST comply. Never substitute.**
| User Says | You Must Do | Forbidden |
|-----------|------------|-----------|
| "use mermaid code" / "用Mermaid格式输出" / "转化为mermaid" / "mermaid流程" | ① Output Mermaid code block (```mermaid ... ```) ② Also provide a rendered image preview | ❌ Cannot only give image without code; ❌ Cannot screenshot raw code text as image |
| "use markdown code" | Output markdown-formatted hierarchy | ❌ Cannot switch to HTML/CSS |
| "via mermaid or markdown code" | Choose one of the two, output code text | ❌ Cannot switch to any non-specified format |
| "flowchart" / "mind map" (no format specified) | Free to choose the best approach | - |
| "use echarts/d3" | Must use the specified framework | ❌ Cannot switch |
### 🚫 FORBIDDEN: Mermaid Code Screenshot
**NEVER take a screenshot of raw Mermaid source code and deliver it as the "diagram image".** This is the worst possible outcome — the user gets neither usable code nor a visual diagram. When the user requests Mermaid format:
1. **MUST** output the Mermaid code in a fenced code block (````mermaid`)
2. **SHOULD** also render the code into a visual diagram image (via mermaid-cli or Playwright + mermaid.js)
3. If rendering fails, deliver the code block and tell the user to paste it into mermaid.live
### Format Specified vs Auto-Upgrade Conflict
When the user specifies Mermaid but content triggers auto-upgrade conditions (>8 nodes, CJK-heavy, etc.):
1. **User choice wins** — still use Mermaid, deliver code block + rendered image
2. **Proactively guide** — after delivery, suggest the user try without specifying Mermaid for better layout quality
3. **Never silently switch** to Playwright+CSS when user explicitly asked for Mermaid
When a specified tool hits rendering difficulties (e.g., mermaid CDN fails):
- ✅ Output raw mermaid code text, tell user to view at mermaid.live
- ❌ Secretly switch to another framework
- ❌ Screenshot the code text as an "image"
---
## Routing Decision Tree
### 1. Structural Diagrams
#### 🔴 Flowchart Default: Phased Vertical (HIGHEST PRIORITY)
**When the user asks to "generate/create a XXX flowchart/流程图" without specifying format, the DEFAULT layout is Phased Vertical (Layout C in `references/playwright-css.md`).**
This is because nearly all real-world processes (manufacturing, legal proceedings, project management, business operations, cooking recipes, etc.) have natural phases/stages. Layout C produces the most professional, readable result.
**Flowchart routing priority:**
1. **User specified Mermaid/markdown** → follow user choice (Format Constraint Rule)
2. **≤6 nodes AND no phases AND short text** → Mermaid (simple flowchart)
3. **Everything else****Playwright + CSS, Layout C (Phased Vertical)**`references/playwright-css.md`
**Phase detection — treat as "has phases" when ANY is true:**
- Content has numbered sections (一、二、三 or 1. 2. 3. or Phase 1/Stage 1)
- Process can be grouped by time/stage/role (e.g., "preparation → execution → review")
- Total steps ≥ 5 (almost always groupable into 2+ phases)
- Process involves multiple roles/departments
- Process has clear start/end with intermediate stages
**⚠️ When in doubt, default to Layout C.** A phased layout with only 1 phase still looks professional. A Grid layout with phases looks like a mess.
#### Other Structural Diagrams
- Simple flowchart (≤6 nodes, truly flat, no phases): **Mermaid**
- Complex flowchart (>6 nodes / CJK-heavy / branches / phases): **Playwright + CSS Layout C**`references/playwright-css.md`
- Mind map / tree / org chart: **Playwright + CSS**`references/mindmap-css.md`
- Relationship / network diagram: **ECharts graph**
- Center-radial analysis (SWOT / BSC / Porter's Five Forces / PEST): **Playwright + CSS**`references/radial-grid.md`
### 2. Data Charts (matplotlib / seaborn)
- Standard bar/line/scatter/heatmap/radar/pie: **matplotlib**
- Regression/distribution/boxplot: **Seaborn**
### 3. Interactive Charts / Dashboards
- Data dashboard / candlestick / real-time: **ECharts**
- Fully custom interactive: **D3.js**
### Default Strategy
**One scene, one tool — don't hesitate:**
| Scene | Tool | Template |
|-------|------|----------|
| Data chart (bar/line/scatter/pie/radar) | matplotlib | `references/matplotlib.md` |
| Statistical (regression/box/dist) | Seaborn | `references/seaborn.md` |
| Mind map / tree / org chart | Playwright + CSS | `references/mindmap-css.md` |
| Center-radial (SWOT/BSC/PEST/Five Forces) | Playwright + CSS | `references/radial-grid.md` |
| **Any flowchart (default)** | **Playwright + CSS Layout C** | **`references/playwright-css.md`** |
| Simple flowchart (≤6 nodes, truly flat) | Mermaid | `references/mermaid.md` |
| Relationship / force-directed | ECharts graph | `references/echarts.md` |
| Data dashboard | ECharts | `references/echarts.md` |
| Academic paper figures | matplotlib | `references/matplotlib.md` |
---
## Mermaid Auto-Upgrade Rules
Mermaid's dagre/elk layout estimates CJK widths incorrectly. **Auto-switch to Playwright+CSS when ANY condition is met:**
| Trigger | Action |
|---------|--------|
| Total nodes > **6** | → CSS flowchart (Layout C) |
| Any node text > **12 Chinese characters** | → CSS flowchart |
| More than **3 parallel branches** | → CSS flowchart |
| Nested subgraphs > **2 levels** | → CSS flowchart |
| Connector crossings > **2** | → CSS flowchart |
| **Side annotations / dashed note boxes** | → CSS flowchart |
| **Loop-back / cycle arrows** | → CSS flowchart |
| **Process has identifiable phases/stages** | → CSS flowchart (Layout C) |
**If staying with Mermaid**: `padding: 32`, `nodeSpacing: 80`, `rankSpacing: 80`. Node text ≤ 10 CJK chars/line, wrap with `<br>`, quote all text `A["text"]`.
---
## Large Dataset Rendering
| Data Size | Approach |
|-----------|----------|
| < 1,000 points | matplotlib / any |
| 1,000 - 10,000 | matplotlib (no markers) or ECharts |
| 10,000 - 100,000 | ECharts (Canvas mode) |
| > 100,000 | ECharts (`large: true`) or WebGL |
---
# Part 2: Universal Rules
These rules apply to ALL charts regardless of framework. Framework-specific rules live in each template file.
## 7 Core Rules
1. **Zero overlap.** No element may cover another's text. Overlap = information loss = task failure. Post-generation: verify every element has clear separation.
2. **Hierarchy over uniformity.** Primary nodes larger/bolder than secondary. Annotation nodes smaller/muted. Spacing between groups > within groups. If every box looks identical, the layout has failed.
3. **Low-saturation palette.** 70% background/neutral, 20% secondary, 10% accent (one highlight only). No high-saturation large fills. Saturated colors only on borders (2px), text, and small elements.
4. **Insight first.** Titles express conclusions, not field names. Remove non-essential elements: top/right borders, grid lines, tick marks, legend box borders. If removing it doesn't reduce understanding, it shouldn't exist.
5. **Label clarity over label method.** The goal is zero overlap — choose the method that achieves it for each chart type. Direct labels, legends, and leader lines are all valid; what matters is that nothing overlaps.
### 🚫 FORBIDDEN: Any Text Overlapping Any Other Element
**No label, legend, annotation, or title may overlap any other visual element.** This is the single most common matplotlib defect. Both direct labels AND legends can cause overlap — neither is inherently safe.
**Anti-overlap decision tree:**
1. **Check if direct labels fit** — if all labels have enough space (bar tops, line endpoints, large pie slices), label directly. No legend needed.
2. **If some labels would collide** (small pie slices, dense scatter points, clustered bars) → use legend outside plot area instead of forcing labels into tight spaces.
3. **Mixed approach** — label the major items directly, group small items into "其他" or use leader lines + legend for the small ones.
**Pie chart specific (the worst offender):**
- Slices < 5%: MUST use leader lines (`wedgeprops + texts` manual repositioning, or `matplotlib.patches.ConnectionPatch`) to pull labels outside. Do NOT rely on `autopct` alone — it places text inside/near the slice.
- Multiple small adjacent slices: use `bbox_to_anchor` legend outside, NOT direct labels
- `labeldistance=1.25` minimum to keep labels outside the pie
- When >2 slices are < 5%, consider grouping all < 3% into "其他X项"
- Use `adjustText` library to auto-resolve label collisions when available
**Legend placement (when legend is needed):**
- Place legend **outside** the plot area using `bbox_to_anchor`
- Suggested starting positions:
- Bar/line/scatter: right side outside (`bbox_to_anchor=(1.02, 1), loc='upper left'`)
- Pie: right side outside (`bbox_to_anchor=(1.1, 0.5), loc='center left'`)
- Radar: below chart (`bbox_to_anchor=(0.5, -0.15), loc='upper center'`)
- Heatmap: no legend needed (colorbar suffices)
**🔧 Mandatory: auto-adjust legend to prevent overlap.** Copy this snippet after placing any legend:
```python
# ── Auto-adjust legend position to prevent overlap ──
fig.canvas.draw() # must render first to get bboxes
legend = ax.get_legend()
if legend:
renderer = fig.canvas.get_renderer()
# Try shifting up to 5 times to resolve overlap
for _ in range(5):
leg_bb = legend.get_window_extent(renderer).transformed(ax.transAxes.inverted())
has_overlap = False
for text in ax.texts + [ax.title] + ax.get_xticklabels() + ax.get_yticklabels():
if not text.get_text():
continue
txt_bb = text.get_window_extent(renderer).transformed(ax.transAxes.inverted())
if leg_bb.overlaps(txt_bb):
has_overlap = True
break
if not has_overlap:
break
# Move legend further outside (direction depends on current loc)
bbox = legend.get_bbox_to_anchor().transformed(ax.transAxes.inverted())
x0, y0 = bbox.x0, bbox.y0
# Heuristic: if legend is below center, move down; if right of center, move right
if y0 < 0.5:
legend.set_bbox_to_anchor((x0, y0 - 0.08), transform=ax.transAxes)
else:
legend.set_bbox_to_anchor((x0 + 0.08, y0), transform=ax.transAxes)
fig.canvas.draw()
```
- **After placing legend**: always call `plt.tight_layout()` or `fig.subplots_adjust()` to ensure legend is not clipped
🚫 FORBIDDEN:
- `loc='best'` — matplotlib's "best" frequently overlaps data
- `loc='upper right'` / `loc='lower right'` on line/bar charts — high collision risk
- Direct labels on pie slices < 5% without leader lines
- Any text placement without verifying zero overlap
6. **Font discipline.** Max 2 fonts. Chinese: SimHei/PingFang SC. Always explicitly set fonts in code. Font size follows hierarchy (title 18-24px → body 13-15px → annotation 11-13px). Never go below 10px floor. When text overflows: condense text → enlarge canvas → last resort: shrink font (but never below floor).
7. **Whitespace is design.** Chart area 60-70% of canvas, margins 15-20%. At least 16pt between title and chart. Crowded ≠ information-rich.
---
## Color System
### Recommended Palettes
| Palette | Text | Background | Block Fill | Accent |
|---------|------|------------|------------|--------|
| Business Cool | `#243447` | `#F8FAFC` | `#E9EEF3` | `#4C6EF5` |
| Tech Cyan-Gray | `#1F2937` | `#F5F7FA` | `#E6ECF2` | `#3AAFA9` |
| Morandi Warm | `#4B4A45` | `#FAF8F4` | `#EAE4DB` | `#C6866A` |
| Invisible Precision | `#37352F` | `#FFFFFF` | `#F7F7F7` | `#2383E2` |
### 🚫 Forbidden Background Colors
| Color | Forbidden Hex Values |
|-------|---------------------|
| 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` |
### ✅ Allowed Background Colors
| Color | Hex Values |
|-------|------------|
| Ice blue | `#EFF6FF`, `#DBEAFE` |
| Mint green | `#F0FDF4`, `#D1FAE5` |
| Light amber | `#FFF7ED`, `#FEF3C7` |
| Lavender | `#F5F3FF`, `#EDE9FE` |
| Light gray | `#F8FAFC`, `#F1F5F9` |
### Functional Color (states only, not decoration)
- Active/Selected: brand accent or `2px` accent line
- Error: `#EF4444`
- Success: `#10B981`
- Tags: light bg + dark text, never high-sat pills
### Colorblind-Safe
Don't rely on color alone — pair with shape, line style, or direct labels.
Paul Tol palette: `['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']`
### Dark Theme
- Background: `#0F172A` (not pure black)
- Text: `#F1F5F9` (not pure white)
- Grid: `#1E293B`, low alpha
- Export: `savefig(facecolor='#0F172A')`
---
## Export Rules
- Static charts: minimum 200 DPI, recommended 300 DPI
- Pie/radar: **square `figsize=(8, 8)`** — non-square = elliptical
- No more than 6 colors per chart (split if more)
- Bar chart Y-axis starts at 0 (line charts may truncate)
- Never use 3D (distorts proportions)
### Playwright Screenshot
Default `device_scale_factor=2`. Large mind maps (3000px+): 1.5. PDF embed: 1-1.5. Print: 3.
After render, read `bounding_box()` and resize viewport to fit. Min viewport: 800px single-col, 1200px multi-col.
### 🚫 FORBIDDEN: `max-width` on Mermaid/SVG Containers
Mermaid's dagre engine produces SVGs with unpredictable width (especially with subgraphs, CJK text, or parallel branches). **NEVER set `max-width` on the Mermaid container element.** Use `width: fit-content; min-width: 800px;` instead.
**Root cause**: Mermaid SVGs overflow their CSS container silently. `bounding_box()` (Playwright) returns the CSS box model size, NOT the SVG's actual rendered size. So auto-resize viewport based on `bounding_box()` alone will still produce clipped screenshots.
**Fix**: Always read the **SVG element's own `getBoundingClientRect()`** via `page.evaluate()`, then use `max(css_size, svg_size) + padding` for viewport dimensions. See `references/mermaid.md` for the corrected screenshot script.
### Aspect Ratio Preservation (embedding)
**MUST read actual image dimensions and calculate height proportionally. NEVER hardcode both width and height.**
---
## matplotlib-Specific Rules
These apply when routing to matplotlib/seaborn:
### Layout & Overlap
- Prefer `constrained_layout=True` over `tight_layout()`
- Use `adjustText` library for automatic label repositioning — **this is the most reliable anti-overlap tool for matplotlib.** Install: `pip install adjustText`. Usage: `from adjustText import adjust_text; adjust_text(texts)`
- Max 4 subplots per canvas. More → split images or `figsize=(20, 16)` minimum
- Multi-subplot: `GridSpec` with `wspace/hspace` ≥ 0.3
- Colorbar: `shrink=0.8` + `pad=0.08`
- Data labels: Y-axis upper limit with 15-20% headroom (`ylim(0, max_val * 1.18)`)
- Long X labels → horizontal bar chart or show every N-th label
### Radar / Spider Charts
- **Every `fill()` MUST have `alpha=0.25`** (max 0.3). Omitting alpha = opaque = hides underlying series.
- Legend: place outside chart with `bbox_to_anchor`, start with `(0.5, -0.15), loc='upper center'`. If dimension labels are long or dimensions > 8, increase offset (e.g., `-0.25` or `-0.3`). Also FORBIDDEN: `loc='lower right'` (collides with radar dimension labels).
- Dimension label padding: `set_rlim(0, max_value * 1.2)`
- Labels with >4 CJK chars: rotate to follow angle or abbreviate
- `figsize=(8, 8)` mandatory (square)
### One Color, Gray the Rest
5 lines → color only the key one, others `#D1D5DB`. 8 bars → accent only the highlight, rest `#E5E7EB`.
---
## Connector Rules (structural diagrams)
- Attach to node edges, not through centers
- Prefer orthogonal polylines or clean curves
- Main paths avoid crossing
- Never pass through text areas
- Start/end points at same level must align (no staggering)
- Same-level connectors follow same direction
- Bend angles consistent (all right-angles or all curves, no mixing)
- Label positions uniform (all above line or all centered)
---
## Pre-Output Checklist
Before delivery, verify:
- [ ] Zero overlap (nodes, connectors, labels, legends — **especially check legend vs data, and adjacent pie/bar labels**)
- [ ] No connectors pass through text boxes
- [ ] Clear hierarchy (primary/secondary/annotation visually distinct)
- [ ] Low-saturation palette (no forbidden background colors)
- [ ] Text readable at final size (standalone: ≥12px body, ≥10px annotation; PDF embed: ≥10pt/8pt/7pt)
- [ ] Legend fully visible, not clipped, not overlapping any chart element
- [ ] Canvas wide/tall enough (check bounding box before screenshot)
- [ ] **If mind map**: each level distinct (≥3 property changes), connectors visible (≥ `#94A3B8`), left-right balanced
- [ ] **If flowchart**: phase titles distinct from steps, arrows only between phases, **using Layout C by default**
- [ ] **If flowchart**: phase colors are same-hue family (blue-gray progression), **NOT rainbow** (blue→green→amber→purple)
- [ ] **If flowchart looks scattered**: STOP — you're using the wrong layout, switch to Layout C
- [ ] **If Mermaid looked rigid**: already switched to Playwright+CSS
---
## Anti-Pattern Quick Reference
| ❌ Don't | ✅ Do This Instead |
|----------|-------------------|
| matplotlib default blue `#1f77b4` | Use this skill's palette |
| 3D bar/pie | Always 2D |
| Rainbow colormap (jet/rainbow) | Single-hue gradient or diverging |
| Thick black grid lines | `alpha=0.08` or remove |
| Different color per bar | Same series same color, highlight only key |
| 45° tilted X labels | Horizontal bar chart or shorten |
| 8+ subplots in one canvas | Split to 2-3 images, max 4 each |
| `tight_layout()` alone | `constrained_layout=True` or `GridSpec` |
| Labels overflowing chart | `ylim` with 18-25% headroom |
| Mind map: all levels same style | Root+L1 get boxes, leaves plain text |
| Mind map: image too tall | Left-right layout for ≥5 branches |
| Mind map: invisible connectors | Lines ≥ `#94A3B8`, root→L1 `#64748B` 2.5px |
| Mind map: unbalanced sides | Alternate large/small branches across sides |
| Flowchart: high-sat node fills | Low-sat bg (`#EFF6FF`) + sat border (`#3B82F6`) |
| Flowchart: dark bg + dark text | Dark bg → white text. Light bg → dark text |
| Flowchart: arrows between every step | Arrows ONLY between phases, steps use indent |
| Flowchart: cross-layer lines through nodes | Connect adjacent layers only |
| Flowchart: Grid layout for phased process | **Always use Layout C (Phased Vertical)** |
| Flowchart: phase titles as floating labels | Phase titles MUST be inside group cards |
| Flowchart: nodes scattered without grouping | Group nodes into phase cards with `.phase-group` |
| Flowchart: rainbow phase colors (blue→green→amber→purple) | Same-hue blue-gray progression for all phases |
| Multiple arrows to same entry point | Merge-then-enter pattern |
| Legend inside plot obscuring data | `bbox_to_anchor` outside plot area |
| Radar fill without alpha | `alpha=0.25` mandatory |
| Decorative icons/emoji | Let the data speak |
| Grid lines where whitespace suffices | Background contrast or spacing instead |
---
## UI Aesthetics (dashboards / card layouts)
When building UI-style outputs (dashboards, panels), apply "Invisible Precision":
- **Boundaries**: Subtle bg shifts (`#F7F7F7` on `#FFFFFF`), not border lines. Reserve `1px` dividers for absolute logical breaks only.
- **Actions**: Primary CTA in dark neutral (`#1A1A1B`). Secondary: ghost/gray. Hover: 5% darker, no size change.
- **Quiet UI**: Action buttons `opacity: 0` by default, `1` on hover. Only active elements get visual indicators.
- **Numbers**: `font-variant-numeric: tabular-nums` for strict vertical alignment.
- **Spacing**: `line-height: 1.625`, generous paragraph spacing.

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.**

228
skills/charts/setup.sh Executable file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env bash
# ---
# name: charts-setup
# author: Z.AI
# version: "1.0"
# description: Environment setup for the Charts skill. Checks and installs all required dependencies.
# ---
#
# Installs only dependencies required by the Charts skill.
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
fail() { echo -e " ${RED}${NC} $1"; }
warn() { echo -e " ${YELLOW}${NC} $1"; }
info() { echo -e " ${BLUE}${NC} $1"; }
echo "============================================"
echo " Charts Skill — Environment Setup"
echo "============================================"
echo ""
OS="$(uname -s)"
ARCH="$(uname -m)"
echo "Platform: $OS $ARCH"
echo ""
# ── 0. macOS: Homebrew ──
if [ "$OS" = "Darwin" ]; then
echo "--- Homebrew (macOS package manager) ---"
if command -v brew &>/dev/null; then
BREW_VER=$(brew --version 2>/dev/null | head -1)
ok "brew ($BREW_VER)"
else
fail "brew not found — most dependencies below need Homebrew on macOS"
info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
fi
echo ""
fi
# ── 1. Python 3 ──
echo "--- Python ---"
if command -v python3 &>/dev/null; then
PY_VER=$(python3 --version 2>&1)
ok "python3 ($PY_VER)"
if [ "$OS" = "Darwin" ]; then
PY_PATH=$(which python3 2>/dev/null)
if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then
warn "Using macOS system Python (limited). Recommend: brew install python3"
fi
fi
else
fail "python3 not found"
case "$OS" in
Darwin) info "Install: brew install python3" ;;
Linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)"
info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;;
*) info "Install: https://www.python.org/downloads/" ;;
esac
fi
# ── 2. pip ──
echo ""
echo "--- pip ---"
if python3 -m pip --version &>/dev/null 2>&1; then
PIP_VER=$(python3 -m pip --version 2>/dev/null | head -1)
ok "pip ($PIP_VER)"
else
fail "pip not found"
case "$OS" in
Darwin) info "Install: python3 -m ensurepip --upgrade"
info " or: brew install python3 (includes pip)" ;;
Linux) info "Install: sudo apt install python3-pip (Debian/Ubuntu)" ;;
*) info "Install: python3 -m ensurepip --upgrade" ;;
esac
fi
# ── 3. Python packages (matplotlib / seaborn for data charts) ──
echo ""
echo "--- Python Packages (Data Charts) ---"
PY_PKGS=(
"matplotlib:matplotlib"
"seaborn:seaborn"
"numpy:numpy"
"adjustText:adjustText"
)
MISSING_PY=()
for entry in "${PY_PKGS[@]}"; do
mod="${entry%%:*}"
pkg="${entry##*:}"
if python3 -c "import $mod" 2>/dev/null; then
ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', 'installed'))" 2>/dev/null)
ok "$pkg ($ver)"
else
fail "$pkg not installed"
MISSING_PY+=("$pkg")
fi
done
if [ ${#MISSING_PY[@]} -gt 0 ]; then
echo ""
if [ -t 0 ]; then
read -p " Install missing Python packages? [Y/n] " -n 1 -r REPLY
echo ""
REPLY=${REPLY:-Y}
else
warn "Non-interactive mode — skipping auto-install. Run interactively or install manually."
REPLY=N
fi
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
python3 -m pip install -q "${MISSING_PY[@]}" 2>/dev/null \
|| python3 -m pip install -q --user "${MISSING_PY[@]}" 2>/dev/null \
|| python3 -m pip install -q --break-system-packages "${MISSING_PY[@]}" 2>/dev/null \
|| { fail "pip install failed. Try manually: pip install ${MISSING_PY[*]}"; }
ok "Installed: ${MISSING_PY[*]}"
fi
fi
# ── 4. Node.js ──
echo ""
echo "--- Node.js (Interactive Charts & Diagrams) ---"
if command -v node &>/dev/null; then
NODE_VER=$(node --version)
ok "node ($NODE_VER)"
else
fail "node not found"
case "$OS" in
Darwin) info "Install: brew install node" ;;
Linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -"
info " sudo apt install -y nodejs" ;;
*) info "Install: https://nodejs.org/" ;;
esac
fi
# ── 5. npm ──
echo ""
echo "--- npm ---"
if command -v npm &>/dev/null; then
NPM_VER=$(npm --version 2>/dev/null)
ok "npm ($NPM_VER)"
else
fail "npm not found"
case "$OS" in
Darwin) info "Install: brew install node (includes npm)" ;;
Linux) info "Install: comes with nodejs" ;;
*) info "Install: https://nodejs.org/" ;;
esac
fi
# ── 6. Playwright + Chromium (for HTML→PNG/PDF rendering) ──
echo ""
echo "--- Playwright (Structural Diagrams & HTML Charts) ---"
if node -e "require('playwright')" 2>/dev/null; then
PW_VER=$(node -e "console.log(require('playwright/package.json').version)" 2>/dev/null)
ok "playwright ($PW_VER)"
else
fail "playwright not installed"
info "Install: npm install -g playwright"
fi
if [ "$OS" = "Darwin" ]; then
PW_CACHE="$HOME/Library/Caches/ms-playwright"
else
PW_CACHE="$HOME/.cache/ms-playwright"
fi
if ls "$PW_CACHE"/chromium-* &>/dev/null 2>&1; then
CR_DIR=$(ls -d "$PW_CACHE"/chromium-* 2>/dev/null | tail -1)
ok "chromium ($(basename "$CR_DIR"))"
else
fail "chromium not installed"
info "Install: npx playwright install chromium"
if [ "$OS" = "Linux" ]; then
info " npx playwright install-deps (system libs, needs sudo)"
fi
fi
# ── 7. CJK Fonts (for Chinese chart labels) ──
echo ""
echo "--- CJK Fonts (Chinese text in charts) ---"
CJK_FOUND=false
# Check matplotlib registered fonts
if python3 -c "
import matplotlib.font_manager as fm
fonts = [f.name for f in fm.fontManager.ttflist]
if 'SimHei' in fonts or 'Heiti SC' in fonts or 'Noto Sans CJK' in fonts or 'PingFang SC' in fonts:
print('ok')
else:
print('missing')
" 2>/dev/null | grep -q "ok"; then
ok "CJK font registered in matplotlib (SimHei/Heiti SC/Noto Sans CJK/PingFang SC)"
CJK_FOUND=true
fi
# Check system CJK fonts
if [ "$OS" = "Darwin" ]; then
if ls /System/Library/Fonts/PingFang.ttc &>/dev/null 2>&1 \
|| ls /System/Library/Fonts/STHeiti*.ttc &>/dev/null 2>&1; then
ok "macOS CJK system fonts available (PingFang/STHeiti)"
CJK_FOUND=true
fi
elif [ "$OS" = "Linux" ]; then
if fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then
ok "system CJK fonts available (fc-list)"
CJK_FOUND=true
fi
fi
if [ "$CJK_FOUND" = false ]; then
warn "No CJK font detected — Chinese labels may show as □"
info "The skill configures font fallback via rcParams at runtime."
info "Ensure a CJK font file exists (e.g., SimHei.ttf) and is registered."
if [ "$OS" = "Darwin" ]; then
info "macOS ships with PingFang — try: plt.rcParams['font.sans-serif'] = ['PingFang SC', 'DejaVu Sans']"
elif [ "$OS" = "Linux" ]; then
info "Install: sudo apt install fonts-noto-cjk"
fi
fi
# ── Summary ──
echo ""
echo "============================================"
echo " Setup complete."
echo " Data charts: matplotlib + seaborn"
echo " Structural diagrams: Playwright + CSS"
echo " Interactive charts: ECharts / D3.js via Node.js"
echo "============================================"