@kanaries/graphic-walker 0.4.1 → 0.4.3
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/leafletRenderer/ChoroplethRenderer.d.ts +22 -0
- package/dist/components/leafletRenderer/POIRenderer.d.ts +20 -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 +15 -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 +37811 -30386
- 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/interfaces.d.ts +28 -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/leafletRenderer/ChoroplethRenderer.tsx +312 -0
- package/src/components/leafletRenderer/POIRenderer.tsx +189 -0
- package/src/components/leafletRenderer/encodings.ts +194 -0
- package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
- package/src/components/leafletRenderer/index.tsx +70 -0
- package/src/components/leafletRenderer/tooltip.tsx +24 -0
- package/src/components/leafletRenderer/utils.ts +52 -0
- 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/visualConfig/index.tsx +17 -1
- package/src/config.ts +27 -16
- package/src/dataSource/table.tsx +7 -11
- 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/index.tsx +11 -9
- package/src/interfaces.ts +35 -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 +1 -1
- package/src/renderer/index.tsx +24 -1
- package/src/renderer/pureRenderer.tsx +27 -13
- package/src/renderer/specRenderer.tsx +46 -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 +38 -23
- package/src/utils/save.ts +28 -1
- package/src/utils/vegaApiExport.ts +3 -0
- package/src/visualSettings/index.tsx +58 -6
- package/src/workers/buildMetricTable.worker.js +27 -0
- package/src/workers/buildPivotTable.ts +27 -0
|
@@ -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
|
))}
|
|
@@ -3,12 +3,13 @@ import { INestNode } from "./inteface";
|
|
|
3
3
|
|
|
4
4
|
const key_prefix = 'nk_';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function insertNode (tree: INestNode, layerKeys: string[], nodeData: IRow, depth: number, collapsedKeyList: string[]) {
|
|
7
7
|
if (depth >= layerKeys.length) {
|
|
8
8
|
// tree.key = nodeData[layerKeys[depth]];
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
const key = nodeData[layerKeys[depth]];
|
|
12
|
+
const uniqueKey = `${tree.uniqueKey}__${key}`;
|
|
12
13
|
// console.log({
|
|
13
14
|
// key,
|
|
14
15
|
// nodeData,
|
|
@@ -23,25 +24,78 @@ export function insertNode (tree: INestNode, layerKeys: string[], nodeData: IRow
|
|
|
23
24
|
child = {
|
|
24
25
|
key,
|
|
25
26
|
value: key,
|
|
27
|
+
uniqueKey: uniqueKey,
|
|
26
28
|
fieldKey: layerKeys[depth],
|
|
27
29
|
children: [],
|
|
30
|
+
path: [...tree.path, {key: layerKeys[depth], value: key}],
|
|
31
|
+
height: layerKeys.length - depth - 1,
|
|
32
|
+
isCollapsed: false,
|
|
28
33
|
}
|
|
29
|
-
tree.
|
|
34
|
+
if (collapsedKeyList.includes(tree.uniqueKey)) {
|
|
35
|
+
tree.isCollapsed = true;
|
|
36
|
+
}
|
|
37
|
+
tree.children.splice(binarySearchIndex(tree.children, child.key), 0, child);
|
|
30
38
|
}
|
|
31
|
-
insertNode(child, layerKeys, nodeData, depth + 1);
|
|
39
|
+
insertNode(child, layerKeys, nodeData, depth + 1, collapsedKeyList);
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Custom binary search function to find appropriate index for insertion.
|
|
44
|
+
function binarySearchIndex(arr: INestNode[], keyVal: string | number): number {
|
|
45
|
+
let start = 0, end = arr.length - 1;
|
|
32
46
|
|
|
47
|
+
while (start <= end) {
|
|
48
|
+
let middle = Math.floor((start + end) / 2);
|
|
49
|
+
let middleVal = arr[middle].key;
|
|
50
|
+
if (typeof middleVal === 'number' && typeof keyVal === 'number') {
|
|
51
|
+
if (middleVal < keyVal) start = middle + 1;
|
|
52
|
+
else end = middle - 1;
|
|
53
|
+
} else {
|
|
54
|
+
let cmp = String(middleVal).localeCompare(String(keyVal));
|
|
55
|
+
if (cmp < 0) start = middle + 1;
|
|
56
|
+
else end = middle - 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return start;
|
|
33
60
|
}
|
|
61
|
+
|
|
34
62
|
const ROOT_KEY = '__root';
|
|
63
|
+
const TOTAL_KEY = '__total'
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
function insertSummaryNode (node: INestNode): void {
|
|
66
|
+
if (node.children.length > 0) {
|
|
67
|
+
node.children.push({
|
|
68
|
+
key: TOTAL_KEY,
|
|
69
|
+
value: 'total',
|
|
70
|
+
fieldKey: node.children[0].fieldKey,
|
|
71
|
+
uniqueKey: `${node.uniqueKey}${TOTAL_KEY}`,
|
|
72
|
+
children: [],
|
|
73
|
+
path: [],
|
|
74
|
+
height: node.children[0].height,
|
|
75
|
+
isCollapsed: true,
|
|
76
|
+
});
|
|
77
|
+
for (let i = 0; i < node.children.length - 1; i ++) {
|
|
78
|
+
insertSummaryNode(node.children[i]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function buildNestTree (layerKeys: string[], data: IRow[], collapsedKeyList: string[], showSummary: boolean): INestNode {
|
|
37
84
|
const tree: INestNode = {
|
|
38
85
|
key: ROOT_KEY,
|
|
39
86
|
value: 'root',
|
|
40
87
|
fieldKey: 'root',
|
|
88
|
+
uniqueKey: ROOT_KEY,
|
|
41
89
|
children: [],
|
|
90
|
+
path: [],
|
|
91
|
+
height: layerKeys.length,
|
|
92
|
+
isCollapsed: false,
|
|
42
93
|
};
|
|
43
94
|
for (let row of data) {
|
|
44
|
-
insertNode(tree, layerKeys, row, 0);
|
|
95
|
+
insertNode(tree, layerKeys, row, 0, collapsedKeyList);
|
|
96
|
+
}
|
|
97
|
+
if (showSummary) {
|
|
98
|
+
insertSummaryNode(tree);
|
|
45
99
|
}
|
|
46
100
|
return tree;
|
|
47
101
|
}
|
|
@@ -56,7 +110,7 @@ class NodeIterator {
|
|
|
56
110
|
public first () {
|
|
57
111
|
let node = this.tree
|
|
58
112
|
this.nodeStack = [node];
|
|
59
|
-
while (node.children.length > 0) {
|
|
113
|
+
while (node.children.length > 0 && !node.isCollapsed) {
|
|
60
114
|
this.nodeStack.push(node.children[0])
|
|
61
115
|
node = node.children[0]
|
|
62
116
|
}
|
|
@@ -75,7 +129,7 @@ class NodeIterator {
|
|
|
75
129
|
if (nodeIndex === -1) break;
|
|
76
130
|
// console.log(this.nodeStack.map(n => `${n.fieldKey}-${n.value}`))
|
|
77
131
|
if (cursorMoved) {
|
|
78
|
-
if (node.children.length > 0) {
|
|
132
|
+
if (node.children.length > 0 && !node.isCollapsed) {
|
|
79
133
|
this.nodeStack.push(node.children[0]);
|
|
80
134
|
continue;
|
|
81
135
|
} else {
|
|
@@ -102,7 +156,7 @@ class NodeIterator {
|
|
|
102
156
|
// console.log(this.current)
|
|
103
157
|
return this.current;
|
|
104
158
|
}
|
|
105
|
-
public predicates (): { key: string; value:
|
|
159
|
+
public predicates (): { key: string; value: string | number }[] {
|
|
106
160
|
return this.nodeStack.filter(node => node.key !== ROOT_KEY).map(node => ({
|
|
107
161
|
key: node.fieldKey,
|
|
108
162
|
value: node.value
|
|
@@ -121,9 +175,14 @@ export function buildMetricTableFromNestTree (leftTree: INestNode, topTree: INes
|
|
|
121
175
|
const vec: any[] = [];
|
|
122
176
|
iteTop.first();
|
|
123
177
|
while (iteTop.current !== null) {
|
|
124
|
-
const predicates = iteLeft.predicates().concat(iteTop.predicates());
|
|
125
|
-
const
|
|
126
|
-
|
|
178
|
+
const predicates = iteLeft.predicates().concat(iteTop.predicates()).filter((ele) => ele.value !== "total");
|
|
179
|
+
const matchedRows = data.filter(r => predicates.every(pre => r[pre.key] === pre.value));
|
|
180
|
+
if (matchedRows.length > 0) {
|
|
181
|
+
// If multiple rows are matched, then find the most matched one (the row with smallest number of keys)
|
|
182
|
+
vec.push(matchedRows.reduce((a, b) => Object.keys(a).length < Object.keys(b).length ? a : b));
|
|
183
|
+
} else {
|
|
184
|
+
vec.push(undefined);
|
|
185
|
+
}
|
|
127
186
|
iteTop.next();
|
|
128
187
|
}
|
|
129
188
|
mat.push(vec)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React, { useEffect,
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { useGlobalStore } from '../../store';
|
|
4
4
|
import { NonPositionChannelConfigList,PositionChannelConfigList } from '../../config';
|
|
5
5
|
|
|
@@ -15,6 +15,8 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
15
15
|
const { commonStore, vizStore } = useGlobalStore();
|
|
16
16
|
const { showVisualConfigPanel } = commonStore;
|
|
17
17
|
const { visualConfig } = vizStore;
|
|
18
|
+
const { coordSystem, geoms: [markType] } = visualConfig;
|
|
19
|
+
const isChoropleth = coordSystem === 'geographic' && markType === 'choropleth';
|
|
18
20
|
const { t } = useTranslation();
|
|
19
21
|
const formatConfigList: (keyof IVisualConfig['format'])[] = [
|
|
20
22
|
'numberFormat',
|
|
@@ -35,12 +37,14 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
35
37
|
size: visualConfig.resolve.size,
|
|
36
38
|
});
|
|
37
39
|
const [zeroScale, setZeroScale] = useState<boolean>(visualConfig.zeroScale);
|
|
40
|
+
const [scaleIncludeUnmatchedChoropleth, setScaleIncludeUnmatchedChoropleth] = useState<boolean>(visualConfig.scaleIncludeUnmatchedChoropleth ?? false);
|
|
38
41
|
const [background, setBackground] = useState<string | undefined>(visualConfig.background);
|
|
39
42
|
|
|
40
43
|
useEffect(() => {
|
|
41
44
|
setZeroScale(visualConfig.zeroScale);
|
|
42
45
|
setBackground(visualConfig.background);
|
|
43
46
|
setResolve(toJS(visualConfig.resolve));
|
|
47
|
+
setScaleIncludeUnmatchedChoropleth(visualConfig.scaleIncludeUnmatchedChoropleth ?? false);
|
|
44
48
|
setFormat({
|
|
45
49
|
numberFormat: visualConfig.format.numberFormat,
|
|
46
50
|
timeFormat: visualConfig.format.timeFormat,
|
|
@@ -140,6 +144,17 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
140
144
|
}}
|
|
141
145
|
/>
|
|
142
146
|
</div>
|
|
147
|
+
{isChoropleth && (
|
|
148
|
+
<div className="my-2">
|
|
149
|
+
<Toggle
|
|
150
|
+
label="include unmatched choropleth in scale"
|
|
151
|
+
enabled={scaleIncludeUnmatchedChoropleth}
|
|
152
|
+
onChange={(en) => {
|
|
153
|
+
setScaleIncludeUnmatchedChoropleth(en);
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
143
158
|
<div className="mt-4">
|
|
144
159
|
<PrimaryButton
|
|
145
160
|
text={t('actions.confirm')}
|
|
@@ -148,6 +163,7 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
148
163
|
runInAction(() => {
|
|
149
164
|
vizStore.setVisualConfig('format', format);
|
|
150
165
|
vizStore.setVisualConfig('zeroScale', zeroScale);
|
|
166
|
+
vizStore.setVisualConfig('scaleIncludeUnmatchedChoropleth', scaleIncludeUnmatchedChoropleth);
|
|
151
167
|
vizStore.setVisualConfig('background', background);
|
|
152
168
|
vizStore.setVisualConfig('resolve', resolve);
|
|
153
169
|
commonStore.setShowVisualConfigPanel(false);
|
package/src/config.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import { DraggableFieldState, IStackMode, IVisualConfig } from "./interfaces";
|
|
1
|
+
import { DraggableFieldState, ICoordMode, IStackMode, IVisualConfig } from "./interfaces";
|
|
2
2
|
|
|
3
|
-
export const GEMO_TYPES: Readonly<string[]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
export const GEMO_TYPES: Record<ICoordMode, Readonly<string[]>> = {
|
|
4
|
+
generic: [
|
|
5
|
+
'auto',
|
|
6
|
+
'bar',
|
|
7
|
+
'line',
|
|
8
|
+
'area',
|
|
9
|
+
'trail',
|
|
10
|
+
'point',
|
|
11
|
+
'circle',
|
|
12
|
+
'tick',
|
|
13
|
+
'rect',
|
|
14
|
+
'arc',
|
|
15
|
+
'text',
|
|
16
|
+
'boxplot',
|
|
17
|
+
'table',
|
|
18
|
+
],
|
|
19
|
+
geographic: [
|
|
20
|
+
'poi',
|
|
21
|
+
'choropleth',
|
|
22
|
+
],
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const COORD_TYPES: Readonly<ICoordMode[]> = [
|
|
26
|
+
'generic',
|
|
27
|
+
'geographic',
|
|
28
|
+
];
|
|
18
29
|
|
|
19
30
|
export const STACK_MODE: Readonly<IStackMode[]> = [
|
|
20
31
|
'none',
|
package/src/dataSource/table.tsx
CHANGED
|
@@ -14,21 +14,17 @@ const Table: React.FC<TableProps> = (props) => {
|
|
|
14
14
|
const { commonStore } = useGlobalStore();
|
|
15
15
|
const { tmpDSRawFields, tmpDataSource } = commonStore;
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
return {
|
|
19
|
-
id: "tmp",
|
|
20
|
-
name: "tmp",
|
|
21
|
-
dataSource: tmpDataSource,
|
|
22
|
-
rawFields: toJS(tmpDSRawFields),
|
|
23
|
-
};
|
|
24
|
-
}, [tmpDataSource, tmpDSRawFields]);
|
|
25
|
-
9
|
|
26
|
-
const computation = React.useMemo(() => getComputation(tempDataset.dataSource), [tempDataset])
|
|
17
|
+
const computation = React.useMemo(() => getComputation(tmpDataSource), [tmpDataSource])
|
|
27
18
|
|
|
28
19
|
return (
|
|
29
20
|
<DataTable
|
|
30
21
|
size={size}
|
|
31
|
-
dataset={
|
|
22
|
+
dataset={{
|
|
23
|
+
id: "tmp",
|
|
24
|
+
name: "tmp",
|
|
25
|
+
dataSource: tmpDataSource,
|
|
26
|
+
rawFields: toJS(tmpDSRawFields),
|
|
27
|
+
}}
|
|
32
28
|
computation={computation}
|
|
33
29
|
total={tmpDataSource.length}
|
|
34
30
|
onMetaChange={(fid, fIndex, diffMeta) => {
|
|
@@ -26,6 +26,10 @@ const AestheticFields: React.FC = props => {
|
|
|
26
26
|
return aestheticFields.filter(f => f.id === 'text' || f.id === 'color' || f.id === 'size' || f.id === 'opacity');
|
|
27
27
|
case 'table':
|
|
28
28
|
return []
|
|
29
|
+
case 'poi':
|
|
30
|
+
return aestheticFields.filter(f => f.id === 'color' || f.id === 'opacity' || f.id === 'size' || f.id === 'details');
|
|
31
|
+
case 'choropleth':
|
|
32
|
+
return aestheticFields.filter(f => f.id === 'color' || f.id === 'opacity' || f.id === 'text' || f.id === 'details');
|
|
29
33
|
default:
|
|
30
34
|
return aestheticFields.filter(f => f.id !== 'text');
|
|
31
35
|
}
|
|
@@ -48,6 +48,9 @@ export const DRAGGABLE_STATE_KEYS: Readonly<IDraggableStateKey[]> = [
|
|
|
48
48
|
{ id: 'shape', mode: 1},
|
|
49
49
|
{ id: 'theta', mode: 1 },
|
|
50
50
|
{ id: 'radius', mode: 1 },
|
|
51
|
+
{ id: 'longitude', mode: 1 },
|
|
52
|
+
{ id: 'latitude', mode: 1 },
|
|
53
|
+
{ id: 'geoId', mode: 1 },
|
|
51
54
|
{ id: 'filters', mode: 1 },
|
|
52
55
|
{ id: 'details', mode: 1 },
|
|
53
56
|
{ id: 'text', mode: 1 },
|
|
@@ -9,14 +9,20 @@ import OBFieldContainer from '../obComponents/obFContainer';
|
|
|
9
9
|
const PosFields: React.FC = props => {
|
|
10
10
|
const { vizStore } = useGlobalStore();
|
|
11
11
|
const { visualConfig } = vizStore;
|
|
12
|
-
const { geoms } = visualConfig;
|
|
12
|
+
const { geoms, coordSystem = 'generic' } = visualConfig;
|
|
13
13
|
|
|
14
14
|
const channels = useMemo(() => {
|
|
15
|
+
if (coordSystem === 'geographic') {
|
|
16
|
+
if (geoms[0] === 'choropleth') {
|
|
17
|
+
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'geoId');
|
|
18
|
+
}
|
|
19
|
+
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'longitude' || f.id === 'latitude');
|
|
20
|
+
}
|
|
15
21
|
if (geoms[0] === 'arc') {
|
|
16
22
|
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'radius' || f.id === 'theta');
|
|
17
23
|
}
|
|
18
24
|
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'columns' || f.id === 'rows');
|
|
19
|
-
}, [geoms[0]])
|
|
25
|
+
}, [geoms[0], coordSystem])
|
|
20
26
|
return <div>
|
|
21
27
|
{
|
|
22
28
|
channels.map(dkey => <FieldListContainer name={dkey.id} key={dkey.id}>
|
package/src/global.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare module '@kanaries/react-beautiful-dnd' {
|
|
2
|
-
export const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
export const DOMProvider: import('react').Provider<{
|
|
3
|
+
head: HTMLElement | ShadowRoot;
|
|
4
|
+
body: HTMLElement | ShadowRoot;
|
|
5
|
+
}>;
|
|
6
6
|
export * from 'react-beautiful-dnd';
|
|
7
7
|
}
|