@kanaries/graphic-walker 0.4.0 → 0.4.2
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/App.d.ts +5 -1
- package/dist/assets/buildMetricTable.worker-5555966a.js.map +1 -0
- package/dist/components/dataTable/index.d.ts +2 -1
- package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +21 -0
- package/dist/components/leafletRenderer/POIRenderer.d.ts +19 -0
- package/dist/components/leafletRenderer/encodings.d.ts +7 -0
- package/dist/components/leafletRenderer/geoConfigPanel.d.ts +3 -0
- package/dist/components/leafletRenderer/index.d.ts +14 -0
- package/dist/components/leafletRenderer/tooltip.d.ts +9 -0
- package/dist/components/leafletRenderer/utils.d.ts +2 -0
- package/dist/components/pivotTable/index.d.ts +2 -1
- package/dist/components/pivotTable/inteface.d.ts +6 -2
- package/dist/components/pivotTable/leftTree.d.ts +1 -0
- package/dist/components/pivotTable/topTree.d.ts +2 -0
- package/dist/components/pivotTable/utils.d.ts +1 -2
- package/dist/config.d.ts +3 -2
- package/dist/graphic-walker.es.js +37802 -30402
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +145 -137
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/interfaces.d.ts +27 -0
- package/dist/renderer/specRenderer.d.ts +2 -1
- package/dist/services.d.ts +7 -1
- package/dist/store/commonStore.d.ts +6 -0
- package/dist/store/visualSpecStore.d.ts +180 -4
- package/dist/utils/save.d.ts +1 -0
- package/dist/workers/buildPivotTable.d.ts +7 -0
- package/package.json +14 -2
- package/src/App.tsx +18 -4
- package/src/components/askViz/index.tsx +2 -1
- package/src/components/dataTable/index.tsx +7 -5
- package/src/components/leafletRenderer/ChoroplethRenderer.tsx +293 -0
- package/src/components/leafletRenderer/POIRenderer.tsx +170 -0
- package/src/components/leafletRenderer/encodings.ts +194 -0
- package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
- package/src/components/leafletRenderer/index.tsx +67 -0
- package/src/components/leafletRenderer/tooltip.tsx +24 -0
- package/src/components/leafletRenderer/utils.ts +52 -0
- package/src/components/limitSetting.tsx +8 -6
- package/src/components/pivotTable/index.tsx +171 -67
- package/src/components/pivotTable/inteface.ts +6 -2
- package/src/components/pivotTable/leftTree.tsx +24 -11
- package/src/components/pivotTable/metricTable.tsx +15 -10
- package/src/components/pivotTable/topTree.tsx +50 -17
- package/src/components/pivotTable/utils.ts +70 -11
- package/src/components/sizeSetting.tsx +9 -7
- package/src/components/visualConfig/index.tsx +17 -1
- package/src/computation/serverComputation.ts +8 -3
- package/src/config.ts +27 -16
- package/src/dataSource/table.tsx +9 -9
- package/src/fields/aestheticFields.tsx +4 -0
- package/src/fields/fieldsContext.tsx +3 -0
- package/src/fields/posFields/index.tsx +8 -2
- package/src/global.d.ts +4 -4
- package/src/hooks/index.ts +8 -0
- package/src/index.tsx +11 -9
- package/src/interfaces.ts +34 -0
- package/src/locales/en-US.json +27 -2
- package/src/locales/ja-JP.json +27 -2
- package/src/locales/zh-CN.json +27 -2
- package/src/renderer/hooks.ts +2 -48
- package/src/renderer/index.tsx +24 -1
- package/src/renderer/pureRenderer.tsx +26 -13
- package/src/renderer/specRenderer.tsx +45 -30
- package/src/services.ts +32 -23
- package/src/shadow-dom.tsx +7 -0
- package/src/store/commonStore.ts +29 -1
- package/src/store/visualSpecStore.ts +40 -24
- package/src/utils/save.ts +28 -1
- package/src/visualSettings/index.tsx +58 -7
- package/src/workers/buildMetricTable.worker.js +27 -0
- package/src/workers/buildPivotTable.ts +27 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|
2
|
+
import { useGlobalStore } from '../../store';
|
|
3
|
+
import { buildPivotTableService } from '../../services';
|
|
4
|
+
import { toWorkflow } from '../../utils/workflow';
|
|
5
|
+
import { dataQueryServer } from '../../computation/serverComputation';
|
|
6
|
+
import { useAppRootContext } from '../../components/appRoot';
|
|
4
7
|
import { observer } from 'mobx-react-lite';
|
|
5
8
|
import LeftTree from './leftTree';
|
|
6
9
|
import TopTree from './topTree';
|
|
7
10
|
import {
|
|
8
11
|
DeepReadonly,
|
|
9
12
|
DraggableFieldState,
|
|
13
|
+
IComputationFunction,
|
|
10
14
|
IDarkMode,
|
|
11
15
|
IRow,
|
|
12
16
|
IThemeKey,
|
|
@@ -14,24 +18,10 @@ import {
|
|
|
14
18
|
IVisualConfig,
|
|
15
19
|
} from '../../interfaces';
|
|
16
20
|
import { INestNode } from './inteface';
|
|
17
|
-
import { buildMetricTableFromNestTree, buildNestTree } from './utils';
|
|
18
21
|
import { unstable_batchedUpdates } from 'react-dom';
|
|
19
22
|
import MetricTable from './metricTable';
|
|
20
23
|
import { toJS } from 'mobx';
|
|
21
|
-
|
|
22
|
-
// const PTStateConnector = observer(function StateWrapper (props: PivotTableProps) {
|
|
23
|
-
// const store = usePivotTableStore();
|
|
24
|
-
// const { vizStore } = useGlobalStore();
|
|
25
|
-
// const { draggableFieldState } = vizStore;
|
|
26
|
-
// const { rows, columns } = draggableFieldState;
|
|
27
|
-
// return (
|
|
28
|
-
// <PivotTable
|
|
29
|
-
// {...props}
|
|
30
|
-
// draggableFieldState={draggableFieldState}
|
|
31
|
-
// visualConfig={visualConfig}
|
|
32
|
-
// />
|
|
33
|
-
// );
|
|
34
|
-
// })
|
|
24
|
+
import LoadingLayer from '../loadingLayer';
|
|
35
25
|
|
|
36
26
|
interface PivotTableProps {
|
|
37
27
|
themeKey?: IThemeKey;
|
|
@@ -40,23 +30,31 @@ interface PivotTableProps {
|
|
|
40
30
|
loading: boolean;
|
|
41
31
|
draggableFieldState: DeepReadonly<DraggableFieldState>;
|
|
42
32
|
visualConfig: DeepReadonly<IVisualConfig>;
|
|
33
|
+
computationFunction: IComputationFunction
|
|
43
34
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// const { draggableFieldState } = vizStore;
|
|
49
|
-
const { rows, columns } = draggableFieldState;
|
|
35
|
+
|
|
36
|
+
const PivotTable: React.FC<PivotTableProps> = observer(function PivotTableComponent (props) {
|
|
37
|
+
const { data, visualConfig, loading, computationFunction } = props;
|
|
38
|
+
const appRef = useAppRootContext();
|
|
50
39
|
const [leftTree, setLeftTree] = useState<INestNode | null>(null);
|
|
51
40
|
const [topTree, setTopTree] = useState<INestNode | null>(null);
|
|
52
41
|
const [metricTable, setMetricTable] = useState<any[][]>([]);
|
|
42
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
43
|
+
|
|
44
|
+
const { vizStore, commonStore } = useGlobalStore();
|
|
45
|
+
const { allFields, viewFilters, viewMeasures, sort, limit, draggableFieldState } = vizStore;
|
|
46
|
+
const { rows, columns } = draggableFieldState;
|
|
47
|
+
const { showTableSummary, defaultAggregated } = visualConfig;
|
|
48
|
+
const { tableCollapsedHeaderMap } = commonStore;
|
|
49
|
+
const aggData = useRef<IRow[]>([]);
|
|
50
|
+
const [ topTreeHeaderRowNum, setTopTreeHeaderRowNum ] = useState<number>(0);
|
|
53
51
|
|
|
54
52
|
const dimsInRow = useMemo(() => {
|
|
55
|
-
return rows.filter((f) => f.analyticType === 'dimension');
|
|
53
|
+
return toJS(rows).filter((f) => f.analyticType === 'dimension');
|
|
56
54
|
}, [rows]);
|
|
57
55
|
|
|
58
56
|
const dimsInColumn = useMemo(() => {
|
|
59
|
-
return columns.filter((f) => f.analyticType === 'dimension');
|
|
57
|
+
return toJS(columns).filter((f) => f.analyticType === 'dimension');
|
|
60
58
|
}, [columns]);
|
|
61
59
|
|
|
62
60
|
const measInRow = useMemo(() => {
|
|
@@ -68,51 +66,157 @@ const PivotTable: React.FC<PivotTableProps> = (props) => {
|
|
|
68
66
|
}, [columns]);
|
|
69
67
|
|
|
70
68
|
useEffect(() => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
if (tableCollapsedHeaderMap.size > 0) {
|
|
70
|
+
// If some visual configs change, clear the collapse state
|
|
71
|
+
// As tableCollapsedHeaderMap is also listened, data will be reaggregated later.
|
|
72
|
+
commonStore.resetTableCollapsedHeader();
|
|
73
|
+
// This forces data to be reaggregated if showTableSummary is on, as aggregation will be skipped later.
|
|
74
|
+
if (showTableSummary) {
|
|
75
|
+
aggregateGroupbyData();
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
aggregateThenGenerate();
|
|
79
|
+
}
|
|
80
|
+
}, [data]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (showTableSummary) {
|
|
84
|
+
// If showTableSummary is on, there is no need to generate extra queries. Directly generate new table.
|
|
85
|
+
generateNewTable();
|
|
86
|
+
} else {
|
|
87
|
+
aggregateThenGenerate();
|
|
86
88
|
}
|
|
87
|
-
}, [
|
|
89
|
+
}, [tableCollapsedHeaderMap]);
|
|
90
|
+
|
|
91
|
+
const aggregateThenGenerate = async() => {
|
|
92
|
+
await aggregateGroupbyData();
|
|
93
|
+
generateNewTable();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const generateNewTable = () => {
|
|
97
|
+
appRef.current?.updateRenderStatus('rendering');
|
|
98
|
+
setIsLoading(true);
|
|
99
|
+
buildPivotTableService(
|
|
100
|
+
dimsInRow,
|
|
101
|
+
dimsInColumn,
|
|
102
|
+
data,
|
|
103
|
+
aggData.current,
|
|
104
|
+
Array.from(tableCollapsedHeaderMap.keys()),
|
|
105
|
+
showTableSummary
|
|
106
|
+
)
|
|
107
|
+
.then((data) => {
|
|
108
|
+
const {lt, tt, metric} = data;
|
|
109
|
+
unstable_batchedUpdates(() => {
|
|
110
|
+
setLeftTree(lt);
|
|
111
|
+
setTopTree(tt);
|
|
112
|
+
setMetricTable(metric);
|
|
113
|
+
});
|
|
114
|
+
appRef.current?.updateRenderStatus('idle');
|
|
115
|
+
setIsLoading(false);
|
|
116
|
+
})
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
appRef.current?.updateRenderStatus('error');
|
|
119
|
+
console.log(err);
|
|
120
|
+
setIsLoading(false);
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const aggregateGroupbyData = () => {
|
|
125
|
+
if (dimsInRow.length === 0 && dimsInColumn.length === 0) return;
|
|
126
|
+
if (data.length === 0) return;
|
|
127
|
+
let groupbyCombListInRow:IViewField[][] = [];
|
|
128
|
+
let groupbyCombListInCol:IViewField[][] = [];
|
|
129
|
+
if (showTableSummary) {
|
|
130
|
+
groupbyCombListInRow = dimsInRow.map((dim, idx) => dimsInRow.slice(0, idx));
|
|
131
|
+
groupbyCombListInCol = dimsInColumn.map((dim, idx) => dimsInColumn.slice(0, idx));
|
|
132
|
+
} else {
|
|
133
|
+
const collapsedDimList = Array.from(tableCollapsedHeaderMap).map(([key, path]) => path[path.length - 1].key);
|
|
134
|
+
const collapsedDimsInRow = dimsInRow.filter((dim) => collapsedDimList.includes(dim.fid));
|
|
135
|
+
const collapsedDimsInColumn = dimsInColumn.filter((dim) => collapsedDimList.includes(dim.fid));
|
|
136
|
+
groupbyCombListInRow = collapsedDimsInRow.map((dim) => dimsInRow.slice(0, dimsInRow.indexOf(dim) + 1));
|
|
137
|
+
groupbyCombListInCol = collapsedDimsInColumn.map((dim) => dimsInColumn.slice(0, dimsInColumn.indexOf(dim) + 1));
|
|
138
|
+
}
|
|
139
|
+
groupbyCombListInRow.push(dimsInRow);
|
|
140
|
+
groupbyCombListInCol.push(dimsInColumn);
|
|
141
|
+
const groupbyCombList:IViewField[][] = groupbyCombListInCol.flatMap(combInCol =>
|
|
142
|
+
groupbyCombListInRow.map(combInRow => [...combInCol, ...combInRow])
|
|
143
|
+
).slice(0, -1);
|
|
144
|
+
setIsLoading(true);
|
|
145
|
+
appRef.current?.updateRenderStatus('computing');
|
|
146
|
+
const groupbyPromises: Promise<IRow[]>[] = groupbyCombList.map((dimComb) => {
|
|
147
|
+
const workflow = toWorkflow(
|
|
148
|
+
viewFilters,
|
|
149
|
+
allFields,
|
|
150
|
+
dimComb,
|
|
151
|
+
viewMeasures,
|
|
152
|
+
defaultAggregated,
|
|
153
|
+
sort,
|
|
154
|
+
limit > 0 ? limit : undefined
|
|
155
|
+
);
|
|
156
|
+
return dataQueryServer(computationFunction, workflow, limit > 0 ? limit : undefined)
|
|
157
|
+
.catch((err) => {
|
|
158
|
+
appRef.current?.updateRenderStatus('error');
|
|
159
|
+
return [];
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
return new Promise<void>((resolve, reject) => {
|
|
163
|
+
Promise.all(groupbyPromises)
|
|
164
|
+
.then((result) => {
|
|
165
|
+
setIsLoading(false);
|
|
166
|
+
const finalizedData = [...result.flat()];
|
|
167
|
+
aggData.current = finalizedData;
|
|
168
|
+
resolve();
|
|
169
|
+
})
|
|
170
|
+
.catch((err) => {
|
|
171
|
+
console.error(err);
|
|
172
|
+
setIsLoading(false);
|
|
173
|
+
reject();
|
|
174
|
+
});
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
};
|
|
88
178
|
|
|
89
179
|
// const { leftTree, topTree, metricTable } = store;
|
|
90
180
|
return (
|
|
91
|
-
<div className="
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
181
|
+
<div className="relative">
|
|
182
|
+
{(isLoading || loading) && <LoadingLayer />}
|
|
183
|
+
<div className="flex">
|
|
184
|
+
<table className="border border-gray-300 border-collapse">
|
|
185
|
+
<thead className="border border-gray-300">
|
|
186
|
+
{new Array(topTreeHeaderRowNum).fill(0).map((_, i) => (
|
|
187
|
+
<tr className="" key={i}>
|
|
188
|
+
<td className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 p-2 m-1 text-xs border border-gray-300" colSpan={dimsInRow.length + (measInRow.length > 0 ? 1 : 0)}>_</td>
|
|
189
|
+
</tr>
|
|
190
|
+
))}
|
|
191
|
+
</thead>
|
|
192
|
+
{leftTree &&
|
|
193
|
+
<LeftTree
|
|
194
|
+
data={leftTree}
|
|
195
|
+
dimsInRow={dimsInRow}
|
|
196
|
+
measInRow={measInRow}
|
|
197
|
+
onHeaderCollapse={commonStore.updateTableCollapsedHeader.bind(commonStore)}
|
|
198
|
+
/>}
|
|
199
|
+
</table>
|
|
200
|
+
<table className="border border-gray-300 border-collapse">
|
|
201
|
+
{topTree &&
|
|
202
|
+
<TopTree
|
|
203
|
+
data={topTree}
|
|
204
|
+
dimsInCol={dimsInColumn}
|
|
205
|
+
measInCol={measInColumn}
|
|
206
|
+
onHeaderCollapse={commonStore.updateTableCollapsedHeader.bind(commonStore)}
|
|
207
|
+
onTopTreeHeaderRowNumChange={(num) => setTopTreeHeaderRowNum(num)}
|
|
208
|
+
/>}
|
|
209
|
+
{metricTable &&
|
|
210
|
+
<MetricTable
|
|
211
|
+
matrix={metricTable}
|
|
212
|
+
meaInColumns={measInColumn}
|
|
213
|
+
meaInRows={measInRow}
|
|
214
|
+
/>}
|
|
215
|
+
</table>
|
|
216
|
+
</div>
|
|
106
217
|
</div>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
218
|
|
|
110
|
-
|
|
219
|
+
);
|
|
220
|
+
});
|
|
111
221
|
|
|
112
|
-
|
|
113
|
-
// return (
|
|
114
|
-
// <PivotTableStoreWrapper {...props}>
|
|
115
|
-
// <PivotTable />
|
|
116
|
-
// </PivotTableStoreWrapper>
|
|
117
|
-
// );
|
|
118
|
-
// };
|
|
222
|
+
export default PivotTable;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { IAggregator } from "../../interfaces";
|
|
2
2
|
|
|
3
3
|
export interface INestNode {
|
|
4
|
-
key: string;
|
|
5
|
-
value: string;
|
|
4
|
+
key: string | number;
|
|
5
|
+
value: string | number;
|
|
6
|
+
uniqueKey: string;
|
|
6
7
|
fieldKey: string;
|
|
7
8
|
children: INestNode[];
|
|
9
|
+
height: number;
|
|
10
|
+
isCollapsed: boolean;
|
|
11
|
+
path: Record<INestNode["fieldKey"], INestNode["value"]>[];
|
|
8
12
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { ReactNode, useMemo } from 'react';
|
|
2
2
|
import { INestNode } from './inteface';
|
|
3
3
|
import { IField } from '../../interfaces';
|
|
4
|
+
import { MinusCircleIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
|
|
4
5
|
|
|
5
6
|
function getChildCount(node: INestNode): number {
|
|
6
|
-
if (node.children.length === 0) {
|
|
7
|
+
if (node.isCollapsed || node.children.length === 0) {
|
|
7
8
|
return 1;
|
|
8
9
|
}
|
|
9
10
|
return node.children.map(getChildCount).reduce((a, b) => a + b, 0);
|
|
@@ -17,23 +18,34 @@ function getChildCount(node: INestNode): number {
|
|
|
17
18
|
* @param cellRows
|
|
18
19
|
* @returns
|
|
19
20
|
*/
|
|
20
|
-
function renderTree(node: INestNode, dimsInRow: IField[], depth: number, cellRows: ReactNode[][], meaNumber: number) {
|
|
21
|
+
function renderTree(node: INestNode, dimsInRow: IField[], depth: number, cellRows: ReactNode[][], meaNumber: number, onHeaderCollapse: (node: INestNode) => void) {
|
|
21
22
|
const childrenSize = getChildCount(node);
|
|
23
|
+
const { isCollapsed } = node;
|
|
22
24
|
if (depth > dimsInRow.length) {
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
27
|
cellRows[cellRows.length - 1].push(
|
|
26
28
|
<td
|
|
27
29
|
key={`${depth}-${node.fieldKey}-${node.value}`}
|
|
28
|
-
className=
|
|
29
|
-
|
|
30
|
+
className={`bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 align-top whitespace-nowrap p-2 text-xs m-1 border border-gray-300`}
|
|
31
|
+
colSpan={isCollapsed ? node.height + 1 : 1}
|
|
32
|
+
rowSpan={isCollapsed ? Math.max(meaNumber, 1) : childrenSize * Math.max(meaNumber, 1)}
|
|
30
33
|
>
|
|
31
|
-
|
|
34
|
+
<div className="flex">
|
|
35
|
+
<div>{node.value}</div>
|
|
36
|
+
{node.height > 0 && node.key !== "__total" && (
|
|
37
|
+
<>
|
|
38
|
+
{isCollapsed && <PlusCircleIcon className="w-3 ml-1 self-center cursor-pointer" onClick={() => onHeaderCollapse(node)} />}
|
|
39
|
+
{!isCollapsed && <MinusCircleIcon className="w-3 ml-1 self-center cursor-pointer" onClick={() => onHeaderCollapse(node)} />}
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
32
43
|
</td>
|
|
33
44
|
);
|
|
45
|
+
if (isCollapsed) return
|
|
34
46
|
for (let i = 0; i < node.children.length; i++) {
|
|
35
47
|
const child = node.children[i];
|
|
36
|
-
renderTree(child, dimsInRow, depth + 1, cellRows, meaNumber);
|
|
48
|
+
renderTree(child, dimsInRow, depth + 1, cellRows, meaNumber, onHeaderCollapse);
|
|
37
49
|
if (i < node.children.length - 1) {
|
|
38
50
|
cellRows.push([]);
|
|
39
51
|
}
|
|
@@ -44,12 +56,13 @@ export interface TreeProps {
|
|
|
44
56
|
data: INestNode;
|
|
45
57
|
dimsInRow: IField[];
|
|
46
58
|
measInRow: IField[];
|
|
59
|
+
onHeaderCollapse: (node: INestNode) => void;
|
|
47
60
|
}
|
|
48
61
|
const LeftTree: React.FC<TreeProps> = (props) => {
|
|
49
|
-
const { data, dimsInRow, measInRow } = props;
|
|
62
|
+
const { data, dimsInRow, measInRow, onHeaderCollapse } = props;
|
|
50
63
|
const nodeCells: ReactNode[] = useMemo(() => {
|
|
51
64
|
const cellRows: ReactNode[][] = [[]];
|
|
52
|
-
renderTree(data, dimsInRow, 0, cellRows, measInRow.length);
|
|
65
|
+
renderTree(data, dimsInRow, 0, cellRows, measInRow.length, onHeaderCollapse);
|
|
53
66
|
cellRows[0].shift();
|
|
54
67
|
if (measInRow.length > 0) {
|
|
55
68
|
const ans: ReactNode[][] = [];
|
|
@@ -58,7 +71,7 @@ const LeftTree: React.FC<TreeProps> = (props) => {
|
|
|
58
71
|
...row,
|
|
59
72
|
<td
|
|
60
73
|
key={`0-${measInRow[0].fid}-${measInRow[0].aggName}`}
|
|
61
|
-
className="whitespace-nowrap p-2 text-xs
|
|
74
|
+
className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 whitespace-nowrap p-2 text-xs m-1 border border-gray-300"
|
|
62
75
|
>
|
|
63
76
|
{measInRow[0].aggName}({measInRow[0].name})
|
|
64
77
|
</td>,
|
|
@@ -67,7 +80,7 @@ const LeftTree: React.FC<TreeProps> = (props) => {
|
|
|
67
80
|
ans.push([
|
|
68
81
|
<td
|
|
69
82
|
key={`${j}-${measInRow[j].fid}-${measInRow[j].aggName}`}
|
|
70
|
-
className="whitespace-nowrap p-2 text-xs
|
|
83
|
+
className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 whitespace-nowrap p-2 text-xs m-1 border border-gray-300"
|
|
71
84
|
>
|
|
72
85
|
{measInRow[j].aggName}({measInRow[j].name})
|
|
73
86
|
</td>,
|
|
@@ -79,7 +92,7 @@ const LeftTree: React.FC<TreeProps> = (props) => {
|
|
|
79
92
|
return cellRows;
|
|
80
93
|
}, [data, dimsInRow, measInRow]);
|
|
81
94
|
return (
|
|
82
|
-
<thead className="bg-gray-50 border border-gray-300
|
|
95
|
+
<thead className="bg-gray-50 border border-gray-300">
|
|
83
96
|
{nodeCells.map((row, rIndex) => (
|
|
84
97
|
<tr className="border border-gray-300" key={rIndex}>
|
|
85
98
|
{row}
|
|
@@ -15,11 +15,10 @@ function getCellData (cell: IRow, measure: IField) {
|
|
|
15
15
|
}
|
|
16
16
|
return cell[meaKey];
|
|
17
17
|
}
|
|
18
|
-
const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
18
|
+
const MetricTable: React.FC<MetricTableProps> = React.memo((props) => {
|
|
19
19
|
const { matrix, meaInRows, meaInColumns } = props;
|
|
20
|
-
|
|
21
20
|
return (
|
|
22
|
-
<tbody className="bg-white border-r border-b border-gray-300">
|
|
21
|
+
<tbody className="bg-white dark:bg-black text-gray-800 dark:text-gray-100 border-r border-b border-gray-300">
|
|
23
22
|
{matrix.map((row, rIndex) => {
|
|
24
23
|
if (meaInRows.length !== 0) {
|
|
25
24
|
return meaInRows.map((rowMea, rmIndex) => {
|
|
@@ -31,7 +30,7 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
31
30
|
if (meaInColumns.length !== 0) {
|
|
32
31
|
return meaInColumns.map((colMea, cmIndex) => (
|
|
33
32
|
<td
|
|
34
|
-
className="whitespace-nowrap p-2 text-xs
|
|
33
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
35
34
|
key={`${rIndex}-${cIndex}-${rowMea.fid}-${rowMea.aggName}-${colMea.fid}-${colMea.aggName}`}
|
|
36
35
|
>
|
|
37
36
|
{getCellData(cell, rowMea)} , {getCellData(cell, colMea)}
|
|
@@ -40,7 +39,7 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
40
39
|
}
|
|
41
40
|
return (
|
|
42
41
|
<td
|
|
43
|
-
className="whitespace-nowrap p-2 text-xs
|
|
42
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
44
43
|
key={`${rIndex}-${cIndex}-${rowMea.fid}-${rowMea.aggName}`}
|
|
45
44
|
>
|
|
46
45
|
{getCellData(cell, rowMea)}
|
|
@@ -59,7 +58,7 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
59
58
|
if (meaInRows.length === 0 && meaInColumns.length !== 0) {
|
|
60
59
|
return meaInColumns.map((colMea, cmIndex) => (
|
|
61
60
|
<td
|
|
62
|
-
className="whitespace-nowrap p-2 text-xs
|
|
61
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
63
62
|
key={`${rIndex}-${cIndex}-${cmIndex}-${colMea.fid}-${colMea.aggName}`}
|
|
64
63
|
>
|
|
65
64
|
{
|
|
@@ -70,7 +69,7 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
70
69
|
}else if (meaInRows.length === 0 && meaInColumns.length === 0) {
|
|
71
70
|
return (
|
|
72
71
|
<td
|
|
73
|
-
className="whitespace-nowrap p-2 text-xs
|
|
72
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
74
73
|
key={`${rIndex}-${cIndex}`}
|
|
75
74
|
>
|
|
76
75
|
{`True`}
|
|
@@ -79,12 +78,12 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
79
78
|
} else {
|
|
80
79
|
return meaInRows.flatMap((rowMea, rmIndex) => (
|
|
81
80
|
<td
|
|
82
|
-
className="whitespace-nowrap p-2 text-xs
|
|
81
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
83
82
|
key={`${rIndex}-${cIndex}-${rmIndex}-${rowMea.fid}-${rowMea.aggName}`}
|
|
84
83
|
>
|
|
85
84
|
{meaInColumns.flatMap((colMea, cmIndex) => (
|
|
86
85
|
<td
|
|
87
|
-
className="whitespace-nowrap p-2 text-xs
|
|
86
|
+
className="whitespace-nowrap p-2 text-xs"
|
|
88
87
|
key={`${rIndex}-${cIndex}-${rmIndex}-${cmIndex}-${colMea.fid}-${colMea.aggName}`}
|
|
89
88
|
>
|
|
90
89
|
{ getCellData(cell, rowMea) } , { getCellData(cell, colMea) }
|
|
@@ -102,6 +101,12 @@ const MetricTable: React.FC<MetricTableProps> = (props) => {
|
|
|
102
101
|
})}
|
|
103
102
|
</tbody>
|
|
104
103
|
);
|
|
105
|
-
}
|
|
104
|
+
}, function areEqual(prevProps, nextProps) {
|
|
105
|
+
if (JSON.stringify(prevProps.matrix) === JSON.stringify(nextProps.matrix)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
106
111
|
|
|
107
112
|
export default MetricTable;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React, { ReactNode, useMemo } from 'react';
|
|
1
|
+
import React, { ReactNode, useEffect, useMemo } from 'react';
|
|
2
2
|
import { INestNode } from './inteface';
|
|
3
3
|
import { IField } from '../../interfaces';
|
|
4
|
+
import { MinusCircleIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
|
|
4
5
|
|
|
5
6
|
function getChildCount(node: INestNode): number {
|
|
6
|
-
if (node.children.length === 0) {
|
|
7
|
+
if (node.isCollapsed || node.children.length === 0) {
|
|
7
8
|
return 1;
|
|
8
9
|
}
|
|
9
10
|
return node.children.map(getChildCount).reduce((a, b) => a + b, 0);
|
|
@@ -17,23 +18,34 @@ function getChildCount(node: INestNode): number {
|
|
|
17
18
|
* @param cellRows
|
|
18
19
|
* @returns
|
|
19
20
|
*/
|
|
20
|
-
function renderTree(node: INestNode, dimsInCol: IField[], depth: number, cellRows: ReactNode[][], meaNumber: number) {
|
|
21
|
+
function renderTree(node: INestNode, dimsInCol: IField[], depth: number, cellRows: ReactNode[][], meaNumber: number, onHeaderCollapse: (node: INestNode) => void) {
|
|
21
22
|
const childrenSize = getChildCount(node);
|
|
23
|
+
const { isCollapsed } = node;
|
|
22
24
|
if (depth > dimsInCol.length) {
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
27
|
cellRows[depth].push(
|
|
26
28
|
<td
|
|
27
|
-
key={`${depth}-${node.fieldKey}-${node.value}`}
|
|
28
|
-
className="whitespace-nowrap p-2 text-xs
|
|
29
|
-
colSpan={childrenSize * Math.max(meaNumber, 1)}
|
|
29
|
+
key={`${depth}-${node.fieldKey}-${node.value}-${cellRows[depth].length}`}
|
|
30
|
+
className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 align-top whitespace-nowrap p-2 text-xs m-1 border border-gray-300"
|
|
31
|
+
colSpan={isCollapsed ? Math.max(meaNumber, 1) : childrenSize * Math.max(meaNumber, 1)}
|
|
32
|
+
rowSpan={isCollapsed ? node.height + 1 : 1}
|
|
30
33
|
>
|
|
31
|
-
|
|
34
|
+
<div className="flex">
|
|
35
|
+
<div>{node.value}</div>
|
|
36
|
+
{node.height > 0 && node.key !== "__total" && (
|
|
37
|
+
<>
|
|
38
|
+
{isCollapsed && <PlusCircleIcon className="w-3 ml-1 self-center cursor-pointer" onClick={() => onHeaderCollapse(node)} />}
|
|
39
|
+
{!isCollapsed && <MinusCircleIcon className="w-3 ml-1 self-center cursor-pointer" onClick={() => onHeaderCollapse(node)} />}
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
32
43
|
</td>
|
|
33
44
|
);
|
|
45
|
+
if (isCollapsed) return;
|
|
34
46
|
for (let i = 0; i < node.children.length; i++) {
|
|
35
47
|
const child = node.children[i];
|
|
36
|
-
renderTree(child, dimsInCol, depth + 1, cellRows, meaNumber);
|
|
48
|
+
renderTree(child, dimsInCol, depth + 1, cellRows, meaNumber, onHeaderCollapse);
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -41,19 +53,35 @@ export interface TreeProps {
|
|
|
41
53
|
data: INestNode;
|
|
42
54
|
dimsInCol: IField[];
|
|
43
55
|
measInCol: IField[];
|
|
56
|
+
onHeaderCollapse: (node: INestNode) => void;
|
|
57
|
+
onTopTreeHeaderRowNumChange: (num: number) => void;
|
|
44
58
|
}
|
|
45
59
|
const TopTree: React.FC<TreeProps> = (props) => {
|
|
46
|
-
const { data, dimsInCol, measInCol } = props;
|
|
47
|
-
const nodeCells: ReactNode[] = useMemo(() => {
|
|
60
|
+
const { data, dimsInCol, measInCol, onHeaderCollapse, onTopTreeHeaderRowNumChange } = props;
|
|
61
|
+
const nodeCells: ReactNode[][] = useMemo(() => {
|
|
48
62
|
const cellRows: ReactNode[][] = new Array(dimsInCol.length + 1).fill(0).map(() => []);
|
|
49
|
-
renderTree(data, dimsInCol, 0, cellRows, measInCol.length);
|
|
50
|
-
const totalChildrenSize =
|
|
63
|
+
renderTree(data, dimsInCol, 0, cellRows, measInCol.length, onHeaderCollapse);
|
|
64
|
+
const totalChildrenSize = getChildCount(data);
|
|
65
|
+
|
|
66
|
+
// if all children in one layer are collapsed, then we need to reset the rowSpan of all children to 1
|
|
67
|
+
cellRows.forEach((row: ReactNode[], rowIdx: number) => {
|
|
68
|
+
const rowSpanArr = row.map(child => React.isValidElement(child) ? child.props.rowSpan : 0)
|
|
69
|
+
if (rowSpanArr.length > 0 && rowSpanArr[0] > 1 && rowSpanArr.every(v => v === rowSpanArr[0])) {
|
|
70
|
+
row.forEach((childObj, childIdx) => {
|
|
71
|
+
if (React.isValidElement(childObj)) {
|
|
72
|
+
const newChild = React.cloneElement(childObj, {...childObj.props, rowSpan: 1});
|
|
73
|
+
cellRows[rowIdx][childIdx] = newChild;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
51
79
|
cellRows.push(
|
|
52
|
-
new Array(totalChildrenSize).fill(0).flatMap(() =>
|
|
80
|
+
new Array(totalChildrenSize).fill(0).flatMap((ele, idx) =>
|
|
53
81
|
measInCol.map((m) => (
|
|
54
82
|
<td
|
|
55
|
-
key={`${cellRows.length}-${m.fid}-${m.aggName}`}
|
|
56
|
-
className="whitespace-nowrap p-2 text-xs
|
|
83
|
+
key={`${cellRows.length}-${m.fid}-${m.aggName}-${idx}`}
|
|
84
|
+
className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 whitespace-nowrap p-2 text-xs m-1 border border-gray-300"
|
|
57
85
|
>
|
|
58
86
|
{m.aggName}({m.name})
|
|
59
87
|
</td>
|
|
@@ -63,10 +91,15 @@ const TopTree: React.FC<TreeProps> = (props) => {
|
|
|
63
91
|
cellRows.shift();
|
|
64
92
|
return cellRows;
|
|
65
93
|
}, [data, dimsInCol, measInCol]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
onTopTreeHeaderRowNumChange(nodeCells.filter((row) => row.length > 0).length);
|
|
97
|
+
}, [nodeCells]);
|
|
98
|
+
|
|
66
99
|
return (
|
|
67
|
-
<thead className="border border-gray-300 bg-gray-50
|
|
100
|
+
<thead className="border border-gray-300 bg-gray-50">
|
|
68
101
|
{nodeCells.map((row, rIndex) => (
|
|
69
|
-
<tr className="border border-gray-300
|
|
102
|
+
<tr className={`${row?.length > 0 ? "" : "hidden"} border border-gray-300`} key={rIndex}>
|
|
70
103
|
{row}
|
|
71
104
|
</tr>
|
|
72
105
|
))}
|