2026 04 07 Docx Comparison Rendering Fixes
DOCX Comparison Report Rendering Fixes Implementation Plan
Section titled “DOCX Comparison Report Rendering Fixes Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix three DOCX rendering issues — transpose Model Totals table, fix chart image clipping, and capture tornado charts individually instead of as one grid image.
Architecture: Three independent fixes. (1) Rewrite buildComparisonModelTotalsRows to produce Year | ScenarioA | ScenarioB | ... rows. (2) Add captureElementFullScroll that passes scrollWidth/scrollHeight to toPng. (3) Add React 19 ref-as-prop to ComparisonTornadoCard, collect card refs in the grid, change tornadoGridElement getter to return HTMLElement[], capture each card as a separate PNG, and loop in the DOCX builder.
Tech Stack: React 19, TypeScript 5.9, html-to-image, docx, Biome
Task 1: Transpose Model Totals table
Section titled “Task 1: Transpose Model Totals table”Files:
-
Modify:
src/features/reports/forecasting/sections/Comparison/export-utils.ts:56-71 -
Step 1: Rewrite
buildComparisonModelTotalsRowsto use years-as-rows
Replace lines 56-71 in export-utils.ts:
export function buildComparisonModelTotalsRows(data: ComparisonScenarioData[]): CSVRow[] { if (data.length === 0) return []; const labels = computeYearLabelUnion(data.map((d) => d.yearLabels)); const cumulatives = data.map(() => 0);
const rows: CSVRow[] = labels.map((year) => { const row: CSVRow = { Year: year }; for (let i = 0; i < data.length; i++) { const scenario = data[i]; const labelToIndex = new Map(scenario.yearLabels.map((l, idx) => [l, idx])); const idx = labelToIndex.get(year); const val = idx != null ? scenario.totalSalesByYear[idx] : 0; row[scenario.meta.label] = Number(val.toFixed(2)); cumulatives[i] += val; } return row; });
const cumulativeRow: CSVRow = { Year: "Cumulative" }; for (let i = 0; i < data.length; i++) { cumulativeRow[data[i].meta.label] = Number(cumulatives[i].toFixed(2)); } rows.push(cumulativeRow);
return rows;}This mirrors buildComparisonSalesRows (lines 14-25) — years as rows, one column per scenario. With 3 scenarios, the table is 4 columns × 21 rows instead of 22 columns × 3 rows.
- Step 2: Verify
pnpm tsc --noEmit- Step 3: Commit
git add src/features/reports/forecasting/sections/Comparison/export-utils.tsgit commit -m "fix(comparison): transpose Model Totals table to years-as-rows for DOCX readability"Task 2: Fix Model Totals chart overflow clipping
Section titled “Task 2: Fix Model Totals chart overflow clipping”Files:
-
Modify:
src/lib/export-csv.ts:106(add new function aftercaptureElementDataUrl) -
Modify:
src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts:29-31 -
Step 1: Add
captureElementFullScrolltoexport-csv.ts
After the captureElementDataUrl function (after line 106), add:
/** * Capture DOM element at its full scroll dimensions (bypasses overflow clipping) */export async function captureElementFullScroll(element: HTMLElement): Promise<string | null> { try { const { toPng } = await import("html-to-image"); return await toPng(element, { pixelRatio: 2, width: element.scrollWidth, height: element.scrollHeight, }); } catch { return null; }}- Step 2: Use
captureElementFullScrollfor model totals capture
In exportComparisonReport.ts, replace lines 29-31:
const modelTotalsDataUrl = selectedSections.has("comp-model-chart") ? await captureElementDataUrl(getters.modelTotalsElement()) : null;With:
const modelTotalsElement = selectedSections.has("comp-model-chart") ? getters.modelTotalsElement() : null; const modelTotalsDataUrl = modelTotalsElement ? await captureElementFullScroll(modelTotalsElement) : null;Update the import at the top of the file — add captureElementFullScroll:
import { captureElementDataUrl, captureElementFullScroll, downloadBlob } from "@/lib/export-csv";- Step 3: Verify
pnpm tsc --noEmit- Step 4: Commit
git add src/lib/export-csv.ts src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.tsgit commit -m "fix(comparison): capture Model Totals at full scroll dimensions to prevent clipping"Task 3: Add ref to ComparisonTornadoCard and collect refs in grid
Section titled “Task 3: Add ref to ComparisonTornadoCard and collect refs in grid”Files:
-
Modify:
src/features/reports/forecasting/sections/Comparison/ComparisonTornadoCard.tsx:6-11,17,92 -
Modify:
src/features/reports/forecasting/sections/Comparison/ComparisonTornadoGrid.tsx:15-22,41-50,72-79 -
Modify:
src/features/reports/forecasting/sections/Comparison/types.ts:45-53 -
Step 1: Add
refprop toComparisonTornadoCard
In ComparisonTornadoCard.tsx, update the imports (line 1-4 area) to add Ref:
import type { Ref } from "react";Update the props interface (lines 6-9) and component signature (line 11):
interface ComparisonTornadoCardProps { data: ComparisonScenarioData; colorIndex: number; ref?: Ref<HTMLDivElement>;}
export function ComparisonTornadoCard({ data, colorIndex, ref }: ComparisonTornadoCardProps) {Add ref to both root <div> elements — the early-return “no data” div (line 17) and the main card div (line 92):
Line 17: change <div className="rounded-lg border ..."> to <div ref={ref} className="rounded-lg border ...">
Line 92: change <div className="rounded-lg border bg-card p-4" to <div ref={ref} className="rounded-lg border bg-card p-4"
- Step 2: Add
getCaptureElementstoComparisonExportHandle
In types.ts, add to the ComparisonExportHandle interface (after line 52):
/** DOM-based grid components return multiple elements for individual capture */ getCaptureElements?: () => HTMLElement[];- Step 3: Collect card refs in
ComparisonTornadoGridand exposegetCaptureElements
In ComparisonTornadoGrid.tsx:
Add useCallback to the react imports (line 1-2):
import { type Ref, useCallback, useImperativeHandle, useMemo, useRef, useState,} from "react";Add a cardRefs map after gridRef (after line 22):
const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map());Add a ref callback factory:
const setCardRef = useCallback((id: string) => (el: HTMLDivElement | null) => { if (el) cardRefs.current.set(id, el); else cardRefs.current.delete(id); }, []);Add getCaptureElements to the useImperativeHandle (lines 41-50), after getCaptureElement:
getCaptureElements: () => { const elements: HTMLElement[] = []; for (const scenario of data) { const el = cardRefs.current.get(scenario.meta.id); if (el) elements.push(el); } return elements; },Update the card rendering (lines 72-79) to pass the ref callback:
<ComparisonTornadoCard key={scenario.meta.id} ref={setCardRef(scenario.meta.id)} data={scenario} colorIndex={tabIndex} />- Step 4: Verify
pnpm tsc --noEmit- Step 5: Commit
git add src/features/reports/forecasting/sections/Comparison/ComparisonTornadoCard.tsx src/features/reports/forecasting/sections/Comparison/ComparisonTornadoGrid.tsx src/features/reports/forecasting/sections/Comparison/types.tsgit commit -m "refactor(comparison): add refs to tornado cards and expose getCaptureElements"Task 4: Update types and capture pipeline for tornado image array
Section titled “Task 4: Update types and capture pipeline for tornado image array”Files:
-
Modify:
src/features/reports/forecasting/sections/Comparison/reporting/types.ts:15,22 -
Modify:
src/features/reports/forecasting/sections/Comparison/ComparisonPage.tsx:38 -
Modify:
src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts:32-41 -
Step 1: Change types from single image to array
In reporting/types.ts:
Change ComparisonChartImages.tornado (line 15) from:
tornado: ComparisonChartImage;To:
tornado: ComparisonChartImage[];Change ComparisonChartGetters.tornadoGridElement (line 22) from:
tornadoGridElement: () => HTMLElement | null;To:
tornadoCardElements: () => HTMLElement[];- Step 2: Update
ComparisonPagegetter
In ComparisonPage.tsx, change line 38 from:
tornadoGridElement: () => tornadoRef.current?.getCaptureElement?.() ?? null,To:
tornadoCardElements: () => tornadoRef.current?.getCaptureElements?.() ?? [],- Step 3: Update
exportComparisonReporttornado capture
In exportComparisonReport.ts, replace the tornado capture block (lines 32-34):
const tornadoDataUrl = selectedSections.has("comp-tornado-chart") ? await captureElementDataUrl(getters.tornadoGridElement()) : null;With:
// Capture each tornado card individually const tornadoImages: ComparisonChartImage[] = []; if (selectedSections.has("comp-tornado-chart")) { const elements = getters.tornadoCardElements(); for (const el of elements) { const dataUrl = await captureElementDataUrl(el); tornadoImages.push({ available: !!dataUrl, dataUrl }); } }Update the chartImages assembly — change the tornado property (around line 39):
const chartImages: ComparisonChartImages = { sales: { available: !!salesDataUrl, dataUrl: salesDataUrl }, monteCarlo: { available: !!mcDataUrl, dataUrl: mcDataUrl }, modelTotals: { available: !!modelTotalsDataUrl, dataUrl: modelTotalsDataUrl }, tornado: tornadoImages, };Also add the ComparisonChartImage type to the imports from ./types:
import type { ComparisonChartGetters, ComparisonChartImage, ComparisonChartImages } from "./types";- Step 4: Verify
pnpm tsc --noEmitThis will likely show errors in buildComparisonReport.ts (Task 5 fixes those). Confirm the errors are only in that file.
- Step 5: Commit
git add src/features/reports/forecasting/sections/Comparison/reporting/types.ts src/features/reports/forecasting/sections/Comparison/ComparisonPage.tsx src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.tsgit commit -m "refactor(comparison): change tornado capture from single image to per-card array"Task 5: Update buildComparisonReport for tornado image loop
Section titled “Task 5: Update buildComparisonReport for tornado image loop”Files:
-
Modify:
src/features/reports/forecasting/sections/Comparison/reporting/buildComparisonReport.ts:146-166 -
Step 1: Replace single tornado image with loop
In buildComparisonReport.ts, replace the Sensitivity Analysis section (lines 146-166):
// Sensitivity Analysis if (hasSection(selectedSections, "comp-tornado-table", "comp-tornado-chart")) { children.push(heading("Sensitivity Analysis", HeadingLevel.HEADING_1)); if (selectedSections.has("comp-tornado-chart")) { children.push( chartImageOrPlaceholder( chartImages.tornado, "Sensitivity", LANDSCAPE_CHART_WIDTH, LANDSCAPE_CHART_HEIGHT, ), ); } if (selectedSections.has("comp-tornado-table")) { const tornadoTable = csvRowsToTable(buildComparisonTornadoRows(data)); if (tornadoTable) { children.push(tornadoTable); } else { children.push(italicPara("No sensitivity analysis data available.")); } } }With:
// Sensitivity Analysis if (hasSection(selectedSections, "comp-tornado-table", "comp-tornado-chart")) { children.push(heading("Sensitivity Analysis", HeadingLevel.HEADING_1)); if (selectedSections.has("comp-tornado-chart")) { if (chartImages.tornado.length > 0) { for (const image of chartImages.tornado) { children.push( chartImageOrPlaceholder( image, "Sensitivity", LANDSCAPE_CHART_WIDTH, LANDSCAPE_CHART_HEIGHT, ), ); } } else { children.push(italicPara("[Sensitivity charts not available]")); } } if (selectedSections.has("comp-tornado-table")) { const tornadoTable = csvRowsToTable(buildComparisonTornadoRows(data)); if (tornadoTable) { children.push(tornadoTable); } else { children.push(italicPara("No sensitivity analysis data available.")); } } }Each tornado card image is inserted at full landscape width, stacked vertically.
- Step 2: Verify
pnpm tsc --noEmitExpected: 0 errors.
- Step 3: Run Biome
pnpm biome check --write src/features/reports/forecasting/ src/lib/export-csv.ts- Step 4: Commit
git add src/features/reports/forecasting/sections/Comparison/reporting/buildComparisonReport.tsgit commit -m "fix(comparison): render tornado charts one-per-row in DOCX export"Task 6: Final verification
Section titled “Task 6: Final verification”- Step 1: Full type check
pnpm tsc --noEmitExpected: 0 errors.
- Step 2: Full lint
pnpm biome check src/Expected: only pre-existing warnings (not from our changes).
- Step 3: Production build
pnpm buildExpected: exit 0 (with clean working tree — stash any pre-existing WIP first).
Files Modified
Section titled “Files Modified”| Action | File |
|---|---|
| Modify | src/features/reports/forecasting/sections/Comparison/export-utils.ts |
| Modify | src/lib/export-csv.ts |
| Modify | src/features/reports/forecasting/sections/Comparison/ComparisonTornadoCard.tsx |
| Modify | src/features/reports/forecasting/sections/Comparison/ComparisonTornadoGrid.tsx |
| Modify | src/features/reports/forecasting/sections/Comparison/types.ts |
| Modify | src/features/reports/forecasting/sections/Comparison/reporting/types.ts |
| Modify | src/features/reports/forecasting/sections/Comparison/ComparisonPage.tsx |
| Modify | src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts |
| Modify | src/features/reports/forecasting/sections/Comparison/reporting/buildComparisonReport.ts |