From 04ec22180811a00b44a30be0e41338e0f6179259 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sun, 22 Feb 2026 10:00:42 -0500 Subject: [PATCH] feat: support multiple column dimensions in dynamic reports Combine column dimensions into composite keys instead of using only the first column dimension, enabling richer pivot tables and charts. Co-Authored-By: Claude Opus 4.6 --- src/components/reports/DynamicReportChart.tsx | 24 ++++++++++++------- src/components/reports/DynamicReportTable.tsx | 19 +++++++++------ src/services/reportService.ts | 8 +++---- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/reports/DynamicReportChart.tsx b/src/components/reports/DynamicReportChart.tsx index 27e0385..0d25bb7 100644 --- a/src/components/reports/DynamicReportChart.tsx +++ b/src/components/reports/DynamicReportChart.tsx @@ -36,19 +36,25 @@ export default function DynamicReportChart({ config, result }: DynamicReportChar return { chartData: [], seriesKeys: [], seriesColors: {} }; } - const colDim = config.columns[0]; + const colDims = config.columns; const rowDim = config.rows[0]; const measure = config.values[0] || "periodic"; - // X-axis = first column dimension (or first row dimension if no columns) - const xDim = colDim || rowDim; - if (!xDim) return { chartData: [], seriesKeys: [], seriesColors: {} }; + // X-axis = composite column key (or first row dimension if no columns) + const hasColDims = colDims.length > 0; + if (!hasColDims && !rowDim) return { chartData: [], seriesKeys: [], seriesColors: {} }; + + // Build composite column key per row + const getColKey = (r: typeof result.rows[0]) => + colDims.map((d) => r.keys[d] || "").join(" — "); // Series = first row dimension (or no stacking if no rows, or first row if columns exist) - const seriesDim = colDim ? rowDim : undefined; + const seriesDim = hasColDims ? rowDim : undefined; // Collect unique x and series values - const xValues = [...new Set(result.rows.map((r) => r.keys[xDim]))].sort(); + const xValues = hasColDims + ? [...new Set(result.rows.map(getColKey))].sort() + : [...new Set(result.rows.map((r) => r.keys[rowDim]))].sort(); const seriesVals = seriesDim ? [...new Set(result.rows.map((r) => r.keys[seriesDim]))].sort() : [measure]; @@ -59,12 +65,14 @@ export default function DynamicReportChart({ config, result }: DynamicReportChar if (seriesDim) { for (const sv of seriesVals) { const matchingRows = result.rows.filter( - (r) => r.keys[xDim] === xVal && r.keys[seriesDim] === sv + (r) => (hasColDims ? getColKey(r) : r.keys[rowDim]) === xVal && r.keys[seriesDim] === sv ); entry[sv] = matchingRows.reduce((sum, r) => sum + (r.measures[measure] || 0), 0); } } else { - const matchingRows = result.rows.filter((r) => r.keys[xDim] === xVal); + const matchingRows = result.rows.filter((r) => + hasColDims ? getColKey(r) === xVal : r.keys[rowDim] === xVal + ); entry[measure] = matchingRows.reduce((sum, r) => sum + (r.measures[measure] || 0), 0); } return entry; diff --git a/src/components/reports/DynamicReportTable.tsx b/src/components/reports/DynamicReportTable.tsx index 53af951..2e1e742 100644 --- a/src/components/reports/DynamicReportTable.tsx +++ b/src/components/reports/DynamicReportTable.tsx @@ -23,7 +23,7 @@ interface PivotedRow { function pivotRows( rows: PivotResultRow[], rowDims: string[], - colDim: string | undefined, + colDims: string[], measures: string[], ): PivotedRow[] { const map = new Map(); @@ -38,7 +38,9 @@ function pivotRows( map.set(rowKey, pivoted); } - const colKey = colDim ? (row.keys[colDim] || "") : "__all__"; + const colKey = colDims.length > 0 + ? colDims.map((d) => row.keys[d] || "").join("\0") + : "__all__"; if (!pivoted.cells[colKey]) pivoted.cells[colKey] = {}; for (const m of measures) { pivoted.cells[colKey][m] = (pivoted.cells[colKey][m] || 0) + (row.measures[m] || 0); @@ -107,14 +109,17 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl }; const rowDims = config.rows; - const colDim = config.columns[0] || undefined; - const colValues = colDim ? result.columnValues : ["__all__"]; + const colDims = config.columns; + const colValues = colDims.length > 0 ? result.columnValues : ["__all__"]; const measures = config.values; + // Display label for a composite column key (joined with \0) + const colLabel = (compositeKey: string) => compositeKey.split("\0").join(" — "); + // Pivot the flat SQL rows into one PivotedRow per unique row-key combo const pivotedRows = useMemo( - () => pivotRows(result.rows, rowDims, colDim, measures), - [result.rows, rowDims, colDim, measures], + () => pivotRows(result.rows, rowDims, colDims, measures), + [result.rows, rowDims, colDims, measures], ); if (pivotedRows.length === 0) { @@ -156,7 +161,7 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl {colValues.map((colVal) => measures.map((m) => ( - {colDim ? (measures.length > 1 ? `${colVal} — ${measureLabel(m)}` : colVal) : measureLabel(m)} + {colDims.length > 0 ? (measures.length > 1 ? `${colLabel(colVal)} — ${measureLabel(m)}` : colLabel(colVal)) : measureLabel(m)} )) )} diff --git a/src/services/reportService.ts b/src/services/reportService.ts index 7eb6cec..5d7a899 100644 --- a/src/services/reportService.ts +++ b/src/services/reportService.ts @@ -308,10 +308,10 @@ export async function getDynamicReportData( } } - // Extract distinct column values - const columnDim = config.columns[0]; - const columnValues = columnDim - ? [...new Set(rows.map((r) => r.keys[columnDim]))].sort() + // Extract distinct column values (composite key when multiple column dimensions) + const colDims = config.columns; + const columnValues = colDims.length > 0 + ? [...new Set(rows.map((r) => colDims.map((d) => r.keys[d] || "").join("\0")))].sort() : []; // Dimension labels