import * as React from 'react'; import { Text, View } from '@react-pdf/renderer'; import type { DatasheetVoltageTable } from '../../model/types'; import { styles } from '../styles'; function clamp(n: number, min: number, max: number): number { return Math.max(min, Math.min(max, n)); } function normTextForMeasure(v: unknown): string { return String(v ?? '') .replace(/\s+/g, ' ') .trim(); } function textLen(v: unknown): number { return normTextForMeasure(v).length; } function distributeWithMinMax(weights: number[], total: number, minEach: number, maxEach: number): number[] { const n = weights.length; if (!n) return []; const mins = Array.from({ length: n }, () => minEach); const maxs = Array.from({ length: n }, () => maxEach); // If mins don't fit, scale them down proportionally. const minSum = mins.reduce((a, b) => a + b, 0); if (minSum > total) { const k = total / minSum; return mins.map(m => m * k); } const result = mins.slice(); let remaining = total - minSum; let remainingIdx = Array.from({ length: n }, (_, i) => i); // Distribute remaining proportionally, respecting max constraints. // Loop is guaranteed to terminate because each iteration either: // - removes at least one index due to hitting max, or // - exhausts `remaining`. while (remaining > 1e-9 && remainingIdx.length) { const wSum = remainingIdx.reduce((acc, i) => acc + Math.max(0, weights[i] || 0), 0); if (wSum <= 1e-9) { // No meaningful weights: distribute evenly. const even = remaining / remainingIdx.length; for (const i of remainingIdx) result[i] += even; remaining = 0; break; } const nextIdx: number[] = []; for (const i of remainingIdx) { const w = Math.max(0, weights[i] || 0); const add = (w / wSum) * remaining; const capped = Math.min(result[i] + add, maxs[i]); const used = capped - result[i]; result[i] = capped; remaining -= used; if (result[i] + 1e-9 < maxs[i]) nextIdx.push(i); } remainingIdx = nextIdx; } // Numerical guard: force exact sum by adjusting the last column. const sum = result.reduce((a, b) => a + b, 0); const drift = total - sum; if (Math.abs(drift) > 1e-9) result[result.length - 1] += drift; return result; } export function DenseTable(props: { table: Pick; firstColLabel: string; }): React.ReactElement { const cols = props.table.columns; const rows = props.table.rows; const headerText = (label: string): string => { // Table headers must NEVER wrap into a second line. // react-pdf can wrap on spaces, so we replace whitespace with NBSP. return String(label || '').replace(/\s+/g, '\u00A0').trim(); }; // Column widths: use explicit percentages (no rounding gaps) so the table always // consumes the full content width. // Goal: // - keep the designation column *not too wide* // - distribute data columns by estimated content width (header + cells) // so columns better fit their data // Make first column denser so numeric columns get more room. // (Long designations can still wrap in body if needed, but table scanability // benefits more from wider data columns.) const cfgMin = 0.14; const cfgMax = 0.23; // A content-based heuristic. // React-PDF doesn't expose a reliable text-measurement API at render time, // so we approximate width by string length (compressed via sqrt to reduce outliers). const cfgContentLen = Math.max(textLen(props.firstColLabel), ...rows.map(r => textLen(r.configuration)), 8); const dataContentLens = cols.map((c, ci) => { const headerL = textLen(c.label); let cellMax = 0; for (const r of rows) cellMax = Math.max(cellMax, textLen(r.cells[ci])); // Slightly prioritize the header (scanability) over a single long cell. return Math.max(headerL * 1.15, cellMax, 3); }); // Use mostly-linear weights so long headers get noticeably more space. const cfgWeight = cfgContentLen * 1.05; const dataWeights = dataContentLens.map(l => l); const dataWeightSum = dataWeights.reduce((a, b) => a + b, 0); const rawCfgPct = dataWeightSum > 0 ? cfgWeight / (cfgWeight + dataWeightSum) : 0.28; let cfgPct = clamp(rawCfgPct, cfgMin, cfgMax); // Ensure a minimum per-data-column width; if needed, shrink cfgPct. // These floors are intentionally generous. Too-narrow columns are worse than a // slightly narrower first column for scanability. const minDataPct = cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06; const cfgPctMaxForMinData = 1 - cols.length * minDataPct; if (Number.isFinite(cfgPctMaxForMinData)) cfgPct = Math.min(cfgPct, cfgPctMaxForMinData); cfgPct = clamp(cfgPct, cfgMin, cfgMax); const dataTotal = Math.max(0, 1 - cfgPct); const maxDataPct = Math.min(0.24, Math.max(minDataPct * 2.8, dataTotal * 0.55)); const dataPcts = distributeWithMinMax(dataWeights, dataTotal, minDataPct, maxDataPct); const cfgW = `${(cfgPct * 100).toFixed(4)}%`; const dataWs = dataPcts.map((p, idx) => { // Keep the last column as the remainder so percentages sum to exactly 100%. if (idx === dataPcts.length - 1) { const used = dataPcts.slice(0, -1).reduce((a, b) => a + b, 0); const remainder = Math.max(0, dataTotal - used); return `${(remainder * 100).toFixed(4)}%`; } return `${(p * 100).toFixed(4)}%`; }); const headerFontSize = cols.length >= 14 ? 5.7 : cols.length >= 12 ? 5.9 : cols.length >= 10 ? 6.2 : 6.6; return ( {headerText(props.firstColLabel)} {cols.map((c, idx) => { const isLast = idx === cols.length - 1; return ( {headerText(c.label)} ); })} {rows.map((r, ri) => ( {r.configuration} {r.cells.map((cell, ci) => { const isLast = ci === r.cells.length - 1; return ( {cell} ); })} ))} ); }