36 KiB
Executable File
CSS Mind Map Rendering Engine
⚠️ Before writing any code, read
_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:
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.
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)
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):
/* .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-branchmissingdisplay: flex(most critical! Without flex,flex-direction: row-reversedoesn'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/.treeshould not havez-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: nowraporwidth: fit-content). Never addwidth: 100%,flex-grow: 1, oralign-items: stretchto 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 #mindmapmissingposition: 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)tomax(Y), instead of drawing per child node
/**
* 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):
<script>
/* ← paste the drawAllLines function above */
window.addEventListener('load', () => setTimeout(drawAllLines, 300));
</script>
3.3 Universal CSS Base (Shared by All Styles)
* { 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
<!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)
/* ─── 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
<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.
<!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).
<!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:
- Content complete — Every node from the original requirement is in the map, nothing missing
- Clear hierarchy — L1 branches and leaves are instantly distinguishable (different bg/border/font-weight)
- No overlap — No boxes covering boxes, no lines through text
- Connectors visible — Every parent-child pair has a connector, no orphan leaves. Connector color ≥
#94A3B8(too light = invisible) - Connector direction correct — Left-side leaves extend left, right-side extends right
- Proportions reasonable — Map is not extremely narrow/tall (target aspect ratio 1:1 to 3:1), visually comfortable
- Text readable — At final output size, smallest text is legible. Reference: root 18px+, L1 15px+, L2 13px+, leaves 12px+
- Roughly balanced (Style B) — Visual weight approximately equal on both sides, doesn't need to be symmetric. Target ≤30% difference
- Canvas large enough — No nodes clipped, sufficient padding on all sides (reference 60px+)
- 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.