sheet-show
Excel is where the analysis happens; it is not where it gets read. Leadership wants the three numbers, the slice they care about, and one click to the detail — not thirteen tabs. This skill produces that page from the workbook, and keeps every number on it traceable back to the sheet the boss already has.
What you produce
One .html file (≈0.5 MB for ~300 records), no external assets, opens from
disk. Layout: sticky header + search → KPI strip with a caveat → lens rail
(themes / origins / sector×type / cheque bands) → active-filter chips → card
grid of entities → footer with source and join rate and the author's name.
Clicking a card opens a drawer: stats, every record with expandable detail,
vehicles used, co-investor ring graph. Filters AND across lenses and the
counts in each lens are computed with that lens held out, so the page answers
"what if I also pick…" before the reader clicks.
See references/design-system.md for why each of those exists, and keep the
reasons when you adapt it.
Workflow
1. Map the workbook before touching code
pip install openpyxl
python scripts/inspect_xlsx.py "Report.xlsx" # whole workbook
python scripts/inspect_xlsx.py "Report.xlsx" -s "Sheet" # one sheet, more rows
Read the output with these questions:
- Which sheet is the final, reconciled summary? It usually has the
report's own name (here
Buyer Universe) and a totals row. Build from final sheets, never from raw pulls — a number leadership can't find in the workbook is a number they can't trust. - What is the lookup unit? The thing the reader will want to click. It becomes the entity (cards). The thing that happens to it becomes the record (drawer rows). Transaction reports: buyer ↔ deal. Pipelines: target ↔ touchpoint. Comps: peer ↔ data point.
- Which columns are facets? The inspector marks low-cardinality text as
FACET. Pick the 3–4 that carry the analyst's argument (class, geography, sector, size band). More than four lenses is a dashboard. - Where is the narrative? A commentary table (themes, theses, screen outcomes, "what the investor is buying") is the most valuable lens on the page because it is the analyst's reading, not a column. Find it.
- How do entities and records join? Look for a bridge/alias sheet, a "rolled-up name" column, or a semicolon-separated buyers column. Note anything non-unique (ids like "Google search", duplicate target-years).
Write the mapping down in one paragraph before coding. If the lookup unit is genuinely ambiguous (two equally plausible entities), ask — everything else is a judgment call you make.
2. Build data.json
The template consumes a fixed shape — references/data-model.md — entities
(buyers), records (deals), themes, origins, meta, rec. Keep the
keys; change the labels.
Start from scripts/build_buyer_universe.py, the builder for the
Growth Equity Transaction Screening report, and adapt it:
python scripts/build_buyer_universe.py "Report v3.xlsx" -o data.json --author "Vỹ Mạc"
For a sibling version of the same report (v4, next quarter) that usually
means editing SHEETS / the col(...) candidates / THEME_RULES at the top.
For a different report, keep the helpers (find_table locates a table by its
header text so positions may move; col tolerates header rewording;
s/num/split_names normalise the -/n/a/; conventions) and rewrite
build(). references/worked-example.md explains each join and why it is
ordered the way it is.
Rules that make the page trustworthy:
- Entities come from the entity sheet only. Never invent one from a record's buyer string — that is how counts drift from the workbook.
- Join by the most specific key that is actually unique, fall back to
(name, year), and count every miss. Misses go inrecand in the footer. - Narrative tagging is code. If themes are attached by rule, the rules
live in
THEME_RULES, one per theme, and a residual theme catches the rest so no record is invisible from the themes lens. valueis a number ornull. Undisclosed isnull, never 0 or "n/a".- Say what an aggregate means. If you sum round sizes across co-investors
(the template does), the
kpiNotecaveat must say it is round size, not capital deployed. Write the caveat for whatever your aggregate is.
3. Render and reconcile
python scripts/render.py data.json -o "Buyer Universe.html"
render.py validates the invariants (ids = positions, mirrored entity↔record
links, numeric values) and prints the reconciliation block. Every line should
tie to the sheet. If one doesn't, fix the builder — do not ship a page whose
footer says 325 when the sheet says 326 and hope nobody compares.
4. Open it and click three things
Open the HTML in a browser (headless Chrome works for a screenshot:
chrome --headless=new --screenshot=out.png --window-size=1400,1100 file:///…).
Check: KPI numbers match the reconciliation; each lens renders and its counts
change when another lens is filtered; one card opens the drawer; one record
expands; search finds a target name; the footer sentence reads correctly; the
page works at 560px width. Then look at it as the boss would — is the first
screen the answer to "so what"?
5. Adapting the UI (only when the data model isn't enough)
Edit assets/template.html. It is ~860 lines: CSS tokens → components →
responsive → <script id="DATA"> placeholder → ~420 lines of vanilla JS
(derive() computes the filtered set and held-out counts; render*()
paint; one delegated click handler). The smallest lens to copy when adding a
facet is bands. All user-facing strings are Vietnamese literals in the
script block; translating means editing those strings, not the logic.
Keep: the hold-out counting, the chip bar as the single source of filter
truth, aria-* attributes as the state hooks, the first-paint-only stagger,
the reduced-motion block, min-width:0 on grid children, the footer source
sentence and the author signature.
6. Deliver
Hand back the HTML plus the reconciliation block, and tell the user:
- the file embeds every record's comments and descriptions from the workbook — same confidentiality as the workbook; it is not for a public repo or an unrestricted share link;
- what the KPI sum means (the caveat text), so it is quoted correctly;
- which records were excluded and why (undisclosed buyers, unmatched rows).
Repo layout
SKILL.md · scripts/inspect_xlsx.py (workbook map) ·
scripts/build_buyer_universe.py (worked-example builder: xlsx → data.json) ·
scripts/render.py (data.json + template → HTML, validates, reconciles) ·
assets/template.html (the UI) · references/data-model.md ·
references/design-system.md · references/worked-example.md ·
tests/make_demo.py (demo workbook in the same layout) ·
demo/ (generated from demo data only).