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 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-02-22 10:00:42 -05:00
parent 945985c969
commit 04ec221808
3 changed files with 32 additions and 19 deletions

View file

@ -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;

View file

@ -23,7 +23,7 @@ interface PivotedRow {
function pivotRows(
rows: PivotResultRow[],
rowDims: string[],
colDim: string | undefined,
colDims: string[],
measures: string[],
): PivotedRow[] {
const map = new Map<string, PivotedRow>();
@ -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) => (
<th key={`${colVal}-${m}`} className="text-right px-3 py-2 font-medium text-[var(--muted-foreground)] border-l border-[var(--border)]">
{colDim ? (measures.length > 1 ? `${colVal}${measureLabel(m)}` : colVal) : measureLabel(m)}
{colDims.length > 0 ? (measures.length > 1 ? `${colLabel(colVal)}${measureLabel(m)}` : colLabel(colVal)) : measureLabel(m)}
</th>
))
)}

View file

@ -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