Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { useMemo, useState, useCallback, useEffect, useRef } from 'react';
Expand All @@ -22,6 +22,7 @@ import {
getSortValue,
gradientBg,
itemHasId,
numericSortingFn,
} from './utils';
import type {
GroupedDataTableGroupRenderMode,
Expand Down Expand Up @@ -382,8 +383,10 @@ export function PivotedStatTable<TRow>({
for (const row of visiblePivotedRows) {
const v = getSortValue(row, stat, isAggregating, aggMode);
if (v !== null) {
if (v < min) min = v;
if (v > max) max = v;
// Convert to number for gradient color math — precision loss is acceptable here
const n = Number(v);
if (n < min) min = n;
if (n > max) max = n;
}
}
if (min !== Infinity) ranges.set(stat, { min, max });
Expand Down Expand Up @@ -566,6 +569,7 @@ export function PivotedStatTable<TRow>({
header: stat,
enableSorting: true,
sortUndefined: 'last',
sortingFn: numericSortingFn,
accessorFn: (row: PivotedRow) => getSortValue(row, stat, isAggregating, aggMode) ?? undefined,
}));
return [...groupCols, ...statCols];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect } from 'vitest';
Expand Down Expand Up @@ -163,10 +163,10 @@ describe('buildPivotedRows row clustering', () => {
];
const out = buildPivotedRows(rows, [brandIdx, fuelIdx], true);
const agg = out[0]?.aggs.get('output_rows');
expect(agg?.sum).toBe(4000);
expect(agg?.sum).toBe(4000n);
expect(agg?.mean).toBe(2000);
expect(agg?.min).toBe(1000);
expect(agg?.max).toBe(3000);
expect(agg?.min).toBe(1000n);
expect(agg?.max).toBe(3000n);
expect(agg?.count).toBe(2);
expect(agg?.isNumeric).toBe(true);
});
Expand Down
8 changes: 4 additions & 4 deletions ui/packages/@quent/components/src/pivot-table/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { StatValue, ContinuousPaletteName } from '@quent/utils';
Expand Down Expand Up @@ -137,10 +137,10 @@ export interface GroupKeyEntry {
}

export interface PivotedRowAgg {
sum: number | null;
sum: number | bigint | null;
mean: number | null;
min: number | null;
max: number | null;
min: number | bigint | null;
max: number | bigint | null;
stdev: number | null;
count: number;
isNumeric: boolean;
Expand Down
47 changes: 31 additions & 16 deletions ui/packages/@quent/components/src/pivot-table/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { inferFieldFormatter, isNumericValue } from '@quent/utils';
import type { StatValue, ContinuousPaletteName } from '@quent/utils';
import { continuousColor } from '@quent/utils';
import type { SortingFn } from '@tanstack/react-table';

// Re-exported for consumers that still import it from here; defined in `@quent/utils`
export { isNumericValue };
Expand Down Expand Up @@ -32,6 +33,16 @@ export function formatNumericStat(n: number | bigint | null, statName: string):
return inferFieldFormatter(statName)(n);
}

export const numericSortingFn: SortingFn<PivotedRow> = (rowA, rowB, columnId) => {
const a = rowA.getValue<number | bigint | undefined>(columnId);
const b = rowB.getValue<number | bigint | undefined>(columnId);
if (a === b) return 0;
if (a == null) return 1;
if (b == null) return -1;
if (typeof a === typeof b) return a < b ? -1 : 1;
return (a as number) < (b as number) ? -1 : 1;
};

/**
* Returns true when any id in `items` is present in `target`. Equivalent to
* `[...items].some(id => target.has(id))` but without allocating an
Expand All @@ -56,14 +67,17 @@ export function formatStatValue(value: StatValue, statName: string): string {
// --- color gradient ---

export function gradientBg(
value: number,
min: number,
max: number,
value: number | bigint,
min: number | bigint,
max: number | bigint,
palette: ContinuousPaletteName = 'blue',
darkMode = false
): string | undefined {
if (min === max) return undefined;
const t = (value - min) / (max - min);
const vn = Number(value);
const mn = Number(min);
const mx = Number(max);
if (mn === mx) return undefined;
const t = (vn - mn) / (mx - mn);
return continuousColor(t, palette, darkMode);
}

Expand Down Expand Up @@ -169,11 +183,11 @@ export function getSortValue(
stat: string,
isAgg: boolean,
aggMode: AggMode
): number | null {
): number | bigint | null {
if (!isAgg) {
const v = row.values.get(stat);
if (v === undefined) return null;
return isNumericValue(v) ? Number(v) : null;
return isNumericValue(v) ? v : null;
}
const agg = row.aggs.get(stat);
if (!agg || !agg.isNumeric) return null;
Expand Down Expand Up @@ -274,24 +288,25 @@ export function buildPivotedRows(
: [...bucket.nums, ...bucket.bigints.map(Number)];
const hasNum = allNums.length > 0;

let sum: number | null = null;
let min: number | null = null;
let max: number | null = null;
let sum: number | bigint | null = null;
let min: number | bigint | null = null;
let max: number | bigint | null = null;
let mean: number | null = null;
let stdev: number | null = null;

if (hasNum) {
if (onlyBigints) {
// Use bigint arithmetic for sum/min/max to avoid precision loss
sum = Number(bucket.bigints.reduce((a, b) => a + b, 0n));
min = Number(bucket.bigints.reduce((a, b) => (a < b ? a : b)));
max = Number(bucket.bigints.reduce((a, b) => (a > b ? a : b)));
// Use bigint arithmetic for sum/min/max
sum = bucket.bigints.reduce((a, b) => a + b, 0n);
min = bucket.bigints.reduce((a, b) => (a < b ? a : b));
max = bucket.bigints.reduce((a, b) => (a > b ? a : b));
mean = Number(sum) / bucket.bigints.length;
} else {
sum = allNums.reduce((a, b) => a + b, 0);
min = Math.min(...allNums);
max = Math.max(...allNums);
mean = sum / allNums.length;
}
mean = sum / allNums.length;
if (allNums.length > 1) {
const variance =
allNums.reduce((acc, v) => acc + (v - mean!) ** 2, 0) / (allNums.length - 1);
Expand Down