Table Heatmap Generator
Generate a red → white → green heatmap PNG from a GFM pipe-table. Paste the PNG
into a markdown report above the original text table. The text table stays in
the document for search / diff / copy-paste; the PNG carries the color signal.
Script: scripts/make_heatmap.py (bundled).
When to use
- The user asks to colorize, color-code, or heatmap a markdown table.
- Numeric table comparing variants / branches / runs where the reader should
spot best/worst at a glance.
- Renderer does not support inline HTML (
<td style="..."> shows as raw text).
- Multiple tables in one report that should share a consistent visual style.
Workflow
Extract the target table into a temp file. Include only the |...|
lines (header row, |---| separator, and data rows). Drop all surrounding
prose. Bold markers like **0.976** are OK; units (m, rad, %, ms)
must be stripped or the cell becomes NaN.
Decide two parameters:
| Parameter |
Options |
Choose by |
--normalize |
row, column, global |
row (default) when each row is a metric slice and columns are variants being compared. column when columns are different metrics. global for a single-metric grid. |
--direction |
higher-is-better, lower-is-better |
higher: AP, recall, F1, pass-rate. lower: MAE, RMSE, loss, latency, error. |
Run the script:
python ~/.cursor/skills/table-heatmap/scripts/make_heatmap.py \
--md-file /tmp/tbl.md \
--output /path/to/report/folder/report_X_heatmap.png \
--normalize row \
--direction higher-is-better \
--y-label "Forward range bin"
Output path convention: drop the PNG into the same folder that already
hosts the report's other images (alongside report_*.png files).
Insert the image reference ABOVE the existing markdown table:

| Original | Plain | Markdown | Table |
| --- | --- | --- | --- |
| ... | ... | ... | ... |
Do not delete the text table. The image is visual; the text is
authoritative / diffable / searchable / machine-readable.
Key CLI flags
| Flag |
Default |
Notes |
--md-file PATH / --md-stdin |
— |
Exactly one is required. |
--output PATH |
— |
Required. Use .png. |
--normalize {row,column,global} |
row |
See step 2. |
--direction {higher-is-better,lower-is-better} |
higher-is-better |
See step 2. |
--headers-position {top,bottom} |
top |
Column labels above the grid. |
--value-fmt STR |
{:.3f} |
Python format, e.g. {:.1%}, {:.0f}, {:.4f}. |
--y-label STR |
none |
Short axis label. \n for line break. |
--title STR |
none |
Usually leave empty; caption the image via the markdown alt text instead. |
--figsize W,H |
auto |
Override if labels collide (15+ cols → bump width). |
--col-wrap N |
12 |
Wrap column labels wider than N chars at spaces. 0 disables. |
--row-wrap N |
0 |
Wrap row labels wider than N chars at spaces. 0 disables. |
--center-value N |
off |
Center the color scale on N (symmetric, 0 = white). Use with signed-delta tables (e.g. +5% / -3%). Values farther from N become more saturated. Best paired with --normalize global for a consistent scale across the whole grid. |
--no-annotate |
off |
Disables ↑ best / ↓ worst subscripts. |
Common pitfalls
- Table file contains surrounding prose. Parser requires only the
|...| lines of one table. Strip everything else into the temp file.
- Mixed directions in one table. One row AP (higher-better), another row
latency (lower-better) cannot share one heatmap. Split into two tables and
call the script twice.
- Non-numeric cells. Units or text in value cells → NaN → white cell.
Strip units before passing.
- Very small value ranges. If all cells in a row are within 0.001 the
heatmap is visually uninformative. That's diagnostic: note it in the
surrounding prose instead of adding color.
- Signed deltas (e.g. "B2 vs B0 Δ%"). Default
row/column/global
normalization is min-max, so an all-positive grid with vmin = +0.4% would
paint the smallest positive cell red. Pass --normalize global --center-value 0 to anchor white at 0; positives become green, negatives
become red, color intensity scales with |value| / max(|value|).
Example (the one that shipped)
Source table (Main CAR AP by Range, 7 branches × 3 ranges):
| Range | B0 Baseline | B1 A110+NV frozen | B2 A110+NV unfrozen | B3 FE+NV frozen | B4 FE+autolabel frozen | B5 A110+autolabel frozen | B6 A110+autolabel unfrozen |
| --- | --- | --- | --- | --- | --- | --- | --- |
| [0, 25) | 0.952 | 0.969 | 0.897 | 0.975 | **0.976** | 0.956 | 0.967 |
| [25, 50) | 0.869 | 0.867 | **0.890** | 0.863 | 0.877 | 0.847 | 0.838 |
| [50, 100) | 0.521 | 0.518 | **0.548** | 0.493 | 0.533 | 0.469 | 0.464 |
Invocation:
python ~/.cursor/skills/table-heatmap/scripts/make_heatmap.py \
--md-file /tmp/main_ap.md \
--output cruise/mlp/.../notebooks/output/report_main_ap_heatmap.png \
--normalize row \
--direction higher-is-better \
--y-label "Forward range bin\n(bin_x × |y|<10m)"
Insert:

| Range | B0 Baseline | ... |
...
Style contract (do not change per-invocation)
The script's palette (#FF9696 / #FFFFFF / #96FF96) and cell/font sizing
are fixed so every heatmap in a report matches. If a report needs a different
palette, edit the script rather than adding per-call flags, so consistency is
preserved.
1---2name: table-heatmap3description: Generate a color-coded heatmap PNG from a markdown pipe-table so that any markdown renderer (including ones that strip inline HTML) shows per-row or per-column performance comparisons visually. Use when the user wants to colorize a markdown table, highlight best/worst values across branches or variants, visualize performance comparisons, or when inline HTML styling (`<td style>`, `<span style>`) renders as raw code in their viewer.4---56# Table Heatmap Generator78Generate a red → white → green heatmap PNG from a GFM pipe-table. Paste the PNG9into a markdown report above the original text table. The text table stays in10the document for search / diff / copy-paste; the PNG carries the color signal.1112Script: `scripts/make_heatmap.py` (bundled).1314## When to use1516- The user asks to colorize, color-code, or heatmap a markdown table.17- Numeric table comparing variants / branches / runs where the reader should18 spot best/worst at a glance.19- Renderer does not support inline HTML (`<td style="...">` shows as raw text).20- Multiple tables in one report that should share a consistent visual style.2122## Workflow23241. **Extract** the target table into a temp file. Include only the `|...|`25 lines (header row, `|---|` separator, and data rows). Drop all surrounding26 prose. Bold markers like `**0.976**` are OK; units (`m`, `rad`, `%`, `ms`)27 must be stripped or the cell becomes NaN.28292. **Decide two parameters:**3031 | Parameter | Options | Choose by |32 |---|---|---|33 | `--normalize` | `row`, `column`, `global` | `row` (default) when each row is a metric slice and columns are variants being compared. `column` when columns are different metrics. `global` for a single-metric grid. |34 | `--direction` | `higher-is-better`, `lower-is-better` | `higher`: AP, recall, F1, pass-rate. `lower`: MAE, RMSE, loss, latency, error. |35363. **Run the script:**3738 ```bash39 python ~/.cursor/skills/table-heatmap/scripts/make_heatmap.py \40 --md-file /tmp/tbl.md \41 --output /path/to/report/folder/report_X_heatmap.png \42 --normalize row \43 --direction higher-is-better \44 --y-label "Forward range bin"45 ```4647 Output path convention: drop the PNG into the same folder that already48 hosts the report's other images (alongside `report_*.png` files).49504. **Insert the image reference ABOVE the existing markdown table:**5152 ```markdown53 5455 | Original | Plain | Markdown | Table |56 | --- | --- | --- | --- |57 | ... | ... | ... | ... |58 ```5960 Do **not** delete the text table. The image is visual; the text is61 authoritative / diffable / searchable / machine-readable.6263## Key CLI flags6465| Flag | Default | Notes |66|---|---|---|67| `--md-file PATH` / `--md-stdin` | — | Exactly one is required. |68| `--output PATH` | — | Required. Use `.png`. |69| `--normalize {row,column,global}` | `row` | See step 2. |70| `--direction {higher-is-better,lower-is-better}` | `higher-is-better` | See step 2. |71| `--headers-position {top,bottom}` | `top` | Column labels above the grid. |72| `--value-fmt STR` | `{:.3f}` | Python format, e.g. `{:.1%}`, `{:.0f}`, `{:.4f}`. |73| `--y-label STR` | none | Short axis label. `\n` for line break. |74| `--title STR` | none | Usually leave empty; caption the image via the markdown alt text instead. |75| `--figsize W,H` | auto | Override if labels collide (15+ cols → bump width). |76| `--col-wrap N` | `12` | Wrap column labels wider than N chars at spaces. `0` disables. |77| `--row-wrap N` | `0` | Wrap row labels wider than N chars at spaces. `0` disables. |78| `--center-value N` | off | Center the color scale on N (symmetric, `0 = white`). Use with **signed-delta** tables (e.g. `+5%` / `-3%`). Values farther from N become more saturated. Best paired with `--normalize global` for a consistent scale across the whole grid. |79| `--no-annotate` | off | Disables `↑ best` / `↓ worst` subscripts. |8081## Common pitfalls8283- **Table file contains surrounding prose.** Parser requires only the84 `|...|` lines of one table. Strip everything else into the temp file.85- **Mixed directions in one table.** One row AP (higher-better), another row86 latency (lower-better) cannot share one heatmap. Split into two tables and87 call the script twice.88- **Non-numeric cells.** Units or text in value cells → NaN → white cell.89 Strip units before passing.90- **Very small value ranges.** If all cells in a row are within 0.001 the91 heatmap is visually uninformative. That's diagnostic: note it in the92 surrounding prose instead of adding color.93- **Signed deltas (e.g. "B2 vs B0 Δ%").** Default `row`/`column`/`global`94 normalization is min-max, so an all-positive grid with `vmin = +0.4%` would95 paint the smallest positive cell **red**. Pass `--normalize global96 --center-value 0` to anchor white at 0; positives become green, negatives97 become red, color intensity scales with `|value| / max(|value|)`.9899## Example (the one that shipped)100101Source table (Main CAR AP by Range, 7 branches × 3 ranges):102103```104| Range | B0 Baseline | B1 A110+NV frozen | B2 A110+NV unfrozen | B3 FE+NV frozen | B4 FE+autolabel frozen | B5 A110+autolabel frozen | B6 A110+autolabel unfrozen |105| --- | --- | --- | --- | --- | --- | --- | --- |106| [0, 25) | 0.952 | 0.969 | 0.897 | 0.975 | **0.976** | 0.956 | 0.967 |107| [25, 50) | 0.869 | 0.867 | **0.890** | 0.863 | 0.877 | 0.847 | 0.838 |108| [50, 100) | 0.521 | 0.518 | **0.548** | 0.493 | 0.533 | 0.469 | 0.464 |109```110111Invocation:112113```bash114python ~/.cursor/skills/table-heatmap/scripts/make_heatmap.py \115 --md-file /tmp/main_ap.md \116 --output cruise/mlp/.../notebooks/output/report_main_ap_heatmap.png \117 --normalize row \118 --direction higher-is-better \119 --y-label "Forward range bin\n(bin_x × |y|<10m)"120```121122Insert:123124```markdown125126127| Range | B0 Baseline | ... |128...129```130131## Style contract (do not change per-invocation)132133The script's palette (`#FF9696` / `#FFFFFF` / `#96FF96`) and cell/font sizing134are fixed so every heatmap in a report matches. If a report needs a different135palette, edit the script rather than adding per-call flags, so consistency is136preserved.