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:
parent
945985c969
commit
04ec221808
3 changed files with 32 additions and 19 deletions
|
|
@ -36,19 +36,25 @@ export default function DynamicReportChart({ config, result }: DynamicReportChar
|
||||||
return { chartData: [], seriesKeys: [], seriesColors: {} };
|
return { chartData: [], seriesKeys: [], seriesColors: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const colDim = config.columns[0];
|
const colDims = config.columns;
|
||||||
const rowDim = config.rows[0];
|
const rowDim = config.rows[0];
|
||||||
const measure = config.values[0] || "periodic";
|
const measure = config.values[0] || "periodic";
|
||||||
|
|
||||||
// X-axis = first column dimension (or first row dimension if no columns)
|
// X-axis = composite column key (or first row dimension if no columns)
|
||||||
const xDim = colDim || rowDim;
|
const hasColDims = colDims.length > 0;
|
||||||
if (!xDim) return { chartData: [], seriesKeys: [], seriesColors: {} };
|
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)
|
// 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
|
// 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
|
const seriesVals = seriesDim
|
||||||
? [...new Set(result.rows.map((r) => r.keys[seriesDim]))].sort()
|
? [...new Set(result.rows.map((r) => r.keys[seriesDim]))].sort()
|
||||||
: [measure];
|
: [measure];
|
||||||
|
|
@ -59,12 +65,14 @@ export default function DynamicReportChart({ config, result }: DynamicReportChar
|
||||||
if (seriesDim) {
|
if (seriesDim) {
|
||||||
for (const sv of seriesVals) {
|
for (const sv of seriesVals) {
|
||||||
const matchingRows = result.rows.filter(
|
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);
|
entry[sv] = matchingRows.reduce((sum, r) => sum + (r.measures[measure] || 0), 0);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
entry[measure] = matchingRows.reduce((sum, r) => sum + (r.measures[measure] || 0), 0);
|
||||||
}
|
}
|
||||||
return entry;
|
return entry;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ interface PivotedRow {
|
||||||
function pivotRows(
|
function pivotRows(
|
||||||
rows: PivotResultRow[],
|
rows: PivotResultRow[],
|
||||||
rowDims: string[],
|
rowDims: string[],
|
||||||
colDim: string | undefined,
|
colDims: string[],
|
||||||
measures: string[],
|
measures: string[],
|
||||||
): PivotedRow[] {
|
): PivotedRow[] {
|
||||||
const map = new Map<string, PivotedRow>();
|
const map = new Map<string, PivotedRow>();
|
||||||
|
|
@ -38,7 +38,9 @@ function pivotRows(
|
||||||
map.set(rowKey, pivoted);
|
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] = {};
|
if (!pivoted.cells[colKey]) pivoted.cells[colKey] = {};
|
||||||
for (const m of measures) {
|
for (const m of measures) {
|
||||||
pivoted.cells[colKey][m] = (pivoted.cells[colKey][m] || 0) + (row.measures[m] || 0);
|
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 rowDims = config.rows;
|
||||||
const colDim = config.columns[0] || undefined;
|
const colDims = config.columns;
|
||||||
const colValues = colDim ? result.columnValues : ["__all__"];
|
const colValues = colDims.length > 0 ? result.columnValues : ["__all__"];
|
||||||
const measures = config.values;
|
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
|
// Pivot the flat SQL rows into one PivotedRow per unique row-key combo
|
||||||
const pivotedRows = useMemo(
|
const pivotedRows = useMemo(
|
||||||
() => pivotRows(result.rows, rowDims, colDim, measures),
|
() => pivotRows(result.rows, rowDims, colDims, measures),
|
||||||
[result.rows, rowDims, colDim, measures],
|
[result.rows, rowDims, colDims, measures],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pivotedRows.length === 0) {
|
if (pivotedRows.length === 0) {
|
||||||
|
|
@ -156,7 +161,7 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl
|
||||||
{colValues.map((colVal) =>
|
{colValues.map((colVal) =>
|
||||||
measures.map((m) => (
|
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)]">
|
<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>
|
</th>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -308,10 +308,10 @@ export async function getDynamicReportData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract distinct column values
|
// Extract distinct column values (composite key when multiple column dimensions)
|
||||||
const columnDim = config.columns[0];
|
const colDims = config.columns;
|
||||||
const columnValues = columnDim
|
const columnValues = colDims.length > 0
|
||||||
? [...new Set(rows.map((r) => r.keys[columnDim]))].sort()
|
? [...new Set(rows.map((r) => colDims.map((d) => r.keys[d] || "").join("\0")))].sort()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Dimension labels
|
// Dimension labels
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue