Files
2026-06-06 05:21:10 +00:00

20 KiB
Executable File
Raw Permalink Blame History

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):

  1. Explicit style keywords → direct match ("make a warm table" → warm)
  2. Scene keyword inference → indirect match ("sales monthly report" → warm, "student grades" → muji)
  3. 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_BOLD is 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_NAME uniformly across the entire table — do not mix fonts
  • All Font(name=...) in code must use the FONT_NAME variable — 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

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.py is the single code-level implementation. This file (design.md) is the design specification document; base.py is 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