From acaaa65bf746adf8647705a5cef9cf42bc196120 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Fri, 28 Aug 2026 11:18:54 +0800 Subject: [PATCH 1/2] feat(table-core): add mode aggregation function Fixes #5864 Signed-off-by: Liang Xu --- .../row-aggregation/aggregationFns.ts | 34 +++++++++++++++++++ .../tests/unit/fns/aggregationFns.test.ts | 17 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/packages/table-core/src/features/row-aggregation/aggregationFns.ts b/packages/table-core/src/features/row-aggregation/aggregationFns.ts index 22fa2e716f..524dd2808a 100755 --- a/packages/table-core/src/features/row-aggregation/aggregationFns.ts +++ b/packages/table-core/src/features/row-aggregation/aggregationFns.ts @@ -282,6 +282,39 @@ export const aggregationFn_median = constructAggregationFn< }, }) +/** + * Computes the statistical mode (most frequent value) of the row values. + * If multiple values have the same maximum frequency, returns the first one encountered. + * Returns `undefined` when no rows are present. + */ +export const aggregationFn_mode = constructAggregationFn< + any, + any, + unknown, + unknown +>({ + aggregate: (context) => { + const rows = context.rows + if (!rows.length) return undefined + + let maxCount = 0 + let modeValue: unknown = undefined + const counts = new Map() + + for (let i = 0; i < rows.length; i++) { + const value = context.getValue(rows[i]!) + const count = (counts.get(value) ?? 0) + 1 + counts.set(value, count) + if (count > maxCount) { + maxCount = count + modeValue = value + } + } + + return modeValue + }, +}) + /** Collects distinct row values using JavaScript `Set` semantics. */ export const aggregationFn_unique = constructAggregationFn< any, @@ -373,6 +406,7 @@ export const aggregationFns = { extent: aggregationFn_extent, mean: aggregationFn_mean, median: aggregationFn_median, + mode: aggregationFn_mode, unique: aggregationFn_unique, uniqueCount: aggregationFn_uniqueCount, count: aggregationFn_count, diff --git a/packages/table-core/tests/unit/fns/aggregationFns.test.ts b/packages/table-core/tests/unit/fns/aggregationFns.test.ts index 44e125ad67..0770488be0 100644 --- a/packages/table-core/tests/unit/fns/aggregationFns.test.ts +++ b/packages/table-core/tests/unit/fns/aggregationFns.test.ts @@ -8,6 +8,7 @@ import { aggregationFn_mean, aggregationFn_median, aggregationFn_min, + aggregationFn_mode, aggregationFn_sum, aggregationFn_unique, aggregationFn_uniqueCount, @@ -119,6 +120,22 @@ describe('aggregation function definitions', () => { ).toBeUndefined() }) + it('calculates mode and returns first encountered value on tie', () => { + expect( + aggregationFn_mode.aggregate(context(['a', 'b', 'a', 'c', 'b', 'a'])), + ).toBe('a') + expect( + aggregationFn_mode.aggregate(context(['a', 'b', 'a', 'b', 'c'])), + ).toBe('a') + expect( + aggregationFn_mode.aggregate(context([1, 2, 2, 3, 2, 1])), + ).toBe(2) + expect( + aggregationFn_mode.aggregate(context([null, undefined, null, 'x'])), + ).toBeNull() + expect(aggregationFn_mode.aggregate(context([]))).toBeUndefined() + }) + it('preserves custom definition result inference', () => { const joined = constructAggregationFn({ aggregate: ({ rows }) => rows.map((row) => row.id).join(','), From 6f7648948c6a30773004da0fa06107303c1f645c Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Sat, 5 Sep 2026 16:32:37 +0800 Subject: [PATCH 2/2] fix(table-core): preserve first-encountered tie breaking in mode aggregation Update mode aggregation to ensure the first-encountered value is returned when multiple values share the highest frequency, and add test case for ['a', 'b', 'b', 'a']. --- .changeset/neat-rows-mode.md | 5 +++++ .../src/features/row-aggregation/aggregationFns.ts | 13 +++++++++---- .../tests/unit/fns/aggregationFns.test.ts | 7 ++++--- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 .changeset/neat-rows-mode.md diff --git a/.changeset/neat-rows-mode.md b/.changeset/neat-rows-mode.md new file mode 100644 index 0000000000..d325c336aa --- /dev/null +++ b/.changeset/neat-rows-mode.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': minor +--- + +Add `mode` aggregation function to `aggregationFns` to compute statistical mode with first-encountered tie breaking. diff --git a/packages/table-core/src/features/row-aggregation/aggregationFns.ts b/packages/table-core/src/features/row-aggregation/aggregationFns.ts index 524dd2808a..c77e65bf3d 100755 --- a/packages/table-core/src/features/row-aggregation/aggregationFns.ts +++ b/packages/table-core/src/features/row-aggregation/aggregationFns.ts @@ -297,9 +297,8 @@ export const aggregationFn_mode = constructAggregationFn< const rows = context.rows if (!rows.length) return undefined - let maxCount = 0 - let modeValue: unknown = undefined const counts = new Map() + let maxCount = 0 for (let i = 0; i < rows.length; i++) { const value = context.getValue(rows[i]!) @@ -307,11 +306,17 @@ export const aggregationFn_mode = constructAggregationFn< counts.set(value, count) if (count > maxCount) { maxCount = count - modeValue = value } } - return modeValue + for (let i = 0; i < rows.length; i++) { + const value = context.getValue(rows[i]!) + if (counts.get(value) === maxCount) { + return value + } + } + + return undefined }, }) diff --git a/packages/table-core/tests/unit/fns/aggregationFns.test.ts b/packages/table-core/tests/unit/fns/aggregationFns.test.ts index 0770488be0..95614c5c0c 100644 --- a/packages/table-core/tests/unit/fns/aggregationFns.test.ts +++ b/packages/table-core/tests/unit/fns/aggregationFns.test.ts @@ -121,15 +121,16 @@ describe('aggregation function definitions', () => { }) it('calculates mode and returns first encountered value on tie', () => { + expect(aggregationFn_mode.aggregate(context(['a', 'b', 'b', 'a']))).toBe( + 'a', + ) expect( aggregationFn_mode.aggregate(context(['a', 'b', 'a', 'c', 'b', 'a'])), ).toBe('a') expect( aggregationFn_mode.aggregate(context(['a', 'b', 'a', 'b', 'c'])), ).toBe('a') - expect( - aggregationFn_mode.aggregate(context([1, 2, 2, 3, 2, 1])), - ).toBe(2) + expect(aggregationFn_mode.aggregate(context([1, 2, 2, 3, 2, 1]))).toBe(2) expect( aggregationFn_mode.aggregate(context([null, undefined, null, 'x'])), ).toBeNull()