Skip to content

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


Files:

  • Modify: src/features/reports/forecasting/sections/Comparison/export-utils.ts:56-71

  • Step 1: Rewrite buildComparisonModelTotalsRows to 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
Terminal window
pnpm tsc --noEmit
  • Step 3: Commit
Terminal window
git add src/features/reports/forecasting/sections/Comparison/export-utils.ts
git 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 after captureElementDataUrl)

  • Modify: src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts:29-31

  • Step 1: Add captureElementFullScroll to export-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 captureElementFullScroll for 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
Terminal window
pnpm tsc --noEmit
  • Step 4: Commit
Terminal window
git add src/lib/export-csv.ts src/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts
git 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 ref prop to ComparisonTornadoCard

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 getCaptureElements to ComparisonExportHandle

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 ComparisonTornadoGrid and expose getCaptureElements

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
Terminal window
pnpm tsc --noEmit
  • Step 5: Commit
Terminal window
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.ts
git 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 ComparisonPage getter

In ComparisonPage.tsx, change line 38 from:

tornadoGridElement: () => tornadoRef.current?.getCaptureElement?.() ?? null,

To:

tornadoCardElements: () => tornadoRef.current?.getCaptureElements?.() ?? [],
  • Step 3: Update exportComparisonReport tornado 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
Terminal window
pnpm tsc --noEmit

This will likely show errors in buildComparisonReport.ts (Task 5 fixes those). Confirm the errors are only in that file.

  • Step 5: Commit
Terminal window
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.ts
git 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
Terminal window
pnpm tsc --noEmit

Expected: 0 errors.

  • Step 3: Run Biome
Terminal window
pnpm biome check --write src/features/reports/forecasting/ src/lib/export-csv.ts
  • Step 4: Commit
Terminal window
git add src/features/reports/forecasting/sections/Comparison/reporting/buildComparisonReport.ts
git commit -m "fix(comparison): render tornado charts one-per-row in DOCX export"

  • Step 1: Full type check
Terminal window
pnpm tsc --noEmit

Expected: 0 errors.

  • Step 2: Full lint
Terminal window
pnpm biome check src/

Expected: only pre-existing warnings (not from our changes).

  • Step 3: Production build
Terminal window
pnpm build

Expected: exit 0 (with clean working tree — stash any pre-existing WIP first).


ActionFile
Modifysrc/features/reports/forecasting/sections/Comparison/export-utils.ts
Modifysrc/lib/export-csv.ts
Modifysrc/features/reports/forecasting/sections/Comparison/ComparisonTornadoCard.tsx
Modifysrc/features/reports/forecasting/sections/Comparison/ComparisonTornadoGrid.tsx
Modifysrc/features/reports/forecasting/sections/Comparison/types.ts
Modifysrc/features/reports/forecasting/sections/Comparison/reporting/types.ts
Modifysrc/features/reports/forecasting/sections/Comparison/ComparisonPage.tsx
Modifysrc/features/reports/forecasting/sections/Comparison/reporting/exportComparisonReport.ts
Modifysrc/features/reports/forecasting/sections/Comparison/reporting/buildComparisonReport.ts