diff --git a/web-common/src/features/dashboards/pivot/PivotTable.svelte b/web-common/src/features/dashboards/pivot/PivotTable.svelte
index 3fd7256890a..6834ff87c7c 100644
--- a/web-common/src/features/dashboards/pivot/PivotTable.svelte
+++ b/web-common/src/features/dashboards/pivot/PivotTable.svelte
@@ -1,4 +1,5 @@
@@ -256,4 +256,9 @@
th:last-of-type > .header-cell {
@apply border-r-0;
}
+
+ tr:hover,
+ tr:hover .cell {
+ @apply bg-slate-100;
+ }
diff --git a/web-common/src/features/dashboards/pivot/pivot-column-definition.ts b/web-common/src/features/dashboards/pivot/pivot-column-definition.ts
index 9c85184c423..66b05edaceb 100644
--- a/web-common/src/features/dashboards/pivot/pivot-column-definition.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-column-definition.ts
@@ -29,15 +29,19 @@ function createColumnDefinitionForDimensions(
colDimensions: { label: string; name: string }[],
headers: Record,
leafData: ColumnDef[],
+ totals: PivotDataRow,
): ColumnDef[] {
const dimensionNames = config.colDimensionNames;
const timeConfig = config.time;
+ const filterColumns = Boolean(dimensionNames.length);
+
const colValuesIndexMaps = dimensionNames?.map((colDimension) =>
createIndexMap(headers[colDimension]),
);
const levels = dimensionNames.length;
+
// Recursive function to create nested columns
function createNestedColumns(
level: number,
@@ -53,37 +57,51 @@ function createColumnDefinitionForDimensions(
);
// Base case: return leaf columns
- return leafData.map((leaf, i) => ({
+ const leafNodes = leafData.map((leaf, i) => ({
...leaf,
// Change accessor key to match the nested column structure
accessorKey: accessors[i],
}));
+
+ if (!filterColumns) {
+ return leafNodes;
+ }
+ return leafNodes.filter((leaf) =>
+ Object.keys(totals).includes(leaf.accessorKey),
+ );
}
// Recursive case: create nested headers
const headerValues = headers[dimensionNames?.[level]];
- return headerValues?.map((value) => {
- let displayValue = value;
- if (isTimeDimension(dimensionNames?.[level], timeConfig?.timeDimension)) {
- const timeGrain = getTimeGrainFromDimension(dimensionNames?.[level]);
- const dt = addZoneOffset(
- removeLocalTimezoneOffset(new Date(value)),
- timeConfig?.timeZone,
- );
- const timeFormatter = timeFormat(
- timeGrain ? TIME_GRAIN[timeGrain].d3format : "%H:%M",
- ) as (d: Date) => string;
-
- displayValue = timeFormatter(dt);
- } else if (displayValue === null) displayValue = "null";
- return {
- header: displayValue,
- columns: createNestedColumns(level + 1, {
+ return headerValues
+ ?.map((value) => {
+ let displayValue = value;
+ if (
+ isTimeDimension(dimensionNames?.[level], timeConfig?.timeDimension)
+ ) {
+ const timeGrain = getTimeGrainFromDimension(dimensionNames?.[level]);
+ const dt = addZoneOffset(
+ removeLocalTimezoneOffset(new Date(value)),
+ timeConfig?.timeZone,
+ );
+ const timeFormatter = timeFormat(
+ timeGrain ? TIME_GRAIN[timeGrain].d3format : "%H:%M",
+ ) as (d: Date) => string;
+
+ displayValue = timeFormatter(dt);
+ } else if (displayValue === null) displayValue = "null";
+
+ const nestedColumns = createNestedColumns(level + 1, {
...colValuePair,
[dimensionNames[level]]: value,
- }),
- };
- });
+ });
+
+ return {
+ header: displayValue,
+ columns: nestedColumns,
+ };
+ })
+ .filter((column) => column.columns.length > 0);
}
// Construct column def for Row Totals
@@ -141,6 +159,7 @@ function formatRowDimensionValue(
export function getColumnDefForPivot(
config: PivotDataStoreConfig,
columnDimensionAxes: Record | undefined,
+ totals: PivotDataRow,
) {
const IsNested = true;
@@ -232,6 +251,7 @@ export function getColumnDefForPivot(
colDimensions,
columnDimensionAxes || {},
leafColumns,
+ totals,
);
return [...rowDefinitions, ...groupedColDef];
diff --git a/web-common/src/features/dashboards/pivot/pivot-data-store.ts b/web-common/src/features/dashboards/pivot/pivot-data-store.ts
index fd0f4f5831e..d00679507cf 100644
--- a/web-common/src/features/dashboards/pivot/pivot-data-store.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-data-store.ts
@@ -4,6 +4,7 @@ import { useMetricsView } from "@rilldata/web-common/features/dashboards/selecto
import { memoizeMetricsStore } from "@rilldata/web-common/features/dashboards/state-managers/memoize-metrics-store";
import type { StateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers";
import { useTimeControlStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store";
+import type { TimeRangeString } from "@rilldata/web-common/lib/time/types";
import type {
V1MetricsViewAggregationResponse,
V1MetricsViewAggregationResponseDataItem,
@@ -20,8 +21,10 @@ import { sliceColumnAxesDataForDef } from "./pivot-infinite-scroll";
import {
createPivotAggregationRowQuery,
getAxisForDimensions,
+ getTotalsRowQuery,
} from "./pivot-queries";
import {
+ getTotalsRow,
prepareNestedPivotData,
reduceTableCellDataIntoRows,
} from "./pivot-table-transformations";
@@ -29,6 +32,7 @@ import {
getFilterForPivotTable,
getPivotConfigKey,
getSortForAccessor,
+ getTimeForQuery,
getTimeGrainFromDimension,
getTotalColumnCount,
isTimeDimension,
@@ -128,6 +132,7 @@ export function createTableCellQuery(
config: PivotDataStoreConfig,
anchorDimension: string | undefined,
columnDimensionAxesData: Record | undefined,
+ totalsRow: PivotDataRow,
rowDimensionValues: string[],
) {
let allDimensions = config.colDimensionNames;
@@ -147,13 +152,17 @@ export function createTableCellQuery(
} else return { name: dimension };
});
- const filterForInitialTable = getFilterForPivotTable(
- config,
- columnDimensionAxesData,
- rowDimensionValues,
- true,
- anchorDimension,
- );
+ const { filters: filterForInitialTable, timeFilters } =
+ getFilterForPivotTable(
+ config,
+ columnDimensionAxesData,
+ totalsRow,
+ rowDimensionValues,
+ true,
+ anchorDimension,
+ );
+
+ const timeRange: TimeRangeString = getTimeForQuery(config.time, timeFilters);
const mergedFilter = mergeFilters(filterForInitialTable, config.whereFilter);
@@ -170,7 +179,9 @@ export function createTableCellQuery(
mergedFilter,
config.measureFilter,
sortBy,
- "10000",
+ "5000",
+ "0",
+ timeRange,
);
}
@@ -181,6 +192,7 @@ export function createTableCellQuery(
*/
let lastPivotData: PivotDataRow[] = [];
let lastPivotColumnDef: ColumnDef[] = [];
+let lastTotalColumns: number = 0;
/**
* The expanded table has to iterate over itself to find nested dimension values
@@ -263,8 +275,6 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
columnDimensionAxes?.data,
);
- const totalColumns = getTotalColumnCount(columnDimensionAxes?.data);
-
const rowDimensionAxisQuery = getAxisForDimensions(
ctx,
config,
@@ -285,13 +295,59 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
config.whereFilter,
);
+ let globalTotalsQuery:
+ | Readable
+ | CreateQueryResult =
+ readable(null);
+ let totalsRowQuery:
+ | Readable
+ | CreateQueryResult =
+ readable(null);
+ if (rowDimensionNames.length && measureNames.length) {
+ globalTotalsQuery = createPivotAggregationRowQuery(
+ ctx,
+ config.measureNames,
+ [],
+ config.whereFilter,
+ config.measureFilter,
+ [],
+ "5000", // Using 5000 for cache hit
+ );
+ }
+ if (
+ (rowDimensionNames.length || colDimensionNames.length) &&
+ measureNames.length
+ ) {
+ totalsRowQuery = getTotalsRowQuery(
+ ctx,
+ config,
+ columnDimensionAxes?.data,
+ );
+ }
+
/**
* Derive a store from axes queries
*/
return derived(
- [rowDimensionAxisQuery, rowDimensionUnsortedAxisQuery],
- ([rowDimensionAxes, rowDimensionUnsortedAxis], axesSet) => {
+ [
+ rowDimensionAxisQuery,
+ rowDimensionUnsortedAxisQuery,
+ globalTotalsQuery,
+ totalsRowQuery,
+ ],
+ (
+ [
+ rowDimensionAxes,
+ rowDimensionUnsortedAxis,
+ globalTotalsResponse,
+ totalsRowResponse,
+ ],
+ axesSet,
+ ) => {
if (
+ (globalTotalsResponse !== null &&
+ globalTotalsResponse?.isFetching) ||
+ (totalsRowResponse !== null && totalsRowResponse?.isFetching) ||
rowDimensionAxes?.isFetching ||
rowDimensionUnsortedAxis?.isFetching
) {
@@ -300,10 +356,19 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
data: lastPivotData,
columnDef: lastPivotColumnDef,
assembled: false,
- totalColumns,
+ totalColumns: lastTotalColumns,
});
}
+ const totalsRow = getTotalsRow(
+ config,
+ columnDimensionAxes?.data,
+ totalsRowResponse?.data?.data,
+ globalTotalsResponse?.data?.data,
+ );
+
+ const totalColumns = getTotalColumnCount(totalsRow);
+
const { rows: rowDimensionValues, totals: rowTotals } =
reconcileMissingDimensionValues(
anchorDimension,
@@ -311,35 +376,40 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
rowDimensionUnsortedAxis,
);
- let columnDef = getColumnDefForPivot(
- config,
- columnDimensionAxes?.data,
- );
-
let initialTableCellQuery:
| Readable
| CreateQueryResult =
readable(null);
+ let columnDef: ColumnDef[] = [];
if (colDimensionNames.length || !rowDimensionNames.length) {
- const slicedAxesDataForPage = sliceColumnAxesDataForDef(
- colDimensionNames,
+ const slicedAxesDataForDef = sliceColumnAxesDataForDef(
+ config,
columnDimensionAxes?.data,
- config.pivot.columnPage,
- measureNames.length,
+ totalsRow,
);
- columnDef = getColumnDefForPivot(config, slicedAxesDataForPage);
+ columnDef = getColumnDefForPivot(
+ config,
+ slicedAxesDataForDef,
+ totalsRow,
+ );
initialTableCellQuery = createTableCellQuery(
ctx,
config,
rowDimensionNames[0],
columnDimensionAxes?.data,
+ totalsRow,
rowDimensionValues,
);
+ } else {
+ columnDef = getColumnDefForPivot(
+ config,
+ columnDimensionAxes?.data,
+ totalsRow,
+ );
}
-
/**
* Derive a store from initial table cell data query
*/
@@ -381,56 +451,15 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
config,
pivotData,
columnDimensionAxes?.data,
+ totalsRow,
);
- let globalTotalsQuery:
- | Readable
- | CreateQueryResult<
- V1MetricsViewAggregationResponse,
- unknown
- > = readable(null);
- let totalsRowQuery:
- | Readable
- | CreateQueryResult<
- V1MetricsViewAggregationResponse,
- unknown
- > = readable(null);
- if (rowDimensionNames.length && measureNames.length) {
- /** In some cases the totals query would be the same query as that
- * for the initial table cell data. With svelte query cache we would not hit the
- * API twice
- */
- globalTotalsQuery = createPivotAggregationRowQuery(
- ctx,
- config.measureNames,
- [],
- config.whereFilter,
- config.measureFilter,
- [],
- "10000", // Using 10000 for cache hit
- );
- totalsRowQuery = createTableCellQuery(
- ctx,
- config,
- undefined,
- columnDimensionAxes?.data,
- [],
- );
- }
/**
* Derive a store based on expanded rows and totals
*/
return derived(
- [
- globalTotalsQuery,
- totalsRowQuery,
- expandedSubTableCellQuery,
- ],
- ([
- globalTotalsResponse,
- totalsRowResponse,
- expandedRowMeasureValues,
- ]) => {
+ [expandedSubTableCellQuery],
+ ([expandedRowMeasureValues]) => {
prepareNestedPivotData(pivotData, rowDimensionNames);
let tableDataExpanded: PivotDataRow[] = pivotData;
if (expandedRowMeasureValues?.length) {
@@ -448,29 +477,10 @@ function createPivotDataStore(ctx: StateManagers): PivotDataStore {
}
lastPivotData = tableDataExpanded;
lastPivotColumnDef = columnDef;
+ lastTotalColumns = totalColumns;
let assembledTableData = tableDataExpanded;
if (rowDimensionNames.length && measureNames.length) {
- const totalsRowData = totalsRowResponse?.data?.data;
-
- const globalTotalsData =
- globalTotalsResponse?.data?.data || [];
- const totalsRowTable = reduceTableCellDataIntoRows(
- config,
- "",
- [],
- columnDimensionAxes?.data || {},
- [],
- totalsRowData || [],
- );
-
- let totalsRow = totalsRowTable[0] || {};
- totalsRow[anchorDimension] = "Total";
-
- globalTotalsData.forEach((total) => {
- totalsRow = { ...total, ...totalsRow };
- });
-
assembledTableData = [totalsRow, ...tableDataExpanded];
}
diff --git a/web-common/src/features/dashboards/pivot/pivot-expansion.ts b/web-common/src/features/dashboards/pivot/pivot-expansion.ts
index dd88361fd5c..2c997941a91 100644
--- a/web-common/src/features/dashboards/pivot/pivot-expansion.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-expansion.ts
@@ -68,8 +68,9 @@ export function createSubTableCellQuery(
config: PivotDataStoreConfig,
anchorDimension: string,
columnDimensionAxesData: Record | undefined,
+ totalsRow: PivotDataRow,
rowNestFilters: V1Expression,
- timeRange: TimeRangeString | undefined,
+ timeFilters: TimeFilters[],
) {
const allDimensions = config.colDimensionNames.concat([anchorDimension]);
@@ -86,10 +87,14 @@ export function createSubTableCellQuery(
} else return { name: dimension };
});
- const filterForSubTable = getFilterForPivotTable(
- config,
- columnDimensionAxesData,
+ const { filters: filterForSubTable, timeFilters: colTimeFilters } =
+ getFilterForPivotTable(config, columnDimensionAxesData, totalsRow);
+
+ const timeRange: TimeRangeString = getTimeForQuery(
+ time,
+ timeFilters.concat(colTimeFilters),
);
+
filterForSubTable.cond?.exprs?.push(...(rowNestFilters?.cond?.exprs ?? []));
const sortBy = [
@@ -105,7 +110,7 @@ export function createSubTableCellQuery(
filterForSubTable,
config.measureFilter,
sortBy,
- "10000",
+ "5000",
"0",
timeRange,
);
@@ -129,6 +134,7 @@ export function queryExpandedRowMeasureValues(
config: PivotDataStoreConfig,
tableData: PivotDataRow[],
columnDimensionAxesData: Record | undefined,
+ totalsRow: PivotDataRow,
): Readable {
const { rowDimensionNames } = config;
const expanded = config.pivot.expanded;
@@ -228,8 +234,9 @@ export function queryExpandedRowMeasureValues(
config,
anchorDimension,
columnDimensionAxesData,
+ totalsRow,
allMergedFilters,
- timeRange,
+ timeFilters,
),
],
([expandIndex, subRowDimensions, subTableData]) => {
diff --git a/web-common/src/features/dashboards/pivot/pivot-infinite-scroll.ts b/web-common/src/features/dashboards/pivot/pivot-infinite-scroll.ts
index 3aa7af8c517..64a88cecc26 100644
--- a/web-common/src/features/dashboards/pivot/pivot-infinite-scroll.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-infinite-scroll.ts
@@ -1,92 +1,80 @@
+import type {
+ PivotDataRow,
+ PivotDataStoreConfig,
+ TimeFilters,
+} from "@rilldata/web-common/features/dashboards/pivot/types";
import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils";
-import type { V1Expression } from "@rilldata/web-common/runtime-client";
-
-const NUM_COLUMNS_PER_PAGE = 50;
-
-type ColumnNode = {
- value: string;
- depth: number;
-};
-
-function getTotalNodes(tree: Array>): number {
- return tree.reduce((acc, level) => acc * level.length, 1);
-}
-
-function getColumnPage(
- tree: Array>,
- pageNumber: number,
- pageSize: number,
-): Record> {
- const startIndex = (pageNumber - 1) * pageSize;
- const totalNodes = getTotalNodes(tree);
-
- if (startIndex >= totalNodes || startIndex < 0) {
- return []; // Page number out of range
- }
-
- const result: ColumnNode[] = [];
- let currentIndex = 0;
-
- function dfs(level: number, path: ColumnNode[]) {
- if (level === tree.length) {
- if (currentIndex >= startIndex && currentIndex < startIndex + pageSize) {
- result.push(...path);
- }
- currentIndex++;
- return;
- }
- for (const child of tree[level]) {
- if (currentIndex >= startIndex + pageSize) {
- break; // Stop processing once the page is filled
- }
- dfs(level + 1, [...path, { value: child, depth: level }]);
- }
- }
-
- dfs(0, []);
-
- const groups: Record> = {};
-
- result.forEach(({ value, depth }) => {
- groups[depth] = groups[depth] || new Set();
- groups[depth].add(value);
- });
+import {
+ extractNumbers,
+ getTimeGrainFromDimension,
+ isTimeDimension,
+ sortAcessors,
+} from "./pivot-utils";
+
+const NUM_COLUMNS_PER_PAGE = 40;
+
+function getSortedColumnKeys(
+ config: PivotDataStoreConfig,
+ totalsRow: PivotDataRow,
+) {
+ const { measureNames, rowDimensionNames } = config;
- return groups;
+ const allColumnKeys = totalsRow ? Object.keys(totalsRow) : [];
+ const colHeaderKeys = allColumnKeys.filter(
+ (key) => !(measureNames.includes(key) || rowDimensionNames[0] === key),
+ );
+ return sortAcessors(colHeaderKeys);
}
-/** Slice column axes databased on page
+/**
+ * Slice column axes data based on page
* number. This is used for column definition in pivot table.
*/
export function sliceColumnAxesDataForDef(
- colDimensionNames: string[],
+ config: PivotDataStoreConfig,
colDimensionAxes: Record = {},
- colDimensionPageNumber: number,
- numMeasures: number,
+ totalsRow: PivotDataRow,
) {
+ const { colDimensionNames } = config;
+ const colDimensionPageNumber = config.pivot.columnPage;
if (!colDimensionNames.length) return colDimensionAxes;
const totalColumnsToBeDisplayed =
- Math.floor(NUM_COLUMNS_PER_PAGE / numMeasures) * colDimensionPageNumber;
+ NUM_COLUMNS_PER_PAGE * colDimensionPageNumber;
- const colDimensionValues = colDimensionNames.map((colDimensionName) => {
- return colDimensionAxes[colDimensionName];
- });
+ const maxIndexVisible: Record = {};
+
+ const sortedColumnKeys = getSortedColumnKeys(config, totalsRow);
- const pageGroups = getColumnPage(
- colDimensionValues,
- 1,
+ const columnKeysForPage = sortedColumnKeys.slice(
+ 0,
totalColumnsToBeDisplayed,
);
+ columnKeysForPage.forEach((accessor) => {
+ // Strip the measure string from the accessor
+ const [accessorWithoutMeasure] = accessor.split("m");
+ accessorWithoutMeasure.split("_").forEach((part) => {
+ const { c, v } = extractNumbers(part);
+ const columnDimensionName = colDimensionNames[c];
+ maxIndexVisible[columnDimensionName] = Math.max(
+ maxIndexVisible[columnDimensionName] || 0,
+ v + 1,
+ );
+ });
+ });
+
const slicedAxesData: Record = {};
- Object.keys(pageGroups).forEach((key) => {
- const colDimensionName = colDimensionNames[parseInt(key)];
- slicedAxesData[colDimensionName] = Array.from(
- pageGroups[key] as Set,
- );
+ Object.keys(maxIndexVisible).forEach((dimensionName) => {
+ if (maxIndexVisible[dimensionName] > 0) {
+ slicedAxesData[dimensionName] = colDimensionAxes[dimensionName].slice(
+ 0,
+ maxIndexVisible[dimensionName],
+ );
+ }
});
+
return slicedAxesData;
}
@@ -95,31 +83,72 @@ export function sliceColumnAxesDataForDef(
* page number and page size
*/
export function getColumnFiltersForPage(
- colDimensionNames: string[],
+ config: PivotDataStoreConfig,
colDimensionAxes: Record = {},
- colDimensionPageNumber: number,
- numMeasures: number,
-): V1Expression[] {
- if (!colDimensionNames.length || numMeasures == 0) return [];
+ totalsRow: PivotDataRow,
+) {
+ const { measureNames, colDimensionNames } = config;
+ const colDimensionPageNumber = config.pivot.columnPage;
+
+ if (!colDimensionNames.length || measureNames.length == 0)
+ return { filters: [], timeFilters: [] };
+
+ const pageStartIndex = NUM_COLUMNS_PER_PAGE * (colDimensionPageNumber - 1);
+
+ const sortedColumnKeys = getSortedColumnKeys(config, totalsRow);
- const effectiveColumnsPerPage = Math.floor(
- NUM_COLUMNS_PER_PAGE / numMeasures,
+ const columnKeysForPage = sortedColumnKeys.slice(
+ pageStartIndex,
+ pageStartIndex + NUM_COLUMNS_PER_PAGE,
);
- const colDimensionValues = colDimensionNames.map((colDimensionName) => {
- return colDimensionAxes[colDimensionName];
+ const minIndexVisible: Record = {};
+ const maxIndexVisible: Record = {};
+
+ columnKeysForPage.forEach((accessor) => {
+ // Strip the measure string from the accessor
+ const [accessorWithoutMeasure] = accessor.split("m");
+ accessorWithoutMeasure.split("_").forEach((part) => {
+ const { c, v } = extractNumbers(part);
+ const dimension = colDimensionNames[c];
+ maxIndexVisible[dimension] = Math.max(
+ maxIndexVisible[dimension] || 0,
+ v + 1,
+ );
+ minIndexVisible[dimension] = Math.min(
+ minIndexVisible[dimension] ?? Number.MAX_SAFE_INTEGER,
+ v,
+ );
+ });
});
- const pageGroups = getColumnPage(
- colDimensionValues,
- colDimensionPageNumber,
- effectiveColumnsPerPage,
- );
+ const slicedAxesData: Record = {};
- return Object.entries(pageGroups).map(([colDimensionId, values]) =>
- createInExpression(
- colDimensionNames[parseInt(colDimensionId)],
- Array.from(values),
- ),
- );
+ Object.keys(minIndexVisible).forEach((dimension) => {
+ slicedAxesData[dimension] = colDimensionAxes[dimension].slice(
+ minIndexVisible[dimension],
+ maxIndexVisible[dimension],
+ );
+ });
+
+ const timeFilters: TimeFilters[] = [];
+ const filters = colDimensionNames
+ .filter((dimension) => {
+ if (isTimeDimension(dimension, config.time.timeDimension)) {
+ const dates = slicedAxesData[dimension].map((d) =>
+ new Date(d).getTime(),
+ );
+ const timeStart = new Date(Math.min(...dates)).toISOString();
+ const timeEnd = new Date(Math.max(...dates)).toISOString();
+ const interval = getTimeGrainFromDimension(dimension);
+ timeFilters.push({ timeStart, timeEnd, interval });
+ return false;
+ }
+ return true;
+ })
+ .map((dimension) =>
+ createInExpression(dimension, slicedAxesData[dimension]),
+ );
+
+ return { filters, timeFilters };
}
diff --git a/web-common/src/features/dashboards/pivot/pivot-queries.ts b/web-common/src/features/dashboards/pivot/pivot-queries.ts
index 09d5f2703c4..68028b640c1 100644
--- a/web-common/src/features/dashboards/pivot/pivot-queries.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-queries.ts
@@ -8,7 +8,11 @@ import type {
PivotDataStoreConfig,
} from "@rilldata/web-common/features/dashboards/pivot/types";
import type { StateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers";
-import { sanitiseExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils";
+import {
+ createAndExpression,
+ createInExpression,
+ sanitiseExpression,
+} from "@rilldata/web-common/features/dashboards/stores/filter-utils";
import { useTimeControlStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store";
import type { TimeRangeString } from "@rilldata/web-common/lib/time/types";
import {
@@ -21,6 +25,7 @@ import {
} from "@rilldata/web-common/runtime-client";
import type { CreateQueryResult } from "@tanstack/svelte-query";
import { Readable, derived, readable } from "svelte/store";
+import { mergeFilters } from "./pivot-merge-filters";
/**
* Wrapper function for Aggregate Query API
@@ -166,3 +171,50 @@ export function getAxisForDimensions(
},
);
}
+
+export function getTotalsRowQuery(
+ ctx: StateManagers,
+ config: PivotDataStoreConfig,
+ colDimensionAxes: Record = {},
+) {
+ const { colDimensionNames } = config;
+
+ const { time } = config;
+ const dimensionBody = colDimensionNames.map((dimension) => {
+ if (isTimeDimension(dimension, time.timeDimension)) {
+ return {
+ name: time.timeDimension,
+ timeGrain: getTimeGrainFromDimension(dimension),
+ timeZone: time.timeZone,
+ alias: dimension,
+ };
+ } else return { name: dimension };
+ });
+
+ const colFilters = colDimensionNames
+ .filter((d) => !isTimeDimension(d, time.timeDimension))
+ .map((dimension) =>
+ createInExpression(dimension, colDimensionAxes[dimension]),
+ );
+
+ const mergedFilter = mergeFilters(
+ createAndExpression(colFilters),
+ config.whereFilter,
+ );
+
+ const sortBy = [
+ {
+ desc: true,
+ name: config.measureNames[0],
+ },
+ ];
+ return createPivotAggregationRowQuery(
+ ctx,
+ config.measureNames,
+ dimensionBody,
+ mergedFilter,
+ config.measureFilter,
+ sortBy,
+ "1000",
+ );
+}
diff --git a/web-common/src/features/dashboards/pivot/pivot-table-transformations.ts b/web-common/src/features/dashboards/pivot/pivot-table-transformations.ts
index f2f3598b286..cfa2a5a23f9 100644
--- a/web-common/src/features/dashboards/pivot/pivot-table-transformations.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-table-transformations.ts
@@ -103,3 +103,37 @@ export function reduceTableCellDataIntoRows(
return tableData;
}
+
+export function getTotalsRow(
+ config: PivotDataStoreConfig,
+ columnDimensionAxes: Record = {},
+ totalsRowData: V1MetricsViewAggregationResponseDataItem[] = [],
+ globalTotalsData: V1MetricsViewAggregationResponseDataItem[] = [],
+) {
+ const { rowDimensionNames, measureNames } = config;
+ const anchorDimensionName = rowDimensionNames[0];
+
+ let totalsRow: PivotDataRow = {};
+ if (measureNames.length) {
+ const totalsRowTable = reduceTableCellDataIntoRows(
+ config,
+ "",
+ [],
+ columnDimensionAxes || {},
+ [],
+ totalsRowData || [],
+ );
+
+ totalsRow = totalsRowTable[0] || {};
+
+ globalTotalsData.forEach((total) => {
+ totalsRow = { ...total, ...totalsRow };
+ });
+
+ if (anchorDimensionName) {
+ totalsRow[anchorDimensionName] = "Total";
+ }
+ }
+
+ return totalsRow;
+}
diff --git a/web-common/src/features/dashboards/pivot/pivot-utils.ts b/web-common/src/features/dashboards/pivot/pivot-utils.ts
index 184e989d69c..0f31caaa079 100644
--- a/web-common/src/features/dashboards/pivot/pivot-utils.ts
+++ b/web-common/src/features/dashboards/pivot/pivot-utils.ts
@@ -18,6 +18,7 @@ import type {
import { getColumnFiltersForPage } from "./pivot-infinite-scroll";
import type {
PivotAxesData,
+ PivotDataRow,
PivotDataStoreConfig,
PivotTimeConfig,
TimeFilters,
@@ -123,17 +124,23 @@ export function getTimeForQuery(
}
timeFilters.forEach((filter) => {
- // FIXME: Fix type warnings. Are these false positives?
- // Using `as` to avoid type warnings
- const duration: Period = TIME_GRAIN[filter.interval]?.duration as Period;
-
const startTimeDt = new Date(filter.timeStart);
+ let startTimeOfLastInterval: Date | undefined = undefined;
+
+ if (filter.timeEnd) {
+ startTimeOfLastInterval = new Date(filter.timeEnd);
+ } else {
+ startTimeOfLastInterval = startTimeDt;
+ }
+
+ const duration = TIME_GRAIN[filter.interval]?.duration as Period;
const endTimeDt = getOffset(
- startTimeDt,
+ startTimeOfLastInterval,
duration,
TimeOffsetType.ADD,
timeZone,
) as Date;
+
if (startTimeDt > new Date(timeStart as string)) {
timeStart = filter.timeStart;
}
@@ -184,15 +191,8 @@ export function createIndexMap(arr: T[]): Map {
* Returns total number of columns for the table
* excluding row and group totals columns
*/
-export function getTotalColumnCount(
- columnDimensionAxes: Record | undefined,
-) {
- if (!columnDimensionAxes) return 0;
-
- return Object.values(columnDimensionAxes).reduce(
- (acc, columnDimension) => acc * columnDimension.length,
- 1,
- );
+export function getTotalColumnCount(totalsRow: PivotDataRow) {
+ return Object.keys(totalsRow).length;
}
/***
@@ -201,14 +201,15 @@ export function getTotalColumnCount(
export function getFilterForPivotTable(
config: PivotDataStoreConfig,
colDimensionAxes: Record = {},
+ totalsRow: PivotDataRow,
rowDimensionValues: string[] = [],
isInitialTable = false,
anchorDimension: string | undefined = undefined,
yLimit = 100,
-): V1Expression {
+) {
// TODO: handle for already existing global filters
- const { colDimensionNames, rowDimensionNames, time } = config;
+ const { rowDimensionNames, time } = config;
let rowFilters: V1Expression | undefined;
if (
@@ -222,19 +223,18 @@ export function getFilterForPivotTable(
);
}
- const colFiltersForPage = getColumnFiltersForPage(
- colDimensionNames.filter(
- (dimension) => !isTimeDimension(dimension, time.timeDimension),
- ),
+ const { filters: colFiltersForPage, timeFilters } = getColumnFiltersForPage(
+ config,
colDimensionAxes,
- config.pivot.columnPage,
- config.measureNames.length,
+ totalsRow,
);
- return createAndExpression([
+ const filters = createAndExpression([
...colFiltersForPage,
...(rowFilters ? [rowFilters] : []),
]);
+
+ return { filters, timeFilters };
}
/**
@@ -272,7 +272,7 @@ export function getAccessorForCell(
/**
* Extract the numbers after c and v in a accessor part string
*/
-function extractNumbers(str: string) {
+export function extractNumbers(str: string) {
const indexOfC = str.indexOf("c");
const indexOfV = str.indexOf("v");
@@ -282,6 +282,47 @@ function extractNumbers(str: string) {
return { c: numberAfterC, v: numberAfterV };
}
+export function sortAcessors(accessors: string[]) {
+ function parseParts(str: string): number[] {
+ // Extract all occurrences of patterns like cv
+ const matches = str.match(/c(\d+)v(\d+)/g);
+ if (!matches) {
+ return [];
+ }
+ // Map each found pattern to its numeric components
+ const parts: number[] = matches.flatMap((match) => {
+ const result = /c(\d+)v(\d+)/.exec(match);
+ if (!result) return [];
+ const [, cPart, vPart] = result;
+ return [parseInt(cPart, 10), parseInt(vPart, 10)]; // Convert to numbers for proper comparison
+ });
+
+ // Extract m part
+ const mPartMatch = str.match(/m(\d+)$/);
+ if (mPartMatch) {
+ parts.push(parseInt(mPartMatch[1], 10)); // Add m part as a number
+ }
+ return parts;
+ }
+
+ return accessors.sort((a: string, b: string): number => {
+ const partsA = parseParts(a);
+ const partsB = parseParts(b);
+
+ // Compare each part until a difference is found
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
+ const partA = partsA[i] || 0; // Default to 0 if undefined
+ const partB = partsB[i] || 0; // Default to 0 if undefined
+ if (partA !== partB) {
+ return partA - partB;
+ }
+ }
+
+ // If all parts are equal, consider them equal
+ return 0;
+ });
+}
+
/**
* For a given accessor created by getAccessorForCell, get the filter
* that can be applied to the table to get sorted data based on the
diff --git a/web-common/src/features/dashboards/pivot/tests/pivot-utilts.test.ts b/web-common/src/features/dashboards/pivot/tests/pivot-utilts.test.ts
new file mode 100644
index 00000000000..0d38a89223c
--- /dev/null
+++ b/web-common/src/features/dashboards/pivot/tests/pivot-utilts.test.ts
@@ -0,0 +1,28 @@
+import { sortAcessors } from "@rilldata/web-common/features/dashboards/pivot/pivot-utils";
+import { describe, expect, it } from "vitest";
+
+describe("sortAcessors function", () => {
+ it("should correctly sort accessors with basic sorting", () => {
+ const input = ["c1v2m3", "c0v0m0", "c2v3m1"];
+ const expected = ["c0v0m0", "c1v2m3", "c2v3m1"];
+ expect(sortAcessors(input)).toEqual(expected);
+ });
+
+ it("should sort accessors with varying numbers of cv sequences", () => {
+ const input = ["c0v1_c1v2m0", "c0v0_c1v1m2", "c0v0_c1v0m1"];
+ const expected = ["c0v0_c1v0m1", "c0v0_c1v1m2", "c0v1_c1v2m0"];
+ expect(sortAcessors(input)).toEqual(expected);
+ });
+
+ it("should sort accessors with the same c-v values but different m values", () => {
+ const input = ["c0v1m3", "c0v1m1", "c0v1m2"];
+ const expected = ["c0v1m1", "c0v1m2", "c0v1m3"];
+ expect(sortAcessors(input)).toEqual(expected);
+ });
+
+ it("should sort accessors with multiple c-v-m sequences, including different-number lengths", () => {
+ const input = ["c1v10_c2v20m30", "c1v2_c2v3m4", "c1v10_c2v3m4"];
+ const expected = ["c1v2_c2v3m4", "c1v10_c2v3m4", "c1v10_c2v20m30"];
+ expect(sortAcessors(input)).toEqual(expected);
+ });
+});
diff --git a/web-common/src/features/dashboards/pivot/types.ts b/web-common/src/features/dashboards/pivot/types.ts
index 2269e94c7ae..12422b31f5f 100644
--- a/web-common/src/features/dashboards/pivot/types.ts
+++ b/web-common/src/features/dashboards/pivot/types.ts
@@ -53,6 +53,8 @@ export interface PivotDataRow {
export interface TimeFilters {
timeStart: string;
interval: V1TimeGrain;
+ // Time end represents the start time of the last interval for a range
+ timeEnd?: string;
}
export interface PivotTimeConfig {