@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
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
import { ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
2
2
|
import React, { useState, useEffect } from "react";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { useDebounceValueBind } from "../hooks";
|
|
4
5
|
|
|
5
6
|
interface SizeSettingProps {
|
|
6
7
|
onWidthChange: (val: number) => void;
|
|
@@ -12,7 +13,8 @@ interface SizeSettingProps {
|
|
|
12
13
|
export const ResizeDialog: React.FC<SizeSettingProps> = (props) => {
|
|
13
14
|
const { onWidthChange, onHeightChange, width, height, children } = props;
|
|
14
15
|
const { t } = useTranslation("translation", { keyPrefix: "main.tabpanel.settings.size_setting" });
|
|
15
|
-
|
|
16
|
+
const [innerWidth, setInnerWidth] = useDebounceValueBind(width, onWidthChange);
|
|
17
|
+
const [innerHeight, setInnerHeight] = useDebounceValueBind(height, onHeightChange);
|
|
16
18
|
return (
|
|
17
19
|
<div className="text-zinc-400">
|
|
18
20
|
{children}
|
|
@@ -22,16 +24,16 @@ export const ResizeDialog: React.FC<SizeSettingProps> = (props) => {
|
|
|
22
24
|
style={{ cursor: "ew-resize" }}
|
|
23
25
|
type="range"
|
|
24
26
|
name="width"
|
|
25
|
-
value={Math.sqrt(
|
|
27
|
+
value={Math.sqrt(innerWidth / 1000)}
|
|
26
28
|
min="0"
|
|
27
29
|
max="1"
|
|
28
30
|
step="0.01"
|
|
29
31
|
onChange={(e) => {
|
|
30
|
-
|
|
32
|
+
setInnerWidth(Math.round(Number(e.target.value) ** 2 * 1000));
|
|
31
33
|
}}
|
|
32
34
|
/>
|
|
33
35
|
<output className="text-sm ml-1" htmlFor="width">
|
|
34
|
-
{`${t("width")}: ${
|
|
36
|
+
{`${t("width")}: ${innerWidth}`}
|
|
35
37
|
</output>
|
|
36
38
|
</div>
|
|
37
39
|
<div className=" mt-2">
|
|
@@ -40,16 +42,16 @@ export const ResizeDialog: React.FC<SizeSettingProps> = (props) => {
|
|
|
40
42
|
style={{ cursor: "ew-resize" }}
|
|
41
43
|
type="range"
|
|
42
44
|
name="height"
|
|
43
|
-
value={Math.sqrt(
|
|
45
|
+
value={Math.sqrt(innerHeight / 1000)}
|
|
44
46
|
min="0"
|
|
45
47
|
max="1"
|
|
46
48
|
step="0.01"
|
|
47
49
|
onChange={(e) => {
|
|
48
|
-
|
|
50
|
+
setInnerHeight(Math.round(Number(e.target.value) ** 2 * 1000));
|
|
49
51
|
}}
|
|
50
52
|
/>
|
|
51
53
|
<output className="text-sm ml-1" htmlFor="height">
|
|
52
|
-
{`${t("height")}: ${
|
|
54
|
+
{`${t("height")}: ${innerHeight}`}
|
|
53
55
|
</output>
|
|
54
56
|
</div>
|
|
55
57
|
</div>
|
|
@@ -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);
|
|
@@ -29,7 +29,7 @@ export const datasetStatsServer = async (service: IComputationFunction): Promise
|
|
|
29
29
|
],
|
|
30
30
|
})) as [{ count: number }];
|
|
31
31
|
return {
|
|
32
|
-
rowCount: res[0]
|
|
32
|
+
rowCount: res[0]?.count ?? 0,
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -72,7 +72,7 @@ export const dataQueryServer = async (
|
|
|
72
72
|
}
|
|
73
73
|
const res = await service({
|
|
74
74
|
workflow,
|
|
75
|
-
limit
|
|
75
|
+
limit,
|
|
76
76
|
});
|
|
77
77
|
return res;
|
|
78
78
|
};
|
|
@@ -132,7 +132,12 @@ export const fieldStatServer = async (
|
|
|
132
132
|
},
|
|
133
133
|
],
|
|
134
134
|
};
|
|
135
|
-
const [
|
|
135
|
+
const [
|
|
136
|
+
rangeRes = {
|
|
137
|
+
[MIN_ID]: 0,
|
|
138
|
+
[MAX_ID]: 0,
|
|
139
|
+
},
|
|
140
|
+
] = range
|
|
136
141
|
? await service(rangeQueryPayload)
|
|
137
142
|
: [
|
|
138
143
|
{
|
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
|
@@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|
|
3
3
|
import { useGlobalStore } from "../store";
|
|
4
4
|
import DataTable from "../components/dataTable";
|
|
5
5
|
import { toJS } from "mobx";
|
|
6
|
+
import { getComputation } from "../computation/clientComputation";
|
|
6
7
|
|
|
7
8
|
interface TableProps {
|
|
8
9
|
size?: number;
|
|
@@ -13,19 +14,18 @@ const Table: React.FC<TableProps> = (props) => {
|
|
|
13
14
|
const { commonStore } = useGlobalStore();
|
|
14
15
|
const { tmpDSRawFields, tmpDataSource } = commonStore;
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
return {
|
|
18
|
-
id: "tmp",
|
|
19
|
-
name: "tmp",
|
|
20
|
-
dataSource: tmpDataSource,
|
|
21
|
-
rawFields: toJS(tmpDSRawFields),
|
|
22
|
-
};
|
|
23
|
-
}, [tmpDataSource, tmpDSRawFields]);
|
|
17
|
+
const computation = React.useMemo(() => getComputation(tmpDataSource), [tmpDataSource])
|
|
24
18
|
|
|
25
19
|
return (
|
|
26
20
|
<DataTable
|
|
27
21
|
size={size}
|
|
28
|
-
dataset={
|
|
22
|
+
dataset={{
|
|
23
|
+
id: "tmp",
|
|
24
|
+
name: "tmp",
|
|
25
|
+
dataSource: tmpDataSource,
|
|
26
|
+
rawFields: toJS(tmpDSRawFields),
|
|
27
|
+
}}
|
|
28
|
+
computation={computation}
|
|
29
29
|
total={tmpDataSource.length}
|
|
30
30
|
onMetaChange={(fid, fIndex, diffMeta) => {
|
|
31
31
|
commonStore.updateTempDatasetMetas(fid, 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
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -8,3 +8,11 @@ export function useDebounceValue<T>(value: T, timeout = 200): T {
|
|
|
8
8
|
}, [value]);
|
|
9
9
|
return innerValue;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
export function useDebounceValueBind<T>(value: T, setter: (v: T) => void, timeout = 200): [T, (v: T) => void] {
|
|
13
|
+
const [innerValue, setInnerValue] = useState(value);
|
|
14
|
+
const valueToSet = useDebounceValue(innerValue, timeout);
|
|
15
|
+
useEffect(() => setInnerValue(value), [value])
|
|
16
|
+
useEffect(() => setter(valueToSet), [valueToSet]);
|
|
17
|
+
return [innerValue, setInnerValue];
|
|
18
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { type ForwardedRef, forwardRef } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import React, { type ForwardedRef, forwardRef, useState } from "react";
|
|
2
|
+
import { DOMProvider } from "@kanaries/react-beautiful-dnd";
|
|
3
3
|
import { observer } from "mobx-react-lite";
|
|
4
4
|
import App, { IGWProps } from "./App";
|
|
5
5
|
import { StoreWrapper } from "./store/index";
|
|
@@ -13,22 +13,24 @@ import "./empty_sheet.css";
|
|
|
13
13
|
export const GraphicWalker = observer(forwardRef<IGWHandler, IGWProps>((props, ref) => {
|
|
14
14
|
const { storeRef } = props;
|
|
15
15
|
|
|
16
|
+
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
|
|
17
|
+
|
|
16
18
|
const handleMount = (shadowRoot: ShadowRoot) => {
|
|
17
|
-
|
|
18
|
-
DOM.setHead(shadowRoot);
|
|
19
|
+
setShadowRoot(shadowRoot);
|
|
19
20
|
};
|
|
20
21
|
const handleUnmount = () => {
|
|
21
|
-
|
|
22
|
-
DOM.setHead(document.head);
|
|
22
|
+
setShadowRoot(null);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<StoreWrapper keepAlive={props.keepAlive} storeRef={storeRef}>
|
|
27
27
|
<AppRoot ref={ref as ForwardedRef<IGWHandlerInsider>}>
|
|
28
28
|
<ShadowDom onMount={handleMount} onUnmount={handleUnmount}>
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
29
|
+
<DOMProvider value={{ head: shadowRoot ?? document.head, body: shadowRoot ?? document.body }}>
|
|
30
|
+
<FieldsContextWrapper>
|
|
31
|
+
<App {...props} />
|
|
32
|
+
</FieldsContextWrapper>
|
|
33
|
+
</DOMProvider>
|
|
32
34
|
</ShadowDom>
|
|
33
35
|
</AppRoot>
|
|
34
36
|
</StoreWrapper>
|
package/src/interfaces.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {Config as VgConfig, View} from 'vega';
|
|
2
2
|
import {Config as VlConfig} from 'vega-lite';
|
|
3
|
+
import type { FeatureCollection } from 'geojson';
|
|
4
|
+
import type { feature } from 'topojson-client';
|
|
3
5
|
import type {IViewQuery} from "./lib/viewQuery";
|
|
4
6
|
|
|
5
7
|
export type DeepReadonly<T extends Record<keyof any, any>> = {
|
|
@@ -86,6 +88,8 @@ export interface IExpression {
|
|
|
86
88
|
as: string;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
export type IGeoRole = 'longitude' | 'latitude' | 'none';
|
|
92
|
+
|
|
89
93
|
export interface IField {
|
|
90
94
|
/**
|
|
91
95
|
* fid: key in data record
|
|
@@ -101,6 +105,7 @@ export interface IField {
|
|
|
101
105
|
aggName?: string;
|
|
102
106
|
semanticType: ISemanticType;
|
|
103
107
|
analyticType: IAnalyticType;
|
|
108
|
+
geoRole?: IGeoRole;
|
|
104
109
|
cmp?: (a: any, b: any) => number;
|
|
105
110
|
computed?: boolean;
|
|
106
111
|
expression?: IExpression;
|
|
@@ -183,6 +188,9 @@ export interface DraggableFieldState {
|
|
|
183
188
|
shape: IViewField[];
|
|
184
189
|
theta: IViewField[];
|
|
185
190
|
radius: IViewField[];
|
|
191
|
+
longitude: IViewField[];
|
|
192
|
+
latitude: IViewField[];
|
|
193
|
+
geoId: IViewField[];
|
|
186
194
|
details: IViewField[];
|
|
187
195
|
filters: IFilterField[];
|
|
188
196
|
text: IViewField[];
|
|
@@ -209,14 +217,21 @@ export type IFilterRule =
|
|
|
209
217
|
|
|
210
218
|
export type IStackMode = 'none' | 'stack' | 'normalize' | 'zero' | 'center';
|
|
211
219
|
|
|
220
|
+
export type ICoordMode = 'generic' | 'geographic';
|
|
221
|
+
|
|
212
222
|
export interface IVisualConfig {
|
|
213
223
|
defaultAggregated: boolean;
|
|
214
224
|
geoms: string[];
|
|
225
|
+
showTableSummary: boolean;
|
|
226
|
+
/** @default "generic" */
|
|
227
|
+
coordSystem?: ICoordMode;
|
|
215
228
|
stack: IStackMode;
|
|
216
229
|
showActions: boolean;
|
|
217
230
|
interactiveScale: boolean;
|
|
218
231
|
sorted: ISortMode;
|
|
219
232
|
zeroScale: boolean;
|
|
233
|
+
/** @default false */
|
|
234
|
+
scaleIncludeUnmatchedChoropleth?: boolean;
|
|
220
235
|
background?: string;
|
|
221
236
|
format: {
|
|
222
237
|
numberFormat?: string;
|
|
@@ -236,6 +251,8 @@ export interface IVisualConfig {
|
|
|
236
251
|
width: number;
|
|
237
252
|
height: number;
|
|
238
253
|
};
|
|
254
|
+
geojson?: FeatureCollection;
|
|
255
|
+
geoKey?: string;
|
|
239
256
|
limit: number;
|
|
240
257
|
}
|
|
241
258
|
|
|
@@ -448,3 +465,20 @@ export type IResponse<T> = (
|
|
|
448
465
|
};
|
|
449
466
|
}
|
|
450
467
|
);
|
|
468
|
+
|
|
469
|
+
export type Topology = Parameters<typeof feature>[0];
|
|
470
|
+
|
|
471
|
+
export type IGeographicData = (
|
|
472
|
+
| {
|
|
473
|
+
type: 'GeoJSON';
|
|
474
|
+
data: FeatureCollection;
|
|
475
|
+
}
|
|
476
|
+
| {
|
|
477
|
+
type: 'TopoJSON';
|
|
478
|
+
data: Topology;
|
|
479
|
+
/**
|
|
480
|
+
* default to the first key of `objects` in Topology
|
|
481
|
+
*/
|
|
482
|
+
objectKey?: string;
|
|
483
|
+
}
|
|
484
|
+
);
|
package/src/locales/en-US.json
CHANGED
|
@@ -39,7 +39,14 @@
|
|
|
39
39
|
"arc": "Arc",
|
|
40
40
|
"boxplot": "Box (Box Plot)",
|
|
41
41
|
"table": "Table",
|
|
42
|
-
"text": "Text"
|
|
42
|
+
"text": "Text",
|
|
43
|
+
"poi": "POI",
|
|
44
|
+
"choropleth": "Choropleth"
|
|
45
|
+
},
|
|
46
|
+
"coord_system": {
|
|
47
|
+
"__enum__": "Coordinate System",
|
|
48
|
+
"generic": "Generic",
|
|
49
|
+
"geographic": "Geographic"
|
|
43
50
|
},
|
|
44
51
|
"stack_mode": {
|
|
45
52
|
"__enum__": "Stack Mode",
|
|
@@ -77,7 +84,10 @@
|
|
|
77
84
|
"radius": "Radius",
|
|
78
85
|
"filters": "Filters",
|
|
79
86
|
"details": "Details",
|
|
80
|
-
"text": "Text"
|
|
87
|
+
"text": "Text",
|
|
88
|
+
"longitude": "Longitude",
|
|
89
|
+
"latitude": "Latitude",
|
|
90
|
+
"geoId": "Geometry ID"
|
|
81
91
|
},
|
|
82
92
|
"aggregator": {
|
|
83
93
|
"sum": "Sum",
|
|
@@ -168,6 +178,21 @@
|
|
|
168
178
|
"size_setting": {
|
|
169
179
|
"width": "Width",
|
|
170
180
|
"height": "Height"
|
|
181
|
+
},
|
|
182
|
+
"table": {
|
|
183
|
+
"summary": "Show summary"
|
|
184
|
+
},
|
|
185
|
+
"geography": "Geography Configuration",
|
|
186
|
+
"geography_settings": {
|
|
187
|
+
"geoKey": "Feature ID",
|
|
188
|
+
"format": "Format",
|
|
189
|
+
"geojson": "GeoJSON",
|
|
190
|
+
"topojson": "TopoJSON",
|
|
191
|
+
"objectKey": "Extract Feature Key",
|
|
192
|
+
"jsonInputPlaceholder": "Paste {{format}} here",
|
|
193
|
+
"href": "{{format}} URL",
|
|
194
|
+
"hrefPlaceholder": "Enter {{format}} URL",
|
|
195
|
+
"load": "Load"
|
|
171
196
|
}
|
|
172
197
|
},
|
|
173
198
|
"DatasetFields": {
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -39,7 +39,14 @@
|
|
|
39
39
|
"arc": "アーク",
|
|
40
40
|
"boxplot": "ボックスプロット",
|
|
41
41
|
"table": "表",
|
|
42
|
-
"text": "
|
|
42
|
+
"text": "テキスト",
|
|
43
|
+
"poi": "POI",
|
|
44
|
+
"choropleth": "コロプレス"
|
|
45
|
+
},
|
|
46
|
+
"coord_system": {
|
|
47
|
+
"__enum__": "座標系",
|
|
48
|
+
"generic": "ジェネリック",
|
|
49
|
+
"geographic": "地理"
|
|
43
50
|
},
|
|
44
51
|
"stack_mode": {
|
|
45
52
|
"__enum__": "スタックモード",
|
|
@@ -76,7 +83,10 @@
|
|
|
76
83
|
"theta": "角度",
|
|
77
84
|
"radius": "半径",
|
|
78
85
|
"filters": "フィルター",
|
|
79
|
-
"text": "本文"
|
|
86
|
+
"text": "本文",
|
|
87
|
+
"longitude": "経度",
|
|
88
|
+
"latitude": "緯度",
|
|
89
|
+
"geoId": "地理ID"
|
|
80
90
|
},
|
|
81
91
|
"aggregator": {
|
|
82
92
|
"sum": "合計",
|
|
@@ -167,6 +177,21 @@
|
|
|
167
177
|
"size_setting": {
|
|
168
178
|
"width": "幅",
|
|
169
179
|
"height": "高さ"
|
|
180
|
+
},
|
|
181
|
+
"table": {
|
|
182
|
+
"summary": "サマリを表示"
|
|
183
|
+
},
|
|
184
|
+
"geography": "地理情報設定",
|
|
185
|
+
"geography_settings": {
|
|
186
|
+
"geoKey": "フィーチャーID",
|
|
187
|
+
"format": "データ形式",
|
|
188
|
+
"geojson": "GeoJSON",
|
|
189
|
+
"topojson": "TopoJSON",
|
|
190
|
+
"objectKey": "Feature のキー",
|
|
191
|
+
"jsonInputPlaceholder": "{{format}}をここに貼り付けてください",
|
|
192
|
+
"href": "{{format}} URL",
|
|
193
|
+
"hrefPlaceholder": "{{format}} URLを入力",
|
|
194
|
+
"load": "ロード"
|
|
170
195
|
}
|
|
171
196
|
},
|
|
172
197
|
"DatasetFields": {
|