@papernote/ui 1.10.20 → 1.10.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/PivotTable.d.ts +47 -0
- package/dist/components/PivotTable.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +48 -2
- package/dist/index.esm.js +257 -38
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +257 -37
- package/dist/index.js.map +1 -1
- package/dist/styles.css +5 -0
- package/package.json +1 -1
- package/src/components/PivotTable.stories.tsx +188 -0
- package/src/components/PivotTable.tsx +422 -0
- package/src/components/index.ts +4 -0
package/dist/styles.css
CHANGED
|
@@ -4423,6 +4423,11 @@ input:checked + .slider:before{
|
|
|
4423
4423
|
font-style: italic;
|
|
4424
4424
|
}
|
|
4425
4425
|
|
|
4426
|
+
.tabular-nums{
|
|
4427
|
+
--tw-numeric-spacing: tabular-nums;
|
|
4428
|
+
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4426
4431
|
.leading-5{
|
|
4427
4432
|
line-height: 1.25rem;
|
|
4428
4433
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { PivotTable } from './PivotTable';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof PivotTable> = {
|
|
5
|
+
title: 'Data Display/PivotTable',
|
|
6
|
+
component: PivotTable,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'padded',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'PivotTable transforms flat data into a cross-tabulation table with row labels, column headers, and aggregated values. Ideal for financial reports, sales summaries, and time-based data analysis.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof PivotTable>;
|
|
21
|
+
|
|
22
|
+
// Sample commission data by principal and month
|
|
23
|
+
const commissionData = [
|
|
24
|
+
{ PrincipalName: 'Allwire', CommissionFiscalMonth: 1, MonthlyCommission: 1250.5 },
|
|
25
|
+
{ PrincipalName: 'Allwire', CommissionFiscalMonth: 2, MonthlyCommission: 1875.25 },
|
|
26
|
+
{ PrincipalName: 'Allwire', CommissionFiscalMonth: 3, MonthlyCommission: 2100.0 },
|
|
27
|
+
{ PrincipalName: 'Allwire', CommissionFiscalMonth: 4, MonthlyCommission: 1950.75 },
|
|
28
|
+
{ PrincipalName: 'AMSC', CommissionFiscalMonth: 1, MonthlyCommission: 3200.0 },
|
|
29
|
+
{ PrincipalName: 'AMSC', CommissionFiscalMonth: 2, MonthlyCommission: 2850.5 },
|
|
30
|
+
{ PrincipalName: 'AMSC', CommissionFiscalMonth: 3, MonthlyCommission: 3100.25 },
|
|
31
|
+
{ PrincipalName: 'AMSC', CommissionFiscalMonth: 4, MonthlyCommission: 3450.0 },
|
|
32
|
+
{ PrincipalName: 'Buckingham', CommissionFiscalMonth: 1, MonthlyCommission: 850.0 },
|
|
33
|
+
{ PrincipalName: 'Buckingham', CommissionFiscalMonth: 2, MonthlyCommission: 925.75 },
|
|
34
|
+
{ PrincipalName: 'Buckingham', CommissionFiscalMonth: 3, MonthlyCommission: 1100.5 },
|
|
35
|
+
{ PrincipalName: 'Buckingham', CommissionFiscalMonth: 4, MonthlyCommission: 975.25 },
|
|
36
|
+
{ PrincipalName: 'Delta Corp', CommissionFiscalMonth: 1, MonthlyCommission: 5500.0 },
|
|
37
|
+
{ PrincipalName: 'Delta Corp', CommissionFiscalMonth: 2, MonthlyCommission: 4800.25 },
|
|
38
|
+
{ PrincipalName: 'Delta Corp', CommissionFiscalMonth: 3, MonthlyCommission: 5200.5 },
|
|
39
|
+
{ PrincipalName: 'Delta Corp', CommissionFiscalMonth: 4, MonthlyCommission: 5750.75 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const monthLabels: Record<number, string> = {
|
|
43
|
+
1: 'January',
|
|
44
|
+
2: 'February',
|
|
45
|
+
3: 'March',
|
|
46
|
+
4: 'April',
|
|
47
|
+
5: 'May',
|
|
48
|
+
6: 'June',
|
|
49
|
+
7: 'July',
|
|
50
|
+
8: 'August',
|
|
51
|
+
9: 'September',
|
|
52
|
+
10: 'October',
|
|
53
|
+
11: 'November',
|
|
54
|
+
12: 'December',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default pivot table showing commission by principal and month
|
|
59
|
+
*/
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
data: commissionData,
|
|
63
|
+
rowField: 'PrincipalName',
|
|
64
|
+
columnField: 'CommissionFiscalMonth',
|
|
65
|
+
valueField: 'MonthlyCommission',
|
|
66
|
+
columnLabels: monthLabels,
|
|
67
|
+
rowLabel: 'Principal',
|
|
68
|
+
showRowTotals: true,
|
|
69
|
+
showRowAverages: true,
|
|
70
|
+
showColumnTotals: true,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Without averages column
|
|
76
|
+
*/
|
|
77
|
+
export const WithoutAverages: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
...Default.args,
|
|
80
|
+
showRowAverages: false,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Without totals row
|
|
86
|
+
*/
|
|
87
|
+
export const WithoutColumnTotals: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
...Default.args,
|
|
90
|
+
showColumnTotals: false,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Minimal - just the pivot data
|
|
96
|
+
*/
|
|
97
|
+
export const Minimal: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
...Default.args,
|
|
100
|
+
showRowTotals: false,
|
|
101
|
+
showRowAverages: false,
|
|
102
|
+
showColumnTotals: false,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Sales data by region and quarter
|
|
107
|
+
const salesData = [
|
|
108
|
+
{ Region: 'North', Quarter: 'Q1', Sales: 125000 },
|
|
109
|
+
{ Region: 'North', Quarter: 'Q2', Sales: 145000 },
|
|
110
|
+
{ Region: 'North', Quarter: 'Q3', Sales: 132000 },
|
|
111
|
+
{ Region: 'North', Quarter: 'Q4', Sales: 178000 },
|
|
112
|
+
{ Region: 'South', Quarter: 'Q1', Sales: 98000 },
|
|
113
|
+
{ Region: 'South', Quarter: 'Q2', Sales: 112000 },
|
|
114
|
+
{ Region: 'South', Quarter: 'Q3', Sales: 105000 },
|
|
115
|
+
{ Region: 'South', Quarter: 'Q4', Sales: 142000 },
|
|
116
|
+
{ Region: 'East', Quarter: 'Q1', Sales: 210000 },
|
|
117
|
+
{ Region: 'East', Quarter: 'Q2', Sales: 195000 },
|
|
118
|
+
{ Region: 'East', Quarter: 'Q3', Sales: 225000 },
|
|
119
|
+
{ Region: 'East', Quarter: 'Q4', Sales: 248000 },
|
|
120
|
+
{ Region: 'West', Quarter: 'Q1', Sales: 175000 },
|
|
121
|
+
{ Region: 'West', Quarter: 'Q2', Sales: 168000 },
|
|
122
|
+
{ Region: 'West', Quarter: 'Q3', Sales: 192000 },
|
|
123
|
+
{ Region: 'West', Quarter: 'Q4', Sales: 215000 },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sales by region and quarter
|
|
128
|
+
*/
|
|
129
|
+
export const SalesByRegion: Story = {
|
|
130
|
+
args: {
|
|
131
|
+
data: salesData,
|
|
132
|
+
rowField: 'Region',
|
|
133
|
+
columnField: 'Quarter',
|
|
134
|
+
valueField: 'Sales',
|
|
135
|
+
rowLabel: 'Region',
|
|
136
|
+
showRowTotals: true,
|
|
137
|
+
showRowAverages: true,
|
|
138
|
+
showColumnTotals: true,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Custom number formatter (no currency symbol)
|
|
144
|
+
*/
|
|
145
|
+
export const CustomFormatter: Story = {
|
|
146
|
+
args: {
|
|
147
|
+
...Default.args,
|
|
148
|
+
formatValue: (value: number | null) => {
|
|
149
|
+
if (value === null) return '-';
|
|
150
|
+
return new Intl.NumberFormat('en-US', {
|
|
151
|
+
minimumFractionDigits: 0,
|
|
152
|
+
maximumFractionDigits: 0,
|
|
153
|
+
}).format(value);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Sparse data with missing values
|
|
159
|
+
const sparseData = [
|
|
160
|
+
{ Product: 'Widget A', Month: 1, Units: 150 },
|
|
161
|
+
{ Product: 'Widget A', Month: 3, Units: 200 },
|
|
162
|
+
{ Product: 'Widget A', Month: 4, Units: 175 },
|
|
163
|
+
{ Product: 'Widget B', Month: 1, Units: 300 },
|
|
164
|
+
{ Product: 'Widget B', Month: 2, Units: 280 },
|
|
165
|
+
{ Product: 'Widget B', Month: 4, Units: 320 },
|
|
166
|
+
{ Product: 'Widget C', Month: 2, Units: 90 },
|
|
167
|
+
{ Product: 'Widget C', Month: 3, Units: 110 },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sparse data with empty cells
|
|
172
|
+
*/
|
|
173
|
+
export const SparseData: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
data: sparseData,
|
|
176
|
+
rowField: 'Product',
|
|
177
|
+
columnField: 'Month',
|
|
178
|
+
valueField: 'Units',
|
|
179
|
+
columnLabels: monthLabels,
|
|
180
|
+
rowLabel: 'Product',
|
|
181
|
+
showRowTotals: true,
|
|
182
|
+
showColumnTotals: true,
|
|
183
|
+
formatValue: (value: number | null) => {
|
|
184
|
+
if (value === null) return '-';
|
|
185
|
+
return value.toLocaleString();
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PivotTable - Transforms flat data into a cross-tabulation table
|
|
5
|
+
*
|
|
6
|
+
* Takes data like:
|
|
7
|
+
* [{ principal: 'A', month: 1, amount: 100 }, { principal: 'A', month: 2, amount: 200 }, ...]
|
|
8
|
+
*
|
|
9
|
+
* And displays it as:
|
|
10
|
+
* | Principal | January | February | ... | Total | Average |
|
|
11
|
+
* |-----------|---------|----------|-----|-------|---------|
|
|
12
|
+
* | A | $100 | $200 | ... | $300 | $150 |
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface PivotTableProps<T = Record<string, unknown>> {
|
|
16
|
+
/** Array of data rows to pivot */
|
|
17
|
+
data: T[];
|
|
18
|
+
/** Field name to use for row labels */
|
|
19
|
+
rowField: string;
|
|
20
|
+
/** Field name to use for column headers */
|
|
21
|
+
columnField: string;
|
|
22
|
+
/** Field name containing the values to display */
|
|
23
|
+
valueField: string;
|
|
24
|
+
/** Optional map of column field values to display labels */
|
|
25
|
+
columnLabels?: Record<string | number, string>;
|
|
26
|
+
/** Optional row label header text */
|
|
27
|
+
rowLabel?: string;
|
|
28
|
+
/** Format function for values */
|
|
29
|
+
formatValue?: (value: number | null) => string;
|
|
30
|
+
/** Show row totals column */
|
|
31
|
+
showRowTotals?: boolean;
|
|
32
|
+
/** Show row averages column */
|
|
33
|
+
showRowAverages?: boolean;
|
|
34
|
+
/** Show column totals row */
|
|
35
|
+
showColumnTotals?: boolean;
|
|
36
|
+
/** Custom className */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Sort columns by label or value */
|
|
39
|
+
sortColumns?: 'label' | 'value' | 'none';
|
|
40
|
+
/** Sort rows alphabetically */
|
|
41
|
+
sortRows?: boolean;
|
|
42
|
+
/** Empty cell placeholder */
|
|
43
|
+
emptyValue?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PivotedData {
|
|
47
|
+
rowLabels: string[];
|
|
48
|
+
columnLabels: string[];
|
|
49
|
+
columnKeys: (string | number)[];
|
|
50
|
+
matrix: (number | null)[][];
|
|
51
|
+
rowTotals: number[];
|
|
52
|
+
rowAverages: number[];
|
|
53
|
+
columnTotals: number[];
|
|
54
|
+
grandTotal: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default currency formatter
|
|
59
|
+
*/
|
|
60
|
+
const defaultFormatValue = (value: number | null): string => {
|
|
61
|
+
if (value === null || value === undefined) return '-';
|
|
62
|
+
return new Intl.NumberFormat('en-US', {
|
|
63
|
+
style: 'currency',
|
|
64
|
+
currency: 'USD',
|
|
65
|
+
minimumFractionDigits: 2,
|
|
66
|
+
maximumFractionDigits: 2,
|
|
67
|
+
}).format(value);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Transform flat data into pivoted structure
|
|
72
|
+
*/
|
|
73
|
+
function pivotData<T>(
|
|
74
|
+
data: T[],
|
|
75
|
+
rowField: string,
|
|
76
|
+
columnField: string,
|
|
77
|
+
valueField: string,
|
|
78
|
+
columnLabels?: Record<string | number, string>,
|
|
79
|
+
sortColumns?: 'label' | 'value' | 'none',
|
|
80
|
+
sortRows?: boolean
|
|
81
|
+
): PivotedData {
|
|
82
|
+
// Extract unique row and column values
|
|
83
|
+
const rowSet = new Set<string>();
|
|
84
|
+
const columnSet = new Set<string | number>();
|
|
85
|
+
const valueMap = new Map<string, number>();
|
|
86
|
+
|
|
87
|
+
for (const row of data) {
|
|
88
|
+
const rowValue = String((row as Record<string, unknown>)[rowField] ?? '');
|
|
89
|
+
const colValue = (row as Record<string, unknown>)[columnField];
|
|
90
|
+
const val = (row as Record<string, unknown>)[valueField];
|
|
91
|
+
|
|
92
|
+
if (rowValue) rowSet.add(rowValue);
|
|
93
|
+
if (colValue !== undefined && colValue !== null) columnSet.add(colValue as string | number);
|
|
94
|
+
|
|
95
|
+
// Create composite key for the cell
|
|
96
|
+
const key = `${rowValue}|||${colValue}`;
|
|
97
|
+
const numVal = typeof val === 'number' ? val : parseFloat(String(val)) || 0;
|
|
98
|
+
valueMap.set(key, (valueMap.get(key) || 0) + numVal);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Sort row labels
|
|
102
|
+
let rowLabels = Array.from(rowSet);
|
|
103
|
+
if (sortRows !== false) {
|
|
104
|
+
rowLabels.sort((a, b) => a.localeCompare(b));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort column keys
|
|
108
|
+
let columnKeys = Array.from(columnSet);
|
|
109
|
+
if (sortColumns === 'label') {
|
|
110
|
+
columnKeys.sort((a, b) => {
|
|
111
|
+
const labelA = columnLabels?.[a] ?? String(a);
|
|
112
|
+
const labelB = columnLabels?.[b] ?? String(b);
|
|
113
|
+
return labelA.localeCompare(labelB);
|
|
114
|
+
});
|
|
115
|
+
} else if (sortColumns !== 'none') {
|
|
116
|
+
// Default: sort by value (numeric if possible)
|
|
117
|
+
columnKeys.sort((a, b) => {
|
|
118
|
+
const numA = typeof a === 'number' ? a : parseFloat(String(a));
|
|
119
|
+
const numB = typeof b === 'number' ? b : parseFloat(String(b));
|
|
120
|
+
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
|
|
121
|
+
return String(a).localeCompare(String(b));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build column labels array
|
|
126
|
+
const colLabels = columnKeys.map((k) => columnLabels?.[k] ?? String(k));
|
|
127
|
+
|
|
128
|
+
// Build the matrix
|
|
129
|
+
const matrix: (number | null)[][] = [];
|
|
130
|
+
const rowTotals: number[] = [];
|
|
131
|
+
const rowAverages: number[] = [];
|
|
132
|
+
const columnTotals: number[] = new Array(columnKeys.length).fill(0);
|
|
133
|
+
let grandTotal = 0;
|
|
134
|
+
|
|
135
|
+
for (const rowLabel of rowLabels) {
|
|
136
|
+
const rowData: (number | null)[] = [];
|
|
137
|
+
let rowSum = 0;
|
|
138
|
+
let rowCount = 0;
|
|
139
|
+
|
|
140
|
+
for (let colIdx = 0; colIdx < columnKeys.length; colIdx++) {
|
|
141
|
+
const colKey = columnKeys[colIdx];
|
|
142
|
+
const key = `${rowLabel}|||${colKey}`;
|
|
143
|
+
const value = valueMap.get(key) ?? null;
|
|
144
|
+
rowData.push(value);
|
|
145
|
+
|
|
146
|
+
if (value !== null) {
|
|
147
|
+
rowSum += value;
|
|
148
|
+
rowCount++;
|
|
149
|
+
columnTotals[colIdx] += value;
|
|
150
|
+
grandTotal += value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
matrix.push(rowData);
|
|
155
|
+
rowTotals.push(rowSum);
|
|
156
|
+
rowAverages.push(rowCount > 0 ? rowSum / rowCount : 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
rowLabels,
|
|
161
|
+
columnLabels: colLabels,
|
|
162
|
+
columnKeys,
|
|
163
|
+
matrix,
|
|
164
|
+
rowTotals,
|
|
165
|
+
rowAverages,
|
|
166
|
+
columnTotals,
|
|
167
|
+
grandTotal,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* PivotTable Component
|
|
173
|
+
*/
|
|
174
|
+
export function PivotTable<T = Record<string, unknown>>({
|
|
175
|
+
data,
|
|
176
|
+
rowField,
|
|
177
|
+
columnField,
|
|
178
|
+
valueField,
|
|
179
|
+
columnLabels,
|
|
180
|
+
rowLabel = '',
|
|
181
|
+
formatValue = defaultFormatValue,
|
|
182
|
+
showRowTotals = true,
|
|
183
|
+
showRowAverages = false,
|
|
184
|
+
showColumnTotals = true,
|
|
185
|
+
className = '',
|
|
186
|
+
sortColumns = 'value',
|
|
187
|
+
sortRows = true,
|
|
188
|
+
emptyValue = '-',
|
|
189
|
+
}: PivotTableProps<T>) {
|
|
190
|
+
const pivoted = useMemo(
|
|
191
|
+
() => pivotData(data, rowField, columnField, valueField, columnLabels, sortColumns, sortRows),
|
|
192
|
+
[data, rowField, columnField, valueField, columnLabels, sortColumns, sortRows]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const formatCell = (value: number | null): string => {
|
|
196
|
+
if (value === null || value === undefined) return emptyValue;
|
|
197
|
+
return formatValue(value);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Calculate column totals row average
|
|
201
|
+
const columnTotalsAverage =
|
|
202
|
+
pivoted.columnTotals.length > 0
|
|
203
|
+
? pivoted.columnTotals.reduce((a, b) => a + b, 0) / pivoted.columnTotals.length
|
|
204
|
+
: 0;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className={`pivot-table-container ${className}`} style={{ overflowX: 'auto' }}>
|
|
208
|
+
<table
|
|
209
|
+
style={{
|
|
210
|
+
width: '100%',
|
|
211
|
+
borderCollapse: 'collapse',
|
|
212
|
+
fontSize: '14px',
|
|
213
|
+
fontFamily: 'inherit',
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<thead>
|
|
217
|
+
<tr style={{ backgroundColor: '#f8fafc', borderBottom: '2px solid #e2e8f0' }}>
|
|
218
|
+
{/* Row label header */}
|
|
219
|
+
<th
|
|
220
|
+
style={{
|
|
221
|
+
padding: '12px 16px',
|
|
222
|
+
textAlign: 'left',
|
|
223
|
+
fontWeight: 600,
|
|
224
|
+
color: '#334155',
|
|
225
|
+
position: 'sticky',
|
|
226
|
+
left: 0,
|
|
227
|
+
backgroundColor: '#f8fafc',
|
|
228
|
+
zIndex: 1,
|
|
229
|
+
minWidth: '150px',
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{rowLabel}
|
|
233
|
+
</th>
|
|
234
|
+
{/* Column headers */}
|
|
235
|
+
{pivoted.columnLabels.map((label, idx) => (
|
|
236
|
+
<th
|
|
237
|
+
key={idx}
|
|
238
|
+
style={{
|
|
239
|
+
padding: '12px 16px',
|
|
240
|
+
textAlign: 'right',
|
|
241
|
+
fontWeight: 600,
|
|
242
|
+
color: '#334155',
|
|
243
|
+
minWidth: '100px',
|
|
244
|
+
whiteSpace: 'nowrap',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{label}
|
|
248
|
+
</th>
|
|
249
|
+
))}
|
|
250
|
+
{/* Total header */}
|
|
251
|
+
{showRowTotals && (
|
|
252
|
+
<th
|
|
253
|
+
style={{
|
|
254
|
+
padding: '12px 16px',
|
|
255
|
+
textAlign: 'right',
|
|
256
|
+
fontWeight: 700,
|
|
257
|
+
color: '#1e40af',
|
|
258
|
+
backgroundColor: '#eff6ff',
|
|
259
|
+
minWidth: '100px',
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
Total
|
|
263
|
+
</th>
|
|
264
|
+
)}
|
|
265
|
+
{/* Average header */}
|
|
266
|
+
{showRowAverages && (
|
|
267
|
+
<th
|
|
268
|
+
style={{
|
|
269
|
+
padding: '12px 16px',
|
|
270
|
+
textAlign: 'right',
|
|
271
|
+
fontWeight: 700,
|
|
272
|
+
color: '#166534',
|
|
273
|
+
backgroundColor: '#f0fdf4',
|
|
274
|
+
minWidth: '100px',
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
Average
|
|
278
|
+
</th>
|
|
279
|
+
)}
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
{pivoted.rowLabels.map((rowLabel, rowIdx) => (
|
|
284
|
+
<tr
|
|
285
|
+
key={rowIdx}
|
|
286
|
+
style={{
|
|
287
|
+
borderBottom: '1px solid #e2e8f0',
|
|
288
|
+
backgroundColor: rowIdx % 2 === 0 ? '#ffffff' : '#f8fafc',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{/* Row label */}
|
|
292
|
+
<td
|
|
293
|
+
style={{
|
|
294
|
+
padding: '10px 16px',
|
|
295
|
+
fontWeight: 500,
|
|
296
|
+
color: '#1e293b',
|
|
297
|
+
position: 'sticky',
|
|
298
|
+
left: 0,
|
|
299
|
+
backgroundColor: rowIdx % 2 === 0 ? '#ffffff' : '#f8fafc',
|
|
300
|
+
zIndex: 1,
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{rowLabel}
|
|
304
|
+
</td>
|
|
305
|
+
{/* Data cells */}
|
|
306
|
+
{pivoted.matrix[rowIdx].map((value, colIdx) => (
|
|
307
|
+
<td
|
|
308
|
+
key={colIdx}
|
|
309
|
+
style={{
|
|
310
|
+
padding: '10px 16px',
|
|
311
|
+
textAlign: 'right',
|
|
312
|
+
color: value === null ? '#94a3b8' : '#334155',
|
|
313
|
+
fontVariantNumeric: 'tabular-nums',
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{formatCell(value)}
|
|
317
|
+
</td>
|
|
318
|
+
))}
|
|
319
|
+
{/* Row total */}
|
|
320
|
+
{showRowTotals && (
|
|
321
|
+
<td
|
|
322
|
+
style={{
|
|
323
|
+
padding: '10px 16px',
|
|
324
|
+
textAlign: 'right',
|
|
325
|
+
fontWeight: 600,
|
|
326
|
+
color: '#1e40af',
|
|
327
|
+
backgroundColor: '#eff6ff',
|
|
328
|
+
fontVariantNumeric: 'tabular-nums',
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
{formatCell(pivoted.rowTotals[rowIdx])}
|
|
332
|
+
</td>
|
|
333
|
+
)}
|
|
334
|
+
{/* Row average */}
|
|
335
|
+
{showRowAverages && (
|
|
336
|
+
<td
|
|
337
|
+
style={{
|
|
338
|
+
padding: '10px 16px',
|
|
339
|
+
textAlign: 'right',
|
|
340
|
+
fontWeight: 600,
|
|
341
|
+
color: '#166534',
|
|
342
|
+
backgroundColor: '#f0fdf4',
|
|
343
|
+
fontVariantNumeric: 'tabular-nums',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{formatCell(pivoted.rowAverages[rowIdx])}
|
|
347
|
+
</td>
|
|
348
|
+
)}
|
|
349
|
+
</tr>
|
|
350
|
+
))}
|
|
351
|
+
{/* Column totals row */}
|
|
352
|
+
{showColumnTotals && (
|
|
353
|
+
<tr
|
|
354
|
+
style={{
|
|
355
|
+
borderTop: '2px solid #e2e8f0',
|
|
356
|
+
backgroundColor: '#f1f5f9',
|
|
357
|
+
fontWeight: 600,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<td
|
|
361
|
+
style={{
|
|
362
|
+
padding: '12px 16px',
|
|
363
|
+
fontWeight: 700,
|
|
364
|
+
color: '#1e293b',
|
|
365
|
+
position: 'sticky',
|
|
366
|
+
left: 0,
|
|
367
|
+
backgroundColor: '#f1f5f9',
|
|
368
|
+
zIndex: 1,
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
Total
|
|
372
|
+
</td>
|
|
373
|
+
{pivoted.columnTotals.map((total, idx) => (
|
|
374
|
+
<td
|
|
375
|
+
key={idx}
|
|
376
|
+
style={{
|
|
377
|
+
padding: '12px 16px',
|
|
378
|
+
textAlign: 'right',
|
|
379
|
+
color: '#1e293b',
|
|
380
|
+
fontVariantNumeric: 'tabular-nums',
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
{formatCell(total)}
|
|
384
|
+
</td>
|
|
385
|
+
))}
|
|
386
|
+
{showRowTotals && (
|
|
387
|
+
<td
|
|
388
|
+
style={{
|
|
389
|
+
padding: '12px 16px',
|
|
390
|
+
textAlign: 'right',
|
|
391
|
+
fontWeight: 700,
|
|
392
|
+
color: '#1e40af',
|
|
393
|
+
backgroundColor: '#dbeafe',
|
|
394
|
+
fontVariantNumeric: 'tabular-nums',
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
{formatCell(pivoted.grandTotal)}
|
|
398
|
+
</td>
|
|
399
|
+
)}
|
|
400
|
+
{showRowAverages && (
|
|
401
|
+
<td
|
|
402
|
+
style={{
|
|
403
|
+
padding: '12px 16px',
|
|
404
|
+
textAlign: 'right',
|
|
405
|
+
fontWeight: 700,
|
|
406
|
+
color: '#166534',
|
|
407
|
+
backgroundColor: '#dcfce7',
|
|
408
|
+
fontVariantNumeric: 'tabular-nums',
|
|
409
|
+
}}
|
|
410
|
+
>
|
|
411
|
+
{formatCell(columnTotalsAverage)}
|
|
412
|
+
</td>
|
|
413
|
+
)}
|
|
414
|
+
</tr>
|
|
415
|
+
)}
|
|
416
|
+
</tbody>
|
|
417
|
+
</table>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default PivotTable;
|
package/src/components/index.ts
CHANGED
|
@@ -404,6 +404,10 @@ export type {
|
|
|
404
404
|
FrozenRowMode,
|
|
405
405
|
} from './DataGrid';
|
|
406
406
|
|
|
407
|
+
// PivotTable (cross-tabulation display)
|
|
408
|
+
export { default as PivotTable } from './PivotTable';
|
|
409
|
+
export type { PivotTableProps } from './PivotTable';
|
|
410
|
+
|
|
407
411
|
export { default as SwipeActions } from './SwipeActions';
|
|
408
412
|
export type { SwipeActionsProps, SwipeAction } from './SwipeActions';
|
|
409
413
|
|