Initial commit
This commit is contained in:
13
skills/charts/LICENSE.txt
Executable file
13
skills/charts/LICENSE.txt
Executable 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
427
skills/charts/SKILL.md
Executable 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.
|
||||
49
skills/charts/references/_rules.md
Executable file
49
skills/charts/references/_rules.md
Executable 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
199
skills/charts/references/d3.md
Executable 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) |
|
||||
651
skills/charts/references/echarts.md
Executable file
651
skills/charts/references/echarts.md
Executable 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 |
|
||||
617
skills/charts/references/matplotlib.md
Executable file
617
skills/charts/references/matplotlib.md
Executable 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)
|
||||
```
|
||||
797
skills/charts/references/mermaid.md
Executable file
797
skills/charts/references/mermaid.md
Executable 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,
|
||||
}
|
||||
});
|
||||
```
|
||||
911
skills/charts/references/mindmap-css.md
Executable file
911
skills/charts/references/mindmap-css.md
Executable 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.
|
||||
801
skills/charts/references/playwright-css.md
Executable file
801
skills/charts/references/playwright-css.md
Executable 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.**
|
||||
576
skills/charts/references/radial-grid.md
Executable file
576
skills/charts/references/radial-grid.md
Executable 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
|
||||
324
skills/charts/references/seaborn.md
Executable file
324
skills/charts/references/seaborn.md
Executable 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
228
skills/charts/setup.sh
Executable 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 "============================================"
|
||||
Reference in New Issue
Block a user