@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.
Files changed (66) hide show
  1. package/dist/App.d.ts +5 -1
  2. package/dist/assets/buildMetricTable.worker-5555966a.js.map +1 -0
  3. package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +22 -0
  4. package/dist/components/leafletRenderer/POIRenderer.d.ts +20 -0
  5. package/dist/components/leafletRenderer/encodings.d.ts +7 -0
  6. package/dist/components/leafletRenderer/geoConfigPanel.d.ts +3 -0
  7. package/dist/components/leafletRenderer/index.d.ts +15 -0
  8. package/dist/components/leafletRenderer/tooltip.d.ts +9 -0
  9. package/dist/components/leafletRenderer/utils.d.ts +2 -0
  10. package/dist/components/pivotTable/index.d.ts +2 -1
  11. package/dist/components/pivotTable/inteface.d.ts +6 -2
  12. package/dist/components/pivotTable/leftTree.d.ts +1 -0
  13. package/dist/components/pivotTable/topTree.d.ts +2 -0
  14. package/dist/components/pivotTable/utils.d.ts +1 -2
  15. package/dist/config.d.ts +3 -2
  16. package/dist/graphic-walker.es.js +37811 -30386
  17. package/dist/graphic-walker.es.js.map +1 -1
  18. package/dist/graphic-walker.umd.js +145 -137
  19. package/dist/graphic-walker.umd.js.map +1 -1
  20. package/dist/interfaces.d.ts +28 -0
  21. package/dist/renderer/specRenderer.d.ts +2 -1
  22. package/dist/services.d.ts +7 -1
  23. package/dist/store/commonStore.d.ts +6 -0
  24. package/dist/store/visualSpecStore.d.ts +180 -4
  25. package/dist/utils/save.d.ts +1 -0
  26. package/dist/workers/buildPivotTable.d.ts +7 -0
  27. package/package.json +14 -2
  28. package/src/App.tsx +18 -4
  29. package/src/components/leafletRenderer/ChoroplethRenderer.tsx +312 -0
  30. package/src/components/leafletRenderer/POIRenderer.tsx +189 -0
  31. package/src/components/leafletRenderer/encodings.ts +194 -0
  32. package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
  33. package/src/components/leafletRenderer/index.tsx +70 -0
  34. package/src/components/leafletRenderer/tooltip.tsx +24 -0
  35. package/src/components/leafletRenderer/utils.ts +52 -0
  36. package/src/components/pivotTable/index.tsx +171 -67
  37. package/src/components/pivotTable/inteface.ts +6 -2
  38. package/src/components/pivotTable/leftTree.tsx +24 -11
  39. package/src/components/pivotTable/metricTable.tsx +15 -10
  40. package/src/components/pivotTable/topTree.tsx +50 -17
  41. package/src/components/pivotTable/utils.ts +70 -11
  42. package/src/components/visualConfig/index.tsx +17 -1
  43. package/src/config.ts +27 -16
  44. package/src/dataSource/table.tsx +7 -11
  45. package/src/fields/aestheticFields.tsx +4 -0
  46. package/src/fields/fieldsContext.tsx +3 -0
  47. package/src/fields/posFields/index.tsx +8 -2
  48. package/src/global.d.ts +4 -4
  49. package/src/index.tsx +11 -9
  50. package/src/interfaces.ts +35 -0
  51. package/src/locales/en-US.json +27 -2
  52. package/src/locales/ja-JP.json +27 -2
  53. package/src/locales/zh-CN.json +27 -2
  54. package/src/renderer/hooks.ts +1 -1
  55. package/src/renderer/index.tsx +24 -1
  56. package/src/renderer/pureRenderer.tsx +27 -13
  57. package/src/renderer/specRenderer.tsx +46 -30
  58. package/src/services.ts +32 -23
  59. package/src/shadow-dom.tsx +7 -0
  60. package/src/store/commonStore.ts +29 -1
  61. package/src/store/visualSpecStore.ts +38 -23
  62. package/src/utils/save.ts +28 -1
  63. package/src/utils/vegaApiExport.ts +3 -0
  64. package/src/visualSettings/index.tsx +58 -6
  65. package/src/workers/buildMetricTable.worker.js +27 -0
  66. 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="whitespace-nowrap p-2 text-xs text-gray-500 m-1 border border-gray-300"
29
- rowSpan={childrenSize * Math.max(meaNumber, 1)}
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
- {node.value}
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 text-gray-500 m-1 border border-gray-300"
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 text-gray-500 m-1 border border-gray-300"
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 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 text-gray-500"
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 text-gray-500"
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 text-gray-500"
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 text-gray-500"
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 text-gray-500"
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 text-gray-500"
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 text-gray-500 m-1 border border-gray-300"
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
- {node.value}
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 = cellRows[cellRows.length - 1].length;
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 text-gray-500 m-1 border border-gray-300"
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 border border-gray-300">
100
+ <thead className="border border-gray-300 bg-gray-50">
68
101
  {nodeCells.map((row, rIndex) => (
69
- <tr className="border border-gray-300" key={rIndex}>
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
- export function insertNode (tree: INestNode, layerKeys: string[], nodeData: IRow, depth: number) {
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.children.push(child);
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
- export function buildNestTree (layerKeys: string[], data: IRow[]): INestNode {
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: any }[] {
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 row = data.find(r => predicates.every(pre => r[pre.key] === pre.value))
126
- vec.push(row)
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, useRef, useState } from 'react';
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
- 'auto',
5
- 'bar',
6
- 'line',
7
- 'area',
8
- 'trail',
9
- 'point',
10
- 'circle',
11
- 'tick',
12
- 'rect',
13
- 'arc',
14
- 'text',
15
- 'boxplot',
16
- 'table'
17
- ] as const;
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',
@@ -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 tempDataset = React.useMemo(() => {
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={tempDataset}
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 DOM: {
3
- setHead: (head: HTMLElement | ShadowRoot) => void;
4
- setBody: (body: HTMLElement | ShadowRoot) => void;
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
  }