20 KiB
Executable File
XLSX Design System
The single authoritative style reference. All table styles must be derived from this file — custom color values are prohibited.
1. Design Philosophy
Borderless-first
Visual elements exist only where data exists; blank areas remain completely clean — no borders, no fills, no visual noise.
- Has content → has style: Data regions use alternating row fills to distinguish rows
- No content → no trace: Cells outside the data range receive no formatting whatsoever
- Minimal borders: Only a single line at the header bottom and totals top; everything else relies on whitespace and alternating fills
Typography First
Establish hierarchy through font size, font weight, and color value, not lines and frames.
Three-Color Discipline
The entire table uses at most 3 color roles: primary, secondary, and accent. Everything else is black/white/gray warm tones.
2. Color System (Three-Color Rule)
2.1 Color Role Definitions
Each table allows only 3 color roles + neutral base:
| Role | Token | Responsibility | Area Ratio |
|---|---|---|---|
| Primary | primary |
Header background, title text | ~5-8% |
| Secondary | secondary |
Section title background, totals row | ~2-3% |
| Accent | accent |
Status indicators (positive/negative/warning) | ≤2% |
| Neutral | neutral-* |
Body text, background, alternating rows | ~90% |
2.2 Default Palette
# === Primary (deep blue family) ===
PRIMARY = "1B2A4A" # Header background, title text color
PRIMARY_LIGHT = "D6E4F0" # Light variant of primary → secondary (section titles, totals row)
# === Secondary (derived from primary) ===
SECONDARY = PRIMARY_LIGHT # Secondary = light version of primary
# === Accent (semantic, used on demand) ===
ACCENT_POSITIVE = "1B7D46" # Positive: growth, completed, on-target (deep green)
ACCENT_NEGATIVE = "C0392B" # Negative: decline, overdue, off-target (deep red)
ACCENT_WARNING = "D4820A" # Warning: approaching, needs attention (deep amber)
# === Neutral palette (warm gray) ===
NEUTRAL_900 = "37352F" # Body text
NEUTRAL_600 = "8C8A84" # Secondary text, annotations
NEUTRAL_200 = "E9E9E8" # Divider lines, header bottom line
NEUTRAL_100 = "F7F7F5" # Alternating row fill (odd rows)
NEUTRAL_50 = "FAFAF9" # Very light base color (optional)
NEUTRAL_0 = "FFFFFF" # White (even rows)
2.3 Style Palette System (Style-First Palette Engine)
Palettes are implemented via templates/palettes.py, purely style-driven, not bound to domains.
Domains (finance/education/sales…) only affect data formats and header conventions, not colors.
12 style palettes:
All theme headers use PRIMARY background + white text.
| # | Style | Keyword Triggers | PRIMARY | Positioning |
|---|---|---|---|---|
| 01 | professional | 正式/商务/汇报/默认 | 1B2A4A deep blue |
Universal default |
| 02 | warm | 温暖/活力/热情 | B85C1E warm orange |
Vibrant and impactful |
| 03 | elegant | 极简/简约 | 2C2C2C charcoal |
High-end minimalist |
| 04 | creative | 文艺/莫兰迪/设计感 | 6C5B7B purple-gray |
Artistic distinction |
| 05 | muji | 无印/呼吸感/素净 | 2C2C2C warm black |
MUJI pencil-on-paper |
| 06 | aesop | 沙岩/大地色/护肤 | 3D3229 earth brown |
Premium skincare packaging |
| 07 | kinfolk | 奶油/刊物/杂志/拿铁 | 5C524C cocoa |
Independent magazine aesthetic |
| 08 | celine | 黑白/时装/冷冽/mono | 000000 pure black |
Fashion house coldness |
| 09 | bottega | 墨绿/深绿/森林/贵气 | 2D4A3E dark green |
Italian luxury restraint |
| 10 | chanel | 米金/香奈儿/奶茶/高级 | 1C1917 ink |
Champagne gold elegance |
| 11 | bloomberg | 终端/深蓝/金融终端/工业/包豪斯 | 0D1B2A deep space |
Financial data aesthetic |
| 12 | original_blue | 原始/经典蓝/传统蓝 | 1B2A4A classic blue |
Original blue-black scheme |
Three-step matching logic (priority from high to low):
- Explicit style keywords → direct match ("make a warm table" → warm)
- Scene keyword inference → indirect match ("sales monthly report" → warm, "student grades" → muji)
- No match → professional (safe default, no guessing)
Usage:
import base
base.use_palette("help me make a warm sales monthly report") # → warm
base.use_palette_explicit("warm") # → warm
base.get_active_style() # → 'warm'
Each palette is a complete color set (PRIMARY + SECONDARY + ACCENT × 3 + NEUTRAL × 6 + HEADER_TEXT + CHART_COLORS + CF backgrounds).
When use_palette is not called, the default behavior is identical to before (professional = deep blue).
2.4 Special Color Rules for Finance Scenarios
Only when the scene is Finance, add the following text color encoding (IB industry convention, overrides default NEUTRAL_900):
| Text Color | Hex | Meaning |
|---|---|---|
Blue 0000FF |
Manual input values (user-modifiable) | |
Black 000000 |
Formula/calculated values | |
Green 008000 |
Cross-sheet references | |
Red FF0000 |
External file references |
2.5 Color Prohibitions
- ❌ Do not introduce any new hues outside of
ACCENT_* - ❌ Do not use color for decoration (primary color is sufficient for colored headers)
- ❌ No gradient fills
- ❌ Do not mix two different PRIMARY colors in the same table
3. Font System
3.1 Font Hierarchy
| Token | Size | Weight | Color | Usage |
|---|---|---|---|---|
font-title |
16pt | HEADER_BOLD* |
PRIMARY |
Table title (B2) |
font-header |
11pt | HEADER_BOLD* |
#FFFFFF |
Column headers (white text on primary background) |
font-subheader |
11pt/12pt | HEADER_BOLD* |
PRIMARY |
Section titles, totals row |
font-body |
11pt | Normal | NEUTRAL_900 |
Body data |
font-caption |
9pt | Normal | NEUTRAL_600 |
Annotations, sources, footnotes |
font-kpi |
22pt | HEADER_BOLD* |
PRIMARY |
KPI large numbers (analysis scenes only) |
font-kpi-label |
9pt | Normal | NEUTRAL_600 |
KPI labels |
*
HEADER_BOLDis determined at runtime by §3.3. Heavy-stroke fonts (SimHei/YaHei/PingFang, etc.) → False, thin-stroke fonts → True.
3.2 Font Selection (Cross-Platform Fallback Chain)
openpyxl's Font(name=...) can only specify a single font name and does not support CSS-style fallback chains.
Therefore, runtime platform detection is needed to select the first available font from the fallback sequence:
import platform, os
from openpyxl.styles import Font
def _resolve_font(candidates: list[str]) -> str:
"""Return the first font name likely available on this OS."""
system = platform.system()
# Quick lookup: match common fonts by platform
_platform_hints = {
"Darwin": {"PingFang SC", "Hiragino Sans GB", ".AppleSystemUIFont"},
"Windows": {"Microsoft YaHei", "SimHei", "SimSun"},
"Linux": {"Noto Sans CJK SC", "WenQuanYi Micro Hei", "Source Han Sans SC"},
}
available_hints = _platform_hints.get(system, set())
for name in candidates:
if name in available_hints:
return name
# Fallback: return the first in sequence (Excel will fallback on its own when opened)
return candidates[0]
# === Font fallback sequences ===
# CJK body text (CJK Sans): prefer platform-native sans-serif fonts
CJK_BODY_CHAIN = [
"PingFang SC", # macOS native, best rendering
"Microsoft YaHei", # Windows native, screen-optimized
"Noto Sans CJK SC", # Linux / Android universal
"Hiragino Sans GB", # macOS alternative
"Source Han Sans SC", # Adobe Source Han Sans, cross-platform
"SimHei", # Classic fallback
]
# Latin/numbers: serif (for formal reports)
LATIN_BODY_CHAIN = [
"Times New Roman", # Available on virtually all platforms
"Georgia",
"serif",
]
# Runtime resolution
FONT_CJK = _resolve_font(CJK_BODY_CHAIN)
FONT_LATIN = _resolve_font(LATIN_BODY_CHAIN)
# openpyxl can only set one name; use the CJK font for Chinese tables (it also covers ASCII characters)
FONT_NAME = FONT_CJK
Rules:
- Use
FONT_NAMEuniformly across the entire table — do not mix fonts - All
Font(name=...)in code must use theFONT_NAMEvariable — hardcoding font names is prohibited - If the user explicitly specifies a font, respect the user's choice
3.3 Header Bold Strategy
Not all fonts are suitable for bold. Heavy-stroke fonts (like SimHei, YaHei) become blurry when bolded — hierarchy should be established through font size differences or color contrast, not font weight:
# Determine whether the font is suitable for bold based on font name
_HEAVY_FONTS = {
"SimHei", "Microsoft YaHei", "PingFang SC",
"Noto Sans CJK SC", "Source Han Sans SC",
"Hiragino Sans GB", "WenQuanYi Micro Hei",
}
HEADER_BOLD = FONT_NAME not in _HEAVY_FONTS
# → Heavy fonts (SimHei/YaHei/PingFang, etc.): headers not bolded, rely on background color + white text
# → Thin fonts (SimSun/Times New Roman, etc.): headers bolded
Hierarchy alternatives when HEADER_BOLD = False:
- Headers: no bold, rely on primary background + white text for distinction
- Titles: no bold, use larger font size (16pt vs 11pt) for hierarchy
- Totals row: no bold, use secondary background + primary text for distinction
- Section titles: no bold, use primary text + slightly larger size (12pt) for distinction
3.4 Alignment Rules
| Data Type | Horizontal Alignment | Notes |
|---|---|---|
| Numbers/amounts/percentages | Right-aligned | Ensures decimal point alignment |
| Dates | Center-aligned | |
| Text | Left-aligned | |
| Headers | Center-aligned | |
| Titles | Left-aligned | ❌ Not centered |
4. Layout System
4.1 Starting Position and Margins
A B C D E ...
1 [blank] [blank] [blank] [blank] [blank] ← Top margin
2 [blank] Title ───────────────────────── ← Title row (starts at B2, merged to data width)
3 [blank] [blank] [blank] [blank] [blank] ← Spacing row (optional: subtitle/date)
4 [blank] Header1 Header2 Header3 Header4 ← Header row
5 [blank] Data Data Data Data ← Data area start
- Canvas Origin:
B2(left margin Column A + top margin Row 1) - Column A width: 3 (pure whitespace for visual breathing room)
- Row 1 height: 15pt (top margin)
4.2 Row Height Standards
| Row Type | Height | Notes |
|---|---|---|
| Title row (Row 2) | 32pt | 16pt font + top/bottom breathing room |
| Spacing row (Row 3) | 8pt | Gap between title and header |
| Header row (Row 4) | 28pt | 11pt font + wrap_text space |
| Data rows | 22pt | 11pt font + comfortable reading |
| Totals row | 26pt | Slightly taller than data rows for emphasis |
4.3 Column Width Guidelines
COLUMN_WIDTHS = {
'margin': 3, # Column A whitespace
'id_short': 8, # Serial number, ID
'name_cn': 16, # Chinese name (2-4 chars)
'name_en': 22, # English name
'description': 32, # Long text
'number': 14, # Amounts, quantities
'percentage': 12, # Percentages
'date': 14, # Dates
'status': 12, # Status labels
}
# CJK character ≈ 2.5 units, Latin ≈ 1.2 units
# Minimum 8, maximum 40
4.4 Auto-Fit Column Widths (Recommended)
After populating data, call auto_fit_columns(ws) from templates/base.py to automatically size columns based on data content (not headers). Headers that exceed the computed width get wrap_text=True instead of stretching the column.
from templates.base import auto_fit_columns
# After all data is written:
auto_fit_columns(ws, min_width=8, max_width=28, header_row=4, data_start_row=5)
Rules:
- Column width is determined by the widest data cell, not the header
- CJK characters are counted as 1.7x width (via
unicodedata.east_asian_width) - Headers wider than the column automatically get
wrap_text=True - This prevents the common problem of headers being wider than data content
5. Border System (Borderless-first)
5.1 Allowed Borders
| Position | Style | Color | Purpose |
|---|---|---|---|
| Header bottom | thin |
NEUTRAL_200 |
Separate header from data |
| Totals top | medium |
NEUTRAL_200 |
Mark summary row |
5.2 Prohibited Borders
- ❌ Full grid (all-sides thin border)
- ❌ Colored borders
- ❌ Double-line borders
- ❌ Thick borders (medium/thick) for decoration
5.3 Row Separation Alternative
Use alternating row fills instead of grid lines:
- Even rows:
NEUTRAL_0(white) - Odd rows:
NEUTRAL_100(warm white#F7F7F5)
5.4 Finance Scene Exception
Finance scene retains section dividers (PRIMARY color), following IB industry convention.
6. Title Row Design
6.1 Title Style
# Title: plain text, no background fill
title_font = Font(name=FONT_NAME, size=16, bold=HEADER_BOLD, color=PRIMARY)
title_align = Alignment(horizontal='left', vertical='center')
# Position: B2, merged to the last data column
ws.merge_cells(start_row=2, start_column=2, end_row=2, end_column=last_col)
ws['B2'].font = title_font
ws['B2'].alignment = title_align
ws.row_dimensions[2].height = 32
6.2 Header Style
# Header: primary color background + white text
header_fill = PatternFill('solid', fgColor=PRIMARY)
header_font = Font(name=FONT_NAME, size=11, bold=HEADER_BOLD, color="FFFFFF")
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
header_border = Border(bottom=Side(style='thin', color=NEUTRAL_200))
for cell in header_row:
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_align
cell.border = header_border
ws.row_dimensions[header_row_num].height = 28
6.3 Totals Row
total_fill = PatternFill('solid', fgColor=SECONDARY) # PRIMARY_LIGHT
total_font = Font(name=FONT_NAME, size=11, bold=HEADER_BOLD, color=PRIMARY)
total_border = Border(top=Side(style='medium', color=NEUTRAL_200))
for cell in total_row:
cell.fill = total_fill
cell.font = total_font
cell.border = total_border
7. Data Area Styles
7.1 Alternating Row Fill
for i, row in enumerate(ws.iter_rows(min_row=data_start, max_row=data_end)):
fill_color = NEUTRAL_0 if i % 2 == 0 else NEUTRAL_100
for cell in row:
cell.fill = PatternFill('solid', fgColor=fill_color)
cell.font = Font(name=FONT_NAME, size=11, color=NEUTRAL_900)
# ❌ No borders
7.2 Empty Data Area
Cells outside the data range receive no formatting — no fill, no borders, no font settings. Keep Excel defaults.
7.3 Grid Lines
ws.sheet_view.showGridLines = False # Disable Excel default grid lines
8. Conditional Formatting
8.1 When to Use
| ✅ Use | ❌ Don't Use |
|---|---|
| Data has comparison/ranking semantics (scores, KPIs, growth rates) | Simple entry forms, reference tables |
| Financial data with positive/negative values (profit/loss, increase/decrease) | Data rows ≤5 |
| User explicitly requests | User requests minimalist style |
8.2 Color Rules
Conditional formatting uses only accent colors:
# Positive → green background + green text
POSITIVE_FILL = PatternFill(bgColor='E8F5E9')
POSITIVE_FONT = Font(color=ACCENT_POSITIVE) # "1B7D46"
# Negative → red background + red text
NEGATIVE_FILL = PatternFill(bgColor='FDEDEC')
NEGATIVE_FONT = Font(color=ACCENT_NEGATIVE) # "C0392B"
# Warning → amber background + amber text
WARNING_FILL = PatternFill(bgColor='FEF9E7')
WARNING_FONT = Font(color=ACCENT_WARNING) # "D4820A"
8.3 Color Scale
from openpyxl.formatting.rule import ColorScaleRule
# Red → Yellow → Green (low → mid → high)
ws.conditional_formatting.add('B5:B100',
ColorScaleRule(
start_type='min', start_color='F8696B',
mid_type='percentile', mid_value=50, mid_color='FFEB84',
end_type='max', end_color='63BE7B'))
8.4 Data Bar
from openpyxl.formatting.rule import DataBarRule
ws.conditional_formatting.add('D5:D100',
DataBarRule(start_type='min', end_type='max',
color=PRIMARY, showValue=True))
# Data Bar color uses primary, maintaining color discipline
9. Chart Colors
Chart colors are derived from the design system, not maintained separately:
CHART_COLORS = [
PRIMARY, # 1st data series = primary
ACCENT_POSITIVE, # 2nd series
ACCENT_WARNING, # 3rd series
ACCENT_NEGATIVE, # 4th series
NEUTRAL_600, # 5th series (gray)
]
- Single series chart → use only
PRIMARY - Two series →
PRIMARY+ACCENT_POSITIVE - Multiple series → pick colors in order from the table above
- Never exceed 5 colors
10. Number Formats
10.1 General Formats
FORMATS = {
'integer': '#,##0',
'decimal_1': '#,##0.0',
'decimal_2': '#,##0.00',
'percentage': '0.0%',
'currency_cny': '¥#,##0.00',
'currency_usd': '$#,##0.00',
'date': 'YYYY-MM-DD',
}
10.2 Financial Formats
→ Full financial number format definitions are in scenes/finance.md §Number Formatting, not repeated here.
Brief rules: zero values "-", negatives in parentheses ($123), headers indicate units "Revenue ($mm)".
11. Code Templates
All design tokens, font resolution, and style factory functions have been extracted into templates/base.py.
templates/base.pyis the single code-level implementation. This file (design.md) is the design specification document;base.pyis the corresponding executable code.
Usage
# In all scene/engine code, import base.py directly
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'templates'))
from base import *
# Then use directly:
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "Sheet1"
headers = ["Column1", "Column2", "Column3"]
last_col = len(headers) + 1 # Starting from B=2
# One call to set up sheet basics + title
setup_sheet(ws, title="Table Title", last_col=last_col)
# Write headers
for col_idx, header in enumerate(headers, start=2):
ws.cell(row=4, column=col_idx, value=header)
style_header_row(ws, row_num=4, col_start=2, col_end=last_col)
# Write data rows
for i, row_data in enumerate(data):
row_num = 5 + i
for col_idx, value in enumerate(row_data, start=2):
ws.cell(row=row_num, column=col_idx, value=value)
style_data_row(ws, row_num=row_num, col_start=2, col_end=last_col, row_index=i)
# Write totals row
total_row_num = 5 + len(data)
style_total_row(ws, row_num=total_row_num, col_start=2, col_end=last_col)
wb.properties.creator = "Z.ai"
wb.save("output.xlsx")
Complete API provided by base.py
| Category | Exports |
|---|---|
| Constants | FONT_NAME, HEADER_BOLD, PRIMARY, PRIMARY_LIGHT, SECONDARY, ACCENT_*, NEUTRAL_*, CHART_COLORS, COLUMN_WIDTHS, FORMATS, ROW_HEIGHTS |
| Conditional Formatting | CF_POSITIVE_FILL/FONT, CF_NEGATIVE_FILL/FONT, CF_WARNING_FILL/FONT |
| Font Factories | font_title(), font_header(), font_subheader(), font_body(), font_caption(), font_kpi(), font_kpi_label() |
| Fill Factories | fill_header(), fill_total(), fill_data_row(row_index) |
| Border Factories | border_header(), border_total() |
| Alignment Factories | align_title(), align_header(), align_number(), align_text(), align_date() |
| Sheet Helpers | setup_sheet(ws, title, last_col), style_header_row(...), style_data_row(...), style_total_row(...) |
| Utility Functions | normalize_cell_value(value), copy_style(source, target) |
12. Design Checklist
Verify each item before delivering a table:
- Colors ≤ 3 hues (primary + accents, excluding neutrals)
- No formatting outside data area
- No full-grid borders (only header bottom line + totals top line)
- Alternating row fill applied
- Title has no background fill, left-aligned
- Numbers right-aligned, text left-aligned
- Grid lines disabled (
showGridLines = False) - Starting position is B2, column A is margin
- Body text color is
NEUTRAL_900(#37352F), not pure black - Chart colors come from Design Tokens, no new colors introduced