From 09925c791ea9180405de3cf070fd1e9478b149f9 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 13:22:20 -0500 Subject: [PATCH 01/15] Add tests to test revised functionality --- .../@quent/utils/src/formatters.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ui/packages/@quent/utils/src/formatters.test.ts b/ui/packages/@quent/utils/src/formatters.test.ts index 7dd2a875b..1f8a421e5 100644 --- a/ui/packages/@quent/utils/src/formatters.test.ts +++ b/ui/packages/@quent/utils/src/formatters.test.ts @@ -252,6 +252,32 @@ describe('formatWithPrefix (Iec)', () => { }); }); +// --------------------------------------------------------------------------- +// formatWithPrefix — bigint values +// --------------------------------------------------------------------------- + +describe('formatWithPrefix (bigint)', () => { + it('formats zero bigint', () => { + expect(formatWithPrefix(0n, 'B', 'Iec')).toBe('0 B'); + expect(formatWithPrefix(0n, '', 'None')).toBe('0'); + }); + + it('scales positive bigints like numbers', () => { + expect(formatWithPrefix(1500n, 'Hz', 'Si')).toBe('1.5 kHz'); + expect(formatWithPrefix(1024n, 'B', 'Iec')).toBe('1.0 KiB'); + expect(formatWithPrefix(1073741824n, 'B', 'Iec')).toBe('1.0 GiB'); + }); + + it('handles negative bigints', () => { + expect(formatWithPrefix(-1500n, 'Hz', 'Si')).toBe('-1.5 kHz'); + expect(formatWithPrefix(-1024n, 'B', 'Iec')).toBe('-1.0 KiB'); + }); + + it('formats bigints with the None prefix system', () => { + expect(formatWithPrefix(42n, 'Hz', 'None')).toBe('42.0 Hz'); + }); +}); + // --------------------------------------------------------------------------- // formatBytes // --------------------------------------------------------------------------- @@ -421,6 +447,12 @@ describe('inferFieldFormatter', () => { expect(inferFieldFormatter('custom_stat')(42)).toBe('42'); expect(inferFieldFormatter('custom_stat')(3.14159)).toBe('3.1416'); }); + + it('accepts bigint values (large U64/I64 stats)', () => { + expect(inferFieldFormatter('spill_bytes')(1073741824n)).toBe('1.00 GiB'); + expect(inferFieldFormatter('output_rows')(1500n)).toBe('1.50 k'); + expect(inferFieldFormatter('custom_stat')(42n)).toBe('42'); + }); }); // --------------------------------------------------------------------------- From 6763e3e88361c4e7383c39b6023a96b668a06a71 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 13:23:02 -0500 Subject: [PATCH 02/15] Ensure bigints hit field formatter --- ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx index 3ec24e0b8..27c9ae620 100644 --- a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx +++ b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx @@ -73,7 +73,9 @@ export const DAGNodeInfoPanel = () => {
{key.replace(/_/g, ' ')}: - {typeof value === 'number' ? inferFieldFormatter(key)(value) : String(value)} + {typeof value === 'number' || typeof value === 'bigint' + ? inferFieldFormatter(key)(value) + : String(value)}
)} From baa31dbccad4b426179ef6fe69eb5dc72cc57c88 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 13:23:39 -0500 Subject: [PATCH 03/15] Ensure bigints hit field formatter here as well --- ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx b/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx index a27c49eb0..3ca645fef 100644 --- a/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx +++ b/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx @@ -117,7 +117,7 @@ export const QueryPlanNode = memo(({ data }: { data: QueryPlanNodeData }) => { const formattedColorFieldValue = colorFieldValue === null ? null - : typeof colorFieldValue === 'number' + : typeof colorFieldValue === 'number' || typeof colorFieldValue === 'bigint' ? inferFieldFormatter(colorField!)(colorFieldValue) : String(colorFieldValue); From 2b656edf68bdc79195715d7d2c4c233747201236 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 13:38:35 -0500 Subject: [PATCH 04/15] Update formatting functions to handle bigints --- ui/packages/@quent/utils/src/dagTypes.ts | 2 +- ui/packages/@quent/utils/src/formatters.ts | 31 +++++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/ui/packages/@quent/utils/src/dagTypes.ts b/ui/packages/@quent/utils/src/dagTypes.ts index f7df56e9f..471a12482 100644 --- a/ui/packages/@quent/utils/src/dagTypes.ts +++ b/ui/packages/@quent/utils/src/dagTypes.ts @@ -59,7 +59,7 @@ export const DAG_LAYOUT_DIRECTION = { export type DagLayoutDirection = (typeof DAG_LAYOUT_DIRECTION)[keyof typeof DAG_LAYOUT_DIRECTION]; -export type StatValue = string | number | boolean | null | string[]; +export type StatValue = string | number | bigint | boolean | null | string[]; export interface DAGNode { id: string; diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index 205c7179b..c3f750a10 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -113,15 +113,17 @@ const IEC: readonly [number, string][] = [ ]; export function formatWithPrefix( - value: number, + value: number | bigint, symbol: string, prefixSystem: PrefixSystem, decimals: number = 1 ): string { - if (value === 0) return symbol ? `0 ${symbol}` : '0'; + const num = typeof value === 'bigint' ? Number(value) : value; + + if (num === 0) return symbol ? `0 ${symbol}` : '0'; - const abs = value < 0 ? -value : value; - const sign = value < 0 ? '-' : ''; + const abs = num < 0 ? -num : num; + const sign = num < 0 ? '-' : ''; if (prefixSystem === 'None') { return symbol ? `${sign}${abs.toFixed(decimals)} ${symbol}` : `${sign}${abs.toFixed(decimals)}`; @@ -270,20 +272,25 @@ function formatSiCount(value: number, decimals = 2): string { * Infer a numeric display formatter from a statistic/field name (DAG labels, pivot cells, legends). * Order: duration (ns) → bytes → row/batch counts → throughput → ratios → default table number. */ -export function inferFieldFormatter(fieldName: string): (value: number) => string { - if (fieldName.endsWith('_ns')) return v => formatDuration(v / 1e6); - if (isBytesStat(fieldName)) return v => formatBytes(v, 2); - if (isCountStat(fieldName)) return v => formatSiCount(v, 2); - if (fieldName.endsWith('_mbs')) return v => `${v.toFixed(1)} MB/s`; - if ( +export function inferFieldFormatter(fieldName: string): (value: number | bigint) => string { + let format: (value: number) => string; + if (fieldName.endsWith('_ns')) format = v => formatDuration(v / 1e6); + else if (isBytesStat(fieldName)) format = v => formatBytes(v, 2); + else if (isCountStat(fieldName)) format = v => formatSiCount(v, 2); + else if (fieldName.endsWith('_mbs')) format = v => `${v.toFixed(1)} MB/s`; + else if ( fieldName.endsWith('_ratio') || fieldName.endsWith('_fraction') || fieldName.endsWith('_fpr') || fieldName.endsWith('_selectivity') || fieldName.endsWith('_rate') ) - return v => `${(v * 100).toFixed(1)}%`; - return v => formatNumberWithMaxFractionDigits(v, 4); + format = v => `${(v * 100).toFixed(1)}%`; + else format = v => formatNumberWithMaxFractionDigits(v, 4); + + // Large U64/I64 stats arrive as bigint (see `parseJsonWithBigInt`); coerce to + // number so the numeric formatters can scale and round them for display + return value => format(typeof value === 'bigint' ? Number(value) : value); } /** From d2988be86155254702ad42cc56d4ae40259ca84d Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 14:49:57 -0500 Subject: [PATCH 05/15] Add bigint support to pivot table helpers for bigint display --- ui/packages/@quent/components/src/pivot-table/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/packages/@quent/components/src/pivot-table/utils.ts b/ui/packages/@quent/components/src/pivot-table/utils.ts index f7411c7b4..ea9ef7731 100644 --- a/ui/packages/@quent/components/src/pivot-table/utils.ts +++ b/ui/packages/@quent/components/src/pivot-table/utils.ts @@ -23,7 +23,7 @@ export interface GroupIndexDef { getLabel: (row: StatGroupExpandedRow) => string; } -export function formatNumericStat(n: number | null, statName: string): string { +export function formatNumericStat(n: number | bigint | null, statName: string): string { if (n === null) return '-'; return inferFieldFormatter(statName)(n); } @@ -43,14 +43,14 @@ export function itemHasId(items: Iterable, target: ReadonlySet): export function formatStatValue(value: StatValue, statName: string): string { if (value === null || value === undefined) return '-'; - if (typeof value === 'number') return formatNumericStat(value, statName); + if (typeof value === 'number' || typeof value === 'bigint') return formatNumericStat(value, statName); if (typeof value === 'boolean') return value ? 'true' : 'false'; if (Array.isArray(value)) return value.join(', '); return String(value); } export function isNumericValue(v: StatValue): v is number { - return typeof v === 'number'; + return typeof v === 'number' || typeof v === bigint; } // --- color gradient --- From b96e14b332aa0bd90814a5160a1b634017d77973 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 16:20:54 -0500 Subject: [PATCH 06/15] Add tests for bigints --- .../src/pivot-table/buildPivotedRows.test.ts | 21 ++++++++++++- .../query-plan/dagFieldProcessing.test.ts | 31 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts b/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts index e0ac317bf..e5aeeb77c 100644 --- a/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts +++ b/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts @@ -9,7 +9,7 @@ function expanded( groups: Record, itemId: string, statisticName: string, - value: number | string | null + value: number | bigint | string | null ): StatGroupExpandedRow { const normalized: Record = {}; for (const [k, v] of Object.entries(groups)) { @@ -156,6 +156,25 @@ describe('buildPivotedRows row clustering', () => { expect(fordHybrid?.aggs.get('price')?.count).toBe(2); }); + it('aggregates bigint stat values (large U64/I64) without throwing', () => { + // Regression guard: bigints are numeric, so they must be included in + // aggregation. Because the sum/min/max math runs on number[], each value is + // coerced with Number() — mixing a bigint with the `0` seed would otherwise + // throw "Cannot mix BigInt and other types". + const rows: StatGroupExpandedRow[] = [ + expanded({ brand: { id: 'Ford' }, fuel: { id: 'Hybrid' } }, 'car-1', 'output_rows', 1000n), + expanded({ brand: { id: 'Ford' }, fuel: { id: 'Hybrid' } }, 'car-2', 'output_rows', 3000n), + ]; + const out = buildPivotedRows(rows, [brandIdx, fuelIdx], true); + const agg = out[0]?.aggs.get('output_rows'); + expect(agg?.sum).toBe(4000); + expect(agg?.mean).toBe(2000); + expect(agg?.min).toBe(1000); + expect(agg?.max).toBe(3000); + expect(agg?.count).toBe(2); + expect(agg?.isNumeric).toBe(true); + }); + it('preserves first-appearance order across both columns of the hierarchy', () => { // `buildPivotedRows` merges rows that share a full group-key path, so // duplicate (brand, fuel) pairs collapse to one output row. What we diff --git a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts index 16d4b1125..b7446f61a 100644 --- a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts +++ b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts @@ -156,6 +156,24 @@ describe('computeEdgeColoring', () => { expect(result.values.get('e2')).toBe(40); }); + it('returns continuous coloring for bigint values (coerced to number)', () => { + // Large U64/I64 port stats arrive as bigint; they are numeric and must + // still produce a continuous scale rather than degrading to categorical. + const edges = [ + makeEdge('e1', [{ key: 'rows', value: 10n }]), + makeEdge('e2', [{ key: 'rows', value: 40n }]), + ]; + const result = computeEdgeColoring(edges, 'rows', 'light'); + expect(result?.type).toBe('continuous'); + if (result?.type !== 'continuous') return; + expect(result.min).toBe(10); + expect(result.max).toBe(40); + expect(result.values.get('e1')).toBe(10); + expect(result.values.get('e2')).toBe(40); + // Coerced to number, not left as bigint. + expect(typeof result.values.get('e1')).toBe('number'); + }); + it('returns categorical coloring for string values', () => { const edges = [ makeEdge('e1', [{ key: 'type', value: 'hash' }]), @@ -231,6 +249,19 @@ describe('computeEdgeWidthConfig', () => { expect(result!.values.get('e3')).toBe(12); }); + it('coerces bigint values (large U64/I64) into the number width scale', () => { + const edges = [ + makeEdge('e1', [{ key: 'rows', value: 5n }]), + makeEdge('e2', [{ key: 'rows', value: 20n }]), + ]; + const result = computeEdgeWidthConfig(edges, 'rows'); + expect(result).not.toBeNull(); + expect(result!.min).toBe(5); + expect(result!.max).toBe(20); + expect(result!.values.get('e1')).toBe(5); + expect(typeof result!.values.get('e1')).toBe('number'); + }); + it('returns equal min/max for a single edge', () => { const edges = [makeEdge('e1', [{ key: 'rows', value: 7 }])]; const result = computeEdgeWidthConfig(edges, 'rows'); From c7e14bce65f8acdd1442b9f17096ea404ec442f6 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 16:24:11 -0500 Subject: [PATCH 07/15] Add bigint tests --- .../components/src/pivot-table/utils.test.ts | 10 ++++++++++ ui/packages/@quent/utils/src/formatters.test.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ui/packages/@quent/components/src/pivot-table/utils.test.ts b/ui/packages/@quent/components/src/pivot-table/utils.test.ts index 2cd8d2901..950e44aa2 100644 --- a/ui/packages/@quent/components/src/pivot-table/utils.test.ts +++ b/ui/packages/@quent/components/src/pivot-table/utils.test.ts @@ -161,6 +161,12 @@ describe('formatStatValue', () => { it('converts a string value using String()', () => { expect(formatStatValue('hello', 'label')).toBe('hello'); }); + + it('formats a bigint value (large U64/I64 stat) via the field formatter', () => { + // Byte fields scale; a raw String() would show unformatted digits. + expect(formatStatValue(1073741824n, 'spill_bytes')).toBe('1.00 GiB'); + expect(formatStatValue(1500n, 'output_rows')).toBe('1.50 k'); + }); }); // ---- isNumericValue -------------------------------------------------------- @@ -170,6 +176,10 @@ describe('isNumericValue', () => { expect(isNumericValue(42)).toBe(true); }); + it('returns true for a bigint', () => { + expect(isNumericValue(42n)).toBe(true); + }); + it('returns false for null', () => { expect(isNumericValue(null)).toBe(false); }); diff --git a/ui/packages/@quent/utils/src/formatters.test.ts b/ui/packages/@quent/utils/src/formatters.test.ts index 1f8a421e5..32435c250 100644 --- a/ui/packages/@quent/utils/src/formatters.test.ts +++ b/ui/packages/@quent/utils/src/formatters.test.ts @@ -17,6 +17,7 @@ import { unwrapTaggedValue, formatAttributeValue, isBytesRateStat, + isNumericValue, } from './formatters'; import type { QuantitySpec } from './types/index'; @@ -524,6 +525,21 @@ describe('unwrapTaggedValue', () => { }); }); +describe('isNumericValue', () => { + it('accepts numbers and bigints', () => { + expect(isNumericValue(42)).toBe(true); + expect(isNumericValue(0)).toBe(true); + expect(isNumericValue(42n)).toBe(true); + }); + + it('rejects non-numeric StatValue members', () => { + expect(isNumericValue('42')).toBe(false); + expect(isNumericValue(true)).toBe(false); + expect(isNumericValue(null)).toBe(false); + expect(isNumericValue(['1', '2'])).toBe(false); + }); +}); + describe('formatAttributeValue', () => { it('byte-formats bytes-like keys', () => { expect(formatAttributeValue('input_bytes', { U64: 1073741824 })).toBe('1.00 GiB'); From 21a16248635b3c3929112808a51f7bf060fa7086 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Wed, 15 Jul 2026 16:36:58 -0500 Subject: [PATCH 08/15] Refactor to use helper func over repeated logic, cleanup --- .../components/src/dag/DAGNodeInfoPanel.tsx | 6 ++---- .../src/pivot-table/buildPivotedRows.test.ts | 4 ---- .../components/src/pivot-table/utils.test.ts | 1 - .../@quent/components/src/pivot-table/utils.ts | 17 +++++++++-------- .../components/src/query-plan/QueryPlanNode.tsx | 4 ++-- .../query-plan/dagFieldProcessing.test.ts | 4 +--- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx index 27c9ae620..5105377aa 100644 --- a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx +++ b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx @@ -6,7 +6,7 @@ import { ChevronUp, ChevronDown } from 'lucide-react'; import { useSelectedNodeData } from '@quent/hooks'; import { DataText } from '../ui/data-text'; import { thinScrollbarClass } from '../ui/thin-scroll'; -import { inferFieldFormatter } from '@quent/utils'; +import { inferFieldFormatter, isNumericValue } from '@quent/utils'; export const DAGNodeInfoPanel = () => { const selectedNodeData = useSelectedNodeData(); @@ -73,9 +73,7 @@ export const DAGNodeInfoPanel = () => {
{key.replace(/_/g, ' ')}: - {typeof value === 'number' || typeof value === 'bigint' - ? inferFieldFormatter(key)(value) - : String(value)} + {isNumericValue(value) ? inferFieldFormatter(key)(value) : String(value)}
)} diff --git a/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts b/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts index e5aeeb77c..7a29806d1 100644 --- a/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts +++ b/ui/packages/@quent/components/src/pivot-table/buildPivotedRows.test.ts @@ -157,10 +157,6 @@ describe('buildPivotedRows row clustering', () => { }); it('aggregates bigint stat values (large U64/I64) without throwing', () => { - // Regression guard: bigints are numeric, so they must be included in - // aggregation. Because the sum/min/max math runs on number[], each value is - // coerced with Number() — mixing a bigint with the `0` seed would otherwise - // throw "Cannot mix BigInt and other types". const rows: StatGroupExpandedRow[] = [ expanded({ brand: { id: 'Ford' }, fuel: { id: 'Hybrid' } }, 'car-1', 'output_rows', 1000n), expanded({ brand: { id: 'Ford' }, fuel: { id: 'Hybrid' } }, 'car-2', 'output_rows', 3000n), diff --git a/ui/packages/@quent/components/src/pivot-table/utils.test.ts b/ui/packages/@quent/components/src/pivot-table/utils.test.ts index 950e44aa2..eb3171488 100644 --- a/ui/packages/@quent/components/src/pivot-table/utils.test.ts +++ b/ui/packages/@quent/components/src/pivot-table/utils.test.ts @@ -163,7 +163,6 @@ describe('formatStatValue', () => { }); it('formats a bigint value (large U64/I64 stat) via the field formatter', () => { - // Byte fields scale; a raw String() would show unformatted digits. expect(formatStatValue(1073741824n, 'spill_bytes')).toBe('1.00 GiB'); expect(formatStatValue(1500n, 'output_rows')).toBe('1.50 k'); }); diff --git a/ui/packages/@quent/components/src/pivot-table/utils.ts b/ui/packages/@quent/components/src/pivot-table/utils.ts index ea9ef7731..3d172753e 100644 --- a/ui/packages/@quent/components/src/pivot-table/utils.ts +++ b/ui/packages/@quent/components/src/pivot-table/utils.ts @@ -1,9 +1,13 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { inferFieldFormatter } from '@quent/utils'; +import { inferFieldFormatter, isNumericValue } from '@quent/utils'; import type { StatValue, ContinuousPaletteName } from '@quent/utils'; import { continuousColor } from '@quent/utils'; + +// Re-exported for consumers that still import it from here; defined in `@quent/utils` +export { isNumericValue }; + import type { StatGroupExpandedRow, GroupKeyEntry, @@ -43,16 +47,12 @@ export function itemHasId(items: Iterable, target: ReadonlySet): export function formatStatValue(value: StatValue, statName: string): string { if (value === null || value === undefined) return '-'; - if (typeof value === 'number' || typeof value === 'bigint') return formatNumericStat(value, statName); + if (isNumericValue(value)) return formatNumericStat(value, statName); if (typeof value === 'boolean') return value ? 'true' : 'false'; if (Array.isArray(value)) return value.join(', '); return String(value); } -export function isNumericValue(v: StatValue): v is number { - return typeof v === 'number' || typeof v === bigint; -} - // --- color gradient --- export function gradientBg( @@ -173,7 +173,7 @@ export function getSortValue( if (!isAgg) { const v = row.values.get(stat); if (v === undefined) return null; - return isNumericValue(v) ? v : null; + return isNumericValue(v) ? Number(v) : null; } const agg = row.aggs.get(stat); if (!agg || !agg.isNumeric) return null; @@ -256,7 +256,8 @@ export function buildPivotedRows( } bucket.count++; if (isNumericValue(row.value)) { - bucket.nums.push(row.value); + // coerce potential bigints + bucket.nums.push(Number(row.value)); } } } diff --git a/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx b/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx index 3ca645fef..1675c53cf 100644 --- a/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx +++ b/ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx @@ -26,7 +26,7 @@ import { useSetHighlightedNodeIds, } from '@quent/hooks'; import { parseCustomStatistics } from '../lib/queryBundle.utils'; -import { inferFieldFormatter } from '@quent/utils'; +import { inferFieldFormatter, isNumericValue } from '@quent/utils'; import { DataText } from '../ui/data-text'; export interface QueryPlanNodeData extends Record { @@ -117,7 +117,7 @@ export const QueryPlanNode = memo(({ data }: { data: QueryPlanNodeData }) => { const formattedColorFieldValue = colorFieldValue === null ? null - : typeof colorFieldValue === 'number' || typeof colorFieldValue === 'bigint' + : isNumericValue(colorFieldValue) ? inferFieldFormatter(colorField!)(colorFieldValue) : String(colorFieldValue); diff --git a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts index b7446f61a..356892e9a 100644 --- a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts +++ b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.test.ts @@ -157,8 +157,7 @@ describe('computeEdgeColoring', () => { }); it('returns continuous coloring for bigint values (coerced to number)', () => { - // Large U64/I64 port stats arrive as bigint; they are numeric and must - // still produce a continuous scale rather than degrading to categorical. + // Bigint stats stay continuous (coerced to number), not categorical. const edges = [ makeEdge('e1', [{ key: 'rows', value: 10n }]), makeEdge('e2', [{ key: 'rows', value: 40n }]), @@ -170,7 +169,6 @@ describe('computeEdgeColoring', () => { expect(result.max).toBe(40); expect(result.values.get('e1')).toBe(10); expect(result.values.get('e2')).toBe(40); - // Coerced to number, not left as bigint. expect(typeof result.values.get('e1')).toBe('number'); }); From 8d7928b9315c99a5d56591742605734ff26346b1 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Thu, 16 Jul 2026 09:27:58 -0500 Subject: [PATCH 09/15] Allow for coloring of bigint values if selected from dag display settings --- .../services/query-plan/dagFieldProcessing.ts | 20 ++++++++++--------- ui/packages/@quent/utils/src/formatters.ts | 6 ++++-- ui/packages/@quent/utils/src/index.ts | 1 + 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.ts b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.ts index 660a477c7..4dd2202fa 100644 --- a/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.ts +++ b/ui/packages/@quent/components/src/services/query-plan/dagFieldProcessing.ts @@ -3,7 +3,7 @@ import type { DAGNode, DAGEdge, NodeColoring, EdgeWidthConfig, EdgeColoring } from './types'; import { parseCustomStatistics } from '../../lib/queryBundle.utils'; -import { getActivePalette, type PaletteTheme } from '@quent/utils'; +import { getActivePalette, isNumericValue, type PaletteTheme } from '@quent/utils'; export function computeNodeColoring( nodes: DAGNode[], @@ -19,11 +19,12 @@ export function computeNodeColoring( }); if (!entries.length) return null; - if (entries.every(e => typeof e.value === 'number')) { - const nums = entries.map(e => e.value as number); + // handle potential bigints + if (entries.every(e => isNumericValue(e.value))) { + const nums = entries.map(e => Number(e.value)); return { type: 'continuous', - values: new Map(entries.map(e => [e.id, e.value as number])), + values: new Map(entries.map(e => [e.id, Number(e.value)])), min: Math.min(...nums), max: Math.max(...nums), }; @@ -53,11 +54,12 @@ export function computeEdgeColoring( }); if (!entries.length) return null; - if (entries.every(e => typeof e.value === 'number')) { - const nums = entries.map(e => e.value as number); + // handle potential bigints + if (entries.every(e => isNumericValue(e.value))) { + const nums = entries.map(e => Number(e.value)); return { type: 'continuous', - values: new Map(entries.map(e => [e.id, e.value as number])), + values: new Map(entries.map(e => [e.id, Number(e.value)])), min: Math.min(...nums), max: Math.max(...nums), }; @@ -79,8 +81,8 @@ export function computeEdgeWidthConfig(edges: DAGEdge[], field: string | null): const entries = edges.flatMap(edge => { const stat = (edge.portStats ?? []).find(s => s.key === field); - if (typeof stat?.value !== 'number') return []; - return [{ id: edge.id, value: stat.value }]; + if (stat?.value == null || !isNumericValue(stat.value)) return []; + return [{ id: edge.id, value: Number(stat.value) }]; }); if (!entries.length) return null; diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index c3f750a10..ad5ec1a02 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -193,6 +193,10 @@ export function isBytesStat(name: string): boolean { ); } +export function isNumericValue(v: StatValue): v is number | bigint { + return typeof v === 'number' || typeof v === 'bigint'; +} + function unwrapToString(val: unknown): string { const result = unwrapTaggedValue(val); return Array.isArray(result) ? result.join('\n') : String(result ?? ''); @@ -288,8 +292,6 @@ export function inferFieldFormatter(fieldName: string): (value: number | bigint) format = v => `${(v * 100).toFixed(1)}%`; else format = v => formatNumberWithMaxFractionDigits(v, 4); - // Large U64/I64 stats arrive as bigint (see `parseJsonWithBigInt`); coerce to - // number so the numeric formatters can scale and round them for display return value => format(typeof value === 'bigint' ? Number(value) : value); } diff --git a/ui/packages/@quent/utils/src/index.ts b/ui/packages/@quent/utils/src/index.ts index 9a811afc2..5694eadeb 100644 --- a/ui/packages/@quent/utils/src/index.ts +++ b/ui/packages/@quent/utils/src/index.ts @@ -41,6 +41,7 @@ export { formatAttributeValue, unwrapTaggedValue, inferFieldFormatter, + isNumericValue, } from './formatters'; // Rust-generated TypeScript types From 585b88183ade8065f7d2f02b831e2e28c0f4d89d Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Thu, 16 Jul 2026 16:08:46 -0500 Subject: [PATCH 10/15] Add isNumericValue check to formatAttributeValue; add tests --- ui/packages/@quent/utils/src/formatters.test.ts | 14 ++++++++++++++ ui/packages/@quent/utils/src/formatters.ts | 9 +++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ui/packages/@quent/utils/src/formatters.test.ts b/ui/packages/@quent/utils/src/formatters.test.ts index 32435c250..9ab9fb363 100644 --- a/ui/packages/@quent/utils/src/formatters.test.ts +++ b/ui/packages/@quent/utils/src/formatters.test.ts @@ -258,6 +258,10 @@ describe('formatWithPrefix (Iec)', () => { // --------------------------------------------------------------------------- describe('formatWithPrefix (bigint)', () => { + it('does not throw on a BigInt (reported repro)', () => { + expect(() => formatWithPrefix(1024n, 'B', 'Iec')).not.toThrow(); + }); + it('formats zero bigint', () => { expect(formatWithPrefix(0n, 'B', 'Iec')).toBe('0 B'); expect(formatWithPrefix(0n, '', 'None')).toBe('0'); @@ -314,6 +318,11 @@ describe('formatNumber', () => { expect(formatNumber(0.001234)).toBe('0.00123'); expect(formatNumber(12345.6)).toBe('12,300'); }); + + it('formats bigints losslessly above Number.MAX_SAFE_INTEGER', () => { + // 9007199254740993 is not representable as a JS number (rounds to ...992). + expect(formatNumber(9007199254740993n)).toBe('9,007,199,254,740,993'); + }); }); // --------------------------------------------------------------------------- @@ -554,6 +563,11 @@ describe('formatAttributeValue', () => { it('renders missing values as a dash', () => { expect(formatAttributeValue('anything', null)).toBe('—'); }); + + it('handles bigint entity-attribute values (large U64/I64)', () => { + expect(formatAttributeValue('input_bytes', 1073741824n)).toBe('1.00 GiB'); + expect(formatAttributeValue('current_operator_id', { U64: 42n })).toBe('42'); + }); }); describe('bytes-rate attribute keys', () => { diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index ad5ec1a02..bf8c8cdd7 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -157,8 +157,9 @@ export function formatWithPrefix( * Integers are formatted with commas (e.g. 1,234,567). * Floats are rounded to 3 significant figures (e.g. 0.00123, 1.23, 12,300). */ -export function formatNumber(value: number): string { - if (Number.isInteger(value)) { +export function formatNumber(value: number | bigint): string { + // bigint formats losslessly via Intl and is always an integer. + if (typeof value === 'bigint' || Number.isInteger(value)) { return new Intl.NumberFormat().format(value); } return new Intl.NumberFormat(undefined, { maximumSignificantDigits: 3 }).format(value); @@ -179,7 +180,7 @@ export function formatNumberWithMaxFractionDigits( return new Intl.NumberFormat(undefined, { maximumFractionDigits }).format(value); } -export function formatBytes(value: number, decimals = 1): string { +export function formatBytes(value: number | bigint, decimals = 1): string { return formatWithPrefix(value, 'B', 'Iec', decimals); } @@ -247,7 +248,7 @@ export function isBytesRateStat(name: string): boolean { export function formatAttributeValue(key: string, value: unknown): string { const v = unwrapTaggedValue(value); if (v == null) return '—'; - if (typeof v === 'number') { + if (isNumericValue(v)) { if (isBytesRateStat(key)) return formatWithPrefix(v, 'B/s', 'Si', 2); if (isBytesStat(key)) return formatBytes(v, 2); return formatNumber(v); From 3134b5bbf85a7809b627ca3e56abb3291b534934 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Tue, 21 Jul 2026 13:16:23 -0500 Subject: [PATCH 11/15] Remove test - no longer useful --- .../@quent/components/src/lib/queryBundle.utils.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ui/packages/@quent/components/src/lib/queryBundle.utils.test.ts b/ui/packages/@quent/components/src/lib/queryBundle.utils.test.ts index 533468781..5efd81047 100644 --- a/ui/packages/@quent/components/src/lib/queryBundle.utils.test.ts +++ b/ui/packages/@quent/components/src/lib/queryBundle.utils.test.ts @@ -95,11 +95,6 @@ describe('parseCustomStatistics', () => { expect(parseCustomStatistics(op)).toEqual([{ key: 'label', value: 'hello' }]); }); - it('unwraps a boolean tagged value', () => { - const op = makeOperator({ enabled: makeTagged('Bool', true) }); - expect(parseCustomStatistics(op)).toEqual([{ key: 'enabled', value: true }]); - }); - it('produces null for a null-valued tagged entry', () => { const op = makeOperator({ missing: null }); expect(parseCustomStatistics(op)).toEqual([{ key: 'missing', value: null }]); From f56f563d8feba9b848b553adaf7ff780271cd93e Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Tue, 21 Jul 2026 13:19:38 -0500 Subject: [PATCH 12/15] Derive StatValue from BE type; avoid BigInt coersion; cast to String for display --- ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx | 2 +- ui/packages/@quent/utils/src/dagTypes.ts | 4 +++- ui/packages/@quent/utils/src/formatters.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx index 5e1a74176..84e0c013f 100644 --- a/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx +++ b/ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx @@ -86,7 +86,7 @@ export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
{value.map((item, i) => ( - {item} + {String(item)} ))}
diff --git a/ui/packages/@quent/utils/src/dagTypes.ts b/ui/packages/@quent/utils/src/dagTypes.ts index 471a12482..6e6090d2e 100644 --- a/ui/packages/@quent/utils/src/dagTypes.ts +++ b/ui/packages/@quent/utils/src/dagTypes.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { DynamicValue } from './types/index'; + // Pure-data types for DAG coloring, width configuration, and node/edge shapes. // These are kept in @quent/utils to avoid circular dependencies between // @quent/hooks (which holds DAG atoms) and @quent/components (which holds DAG rendering). @@ -59,7 +61,7 @@ export const DAG_LAYOUT_DIRECTION = { export type DagLayoutDirection = (typeof DAG_LAYOUT_DIRECTION)[keyof typeof DAG_LAYOUT_DIRECTION]; -export type StatValue = string | number | bigint | boolean | null | string[]; +export type StatValue = DynamicValue | null; export interface DAGNode { id: string; diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index 3e6249585..63f389dd7 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -271,10 +271,10 @@ export function unwrapTaggedValue(val: unknown): StatValue { switch (true) { case val === null || val === undefined: return null; - case typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean': - return val as StatValue; + case typeof val === 'string' || typeof val === 'number': + return val; case typeof val === 'bigint': - return Number(val); + return val; case Array.isArray(val): return (val as unknown[]).map(unwrapToString); case typeof val === 'object': { From f1fa48a9df031b031c4010b1f4b04e7f8003c98d Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Tue, 21 Jul 2026 16:04:32 -0500 Subject: [PATCH 13/15] Handle bigint values when prefix system is off; update tests --- ui/packages/@quent/utils/src/formatters.test.ts | 8 ++++++++ ui/packages/@quent/utils/src/formatters.ts | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ui/packages/@quent/utils/src/formatters.test.ts b/ui/packages/@quent/utils/src/formatters.test.ts index a589f14d1..d62511dc2 100644 --- a/ui/packages/@quent/utils/src/formatters.test.ts +++ b/ui/packages/@quent/utils/src/formatters.test.ts @@ -161,6 +161,10 @@ describe('formatWithPrefix (None)', () => { it('respects decimals', () => { expect(formatWithPrefix(3.14159, 'Hz', 'None', 3)).toBe('3.142 Hz'); }); + + it('preserves unprefixed bigint precision above Number.MAX_SAFE_INTEGER', () => { + expect(formatWithPrefix(9007199254740993n, 'Hz', 'None', 0)).toBe('9007199254740993 Hz'); + }); }); // --------------------------------------------------------------------------- @@ -197,6 +201,10 @@ describe('formatWithPrefix (Si, values >= 1)', () => { it('handles negative values', () => { expect(formatWithPrefix(-1500, 'Hz', 'Si')).toBe('-1.5 kHz'); }); + + it('normalizes a rounded mantissa into the next prefix', () => { + expect(formatWithPrefix(999999999999999n, 'Hz', 'Si', 1)).toBe('1.0 PHz'); + }); }); // --------------------------------------------------------------------------- diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index 63f389dd7..068df2861 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -118,6 +118,15 @@ export function formatWithPrefix( prefixSystem: PrefixSystem, decimals: number = 1 ): string { + // For the unprefixed case with a bigint, avoid Number() coercion to preserve full precision + if (prefixSystem === 'None' && typeof value === 'bigint') { + if (value === 0n) return symbol ? `0 ${symbol}` : '0'; + const absB = value < 0n ? -value : value; + const signB = value < 0n ? '-' : ''; + const str = decimals === 0 ? absB.toString() : `${absB}.${'0'.repeat(decimals)}`; + return symbol ? `${signB}${str} ${symbol}` : `${signB}${str}`; + } + const num = typeof value === 'bigint' ? Number(value) : value; if (num === 0) return symbol ? `0 ${symbol}` : '0'; @@ -141,11 +150,15 @@ export function formatWithPrefix( } const table = prefixSystem === 'Iec' ? IEC : SI_UP; + const roundingFactor = 10 ** decimals; for (let i = 0; i < table.length; i++) { if (abs >= table[i][0]) { const scaled = abs / table[i][0]; - const prefix = table[i][1]; - return `${sign}${scaled.toFixed(decimals)} ${prefix}${symbol}`; + // If floating-point rounding bumps the mantissa into the next prefix, step up + if (i > 0 && Math.round(scaled * roundingFactor) >= (table[i - 1][0] / table[i][0]) * roundingFactor) { + return `${sign}${(abs / table[i - 1][0]).toFixed(decimals)} ${table[i - 1][1]}${symbol}`; + } + return `${sign}${scaled.toFixed(decimals)} ${table[i][1]}${symbol}`; } } const last = table[table.length - 1]; From ec4c3f04ffe4c6024bd4340ddb20820b11dee382 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Mon, 27 Jul 2026 14:59:22 -0500 Subject: [PATCH 14/15] handle byte formatting for potential bigints; fix default case --- ui/packages/@quent/utils/src/formatters.ts | 39 ++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/ui/packages/@quent/utils/src/formatters.ts b/ui/packages/@quent/utils/src/formatters.ts index 068df2861..358b8ed69 100644 --- a/ui/packages/@quent/utils/src/formatters.ts +++ b/ui/packages/@quent/utils/src/formatters.ts @@ -155,7 +155,10 @@ export function formatWithPrefix( if (abs >= table[i][0]) { const scaled = abs / table[i][0]; // If floating-point rounding bumps the mantissa into the next prefix, step up - if (i > 0 && Math.round(scaled * roundingFactor) >= (table[i - 1][0] / table[i][0]) * roundingFactor) { + if ( + i > 0 && + Math.round(scaled * roundingFactor) >= (table[i - 1][0] / table[i][0]) * roundingFactor + ) { return `${sign}${(abs / table[i - 1][0]).toFixed(decimals)} ${table[i - 1][1]}${symbol}`; } return `${sign}${scaled.toFixed(decimals)} ${table[i][1]}${symbol}`; @@ -350,22 +353,24 @@ function formatSiCount(value: number, decimals = 2): string { * Order: duration (ns) → bytes → row/batch counts → throughput → ratios → default table number. */ export function inferFieldFormatter(fieldName: string): (value: number | bigint) => string { - let format: (value: number) => string; - if (fieldName.endsWith('_ns')) format = v => formatDuration(v / 1e6); - else if (isBytesStat(fieldName)) format = v => formatBytes(v, 2); - else if (isCountStat(fieldName)) format = v => formatSiCount(v, 2); - else if (fieldName.endsWith('_mbs')) format = v => `${v.toFixed(1)} MB/s`; - else if ( - fieldName.endsWith('_ratio') || - fieldName.endsWith('_fraction') || - fieldName.endsWith('_fpr') || - fieldName.endsWith('_selectivity') || - fieldName.endsWith('_rate') - ) - format = v => `${(v * 100).toFixed(1)}%`; - else format = v => formatNumberWithMaxFractionDigits(v, 4); - - return value => format(typeof value === 'bigint' ? Number(value) : value); + return (value: number | bigint): string => { + const num = typeof value === 'bigint' ? Number(value) : value; + if (fieldName.endsWith('_ns')) return formatDuration(num / 1e6); + if (isBytesStat(fieldName)) return formatBytes(value, 2); + if (isCountStat(fieldName)) return formatSiCount(num, 2); + if (fieldName.endsWith('_mbs')) return `${num.toFixed(1)} MB/s`; + if ( + fieldName.endsWith('_ratio') || + fieldName.endsWith('_fraction') || + fieldName.endsWith('_fpr') || + fieldName.endsWith('_selectivity') || + fieldName.endsWith('_rate') + ) + return `${(num * 100).toFixed(1)}%`; + return typeof value === 'bigint' + ? formatNumber(value) + : formatNumberWithMaxFractionDigits(num, 4); + }; } /** From d80986bce60e6313380529d86faeac552d80cad2 Mon Sep 17 00:00:00 2001 From: Chris Matzenbach Date: Mon, 27 Jul 2026 15:50:31 -0500 Subject: [PATCH 15/15] Add bigint support for pivot table aggregation --- .../components/src/pivot-table/utils.ts | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/ui/packages/@quent/components/src/pivot-table/utils.ts b/ui/packages/@quent/components/src/pivot-table/utils.ts index 3d172753e..361d18374 100644 --- a/ui/packages/@quent/components/src/pivot-table/utils.ts +++ b/ui/packages/@quent/components/src/pivot-table/utils.ts @@ -197,7 +197,7 @@ type Accumulator = { keys: GroupKeyEntry[]; rowKey: string; values: Map; - aggBuckets: Map; + aggBuckets: Map; itemIds: Set; itemScopeIds: Map; itemType: string; @@ -251,13 +251,14 @@ export function buildPivotedRows( } else { let bucket = group.aggBuckets.get(row.statisticName); if (!bucket) { - bucket = { nums: [], count: 0 }; + bucket = { nums: [], bigints: [], count: 0 }; group.aggBuckets.set(row.statisticName, bucket); } bucket.count++; - if (isNumericValue(row.value)) { - // coerce potential bigints - bucket.nums.push(Number(row.value)); + if (typeof row.value === 'bigint') { + bucket.bigints.push(row.value); + } else if (typeof row.value === 'number') { + bucket.nums.push(row.value); } } } @@ -267,17 +268,37 @@ export function buildPivotedRows( const aggs = new Map(); if (isAggregating) { for (const [stat, bucket] of group.aggBuckets) { - const hasNum = bucket.nums.length > 0; - const sum = hasNum ? bucket.nums.reduce((a, b) => a + b, 0) : null; - const mean = hasNum ? sum! / bucket.nums.length : null; - const min = hasNum ? Math.min(...bucket.nums) : null; - const max = hasNum ? Math.max(...bucket.nums) : null; + const onlyBigints = bucket.bigints.length > 0 && bucket.nums.length === 0; + const allNums = onlyBigints + ? bucket.bigints.map(Number) + : [...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 mean: number | null = null; let stdev: number | null = null; - if (mean !== null && bucket.nums.length > 1) { - const variance = - bucket.nums.reduce((acc, v) => acc + (v - mean) ** 2, 0) / (bucket.nums.length - 1); - stdev = Math.sqrt(variance); + + 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))); + } else { + sum = allNums.reduce((a, b) => a + b, 0); + min = Math.min(...allNums); + max = Math.max(...allNums); + } + mean = sum / allNums.length; + if (allNums.length > 1) { + const variance = + allNums.reduce((acc, v) => acc + (v - mean!) ** 2, 0) / (allNums.length - 1); + stdev = Math.sqrt(variance); + } } + aggs.set(stat, { sum, mean, min, max, stdev, count: bucket.count, isNumeric: hasNum }); } }