Initial commit
This commit is contained in:
167
skills/xlsx/engines/chart-templates.md
Executable file
167
skills/xlsx/engines/chart-templates.md
Executable file
@@ -0,0 +1,167 @@
|
||||
# Chart Templates — Implementation Code
|
||||
|
||||
> Load on demand when you need specific chart code. Do NOT load upfront.
|
||||
|
||||
---
|
||||
|
||||
## Native Excel Charts (openpyxl.chart)
|
||||
|
||||
### Bar Chart
|
||||
```python
|
||||
from openpyxl.chart import BarChart, Reference
|
||||
from templates.base import make_chart_title
|
||||
|
||||
chart = BarChart()
|
||||
chart.type = "col"
|
||||
chart.title = make_chart_title("Revenue by Product", 14)
|
||||
chart.y_axis.title = make_chart_title("Revenue ($)", 10, bold=False, axis=True)
|
||||
chart.x_axis.title = make_chart_title("Product", 10, bold=False)
|
||||
|
||||
data = Reference(ws, min_col=3, min_row=4, max_col=3, max_row=last_row)
|
||||
cats = Reference(ws, min_col=2, min_row=5, max_row=last_row)
|
||||
|
||||
chart.add_data(data, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
chart.shape = 4
|
||||
chart.width = 18
|
||||
chart.height = 10
|
||||
|
||||
ws.add_chart(chart, "J4")
|
||||
```
|
||||
|
||||
### Line Chart
|
||||
```python
|
||||
from openpyxl.chart import LineChart, Reference
|
||||
from templates.base import make_chart_title
|
||||
|
||||
chart = LineChart()
|
||||
chart.title = make_chart_title("Monthly Trend", 14)
|
||||
chart.y_axis.title = make_chart_title("Amount", 10, bold=False, axis=True)
|
||||
chart.style = 10
|
||||
|
||||
data = Reference(ws, min_col=3, max_col=5, min_row=4, max_row=last_row)
|
||||
cats = Reference(ws, min_col=2, min_row=5, max_row=last_row)
|
||||
|
||||
chart.add_data(data, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
for series in chart.series:
|
||||
series.smooth = True
|
||||
|
||||
ws.add_chart(chart, "J4")
|
||||
```
|
||||
|
||||
### Pie Chart
|
||||
```python
|
||||
from openpyxl.chart import PieChart, Reference
|
||||
from openpyxl.chart.label import DataLabelList
|
||||
from templates.base import make_chart_title
|
||||
|
||||
chart = PieChart()
|
||||
chart.title = make_chart_title("Market Share", 14)
|
||||
|
||||
data = Reference(ws, min_col=3, min_row=4, max_row=last_row)
|
||||
cats = Reference(ws, min_col=2, min_row=5, max_row=last_row)
|
||||
|
||||
chart.add_data(data, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
|
||||
chart.dataLabels = DataLabelList()
|
||||
chart.dataLabels.dLblPos = 'bestFit'
|
||||
chart.dataLabels.showLeaderLines = True
|
||||
chart.dataLabels.showCatName = True
|
||||
chart.dataLabels.showPercent = True
|
||||
chart.dataLabels.showVal = False
|
||||
|
||||
ws.add_chart(chart, "J4")
|
||||
```
|
||||
|
||||
### Combo Chart (Bar + Line, dual axis)
|
||||
```python
|
||||
from openpyxl.chart import BarChart, LineChart, Reference
|
||||
from templates.base import make_chart_title
|
||||
|
||||
bar = BarChart()
|
||||
bar.add_data(Reference(ws, min_col=2, max_col=2, min_row=1, max_row=10), titles_from_data=True)
|
||||
bar.title = make_chart_title("Revenue vs Growth", 14)
|
||||
bar.y_axis.title = make_chart_title("Revenue ($)", 10, bold=False, axis=True)
|
||||
|
||||
line = LineChart()
|
||||
line.add_data(Reference(ws, min_col=3, max_col=3, min_row=1, max_row=10), titles_from_data=True)
|
||||
line.y_axis.title = make_chart_title("Growth %", 10, bold=False, axis=True)
|
||||
line.y_axis.axId = 200
|
||||
|
||||
bar += line
|
||||
ws.add_chart(bar, "E2")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Matplotlib Charts (embedded as images)
|
||||
|
||||
### Chinese Font Setup
|
||||
```python
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
|
||||
_font_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'fonts', 'truetype', 'chinese', 'SimHei.ttf')
|
||||
if not os.path.exists(_font_path):
|
||||
# Fallback: try workspace fonts
|
||||
_font_path = os.path.expanduser('/usr/share/fonts/truetype/chinese/SimHei.ttf')
|
||||
if os.path.exists(_font_path):
|
||||
matplotlib.font_manager.fontManager.addfont(_font_path)
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
```
|
||||
|
||||
### Standard Template
|
||||
```python
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.bar(categories, values, color='#4A90D9')
|
||||
ax.set_title('Chart Title', fontsize=14, fontweight='bold', pad=15)
|
||||
ax.set_xlabel('X Label', fontsize=11)
|
||||
ax.set_ylabel('Y Label', fontsize=11)
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.tick_params(axis='x', rotation=45)
|
||||
fig.tight_layout(pad=2.0)
|
||||
plt.legend(loc='best', fontsize='small')
|
||||
fig.savefig('chart.png', dpi=150, bbox_inches='tight', facecolor='white')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
### Embed in Excel (preserving aspect ratio)
|
||||
```python
|
||||
from openpyxl.drawing.image import Image as XlImage
|
||||
from PIL import Image as PILImage
|
||||
|
||||
pil_img = PILImage.open('chart.png')
|
||||
orig_w, orig_h = pil_img.size
|
||||
target_w = 600
|
||||
scale = target_w / orig_w
|
||||
|
||||
xl_img = XlImage('chart.png')
|
||||
xl_img.width = target_w
|
||||
xl_img.height = int(orig_h * scale)
|
||||
|
||||
ws.add_image(xl_img, 'B20')
|
||||
```
|
||||
|
||||
### Smart Chart Recommend Function
|
||||
```python
|
||||
def recommend_chart(df, x_col, y_cols):
|
||||
if pd.api.types.is_datetime64_any_dtype(df[x_col]):
|
||||
return "line"
|
||||
n_categories = df[x_col].nunique()
|
||||
n_series = len(y_cols)
|
||||
if n_series == 1:
|
||||
vals = df[y_cols[0]]
|
||||
if vals.sum() > 95 and vals.sum() < 105:
|
||||
return "pie" if n_categories <= 5 else "bar_horizontal"
|
||||
if n_categories <= 6:
|
||||
return "bar_grouped" if n_series > 1 else "bar"
|
||||
elif n_categories <= 15:
|
||||
return "bar_horizontal"
|
||||
else:
|
||||
return "bar_top10"
|
||||
```
|
||||
87
skills/xlsx/engines/chart.md
Executable file
87
skills/xlsx/engines/chart.md
Executable file
@@ -0,0 +1,87 @@
|
||||
# Chart Engine — Selection & Specs
|
||||
|
||||
> Load this file when the task needs charts. For code templates, load `engines/chart-templates.md` on demand.
|
||||
|
||||
---
|
||||
|
||||
## Decision: Native Excel Chart vs Matplotlib Image
|
||||
|
||||
| Situation | Use |
|
||||
|-----------|-----|
|
||||
| User will interact with chart in Excel (resize, filter, update) | **Native Excel chart** (openpyxl.chart) |
|
||||
| Publication-quality or complex visualization (heatmap, multi-axis) | **Matplotlib image** → embed in Excel |
|
||||
| Dashboard with multiple small charts | **Matplotlib** (more layout control) |
|
||||
| Simple bar/line/pie from sheet data | **Native Excel chart** |
|
||||
|
||||
---
|
||||
|
||||
## Chart Size & Placement
|
||||
|
||||
```python
|
||||
CHART_SIZES = {
|
||||
'small': (12, 7), # ~400x230px — inline with data
|
||||
'medium': (18, 10), # ~600x330px — standard report chart
|
||||
'large': (24, 14), # ~800x460px — full-width dashboard
|
||||
}
|
||||
```
|
||||
|
||||
Multiple charts on one sheet: each chart ≈ 15 rows tall + 2 rows gap. Calculate anchor positions to prevent overlap.
|
||||
|
||||
---
|
||||
|
||||
## Smart Chart Recommendation
|
||||
|
||||
When user doesn't specify chart type, auto-select:
|
||||
|
||||
| Data Pattern | Best Chart | Avoid |
|
||||
|-------------|-----------|-------|
|
||||
| Trend over time | Line | Pie |
|
||||
| Category comparison (≤6) | Bar (vertical) | Pie |
|
||||
| Category comparison (7-15) | Horizontal Bar | Vertical bar |
|
||||
| Category comparison (>15) | Top 10 bar + "Others" | All-in-one |
|
||||
| Part of whole (≤5 slices) | Pie / Donut | Bar |
|
||||
| Part of whole (>8) | Horizontal Bar | Pie |
|
||||
| Distribution | Histogram | Pie |
|
||||
| Correlation | Scatter | Bar |
|
||||
| Budget vs Actual | Clustered Bar + variance line | Pie |
|
||||
| Mixed scales ($ + %) | Combo (bar + line) | Single axis |
|
||||
|
||||
### Auto-Detection from Headers
|
||||
|
||||
| Header patterns | Suggested chart |
|
||||
|----------------|-----------------|
|
||||
| Date, Month, Quarter, Year, 月, 季度, 年 | Line / Area |
|
||||
| Category, Type, Product, Region, 类别, 产品 | Bar |
|
||||
| Percentage, Share, %, 占比, 份额 | Pie / Donut |
|
||||
| Budget + Actual, 预算 + 实际 | Clustered Bar |
|
||||
| Revenue + Cost + Profit, 收入 + 成本 + 利润 | Stacked Bar / Combo |
|
||||
| Growth, Change, Δ, 增长, 变化 | Line with markers |
|
||||
|
||||
---
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Anti-Overlap**: Always `fig.tight_layout(pad=2.0)` before `savefig()`; use `plt.legend(loc='best')`
|
||||
2. **`titles_from_data=True`**: First row of data reference MUST contain text headers
|
||||
3. **Cached Values**: Run `recalc` before adding charts that reference formula cells
|
||||
4. **Hidden Data**: Set `chart.plot_visible_only = False` when chart data comes from hidden rows
|
||||
5. **Aspect Ratio**: When embedding matplotlib PNGs, always calculate proportional height from original dimensions
|
||||
6. **Chinese Font**: Must configure SimHei before any matplotlib plotting
|
||||
|
||||
### Color Palette
|
||||
|
||||
Chart colors are derived from **`engines/design.md §9`**. Do NOT define independent colors.
|
||||
|
||||
```python
|
||||
# Import from design tokens — single source of truth
|
||||
# CHART_COLORS = [PRIMARY, ACCENT_POSITIVE, ACCENT_WARNING, ACCENT_NEGATIVE, NEUTRAL_600]
|
||||
# Single series → PRIMARY only
|
||||
# Two series → PRIMARY + ACCENT_POSITIVE
|
||||
# Never exceed 5 colors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Templates
|
||||
|
||||
For specific chart implementation code (bar, line, pie, scatter, combo, matplotlib embed), load `engines/chart-templates.md`.
|
||||
575
skills/xlsx/engines/design.md
Executable file
575
skills/xlsx/engines/design.md
Executable file
@@ -0,0 +1,575 @@
|
||||
# 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
|
||||
|
||||
```python
|
||||
# === 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:**
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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**:
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
# 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
|
||||
435
skills/xlsx/engines/vba-templates.md
Executable file
435
skills/xlsx/engines/vba-templates.md
Executable file
@@ -0,0 +1,435 @@
|
||||
# VBA Code Templates
|
||||
|
||||
Ready-to-use VBA templates for common automation tasks. Copy and customize.
|
||||
|
||||
Load `scenes/vba.md` first for code standards and injection workflow.
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Auto-Generate Monthly Report
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: ModMonthlyReport
|
||||
' Purpose: Auto-generate monthly summary from raw data sheet
|
||||
' ============================================================
|
||||
|
||||
Public Sub GenerateMonthlyReport()
|
||||
On Error GoTo ErrHandler
|
||||
Application.ScreenUpdating = False
|
||||
Application.Calculation = xlCalculationManual
|
||||
|
||||
Dim wsData As Worksheet
|
||||
Dim wsSummary As Worksheet
|
||||
Dim lastRow As Long
|
||||
Dim reportMonth As String
|
||||
|
||||
' Get target month
|
||||
reportMonth = InputBox("Enter month (YYYY-MM):", "Report Month", Format(Date, "YYYY-MM"))
|
||||
If reportMonth = "" Then GoTo CleanUp
|
||||
|
||||
' Reference sheets
|
||||
Set wsData = ThisWorkbook.Sheets("Data")
|
||||
|
||||
' Create or clear summary sheet
|
||||
On Error Resume Next
|
||||
Set wsSummary = ThisWorkbook.Sheets("Summary_" & reportMonth)
|
||||
On Error GoTo ErrHandler
|
||||
|
||||
If wsSummary Is Nothing Then
|
||||
Set wsSummary = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
|
||||
wsSummary.Name = "Summary_" & reportMonth
|
||||
Else
|
||||
wsSummary.Cells.Clear
|
||||
End If
|
||||
|
||||
lastRow = wsData.Cells(wsData.Rows.Count, "A").End(xlUp).Row
|
||||
|
||||
' Write headers
|
||||
wsSummary.Range("A1").Value = "Monthly Report: " & reportMonth
|
||||
wsSummary.Range("A1").Font.Size = 16
|
||||
wsSummary.Range("A1").Font.Bold = True
|
||||
|
||||
wsSummary.Range("A3").Value = "Category"
|
||||
wsSummary.Range("B3").Value = "Total Amount"
|
||||
wsSummary.Range("C3").Value = "Count"
|
||||
wsSummary.Range("D3").Value = "Average"
|
||||
|
||||
' Aggregate by category (using Dictionary)
|
||||
Dim dict As Object
|
||||
Set dict = CreateObject("Scripting.Dictionary")
|
||||
|
||||
Dim i As Long
|
||||
Dim cat As String
|
||||
Dim amt As Double
|
||||
|
||||
For i = 2 To lastRow
|
||||
' Filter by month (assuming date in column A, category in B, amount in C)
|
||||
If Format(wsData.Cells(i, 1).Value, "YYYY-MM") = reportMonth Then
|
||||
cat = CStr(wsData.Cells(i, 2).Value)
|
||||
amt = CDbl(wsData.Cells(i, 3).Value)
|
||||
|
||||
If dict.Exists(cat) Then
|
||||
dict(cat) = Array(dict(cat)(0) + amt, dict(cat)(1) + 1)
|
||||
Else
|
||||
dict.Add cat, Array(amt, 1)
|
||||
End If
|
||||
End If
|
||||
Next i
|
||||
|
||||
' Write results
|
||||
Dim outRow As Long
|
||||
outRow = 4
|
||||
Dim key As Variant
|
||||
For Each key In dict.Keys
|
||||
wsSummary.Cells(outRow, 1).Value = key
|
||||
wsSummary.Cells(outRow, 2).Value = dict(key)(0)
|
||||
wsSummary.Cells(outRow, 2).NumberFormat = "#,##0.00"
|
||||
wsSummary.Cells(outRow, 3).Value = dict(key)(1)
|
||||
wsSummary.Cells(outRow, 4).Value = dict(key)(0) / dict(key)(1)
|
||||
wsSummary.Cells(outRow, 4).NumberFormat = "#,##0.00"
|
||||
outRow = outRow + 1
|
||||
Next key
|
||||
|
||||
' Auto-fit columns
|
||||
wsSummary.Columns("A:D").AutoFit
|
||||
|
||||
MsgBox "Report generated: " & dict.Count & " categories", vbInformation
|
||||
|
||||
CleanUp:
|
||||
Application.ScreenUpdating = True
|
||||
Application.Calculation = xlCalculationAutomatic
|
||||
Exit Sub
|
||||
|
||||
ErrHandler:
|
||||
MsgBox "Error: " & Err.Description, vbCritical
|
||||
Resume CleanUp
|
||||
End Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Batch Process Multiple Sheets
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: ModBatchProcess
|
||||
' Purpose: Apply same operation to all data sheets
|
||||
' ============================================================
|
||||
|
||||
Public Sub BatchProcessSheets()
|
||||
On Error GoTo ErrHandler
|
||||
Application.ScreenUpdating = False
|
||||
|
||||
Dim ws As Worksheet
|
||||
Dim processedCount As Long
|
||||
|
||||
For Each ws In ThisWorkbook.Worksheets
|
||||
' Skip non-data sheets
|
||||
If Left(ws.Name, 1) <> "_" And ws.Name <> "Summary" And ws.Name <> "Config" Then
|
||||
Call ProcessSingleSheet(ws)
|
||||
processedCount = processedCount + 1
|
||||
End If
|
||||
Next ws
|
||||
|
||||
MsgBox processedCount & " sheets processed.", vbInformation
|
||||
|
||||
CleanUp:
|
||||
Application.ScreenUpdating = True
|
||||
Exit Sub
|
||||
|
||||
ErrHandler:
|
||||
MsgBox "Error on sheet '" & ws.Name & "': " & Err.Description, vbCritical
|
||||
Resume CleanUp
|
||||
End Sub
|
||||
|
||||
Private Sub ProcessSingleSheet(ws As Worksheet)
|
||||
Dim lastRow As Long
|
||||
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
|
||||
|
||||
' Example: Add a "Total" row at the bottom
|
||||
Dim lastCol As Long
|
||||
lastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
|
||||
|
||||
Dim totalRow As Long
|
||||
totalRow = lastRow + 1
|
||||
|
||||
ws.Cells(totalRow, 1).Value = "Total"
|
||||
ws.Cells(totalRow, 1).Font.Bold = True
|
||||
|
||||
Dim col As Long
|
||||
For col = 2 To lastCol
|
||||
' Only sum if column contains numbers
|
||||
If IsNumeric(ws.Cells(2, col).Value) Then
|
||||
ws.Cells(totalRow, col).Formula = "=SUM(" & _
|
||||
ws.Cells(2, col).Address & ":" & ws.Cells(lastRow, col).Address & ")"
|
||||
ws.Cells(totalRow, col).Font.Bold = True
|
||||
End If
|
||||
Next col
|
||||
End Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Data Validation & Cleanup
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: ModDataCleanup
|
||||
' Purpose: Validate and clean data, log issues
|
||||
' ============================================================
|
||||
|
||||
Public Sub ValidateAndClean()
|
||||
On Error GoTo ErrHandler
|
||||
Application.ScreenUpdating = False
|
||||
|
||||
Dim wsData As Worksheet
|
||||
Dim wsLog As Worksheet
|
||||
Dim lastRow As Long
|
||||
Dim logRow As Long
|
||||
Dim issueCount As Long
|
||||
|
||||
Set wsData = ThisWorkbook.Sheets("Data")
|
||||
lastRow = wsData.Cells(wsData.Rows.Count, "A").End(xlUp).Row
|
||||
|
||||
' Create log sheet
|
||||
On Error Resume Next
|
||||
Application.DisplayAlerts = False
|
||||
ThisWorkbook.Sheets("ValidationLog").Delete
|
||||
Application.DisplayAlerts = True
|
||||
On Error GoTo ErrHandler
|
||||
|
||||
Set wsLog = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
|
||||
wsLog.Name = "ValidationLog"
|
||||
wsLog.Range("A1:D1").Value = Array("Row", "Column", "Issue", "Original Value")
|
||||
logRow = 2
|
||||
|
||||
Dim i As Long
|
||||
For i = 2 To lastRow
|
||||
' Check: Empty required fields (columns A-C)
|
||||
Dim col As Long
|
||||
For col = 1 To 3
|
||||
If IsEmpty(wsData.Cells(i, col)) Or Trim(CStr(wsData.Cells(i, col).Value)) = "" Then
|
||||
wsLog.Cells(logRow, 1).Value = i
|
||||
wsLog.Cells(logRow, 2).Value = wsData.Cells(1, col).Value
|
||||
wsLog.Cells(logRow, 3).Value = "Empty required field"
|
||||
logRow = logRow + 1
|
||||
issueCount = issueCount + 1
|
||||
End If
|
||||
Next col
|
||||
|
||||
' Check: Numeric column D should be positive
|
||||
If Not IsEmpty(wsData.Cells(i, 4)) Then
|
||||
If Not IsNumeric(wsData.Cells(i, 4).Value) Then
|
||||
wsLog.Cells(logRow, 1).Value = i
|
||||
wsLog.Cells(logRow, 2).Value = wsData.Cells(1, 4).Value
|
||||
wsLog.Cells(logRow, 3).Value = "Non-numeric value"
|
||||
wsLog.Cells(logRow, 4).Value = wsData.Cells(i, 4).Value
|
||||
logRow = logRow + 1
|
||||
issueCount = issueCount + 1
|
||||
ElseIf CDbl(wsData.Cells(i, 4).Value) < 0 Then
|
||||
wsLog.Cells(logRow, 1).Value = i
|
||||
wsLog.Cells(logRow, 2).Value = wsData.Cells(1, 4).Value
|
||||
wsLog.Cells(logRow, 3).Value = "Negative value"
|
||||
wsLog.Cells(logRow, 4).Value = wsData.Cells(i, 4).Value
|
||||
logRow = logRow + 1
|
||||
issueCount = issueCount + 1
|
||||
End If
|
||||
End If
|
||||
|
||||
' Clean: Trim whitespace from text columns
|
||||
For col = 1 To 3
|
||||
If Not IsEmpty(wsData.Cells(i, col)) Then
|
||||
Dim cleaned As String
|
||||
cleaned = Trim(CStr(wsData.Cells(i, col).Value))
|
||||
If cleaned <> CStr(wsData.Cells(i, col).Value) Then
|
||||
wsData.Cells(i, col).Value = cleaned
|
||||
End If
|
||||
End If
|
||||
Next col
|
||||
Next i
|
||||
|
||||
' Format log
|
||||
wsLog.Columns("A:D").AutoFit
|
||||
wsLog.Range("A1:D1").Font.Bold = True
|
||||
|
||||
If issueCount > 0 Then
|
||||
wsLog.Activate
|
||||
MsgBox issueCount & " issues found. See ValidationLog sheet.", vbExclamation
|
||||
Else
|
||||
MsgBox "All data validated. No issues found.", vbInformation
|
||||
End If
|
||||
|
||||
CleanUp:
|
||||
Application.ScreenUpdating = True
|
||||
Exit Sub
|
||||
|
||||
ErrHandler:
|
||||
MsgBox "Error: " & Err.Description, vbCritical
|
||||
Resume CleanUp
|
||||
End Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Multi-File Consolidation
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: ModConsolidate
|
||||
' Purpose: Merge data from multiple Excel files into one sheet
|
||||
' ============================================================
|
||||
|
||||
Public Sub ConsolidateFiles()
|
||||
On Error GoTo ErrHandler
|
||||
Application.ScreenUpdating = False
|
||||
|
||||
' Let user select files
|
||||
Dim files As Variant
|
||||
files = Application.GetOpenFilename( _
|
||||
FileFilter:="Excel Files (*.xlsx;*.xlsm),*.xlsx;*.xlsm", _
|
||||
Title:="Select Files to Consolidate", _
|
||||
MultiSelect:=True)
|
||||
|
||||
If Not IsArray(files) Then
|
||||
MsgBox "No files selected.", vbInformation
|
||||
GoTo CleanUp
|
||||
End If
|
||||
|
||||
Dim wsDest As Worksheet
|
||||
Set wsDest = ThisWorkbook.Sheets("Consolidated")
|
||||
wsDest.Cells.Clear
|
||||
|
||||
Dim destRow As Long
|
||||
destRow = 1
|
||||
Dim headerWritten As Boolean
|
||||
|
||||
Dim fileIndex As Long
|
||||
For fileIndex = LBound(files) To UBound(files)
|
||||
Dim wbSource As Workbook
|
||||
Set wbSource = Workbooks.Open(CStr(files(fileIndex)), ReadOnly:=True)
|
||||
|
||||
Dim wsSource As Worksheet
|
||||
Set wsSource = wbSource.Sheets(1) ' First sheet
|
||||
|
||||
Dim srcLastRow As Long
|
||||
srcLastRow = wsSource.Cells(wsSource.Rows.Count, "A").End(xlUp).Row
|
||||
|
||||
Dim srcLastCol As Long
|
||||
srcLastCol = wsSource.Cells(1, wsSource.Columns.Count).End(xlToLeft).Column
|
||||
|
||||
' Copy header from first file only
|
||||
If Not headerWritten Then
|
||||
wsSource.Range(wsSource.Cells(1, 1), wsSource.Cells(1, srcLastCol)).Copy _
|
||||
Destination:=wsDest.Cells(destRow, 1)
|
||||
' Add "Source File" column
|
||||
wsDest.Cells(destRow, srcLastCol + 1).Value = "Source File"
|
||||
destRow = destRow + 1
|
||||
headerWritten = True
|
||||
End If
|
||||
|
||||
' Copy data rows
|
||||
If srcLastRow >= 2 Then
|
||||
wsSource.Range(wsSource.Cells(2, 1), wsSource.Cells(srcLastRow, srcLastCol)).Copy _
|
||||
Destination:=wsDest.Cells(destRow, 1)
|
||||
|
||||
' Tag source file
|
||||
Dim r As Long
|
||||
For r = destRow To destRow + srcLastRow - 2
|
||||
wsDest.Cells(r, srcLastCol + 1).Value = Dir(CStr(files(fileIndex)))
|
||||
Next r
|
||||
|
||||
destRow = destRow + srcLastRow - 1
|
||||
End If
|
||||
|
||||
wbSource.Close SaveChanges:=False
|
||||
Next fileIndex
|
||||
|
||||
wsDest.Columns.AutoFit
|
||||
MsgBox "Consolidated " & UBound(files) - LBound(files) + 1 & " files, " & _
|
||||
destRow - 2 & " data rows.", vbInformation
|
||||
|
||||
CleanUp:
|
||||
Application.ScreenUpdating = True
|
||||
Exit Sub
|
||||
|
||||
ErrHandler:
|
||||
MsgBox "Error: " & Err.Description, vbCritical
|
||||
If Not wbSource Is Nothing Then wbSource.Close SaveChanges:=False
|
||||
Resume CleanUp
|
||||
End Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Button-Triggered Automation
|
||||
|
||||
```vba
|
||||
' ============================================================
|
||||
' In ThisWorkbook module — create button on sheet
|
||||
' ============================================================
|
||||
|
||||
' Add button programmatically (run once):
|
||||
Sub CreateRunButton()
|
||||
Dim ws As Worksheet
|
||||
Set ws = ThisWorkbook.Sheets("Dashboard")
|
||||
|
||||
Dim btn As Button
|
||||
Set btn = ws.Buttons.Add(Left:=10, Top:=10, Width:=120, Height:=36)
|
||||
btn.Caption = "Generate Report"
|
||||
btn.OnAction = "ModMonthlyReport.GenerateMonthlyReport"
|
||||
btn.Font.Size = 11
|
||||
End Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 6: Protected Sheet with Editable Ranges
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: ModProtection
|
||||
' Purpose: Lock sheet but allow editing in specific ranges
|
||||
' ============================================================
|
||||
|
||||
Public Sub SetupProtection()
|
||||
Dim ws As Worksheet
|
||||
Set ws = ThisWorkbook.Sheets("Input")
|
||||
|
||||
' First unlock everything
|
||||
ws.Unprotect Password:="admin123"
|
||||
ws.Cells.Locked = True
|
||||
|
||||
' Unlock editable ranges
|
||||
ws.Range("C5:C20").Locked = False ' Input cells
|
||||
ws.Range("E5:E20").Locked = False ' Comment cells
|
||||
|
||||
' Visual hint: light yellow for editable cells
|
||||
ws.Range("C5:C20").Interior.Color = RGB(255, 255, 230)
|
||||
ws.Range("E5:E20").Interior.Color = RGB(255, 255, 230)
|
||||
|
||||
' Protect with options
|
||||
ws.Protect Password:="admin123", _
|
||||
DrawingObjects:=True, _
|
||||
Contents:=True, _
|
||||
Scenarios:=True, _
|
||||
AllowFormattingCells:=False, _
|
||||
AllowInsertingRows:=False, _
|
||||
AllowDeletingRows:=False, _
|
||||
AllowSorting:=True, _
|
||||
AllowFiltering:=True, _
|
||||
AllowUsingPivotTables:=False
|
||||
|
||||
MsgBox "Sheet protected. Editable ranges highlighted in yellow.", vbInformation
|
||||
End Sub
|
||||
```
|
||||
Reference in New Issue
Block a user