Skip to content

feat: PivotTable component (cross-tabulation / pivot table)#592

Merged
hotlong merged 2 commits intomainfrom
copilot/add-pivottable-component
Feb 18, 2026
Merged

feat: PivotTable component (cross-tabulation / pivot table)#592
hotlong merged 2 commits intomainfrom
copilot/add-pivottable-component

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 18, 2026

Adds a PivotTable component rendering rowField × columnField → valueField with configurable aggregation, totals, formatting, and column colors. Integrates as a dashboard widget via type='pivot'.

Types (@object-ui/types)

  • PivotTableSchema interface: rowField, columnField, valueField, aggregation, data, showRowTotals, showColumnTotals, format, columnColors
  • PivotAggregation type: sum | count | avg | min | max

Component (plugin-dashboard/PivotTable.tsx)

  • useMemo-based pivot computation: buckets raw data → aggregates per cell
  • Row/column totals with grand total when both enabled
  • Format string support (prefix, grouping, decimals e.g. $,.2f)
  • columnColors maps column values to Tailwind classes

Dashboard integration

  • Registered with ComponentRegistry as 'pivot'
  • DashboardRenderer handles type='pivot' widget shorthand

Usage

<PivotTable schema={{
  type: 'pivot',
  rowField: 'owner',
  columnField: 'stage',
  valueField: 'amount',
  aggregation: 'sum',
  showRowTotals: true,
  showColumnTotals: true,
  format: '$,.0f',
  data: [
    { owner: 'Alice', stage: 'Discovery', amount: 1000 },
    { owner: 'Bob',   stage: 'Closed',    amount: 5000 },
  ],
}} />

Tests

  • 11 unit tests: aggregation modes, totals, formatting, empty/sparse data
  • All 48 existing dashboard tests still pass

Roadmap

  • Marked as completed under P1.3 Advanced View Features
Original prompt

This section details on the original issue you should resolve

<issue_title>PivotTable 组件(交叉矩阵/透视表)支持</issue_title>
<issue_description>Airtable 仪表盘底部的数据透视表(Owner × Stage → $Amount)ObjectUI 暂未实现。

目标

  • 新增 PivotTable 组件,实现 rowField × columnField → valueField(支持 sum/count/avg 等聚合)
  • 支持 showRowTotals/showColumnTotals、format、columnColors 等高级配置
  • 可作为 dashboard widget 独立渲染
  • 兼容 Spec 协议 type=pivot
  • Storybook 示例 + 单元测试

分析 2.3

  • 类型:新 feature
  • 实现后运行 test & 更新 roadmap</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Error Error Feb 18, 2026 7:59am
objectui-demo Error Error Feb 18, 2026 7:59am
objectui-storybook Error Error Feb 18, 2026 7:59am

Request Review

…ration

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add PivotTable component with aggregation options feat: PivotTable component (cross-tabulation / pivot table) Feb 18, 2026
Copilot AI requested a review from hotlong February 18, 2026 07:59
@hotlong hotlong marked this pull request as ready for review February 18, 2026 08:54
Copilot AI review requested due to automatic review settings February 18, 2026 08:54
@hotlong hotlong merged commit 6dff2bb into main Feb 18, 2026
3 of 6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a PivotTable component to the @object-ui/plugin-dashboard package, enabling cross-tabulation visualization where data is aggregated across two dimensions (rows and columns). The component supports multiple aggregation functions (sum, count, avg, min, max), row and column totals, number formatting, and column-specific color styling.

Changes:

  • Added PivotTableSchema and PivotAggregation types to @object-ui/types
  • Implemented PivotTable component with memoized pivot computation
  • Registered component with ComponentRegistry as 'pivot' widget type
  • Integrated shorthand widget support in DashboardRenderer
  • Added 11 unit tests covering aggregations, totals, formatting, and edge cases
  • Updated ROADMAP.md to mark feature as completed

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/types/src/index.ts Exports new PivotAggregation and PivotTableSchema types
packages/types/src/data-display.ts Defines PivotTableSchema interface with rowField, columnField, valueField, aggregation, totals, format, and columnColors properties
packages/plugin-dashboard/src/index.tsx Registers PivotTable component with ComponentRegistry and exports it from package
packages/plugin-dashboard/src/PivotTable.tsx Implements PivotTable component with pivot computation logic, formatting, and rendering
packages/plugin-dashboard/src/tests/PivotTable.test.tsx Adds 11 unit tests covering aggregation modes, totals, formatting, and edge cases
packages/plugin-dashboard/src/DashboardRenderer.tsx Adds 'pivot' widget shorthand mapping with data extraction pattern
ROADMAP.md Marks PivotTable feature as completed under P1.3 Advanced View Features

Comment on lines +105 to +168
const { rowKeys, colKeys, matrix, rowTotals, colTotals, grandTotal } = useMemo(() => {
// Collect unique row/column values preserving insertion order
const rowSet = new Map<string, true>();
const colSet = new Map<string, true>();
// Bucket raw values: bucket[row][col] = number[]
const bucket: Record<string, Record<string, number[]>> = {};

for (const item of data) {
const r = String(item[rowField] ?? '');
const c = String(item[columnField] ?? '');
const v = Number(item[valueField]) || 0;

rowSet.set(r, true);
colSet.set(c, true);

if (!bucket[r]) bucket[r] = {};
if (!bucket[r][c]) bucket[r][c] = [];
bucket[r][c].push(v);
}

const rKeys = Array.from(rowSet.keys());
const cKeys = Array.from(colSet.keys());

// Build aggregated matrix
const mat: Record<string, Record<string, number>> = {};
const rTotals: Record<string, number> = {};
const cTotals: Record<string, number> = {};

for (const r of rKeys) {
mat[r] = {};
const rowValues: number[] = [];
for (const c of cKeys) {
const cellValues = bucket[r]?.[c] ?? [];
const cellAgg = aggregate(cellValues, aggregation);
mat[r][c] = cellAgg;
rowValues.push(...cellValues);

// Accumulate column bucket values for column totals
if (!cTotals[c] && cTotals[c] !== 0) {
// Will compute after
}
}
rTotals[r] = aggregate(rowValues, aggregation);
}

// Column totals
for (const c of cKeys) {
const colValues: number[] = [];
for (const r of rKeys) {
const cellValues = bucket[r]?.[c] ?? [];
colValues.push(...cellValues);
}
cTotals[c] = aggregate(colValues, aggregation);
}

// Grand total
const allValues: number[] = [];
for (const item of data) {
allValues.push(Number(item[valueField]) || 0);
}
const gt = aggregate(allValues, aggregation);

return { rowKeys: rKeys, colKeys: cKeys, matrix: mat, rowTotals: rTotals, colTotals: cTotals, grandTotal: gt };
}, [data, rowField, columnField, valueField, aggregation]);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pivot computation has O(n*m) complexity where n is the number of data rows and m is the number of unique row/column combinations. While this is acceptable for typical dashboard use cases, consider documenting performance characteristics or adding a warning if the dataset is very large (e.g., > 10,000 rows). The computation is already memoized which helps, but users should be aware of the computational cost for large datasets.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
import { PivotTable } from './PivotTable';

export { DashboardRenderer, DashboardGridLayout, MetricWidget, MetricCard };
export { DashboardRenderer, DashboardGridLayout, MetricWidget, MetricCard, PivotTable };
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Guideline #2 (Documentation Driven Development), documentation must be updated for every feature. The plugin-dashboard README and content/docs/plugins/plugin-dashboard.mdx should document the new PivotTable component with schema API details and usage examples. Additionally, a Storybook story should be added (either a new PivotTable.stories.tsx or an example in DashboardRenderer.stories.tsx) demonstrating the pivot table with different configurations.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +141 to +145

// Accumulate column bucket values for column totals
if (!cTotals[c] && cTotals[c] !== 0) {
// Will compute after
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code detected: lines 143-145 check a condition but do nothing. This code appears to be a leftover from development and should be removed as it serves no purpose and makes the code harder to understand.

Suggested change
// Accumulate column bucket values for column totals
if (!cTotals[c] && cTotals[c] !== 0) {
// Will compute after
}

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +35
// comma inside the prefix-ish area means grouping, not a literal prefix
const raw = prefixMatch[1];
prefix = raw.replace(',', '');
if (raw.includes(',')) useGrouping = true;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatValue function's prefix extraction logic at line 34 removes commas from the prefix (e.g., "$," becomes "$"), which is correct. However, this logic is fragile because it treats any comma in the prefix area as a grouping indicator. For example, if a user wants a literal prefix containing a comma (unlikely but possible), this would fail. Consider documenting this behavior in a comment or adding a more explicit parsing strategy.

Suggested change
// comma inside the prefix-ish area means grouping, not a literal prefix
const raw = prefixMatch[1];
prefix = raw.replace(',', '');
if (raw.includes(',')) useGrouping = true;
// By default, a comma in the prefix-ish area is treated as a grouping indicator,
// not a literal prefix character. To render a literal comma, escape it as "\,".
const raw = prefixMatch[1];
let parsedPrefix = '';
let escaping = false;
for (let i = 0; i < raw.length; i++) {
const ch = raw[i];
if (escaping) {
// Only comma is special; other escaped chars are passed through as-is
if (ch === ',') {
parsedPrefix += ',';
} else {
parsedPrefix += '\\' + ch;
}
escaping = false;
continue;
}
if (ch === '\\') {
escaping = true;
continue;
}
if (ch === ',') {
useGrouping = true;
// do not add unescaped comma to prefix; it only signals grouping
continue;
}
parsedPrefix += ch;
}
prefix = parsedPrefix;

Copilot uses AI. Check for mistakes.
// Alice + Discovery = avg(1000, 500) = 750
expect(screen.getByText('750')).toBeInTheDocument();
});

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for 'min' and 'max' aggregation functions. While tests cover sum, count, and avg aggregations, the min and max cases are not tested. Consider adding test cases to ensure these aggregation modes work correctly, especially with edge cases like single values or negative numbers.

Suggested change
it('should support min aggregation', () => {
render(<PivotTable schema={makeSchema({ aggregation: 'min' })} />);
// Alice + Discovery = min(1000, 500) = 500
expect(screen.getByText('500')).toBeInTheDocument();
// Carol + Proposal = single value 4000 -> min = 4000
expect(screen.getByText('4000')).toBeInTheDocument();
});
it('should support max aggregation, including negative values', () => {
const dataWithNegative = [
...SAMPLE_DATA,
{ owner: 'Dave', stage: 'Discovery', amount: -100 },
];
render(
<PivotTable
schema={makeSchema({
aggregation: 'max',
data: dataWithNegative,
})}
/>,
);
// Alice + Discovery = max(1000, 500) = 1000
expect(screen.getByText('1000')).toBeInTheDocument();
// Dave + Discovery = single negative value -100 -> max = -100
expect(screen.getByText('-100')).toBeInTheDocument();
});

Copilot uses AI. Check for mistakes.
// Bob + Closed = $5,000
expect(screen.getByText('$5,000')).toBeInTheDocument();
});

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the columnColors feature. The PivotTableSchema includes a columnColors property to apply Tailwind color classes to column headers and cells, but no test verifies that these classes are correctly applied to the rendered elements. Consider adding a test that checks if the specified color classes appear in the DOM.

Suggested change
it('should apply columnColors classes to headers and cells', () => {
const schema = makeSchema({
columnColors: {
Discovery: 'bg-red-100',
Proposal: 'bg-blue-100',
},
});
const { container } = render(<PivotTable schema={schema} />);
// Headers should have the corresponding Tailwind color classes
const discoveryHeader = screen.getByText('Discovery');
expect(discoveryHeader).toHaveClass('bg-red-100');
const proposalHeader = screen.getByText('Proposal');
expect(proposalHeader).toHaveClass('bg-blue-100');
// Data cells for those columns should also receive the same classes
const discoveryCells = container.querySelectorAll('td.bg-red-100');
expect(discoveryCells.length).toBeGreaterThan(0);
const proposalCells = container.querySelectorAll('td.bg-blue-100');
expect(proposalCells.length).toBeGreaterThan(0);
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PivotTable 组件(交叉矩阵/透视表)支持

3 participants