20 KiB
Executable File
CSS Radial Grid Layout (Center-Outward Diagrams)
⚠️ Before writing any code, read
_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
<!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: 24pxbetween rows,gap: 40pxwithin 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:
<!-- 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:
// 🚫 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:
<!-- 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
<!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
- Connectors are ALWAYS drawn by script reading bounding boxes — never hardcode x/y values in HTML/CSS
- Straight lines only (horizontal or vertical) — no diagonal lines unless top/bottom cards are offset from center
- Dashed lines (
stroke-dasharray: 6,4) for conceptual relationships - Solid lines for causal/sequential relationships
- Arrow direction: model chooses based on diagram semantics — pick ONE style per diagram, don't mix:
- Outward (
marker-endonly): center influences/drives dimensions (e.g. BSC: strategy → dimensions) - Inward (
marker-startonly): dimensions report/pressure center (e.g. Porter: forces → competition) - Bidirectional (
marker-start+marker-end): mutual influence (e.g. feedback loops)
- Outward (
- 🚫 All lines MUST originate from center EDGE MIDPOINTS (cx or cy) — never from center corners
- Line color:
#94A3B8(gray-blue) — never use dimension-specific colors for connectors (visual chaos)
SVG Arrow Markers (copy-paste into defs)
// 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
- Each dimension card: max 6 bullet items — more than 6 → group into sub-categories or use a separate detail table
- Bullet text: max 15 Chinese characters per line — longer text wraps naturally (word-break: break-word)
- Center node text: max 8 Chinese characters — keep it a short label, not a sentence
- 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
- No overlap — all dimension cards fully visible, none clipped or covering another
- Connectors point to card edges — not to random points in space
- Center node visually dominant — largest, darkest, or most contrast
- Dimension cards visually equal — similar size, similar padding, no one card 3× larger
- Colors follow scheme — each dimension has its own color (border + title), backgrounds stay pale (white or near-white)
- Layout is centered — equal margins on all sides
- Canvas large enough — min 900px wide for cross layout, min 800px for 2×2 quadrant