@cdx-ui/data-grid 0.3.11 → 0.3.13
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/README.md +3 -1
- package/coverage.svg +1 -1
- package/lib/commonjs/DataGrid.js +490 -81
- package/lib/commonjs/DataGrid.js.map +1 -1
- package/lib/commonjs/DataGridFilterPanel.js +547 -0
- package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
- package/lib/commonjs/adapter/columnTypes.js +256 -0
- package/lib/commonjs/adapter/columnTypes.js.map +1 -0
- package/lib/commonjs/adapter/columns.js +140 -4
- package/lib/commonjs/adapter/columns.js.map +1 -1
- package/lib/commonjs/adapter/filterOperators.js +422 -0
- package/lib/commonjs/adapter/filterOperators.js.map +1 -0
- package/lib/commonjs/adapter/pinning.js +105 -0
- package/lib/commonjs/adapter/pinning.js.map +1 -0
- package/lib/commonjs/adapter/selectColumn.js +11 -0
- package/lib/commonjs/adapter/selectColumn.js.map +1 -1
- package/lib/commonjs/adapter/treeData.js +195 -0
- package/lib/commonjs/adapter/treeData.js.map +1 -0
- package/lib/commonjs/index.js +26 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/numberCells.js +280 -0
- package/lib/commonjs/numberCells.js.map +1 -0
- package/lib/commonjs/styles.js +107 -1
- package/lib/commonjs/styles.js.map +1 -1
- package/lib/commonjs/types.js +39 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useDataGridTable.js +234 -18
- package/lib/commonjs/useDataGridTable.js.map +1 -1
- package/lib/module/DataGrid.js +497 -88
- package/lib/module/DataGrid.js.map +1 -1
- package/lib/module/DataGridFilterPanel.js +540 -0
- package/lib/module/DataGridFilterPanel.js.map +1 -0
- package/lib/module/adapter/columnTypes.js +240 -0
- package/lib/module/adapter/columnTypes.js.map +1 -0
- package/lib/module/adapter/columns.js +137 -4
- package/lib/module/adapter/columns.js.map +1 -1
- package/lib/module/adapter/filterOperators.js +406 -0
- package/lib/module/adapter/filterOperators.js.map +1 -0
- package/lib/module/adapter/pinning.js +94 -0
- package/lib/module/adapter/pinning.js.map +1 -0
- package/lib/module/adapter/selectColumn.js +11 -1
- package/lib/module/adapter/selectColumn.js.map +1 -1
- package/lib/module/adapter/treeData.js +187 -0
- package/lib/module/adapter/treeData.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/numberCells.js +275 -0
- package/lib/module/numberCells.js.map +1 -0
- package/lib/module/styles.js +106 -0
- package/lib/module/styles.js.map +1 -1
- package/lib/module/types.js +38 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/useDataGridTable.js +238 -22
- package/lib/module/useDataGridTable.js.map +1 -1
- package/lib/typescript/DataGrid.d.ts.map +1 -1
- package/lib/typescript/DataGridFilterPanel.d.ts +52 -0
- package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
- package/lib/typescript/adapter/columnTypes.d.ts +32 -0
- package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
- package/lib/typescript/adapter/columns.d.ts +14 -5
- package/lib/typescript/adapter/columns.d.ts.map +1 -1
- package/lib/typescript/adapter/filterOperators.d.ts +43 -0
- package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
- package/lib/typescript/adapter/pinning.d.ts +26 -0
- package/lib/typescript/adapter/pinning.d.ts.map +1 -0
- package/lib/typescript/adapter/selectColumn.d.ts +8 -0
- package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
- package/lib/typescript/adapter/treeData.d.ts +36 -0
- package/lib/typescript/adapter/treeData.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/numberCells.d.ts +46 -0
- package/lib/typescript/numberCells.d.ts.map +1 -0
- package/lib/typescript/styles.d.ts +32 -0
- package/lib/typescript/styles.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +154 -7
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useDataGridTable.d.ts +40 -4
- package/lib/typescript/useDataGridTable.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/DataGrid.tsx +745 -95
- package/src/DataGridFilterPanel.tsx +648 -0
- package/src/adapter/columnTypes.ts +327 -0
- package/src/adapter/columns.test.ts +75 -1
- package/src/adapter/columns.ts +192 -3
- package/src/adapter/filterOperators.test.ts +292 -0
- package/src/adapter/filterOperators.ts +524 -0
- package/src/adapter/pinning.ts +134 -0
- package/src/adapter/selectColumn.ts +12 -1
- package/src/adapter/treeData.ts +228 -0
- package/src/index.ts +19 -0
- package/src/numberCells.tsx +379 -0
- package/src/styles.ts +131 -0
- package/src/types.ts +190 -7
- package/src/useDataGridTable.test.ts +69 -0
- package/src/useDataGridTable.ts +338 -17
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export interface GridTreeIndex<R> {
|
|
2
|
+
roots: R[];
|
|
3
|
+
childrenOf: Map<R, R[]>;
|
|
4
|
+
rowOf: Map<string, R>;
|
|
5
|
+
parentKeyOf: Map<string, string>;
|
|
6
|
+
childKeysOf: Map<string, string[]>;
|
|
7
|
+
depthOf: Map<string, number>;
|
|
8
|
+
/** Deepest rows first, so a parent is visited after all of its descendants. */
|
|
9
|
+
keysDeepestFirst: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type GridTreeSelectionStatus = 'all' | 'some' | 'none';
|
|
13
|
+
|
|
14
|
+
/** The shape TanStack rows share; filtered and sorted rows carry their filtered `subRows`. */
|
|
15
|
+
export interface GridTreeRowLike {
|
|
16
|
+
id: string;
|
|
17
|
+
subRows: readonly GridTreeRowLike[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pathKey = (path: readonly string[]) => JSON.stringify(path);
|
|
21
|
+
|
|
22
|
+
const warnedDuplicatePaths = new Set<string>();
|
|
23
|
+
|
|
24
|
+
export function buildTreeIndex<R>(
|
|
25
|
+
rows: readonly R[],
|
|
26
|
+
getTreeDataPath: (row: R) => readonly string[],
|
|
27
|
+
keyOf: (row: R) => string,
|
|
28
|
+
): GridTreeIndex<R> {
|
|
29
|
+
const byPath = new Map<string, R>();
|
|
30
|
+
const paths = new Map<R, readonly string[]>();
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const path = getTreeDataPath(row);
|
|
33
|
+
const key = pathKey(path);
|
|
34
|
+
if (__DEV__ && byPath.has(key) && !warnedDuplicatePaths.has(key)) {
|
|
35
|
+
warnedDuplicatePaths.add(key);
|
|
36
|
+
console.warn(`[DataGrid] Duplicate tree data path ${key}. Paths must be unique.`);
|
|
37
|
+
}
|
|
38
|
+
byPath.set(key, row);
|
|
39
|
+
paths.set(row, path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const roots: R[] = [];
|
|
43
|
+
const childrenOf = new Map<R, R[]>();
|
|
44
|
+
const rowOf = new Map<string, R>();
|
|
45
|
+
const parentKeyOf = new Map<string, string>();
|
|
46
|
+
const childKeysOf = new Map<string, string[]>();
|
|
47
|
+
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const path = paths.get(row) ?? [];
|
|
50
|
+
const key = keyOf(row);
|
|
51
|
+
rowOf.set(key, row);
|
|
52
|
+
let parent: R | undefined;
|
|
53
|
+
for (let length = path.length - 1; length > 0 && parent === undefined; length -= 1) {
|
|
54
|
+
parent = byPath.get(pathKey(path.slice(0, length)));
|
|
55
|
+
}
|
|
56
|
+
if (parent === undefined || parent === row) {
|
|
57
|
+
roots.push(row);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const parentKey = keyOf(parent);
|
|
61
|
+
parentKeyOf.set(key, parentKey);
|
|
62
|
+
const siblings = childrenOf.get(parent);
|
|
63
|
+
if (siblings) {
|
|
64
|
+
siblings.push(row);
|
|
65
|
+
} else {
|
|
66
|
+
childrenOf.set(parent, [row]);
|
|
67
|
+
}
|
|
68
|
+
const siblingKeys = childKeysOf.get(parentKey);
|
|
69
|
+
if (siblingKeys) {
|
|
70
|
+
siblingKeys.push(key);
|
|
71
|
+
} else {
|
|
72
|
+
childKeysOf.set(parentKey, [key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const depthOf = new Map<string, number>();
|
|
77
|
+
const breadthFirst = roots.map((row) => keyOf(row));
|
|
78
|
+
for (const key of breadthFirst) {
|
|
79
|
+
depthOf.set(key, 0);
|
|
80
|
+
}
|
|
81
|
+
// The array iterator also visits children pushed during the loop.
|
|
82
|
+
for (const key of breadthFirst) {
|
|
83
|
+
for (const child of childKeysOf.get(key) ?? []) {
|
|
84
|
+
depthOf.set(child, (depthOf.get(key) ?? 0) + 1);
|
|
85
|
+
breadthFirst.push(child);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
roots,
|
|
91
|
+
childrenOf,
|
|
92
|
+
rowOf,
|
|
93
|
+
parentKeyOf,
|
|
94
|
+
childKeysOf,
|
|
95
|
+
depthOf,
|
|
96
|
+
keysDeepestFirst: breadthFirst.reverse(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* With `propagateParents`, a parent is `all` exactly when every child is `all`, whatever the model
|
|
102
|
+
* says about the parent itself. Without it, a parent's own selection decides, and partly selected
|
|
103
|
+
* descendants show as `some`.
|
|
104
|
+
*/
|
|
105
|
+
export function computeTreeSelection<R>(
|
|
106
|
+
index: GridTreeIndex<R>,
|
|
107
|
+
selected: ReadonlySet<string>,
|
|
108
|
+
propagateParents: boolean,
|
|
109
|
+
): Map<string, GridTreeSelectionStatus> {
|
|
110
|
+
const status = new Map<string, GridTreeSelectionStatus>();
|
|
111
|
+
for (const key of index.keysDeepestFirst) {
|
|
112
|
+
const isSelected = selected.has(key);
|
|
113
|
+
const children = index.childKeysOf.get(key);
|
|
114
|
+
if (!children?.length) {
|
|
115
|
+
status.set(key, isSelected ? 'all' : 'none');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let all = true;
|
|
119
|
+
let some = false;
|
|
120
|
+
for (const child of children) {
|
|
121
|
+
const childStatus = status.get(child) ?? 'none';
|
|
122
|
+
if (childStatus !== 'all') {
|
|
123
|
+
all = false;
|
|
124
|
+
}
|
|
125
|
+
if (childStatus !== 'none') {
|
|
126
|
+
some = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (propagateParents) {
|
|
130
|
+
status.set(key, all ? 'all' : some || isSelected ? 'some' : 'none');
|
|
131
|
+
} else {
|
|
132
|
+
status.set(key, isSelected ? 'all' : some ? 'some' : 'none');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return status;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function applyTreeSelection<R>(
|
|
139
|
+
index: GridTreeIndex<R>,
|
|
140
|
+
selected: ReadonlySet<string>,
|
|
141
|
+
targets: readonly string[],
|
|
142
|
+
value: boolean,
|
|
143
|
+
propagateParents: boolean,
|
|
144
|
+
): Set<string> {
|
|
145
|
+
const next = new Set(selected);
|
|
146
|
+
for (const key of targets) {
|
|
147
|
+
if (value) {
|
|
148
|
+
next.add(key);
|
|
149
|
+
} else {
|
|
150
|
+
next.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (propagateParents) {
|
|
154
|
+
for (const key of index.keysDeepestFirst) {
|
|
155
|
+
const children = index.childKeysOf.get(key);
|
|
156
|
+
if (!children?.length) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (children.every((child) => next.has(child))) {
|
|
160
|
+
next.add(key);
|
|
161
|
+
} else {
|
|
162
|
+
next.delete(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Rows a toggle writes (`targets`) and the rows whose state decides the toggle direction
|
|
171
|
+
* (`units`). With `descendants`, a row's filtered sub-rows are included; a row whose children were
|
|
172
|
+
* all filtered out takes its whole subtree, so a matching group can still be picked.
|
|
173
|
+
*/
|
|
174
|
+
export function selectionScope<R>(
|
|
175
|
+
rows: readonly GridTreeRowLike[],
|
|
176
|
+
index: GridTreeIndex<R>,
|
|
177
|
+
descendants: boolean,
|
|
178
|
+
): { targets: string[]; units: string[] } {
|
|
179
|
+
const targets: string[] = [];
|
|
180
|
+
const units: string[] = [];
|
|
181
|
+
const visitFullSubtree = (key: string) => {
|
|
182
|
+
targets.push(key);
|
|
183
|
+
const children = index.childKeysOf.get(key);
|
|
184
|
+
if (!children?.length) {
|
|
185
|
+
units.push(key);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
children.forEach(visitFullSubtree);
|
|
189
|
+
};
|
|
190
|
+
const visit = (row: GridTreeRowLike) => {
|
|
191
|
+
if (!descendants) {
|
|
192
|
+
targets.push(row.id);
|
|
193
|
+
units.push(row.id);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (row.subRows.length === 0) {
|
|
197
|
+
visitFullSubtree(row.id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
targets.push(row.id);
|
|
201
|
+
row.subRows.forEach(visit);
|
|
202
|
+
};
|
|
203
|
+
rows.forEach(visit);
|
|
204
|
+
return { targets, units };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Keeps every row that matches and every ancestor of a match. */
|
|
208
|
+
export function keepMatchesWithAncestors<R>(
|
|
209
|
+
rows: readonly R[],
|
|
210
|
+
index: GridTreeIndex<R>,
|
|
211
|
+
keyOf: (row: R) => string,
|
|
212
|
+
matches: (row: R) => boolean,
|
|
213
|
+
): R[] {
|
|
214
|
+
const keep = new Set<string>();
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
if (!matches(row)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (let key: string | undefined = keyOf(row); key !== undefined; ) {
|
|
220
|
+
if (keep.has(key)) {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
keep.add(key);
|
|
224
|
+
key = index.parentKeyOf.get(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return rows.filter((row) => keep.has(keyOf(row)));
|
|
228
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
export { DataGrid } from './DataGrid';
|
|
2
|
+
export { getDefaultFilterOperator, getGridFilterOperators } from './adapter/filterOperators';
|
|
3
|
+
export { gridDeltaNumberColDef, gridPolarityNumberColDef } from './numberCells';
|
|
4
|
+
export type {
|
|
5
|
+
GridDeltaNumberColDefOptions,
|
|
6
|
+
GridDeltaNumberSymbol,
|
|
7
|
+
GridNumberCellDensity,
|
|
8
|
+
GridPolarityNumberColDefOptions,
|
|
9
|
+
} from './numberCells';
|
|
2
10
|
export type {
|
|
3
11
|
DataGridProps,
|
|
4
12
|
GridAlignment,
|
|
13
|
+
GridBaseColDef,
|
|
5
14
|
GridColDef,
|
|
15
|
+
GridColType,
|
|
16
|
+
GridColumnVisibilityModel,
|
|
6
17
|
GridFeatureMode,
|
|
7
18
|
GridFilterItem,
|
|
8
19
|
GridFilterModel,
|
|
20
|
+
GridFilterOperator,
|
|
21
|
+
GridLogicOperator,
|
|
9
22
|
GridPaginationModel,
|
|
23
|
+
GridPinnedColumnFields,
|
|
10
24
|
GridRenderCellParams,
|
|
11
25
|
GridRowId,
|
|
26
|
+
GridRowParams,
|
|
12
27
|
GridRowSelectionModel,
|
|
28
|
+
GridRowSelectionPropagation,
|
|
13
29
|
GridRowsProp,
|
|
30
|
+
GridSingleSelectColDef,
|
|
14
31
|
GridSortDirection,
|
|
15
32
|
GridSortItem,
|
|
16
33
|
GridSortModel,
|
|
34
|
+
GridTreeNodeParams,
|
|
17
35
|
GridValidRowModel,
|
|
36
|
+
GridValueOption,
|
|
18
37
|
} from './types';
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Text } from '@cdx-ui/components';
|
|
4
|
+
import {
|
|
5
|
+
ArrowDropDown,
|
|
6
|
+
ArrowDropUp,
|
|
7
|
+
TrendingDown,
|
|
8
|
+
TrendingFlat,
|
|
9
|
+
TrendingUp,
|
|
10
|
+
type ForgeIcon,
|
|
11
|
+
} from '@cdx-ui/icons';
|
|
12
|
+
import { cn } from '@cdx-ui/utils';
|
|
13
|
+
import type { GridBaseColDef, GridColDef, GridValidRowModel } from './types';
|
|
14
|
+
|
|
15
|
+
/** Unicode minus (U+2212). The spec requires this character, not a hyphen. */
|
|
16
|
+
const MINUS = '\u2212';
|
|
17
|
+
|
|
18
|
+
const defaultNumberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 20 });
|
|
19
|
+
|
|
20
|
+
export type GridNumberCellDensity = 'default' | 'compact';
|
|
21
|
+
|
|
22
|
+
export type GridDeltaNumberSymbol = 'arrow' | 'trendline' | 'sign';
|
|
23
|
+
|
|
24
|
+
export interface GridPolarityNumberColDefOptions {
|
|
25
|
+
/**
|
|
26
|
+
* `default` uses body-md. `compact` uses body-sm.
|
|
27
|
+
* @default 'default'
|
|
28
|
+
*/
|
|
29
|
+
density?: GridNumberCellDensity;
|
|
30
|
+
/**
|
|
31
|
+
* Formats the raw signed number. The cell turns a leading hyphen into −.
|
|
32
|
+
* Sorting uses the raw number. The header filter matches the number column's
|
|
33
|
+
* formatted value, not this string.
|
|
34
|
+
*/
|
|
35
|
+
valueFormatter?: (value: number) => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GridDeltaNumberColDefOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Whether an increase is a favorable change. Color follows this together with
|
|
41
|
+
* the value's direction: an increase is success when this is true and danger
|
|
42
|
+
* when it is false; a decrease flips that. Zero stays primary either way.
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
increaseIsGood?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* `arrow` uses drop arrows and leaves flat values blank. `trendline` uses
|
|
48
|
+
* trending icons, including a flat icon at zero. `sign` writes + or − in
|
|
49
|
+
* front of the value. Arrow and trendline hide the minus sign.
|
|
50
|
+
* @default 'arrow'
|
|
51
|
+
*/
|
|
52
|
+
symbol?: GridDeltaNumberSymbol;
|
|
53
|
+
/**
|
|
54
|
+
* `default` uses body-md. `compact` uses body-sm.
|
|
55
|
+
* @default 'default'
|
|
56
|
+
*/
|
|
57
|
+
density?: GridNumberCellDensity;
|
|
58
|
+
/**
|
|
59
|
+
* Formats the raw signed number. The cell applies the symbol's sign treatment
|
|
60
|
+
* on top of this string. Sorting uses the raw number. The header filter matches
|
|
61
|
+
* the number column's formatted value, not this string.
|
|
62
|
+
*/
|
|
63
|
+
valueFormatter?: (value: number) => string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type NumberTone = 'success' | 'danger' | 'neutral';
|
|
67
|
+
type DeltaDirection = 'increase' | 'decrease' | 'flat';
|
|
68
|
+
|
|
69
|
+
const TONE_CLASS: Record<NumberTone, string> = {
|
|
70
|
+
success: 'text-content-success',
|
|
71
|
+
danger: 'text-content-danger',
|
|
72
|
+
neutral: 'text-content-primary',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ARROW_SLOT: Record<GridNumberCellDensity, string> = {
|
|
76
|
+
default: 'h-5 w-3.5 items-center justify-center overflow-hidden',
|
|
77
|
+
compact: 'h-4 w-3 items-center justify-center overflow-hidden',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ARROW_SIZE: Record<GridNumberCellDensity, number> = {
|
|
81
|
+
default: 20,
|
|
82
|
+
compact: 16,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const TREND_SIZE: Record<GridNumberCellDensity, number> = {
|
|
86
|
+
default: 14,
|
|
87
|
+
compact: 12,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function toFiniteNumber(value: unknown): number | null {
|
|
91
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function columnValueFormatter<R extends GridValidRowModel>(
|
|
95
|
+
format: ((value: number) => string) | undefined,
|
|
96
|
+
): GridColDef<R>['valueFormatter'] {
|
|
97
|
+
if (!format) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return (value) => {
|
|
101
|
+
const numeric = toFiniteNumber(value);
|
|
102
|
+
return numeric == null ? '' : format(numeric);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatRaw<R extends GridValidRowModel>(value: number, row: R, col: GridColDef<R>): string {
|
|
107
|
+
const formatted = col.valueFormatter?.(value, row, col);
|
|
108
|
+
if (typeof formatted === 'string' && formatted.length > 0) {
|
|
109
|
+
return formatted;
|
|
110
|
+
}
|
|
111
|
+
return defaultNumberFormat.format(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unsigned(formatted: string): string {
|
|
115
|
+
const withoutSign = formatted.replace(/^[-−+]\s*/, '');
|
|
116
|
+
if (withoutSign.startsWith('(') && withoutSign.endsWith(')')) {
|
|
117
|
+
return withoutSign.slice(1, -1);
|
|
118
|
+
}
|
|
119
|
+
return withoutSign;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function polarityOf(value: number): 'positive' | 'negative' | 'neutral' {
|
|
123
|
+
if (value > 0) {
|
|
124
|
+
return 'positive';
|
|
125
|
+
}
|
|
126
|
+
if (value < 0) {
|
|
127
|
+
return 'negative';
|
|
128
|
+
}
|
|
129
|
+
return 'neutral';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function directionOf(value: number): DeltaDirection {
|
|
133
|
+
if (value > 0) {
|
|
134
|
+
return 'increase';
|
|
135
|
+
}
|
|
136
|
+
if (value < 0) {
|
|
137
|
+
return 'decrease';
|
|
138
|
+
}
|
|
139
|
+
return 'flat';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function polarityTone(polarity: 'positive' | 'negative' | 'neutral'): NumberTone {
|
|
143
|
+
if (polarity === 'positive') {
|
|
144
|
+
return 'success';
|
|
145
|
+
}
|
|
146
|
+
if (polarity === 'negative') {
|
|
147
|
+
return 'danger';
|
|
148
|
+
}
|
|
149
|
+
return 'neutral';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function deltaTone(direction: DeltaDirection, increaseIsGood: boolean): NumberTone {
|
|
153
|
+
if (direction === 'flat') {
|
|
154
|
+
return 'neutral';
|
|
155
|
+
}
|
|
156
|
+
const favorable = direction === 'increase' ? increaseIsGood : !increaseIsGood;
|
|
157
|
+
return favorable ? 'success' : 'danger';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function polarityLabel(polarity: 'positive' | 'negative' | 'neutral'): string {
|
|
161
|
+
if (polarity === 'positive') {
|
|
162
|
+
return 'Positive';
|
|
163
|
+
}
|
|
164
|
+
if (polarity === 'negative') {
|
|
165
|
+
return 'Negative';
|
|
166
|
+
}
|
|
167
|
+
return 'Neutral';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function directionLabel(direction: DeltaDirection): string {
|
|
171
|
+
if (direction === 'increase') {
|
|
172
|
+
return 'Increase';
|
|
173
|
+
}
|
|
174
|
+
if (direction === 'decrease') {
|
|
175
|
+
return 'Decrease';
|
|
176
|
+
}
|
|
177
|
+
return 'No change';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function NumberReadout({
|
|
181
|
+
label,
|
|
182
|
+
display,
|
|
183
|
+
tone,
|
|
184
|
+
density,
|
|
185
|
+
children,
|
|
186
|
+
}: {
|
|
187
|
+
label: string;
|
|
188
|
+
display: string;
|
|
189
|
+
tone: NumberTone;
|
|
190
|
+
density: GridNumberCellDensity;
|
|
191
|
+
children?: ReactNode;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<View className="relative flex-row items-center gap-0.5">
|
|
195
|
+
<Text className="absolute h-px w-px overflow-hidden opacity-0">{label}</Text>
|
|
196
|
+
<View
|
|
197
|
+
aria-hidden
|
|
198
|
+
accessibilityElementsHidden
|
|
199
|
+
importantForAccessibility="no-hide-descendants"
|
|
200
|
+
className="flex-row items-center gap-0.5"
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
<Text
|
|
204
|
+
color={tone === 'neutral' ? 'primary' : tone}
|
|
205
|
+
size={density === 'compact' ? 'sm' : 'md'}
|
|
206
|
+
>
|
|
207
|
+
{display}
|
|
208
|
+
</Text>
|
|
209
|
+
</View>
|
|
210
|
+
</View>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ToneIcon({
|
|
215
|
+
icon: Icon,
|
|
216
|
+
tone,
|
|
217
|
+
size,
|
|
218
|
+
slotClassName,
|
|
219
|
+
}: {
|
|
220
|
+
icon: ForgeIcon;
|
|
221
|
+
tone: NumberTone;
|
|
222
|
+
size: number;
|
|
223
|
+
slotClassName?: string;
|
|
224
|
+
}) {
|
|
225
|
+
return (
|
|
226
|
+
<View className={cn(TONE_CLASS[tone], slotClassName)}>
|
|
227
|
+
<Icon size={size} />
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function PolarityNumberCell<R extends GridValidRowModel>({
|
|
233
|
+
value,
|
|
234
|
+
row,
|
|
235
|
+
col,
|
|
236
|
+
density,
|
|
237
|
+
}: {
|
|
238
|
+
value: unknown;
|
|
239
|
+
row: R;
|
|
240
|
+
col: GridColDef<R>;
|
|
241
|
+
density: GridNumberCellDensity;
|
|
242
|
+
}) {
|
|
243
|
+
const numeric = toFiniteNumber(value);
|
|
244
|
+
if (numeric == null) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const polarity = polarityOf(numeric);
|
|
249
|
+
const magnitude = unsigned(formatRaw(numeric, row, col));
|
|
250
|
+
const display = polarity === 'negative' ? `${MINUS}${magnitude}` : magnitude;
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<NumberReadout
|
|
254
|
+
label={`${polarityLabel(polarity)} ${magnitude}`}
|
|
255
|
+
display={display}
|
|
256
|
+
tone={polarityTone(polarity)}
|
|
257
|
+
density={density}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function DeltaSymbol({
|
|
263
|
+
direction,
|
|
264
|
+
tone,
|
|
265
|
+
density,
|
|
266
|
+
symbol,
|
|
267
|
+
}: {
|
|
268
|
+
direction: DeltaDirection;
|
|
269
|
+
tone: NumberTone;
|
|
270
|
+
density: GridNumberCellDensity;
|
|
271
|
+
symbol: GridDeltaNumberSymbol;
|
|
272
|
+
}) {
|
|
273
|
+
if (symbol === 'sign') {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (symbol === 'arrow') {
|
|
278
|
+
if (direction === 'flat') {
|
|
279
|
+
return <View className={ARROW_SLOT[density]} />;
|
|
280
|
+
}
|
|
281
|
+
return (
|
|
282
|
+
<ToneIcon
|
|
283
|
+
icon={direction === 'increase' ? ArrowDropUp : ArrowDropDown}
|
|
284
|
+
tone={tone}
|
|
285
|
+
size={ARROW_SIZE[density]}
|
|
286
|
+
slotClassName={ARROW_SLOT[density]}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const icon =
|
|
292
|
+
direction === 'increase' ? TrendingUp : direction === 'decrease' ? TrendingDown : TrendingFlat;
|
|
293
|
+
return <ToneIcon icon={icon} tone={tone} size={TREND_SIZE[density]} />;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function DeltaNumberCell<R extends GridValidRowModel>({
|
|
297
|
+
value,
|
|
298
|
+
row,
|
|
299
|
+
col,
|
|
300
|
+
density,
|
|
301
|
+
increaseIsGood,
|
|
302
|
+
symbol,
|
|
303
|
+
}: {
|
|
304
|
+
value: unknown;
|
|
305
|
+
row: R;
|
|
306
|
+
col: GridColDef<R>;
|
|
307
|
+
density: GridNumberCellDensity;
|
|
308
|
+
increaseIsGood: boolean;
|
|
309
|
+
symbol: GridDeltaNumberSymbol;
|
|
310
|
+
}) {
|
|
311
|
+
const numeric = toFiniteNumber(value);
|
|
312
|
+
if (numeric == null) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const direction = directionOf(numeric);
|
|
317
|
+
const tone = deltaTone(direction, increaseIsGood);
|
|
318
|
+
const magnitude = unsigned(formatRaw(numeric, row, col));
|
|
319
|
+
let display = magnitude;
|
|
320
|
+
if (symbol === 'sign' && direction === 'increase') {
|
|
321
|
+
display = `+${magnitude}`;
|
|
322
|
+
} else if (symbol === 'sign' && direction === 'decrease') {
|
|
323
|
+
display = `${MINUS}${magnitude}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<NumberReadout
|
|
328
|
+
label={`${directionLabel(direction)} ${magnitude}`}
|
|
329
|
+
display={display}
|
|
330
|
+
tone={tone}
|
|
331
|
+
density={density}
|
|
332
|
+
>
|
|
333
|
+
<DeltaSymbol direction={direction} tone={tone} density={density} symbol={symbol} />
|
|
334
|
+
</NumberReadout>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function gridPolarityNumberColDef<R extends GridValidRowModel>(
|
|
339
|
+
options: GridPolarityNumberColDefOptions = {},
|
|
340
|
+
): Pick<GridBaseColDef<R>, 'type' | 'align' | 'headerAlign' | 'renderCell' | 'valueFormatter'> {
|
|
341
|
+
const density = options.density ?? 'default';
|
|
342
|
+
return {
|
|
343
|
+
type: 'number',
|
|
344
|
+
align: 'end',
|
|
345
|
+
headerAlign: 'end',
|
|
346
|
+
...(options.valueFormatter
|
|
347
|
+
? { valueFormatter: columnValueFormatter<R>(options.valueFormatter) }
|
|
348
|
+
: {}),
|
|
349
|
+
renderCell: ({ value, row, col }) => (
|
|
350
|
+
<PolarityNumberCell value={value} row={row} col={col} density={density} />
|
|
351
|
+
),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function gridDeltaNumberColDef<R extends GridValidRowModel>(
|
|
356
|
+
options: GridDeltaNumberColDefOptions = {},
|
|
357
|
+
): Pick<GridBaseColDef<R>, 'type' | 'align' | 'headerAlign' | 'renderCell' | 'valueFormatter'> {
|
|
358
|
+
const density = options.density ?? 'default';
|
|
359
|
+
const increaseIsGood = options.increaseIsGood ?? true;
|
|
360
|
+
const symbol = options.symbol ?? 'arrow';
|
|
361
|
+
return {
|
|
362
|
+
type: 'number',
|
|
363
|
+
align: 'end',
|
|
364
|
+
headerAlign: 'end',
|
|
365
|
+
...(options.valueFormatter
|
|
366
|
+
? { valueFormatter: columnValueFormatter<R>(options.valueFormatter) }
|
|
367
|
+
: {}),
|
|
368
|
+
renderCell: ({ value, row, col }) => (
|
|
369
|
+
<DeltaNumberCell
|
|
370
|
+
value={value}
|
|
371
|
+
row={row}
|
|
372
|
+
col={col}
|
|
373
|
+
density={density}
|
|
374
|
+
increaseIsGood={increaseIsGood}
|
|
375
|
+
symbol={symbol}
|
|
376
|
+
/>
|
|
377
|
+
),
|
|
378
|
+
};
|
|
379
|
+
}
|