@kanaries/graphic-walker 0.3.16 → 0.4.0
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 +9 -2
- package/dist/assets/filter.worker-f09fcd6f.js.map +1 -1
- package/dist/assets/sort.worker-f77540ac.js.map +1 -0
- package/dist/assets/transform.worker-bae8e910.js.map +1 -0
- package/dist/assets/{viewQuery.worker-03404216.js.map → viewQuery.worker-bdb6477c.js.map} +1 -1
- package/dist/components/askViz/index.d.ts +6 -0
- package/dist/components/askViz/schemaTransform.d.ts +2 -0
- package/dist/components/dataTable/index.d.ts +8 -5
- package/dist/components/pivotTable/store.d.ts +0 -2
- package/dist/components/spinner.d.ts +2 -0
- package/dist/computation/clientComputation.d.ts +3 -0
- package/dist/computation/serverComputation.d.ts +8 -0
- package/dist/config.d.ts +3 -1
- package/dist/fields/filterField/tabs.d.ts +2 -1
- package/dist/graphic-walker.es.js +22226 -21650
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +137 -137
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/interfaces.d.ts +93 -4
- package/dist/lib/execExp.d.ts +4 -4
- package/dist/lib/interfaces.d.ts +1 -0
- package/dist/lib/viewQuery.d.ts +2 -2
- package/dist/renderer/hooks.d.ts +8 -4
- package/dist/renderer/index.d.ts +2 -1
- package/dist/renderer/pureRenderer.d.ts +17 -1
- package/dist/renderer/specRenderer.d.ts +1 -0
- package/dist/services.d.ts +8 -5
- package/dist/store/commonStore.d.ts +2 -2
- package/dist/store/visualSpecStore.d.ts +58 -42
- package/dist/utils/save.d.ts +10 -2
- package/dist/utils/workflow.d.ts +3 -0
- package/dist/vis/react-vega.d.ts +3 -1
- package/dist/workers/sort.d.ts +2 -2
- package/dist/workers/transform.d.ts +5 -2
- package/package.json +2 -2
- package/src/App.tsx +46 -7
- package/src/components/askViz/index.tsx +92 -0
- package/src/components/askViz/schemaTransform.ts +38 -0
- package/src/components/dataTable/index.tsx +51 -11
- package/src/components/pivotTable/index.tsx +0 -1
- package/src/components/pivotTable/store.tsx +0 -16
- package/src/components/spinner.tsx +14 -0
- package/src/components/toggle.tsx +2 -2
- package/src/components/visualConfig/index.tsx +78 -8
- package/src/computation/clientComputation.ts +55 -0
- package/src/computation/serverComputation.ts +153 -0
- package/src/config.ts +15 -2
- package/src/dataSource/datasetConfig/index.tsx +38 -6
- package/src/dataSource/table.tsx +11 -2
- package/src/fields/filterField/filterEditDialog.tsx +11 -10
- package/src/fields/filterField/tabs.tsx +178 -77
- package/src/index.tsx +2 -0
- package/src/interfaces.ts +108 -5
- package/src/lib/execExp.ts +20 -11
- package/src/lib/interfaces.ts +1 -0
- package/src/lib/op/aggregate.ts +1 -1
- package/src/lib/viewQuery.ts +2 -2
- package/src/locales/en-US.json +11 -2
- package/src/locales/ja-JP.json +11 -2
- package/src/locales/zh-CN.json +11 -2
- package/src/main.tsx +1 -1
- package/src/renderer/hooks.ts +100 -66
- package/src/renderer/index.tsx +10 -6
- package/src/renderer/pureRenderer.tsx +40 -14
- package/src/renderer/specRenderer.tsx +24 -7
- package/src/services.ts +7 -8
- package/src/store/commonStore.ts +7 -7
- package/src/store/visualSpecStore.ts +287 -192
- package/src/utils/save.ts +81 -3
- package/src/utils/workflow.ts +148 -0
- package/src/vis/react-vega.tsx +21 -6
- package/src/vis/spec/aggregate.ts +3 -2
- package/src/vis/spec/stack.ts +7 -6
- package/src/visualSettings/index.tsx +2 -3
- package/src/workers/filter.worker.js +1 -1
- package/src/workers/sort.ts +3 -4
- package/src/workers/sort.worker.ts +2 -2
- package/src/workers/transform.ts +7 -8
- package/src/workers/transform.worker.js +2 -2
- package/src/workers/viewQuery.worker.js +2 -2
- package/dist/assets/sort.worker-4299a6a0.js.map +0 -1
- package/dist/assets/transform.worker-a12fb3d8.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanaries/graphic-walker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev:front_end": "vite --host",
|
|
6
6
|
"dev": "npm run dev:front_end",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"prettier": {
|
|
31
31
|
"tabWidth": 4,
|
|
32
|
-
"printWidth":
|
|
32
|
+
"printWidth": 160,
|
|
33
33
|
"singleQuote": true
|
|
34
34
|
},
|
|
35
35
|
"types": "./dist/index.d.ts",
|
package/src/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useMemo } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useMemo, useState } from 'react';
|
|
2
2
|
import { observer } from 'mobx-react-lite';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { IDarkMode, IMutField, IRow, ISegmentKey, IThemeKey, Specification } from './interfaces';
|
|
4
|
+
import { IComputationFunction, IDarkMode, IMutField, IRow, ISegmentKey, IThemeKey, Specification } from './interfaces';
|
|
5
5
|
import type { IReactVegaHandler } from './vis/react-vega';
|
|
6
6
|
import VisualSettings from './visualSettings';
|
|
7
7
|
import PosFields from './fields/posFields';
|
|
@@ -20,6 +20,8 @@ import { useCurrentMediaTheme } from './utils/media';
|
|
|
20
20
|
import CodeExport from './components/codeExport';
|
|
21
21
|
import VisualConfig from './components/visualConfig';
|
|
22
22
|
import type { ToolbarItemProps } from './components/toolbar';
|
|
23
|
+
import AskViz from './components/askViz';
|
|
24
|
+
import { getComputation } from './computation/clientComputation';
|
|
23
25
|
|
|
24
26
|
export interface IGWProps {
|
|
25
27
|
dataSource?: IRow[];
|
|
@@ -28,7 +30,7 @@ export interface IGWProps {
|
|
|
28
30
|
hideDataSourceConfig?: boolean;
|
|
29
31
|
i18nLang?: string;
|
|
30
32
|
i18nResources?: { [lang: string]: Record<string, string | any> };
|
|
31
|
-
keepAlive?: boolean;
|
|
33
|
+
keepAlive?: boolean | string;
|
|
32
34
|
/**
|
|
33
35
|
* auto parse field key into a safe string. default is true
|
|
34
36
|
*/
|
|
@@ -37,10 +39,17 @@ export interface IGWProps {
|
|
|
37
39
|
themeKey?: IThemeKey;
|
|
38
40
|
dark?: IDarkMode;
|
|
39
41
|
storeRef?: React.MutableRefObject<IGlobalStore | null>;
|
|
42
|
+
computation?: IComputationFunction;
|
|
40
43
|
toolbar?: {
|
|
41
44
|
extra?: ToolbarItemProps[];
|
|
42
45
|
exclude?: string[];
|
|
43
46
|
};
|
|
47
|
+
enhanceAPI?: {
|
|
48
|
+
header?: Record<string, string>;
|
|
49
|
+
features?: {
|
|
50
|
+
askviz?: string | boolean;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
const App = observer<IGWProps>(function App(props) {
|
|
@@ -54,7 +63,9 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
54
63
|
fieldKeyGuard = true,
|
|
55
64
|
themeKey = 'vega',
|
|
56
65
|
dark = 'media',
|
|
66
|
+
computation,
|
|
57
67
|
toolbar,
|
|
68
|
+
enhanceAPI,
|
|
58
69
|
} = props;
|
|
59
70
|
const { commonStore, vizStore } = useGlobalStore();
|
|
60
71
|
|
|
@@ -75,11 +86,28 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
75
86
|
}
|
|
76
87
|
}, [i18nLang, curLang]);
|
|
77
88
|
|
|
89
|
+
const [sampleRemoteData, setSampleRemoteData] = useState<IRow[]>([]);
|
|
90
|
+
const remoteDataContext = useRef(0);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!computation) return;
|
|
94
|
+
async () => {
|
|
95
|
+
const ts = Date.now();
|
|
96
|
+
remoteDataContext.current = ts;
|
|
97
|
+
const resp = await computation({ workflow: [{ type: 'view', query: [{ op: 'raw', fields: ['*'] }] }], limit: 1 });
|
|
98
|
+
if (remoteDataContext.current === ts) {
|
|
99
|
+
setSampleRemoteData(resp);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, [computation]);
|
|
103
|
+
|
|
104
|
+
const remoteDataSource = dataSource.length > 0 ? dataSource : sampleRemoteData;
|
|
105
|
+
|
|
78
106
|
const safeDataset = useMemo(() => {
|
|
79
|
-
let safeData =
|
|
107
|
+
let safeData = remoteDataSource;
|
|
80
108
|
let safeMetas = rawFields;
|
|
81
109
|
if (fieldKeyGuard) {
|
|
82
|
-
const { safeData: _safeData, safeMetas: _safeMetas } = guardDataKeys(
|
|
110
|
+
const { safeData: _safeData, safeMetas: _safeMetas } = guardDataKeys(remoteDataSource, rawFields);
|
|
83
111
|
safeData = _safeData;
|
|
84
112
|
safeMetas = _safeMetas;
|
|
85
113
|
}
|
|
@@ -87,7 +115,7 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
87
115
|
safeData,
|
|
88
116
|
safeMetas,
|
|
89
117
|
};
|
|
90
|
-
}, [rawFields,
|
|
118
|
+
}, [rawFields, remoteDataSource, computation, fieldKeyGuard]);
|
|
91
119
|
|
|
92
120
|
// use as an embeding module, use outside datasource from props.
|
|
93
121
|
useEffect(() => {
|
|
@@ -106,6 +134,14 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
106
134
|
}
|
|
107
135
|
}, [spec, safeDataset]);
|
|
108
136
|
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (computation) {
|
|
139
|
+
vizStore.setComputationFunction(computation);
|
|
140
|
+
} else {
|
|
141
|
+
vizStore.setComputationFunction(getComputation(commonStore.currentDataset.dataSource));
|
|
142
|
+
}
|
|
143
|
+
}, [vizStore, computation ?? commonStore.currentDataset.dataSource]);
|
|
144
|
+
|
|
109
145
|
const darkMode = useCurrentMediaTheme(dark);
|
|
110
146
|
|
|
111
147
|
const rendererRef = useRef<IReactVegaHandler>(null);
|
|
@@ -130,6 +166,9 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
130
166
|
style={{ marginTop: '0em', borderTop: 'none' }}
|
|
131
167
|
className="m-4 p-4 border border-gray-200 dark:border-gray-700"
|
|
132
168
|
>
|
|
169
|
+
{enhanceAPI?.features?.askviz && (
|
|
170
|
+
<AskViz api={typeof enhanceAPI.features.askviz === 'string' ? enhanceAPI.features.askviz : ''} headers={enhanceAPI?.header} />
|
|
171
|
+
)}
|
|
133
172
|
<VisualSettings rendererHandler={rendererRef} darkModePreference={dark} exclude={toolbar?.exclude} extra={toolbar?.extra} />
|
|
134
173
|
<CodeExport />
|
|
135
174
|
<VisualConfig />
|
|
@@ -156,7 +195,7 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
156
195
|
// }}
|
|
157
196
|
>
|
|
158
197
|
{datasets.length > 0 && (
|
|
159
|
-
<ReactiveRenderer ref={rendererRef} themeKey={themeKey} dark={dark} />
|
|
198
|
+
<ReactiveRenderer ref={rendererRef} themeKey={themeKey} dark={dark} computationFunction={vizStore.computationFuction} />
|
|
160
199
|
)}
|
|
161
200
|
{/* {vizEmbededMenu.show && (
|
|
162
201
|
<ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { observer } from 'mobx-react-lite';
|
|
2
|
+
import React, { useCallback, useState } from 'react';
|
|
3
|
+
import { useGlobalStore } from '../../store';
|
|
4
|
+
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
|
|
5
|
+
import Spinner from '../spinner';
|
|
6
|
+
import { IViewField } from '../../interfaces';
|
|
7
|
+
import { VisSpecWithHistory } from '../../models/visSpecHistory';
|
|
8
|
+
|
|
9
|
+
type VEGALite = any;
|
|
10
|
+
|
|
11
|
+
const api = import.meta.env.DEV ? 'http://localhost:2023/api/vis/text2gw' : 'https://enhanceai.kanaries.net/api/vis/text2gw'
|
|
12
|
+
|
|
13
|
+
async function vizQuery(api: string, metas: IViewField[], query: string, headers: Record<string, string>) {
|
|
14
|
+
const res = await fetch(api, {
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...headers,
|
|
18
|
+
},
|
|
19
|
+
credentials: 'include',
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
metas,
|
|
23
|
+
messages: [
|
|
24
|
+
{
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: query,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
const result: {
|
|
32
|
+
success: boolean;
|
|
33
|
+
data: VEGALite;
|
|
34
|
+
message?: string;
|
|
35
|
+
} = await res.json();
|
|
36
|
+
if (result.success) {
|
|
37
|
+
return result.data;
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(result.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AskViz: React.FC<{api?: string; headers?: Record<string, string>}> = (props) => {
|
|
44
|
+
const [query, setQuery] = useState<string>('');
|
|
45
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
46
|
+
const { vizStore } = useGlobalStore();
|
|
47
|
+
|
|
48
|
+
const allFields = vizStore.allFields;
|
|
49
|
+
|
|
50
|
+
const startQuery = useCallback(() => {
|
|
51
|
+
setLoading(true);
|
|
52
|
+
vizQuery(props.api ?? api, allFields, query, props.headers ?? {})
|
|
53
|
+
.then((data) => {
|
|
54
|
+
vizStore.visList.push(new VisSpecWithHistory(data));
|
|
55
|
+
vizStore.selectVisualization(vizStore.visList.length - 1);
|
|
56
|
+
// const liteGW = parseGW(spec);
|
|
57
|
+
// vizStore.renderSpec(liteGW);
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
});
|
|
62
|
+
}, [query, allFields]);
|
|
63
|
+
return (
|
|
64
|
+
<div className="right-0 flex">
|
|
65
|
+
<input
|
|
66
|
+
type="text"
|
|
67
|
+
className="rounded-l-md px-4 block w-full border-0 py-1.5 text-gray-900 dark:text-gray-50 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 dark:bg-gray-900"
|
|
68
|
+
placeholder="What visualization your want to draw from the dataset"
|
|
69
|
+
value={query}
|
|
70
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
71
|
+
onKeyDown={(e) => {
|
|
72
|
+
if (e.key === 'Enter' && loading === false && query.length > 0) {
|
|
73
|
+
startQuery();
|
|
74
|
+
}
|
|
75
|
+
}}
|
|
76
|
+
disabled={loading || allFields.length === 0}
|
|
77
|
+
/>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="flex items-center grow-0 rounded-r-md bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 px-4 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
81
|
+
disabled={loading || query.length === 0 || allFields.length === 0}
|
|
82
|
+
onClick={startQuery}
|
|
83
|
+
>
|
|
84
|
+
Ask
|
|
85
|
+
{!loading && <PaperAirplaneIcon className="w-4 ml-1" />}
|
|
86
|
+
{loading && <Spinner />}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default observer(AskViz);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Specification } from "../../interfaces";
|
|
2
|
+
|
|
3
|
+
export function parseGW(vlSpec: any): Specification {
|
|
4
|
+
const spec: Specification = {};
|
|
5
|
+
if (vlSpec.encoding && vlSpec.mark) {
|
|
6
|
+
spec.geomType = [vlSpec.mark];
|
|
7
|
+
spec.position = [];
|
|
8
|
+
if (vlSpec.encoding.x && vlSpec.encoding.x.field) {
|
|
9
|
+
spec.position.push(vlSpec.encoding.x.field);
|
|
10
|
+
if (vlSpec.encoding.x.aggregate) {
|
|
11
|
+
spec.aggregate = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (vlSpec.encoding.y && vlSpec.encoding.y.field) {
|
|
15
|
+
spec.position.push(vlSpec.encoding.y.field);
|
|
16
|
+
if (vlSpec.encoding.y.aggregate) {
|
|
17
|
+
spec.aggregate = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
spec.facets = [];
|
|
21
|
+
if (vlSpec.encoding.row && vlSpec.encoding.row) {
|
|
22
|
+
spec.facets.push(vlSpec.encoding.row);
|
|
23
|
+
}
|
|
24
|
+
if (vlSpec.encoding.column && vlSpec.encoding.column) {
|
|
25
|
+
spec.facets.push(vlSpec.encoding.column);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
['color', 'opacity', 'shape', 'size'].forEach((ch) => {
|
|
29
|
+
if (vlSpec.encoding[ch] && vlSpec.encoding[ch].field) {
|
|
30
|
+
spec[ch] = [vlSpec.encoding[ch].field];
|
|
31
|
+
if (vlSpec.encoding[ch].aggregate) {
|
|
32
|
+
spec.aggregate = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return spec;
|
|
38
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState, useRef, useEffect } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
|
-
import {
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import type { IMutField, IRow, DataSet } from '../../interfaces';
|
|
4
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import LoadingLayer from "../loadingLayer";
|
|
7
|
+
import { useComputationFunc } from "../../renderer/hooks";
|
|
8
|
+
import { dataReadRawServer } from "../../computation/serverComputation";
|
|
5
9
|
import Pagination from './pagination';
|
|
6
10
|
import { ChevronUpDownIcon } from '@heroicons/react/24/outline';
|
|
7
11
|
import DropdownContext from '../dropdownContext';
|
|
8
12
|
|
|
9
13
|
interface DataTableProps {
|
|
14
|
+
/** page limit */
|
|
10
15
|
size?: number;
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
/** total count of rows */
|
|
17
|
+
total: number;
|
|
18
|
+
dataset: DataSet;
|
|
13
19
|
onMetaChange: (fid: string, fIndex: number, meta: Partial<IMutField>) => void;
|
|
20
|
+
loading?: boolean;
|
|
14
21
|
}
|
|
15
22
|
const Container = styled.div`
|
|
16
23
|
overflow-x: auto;
|
|
@@ -110,9 +117,10 @@ const getHeaderKey = (f: wrapMutField) => {
|
|
|
110
117
|
};
|
|
111
118
|
|
|
112
119
|
const DataTable: React.FC<DataTableProps> = (props) => {
|
|
113
|
-
const { size = 10,
|
|
120
|
+
const { size = 10, onMetaChange, dataset, total, loading: statLoading } = props;
|
|
114
121
|
const [pageIndex, setPageIndex] = useState(0);
|
|
115
122
|
const { t } = useTranslation();
|
|
123
|
+
const computationFuction = useComputationFunc();
|
|
116
124
|
|
|
117
125
|
const analyticTypeList = useMemo<{ value: string; label: string }[]>(() => {
|
|
118
126
|
return ANALYTIC_TYPE_LIST.map((at) => ({
|
|
@@ -129,18 +137,49 @@ const DataTable: React.FC<DataTableProps> = (props) => {
|
|
|
129
137
|
}, []);
|
|
130
138
|
|
|
131
139
|
const from = pageIndex * size;
|
|
132
|
-
const to = Math.min((pageIndex + 1) * size,
|
|
140
|
+
const to = Math.min((pageIndex + 1) * size - 1, total - 1);
|
|
141
|
+
|
|
142
|
+
const [rows, setRows] = useState<IRow[]>([]);
|
|
143
|
+
const [dataLoading, setDataLoading] = useState(false);
|
|
144
|
+
const taskIdRef = useRef(0);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (statLoading) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setDataLoading(true);
|
|
151
|
+
const taskId = ++taskIdRef.current;
|
|
152
|
+
dataReadRawServer(computationFuction, size, pageIndex).then(data => {
|
|
153
|
+
if (taskId === taskIdRef.current) {
|
|
154
|
+
setDataLoading(false);
|
|
155
|
+
setRows(data);
|
|
156
|
+
}
|
|
157
|
+
}).catch(err => {
|
|
158
|
+
if (taskId === taskIdRef.current) {
|
|
159
|
+
console.error(err);
|
|
160
|
+
setDataLoading(false);
|
|
161
|
+
setRows([]);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
taskIdRef.current++;
|
|
166
|
+
};
|
|
167
|
+
}, [computationFuction, pageIndex, size]);
|
|
168
|
+
|
|
169
|
+
const loading = statLoading || dataLoading;
|
|
170
|
+
|
|
171
|
+
const metas = dataset.rawFields;
|
|
133
172
|
|
|
134
173
|
const headers = useMemo(() => getHeaders(metas), [metas]);
|
|
135
174
|
|
|
136
175
|
return (
|
|
137
|
-
<Container className="rounded border-gray-200 dark:border-gray-700 border">
|
|
176
|
+
<Container className="rounded border-gray-200 dark:border-gray-700 border relative">
|
|
138
177
|
<Pagination
|
|
139
|
-
total={
|
|
178
|
+
total={total}
|
|
140
179
|
from={from + 1}
|
|
141
180
|
to={to + 1}
|
|
142
181
|
onNext={() => {
|
|
143
|
-
setPageIndex(Math.min(Math.ceil(
|
|
182
|
+
setPageIndex(Math.min(Math.ceil(total / size) - 1, pageIndex + 1));
|
|
144
183
|
}}
|
|
145
184
|
onPrev={() => {
|
|
146
185
|
setPageIndex(Math.max(0, pageIndex - 1));
|
|
@@ -227,7 +266,7 @@ const DataTable: React.FC<DataTableProps> = (props) => {
|
|
|
227
266
|
))}
|
|
228
267
|
</thead>
|
|
229
268
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-zinc-900">
|
|
230
|
-
{
|
|
269
|
+
{rows.map((row, index) => (
|
|
231
270
|
<tr
|
|
232
271
|
className={
|
|
233
272
|
'divide-x divide-gray-200 dark:divide-gray-700 ' +
|
|
@@ -250,8 +289,9 @@ const DataTable: React.FC<DataTableProps> = (props) => {
|
|
|
250
289
|
))}
|
|
251
290
|
</tbody>
|
|
252
291
|
</table>
|
|
292
|
+
{loading && <LoadingLayer />}
|
|
253
293
|
</Container>
|
|
254
294
|
);
|
|
255
295
|
};
|
|
256
296
|
|
|
257
|
-
export default DataTable;
|
|
297
|
+
export default observer(DataTable);
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { makeAutoObservable, observable } from 'mobx';
|
|
2
2
|
import { INestNode } from './inteface';
|
|
3
|
-
import { IAggQuery } from '../../lib/interfaces';
|
|
4
|
-
import { queryView } from '../../lib/viewQuery';
|
|
5
3
|
import { IField, IRow } from '../../interfaces';
|
|
6
4
|
import React, { createContext, useContext, useEffect } from 'react';
|
|
7
|
-
import { getMeaAggKey } from '../../utils';
|
|
8
5
|
|
|
9
6
|
class PivotTableStore {
|
|
10
7
|
public leftTree: INestNode | null = null;
|
|
@@ -30,19 +27,6 @@ class PivotTableStore {
|
|
|
30
27
|
this.topTree = null;
|
|
31
28
|
this.viewData = [];
|
|
32
29
|
}
|
|
33
|
-
public async queryData(leftQuery: IAggQuery, topQuery: IAggQuery) {
|
|
34
|
-
const viewQuery: IAggQuery = {
|
|
35
|
-
op: 'aggregate',
|
|
36
|
-
groupBy: leftQuery.groupBy.concat(topQuery.groupBy),
|
|
37
|
-
measures: leftQuery.measures.concat(topQuery.measures).map(mea => ({
|
|
38
|
-
field: mea.field,
|
|
39
|
-
agg: mea.agg,
|
|
40
|
-
asFieldKey: getMeaAggKey(mea.field, mea.agg)
|
|
41
|
-
}))
|
|
42
|
-
};
|
|
43
|
-
const viewData = queryView(this.dataSource, this.metas, viewQuery);
|
|
44
|
-
this.viewData = viewData;
|
|
45
|
-
}
|
|
46
30
|
}
|
|
47
31
|
|
|
48
32
|
const initStore = new PivotTableStore();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Spinner() {
|
|
4
|
+
return (
|
|
5
|
+
<svg className="animate-spin ml-2 mr-2 h-5 w-5 text-white" viewBox="0 0 24 24">
|
|
6
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
7
|
+
<path
|
|
8
|
+
className="opacity-75"
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
11
|
+
></path>
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -20,7 +20,7 @@ export default function Toggle(props: ToggleProps) {
|
|
|
20
20
|
checked={enabled}
|
|
21
21
|
onChange={onChange}
|
|
22
22
|
className={classNames(
|
|
23
|
-
enabled ? 'bg-indigo-600' : 'bg-gray-200',
|
|
23
|
+
enabled ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-700',
|
|
24
24
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2'
|
|
25
25
|
)}
|
|
26
26
|
>
|
|
@@ -33,7 +33,7 @@ export default function Toggle(props: ToggleProps) {
|
|
|
33
33
|
/>
|
|
34
34
|
</Switch>
|
|
35
35
|
<Switch.Label as="span" className="ml-3 text-sm">
|
|
36
|
-
<span className="font-medium
|
|
36
|
+
<span className="font-medium">{label}</span>
|
|
37
37
|
</Switch.Label>
|
|
38
38
|
</Switch.Group>
|
|
39
39
|
);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React, { useState } from 'react';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useGlobalStore } from '../../store';
|
|
4
|
+
import { NonPositionChannelConfigList,PositionChannelConfigList } from '../../config';
|
|
5
|
+
|
|
4
6
|
import Modal from '../modal';
|
|
5
7
|
import { IVisualConfig } from '../../interfaces';
|
|
6
8
|
import PrimaryButton from '../button/primary';
|
|
7
9
|
import DefaultButton from '../button/default';
|
|
8
10
|
import { useTranslation } from 'react-i18next';
|
|
9
11
|
import Toggle from '../toggle';
|
|
10
|
-
import { runInAction } from 'mobx';
|
|
12
|
+
import { runInAction, toJS } from 'mobx';
|
|
11
13
|
|
|
12
14
|
const VisualConfigPanel: React.FC = (props) => {
|
|
13
15
|
const { commonStore, vizStore } = useGlobalStore();
|
|
@@ -24,7 +26,27 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
24
26
|
timeFormat: visualConfig.format.timeFormat,
|
|
25
27
|
normalizedNumberFormat: visualConfig.format.normalizedNumberFormat,
|
|
26
28
|
});
|
|
29
|
+
const [resolve, setResolve] = useState<IVisualConfig['resolve']>({
|
|
30
|
+
x: visualConfig.resolve.x,
|
|
31
|
+
y: visualConfig.resolve.y,
|
|
32
|
+
color: visualConfig.resolve.color,
|
|
33
|
+
opacity: visualConfig.resolve.opacity,
|
|
34
|
+
shape: visualConfig.resolve.shape,
|
|
35
|
+
size: visualConfig.resolve.size,
|
|
36
|
+
});
|
|
27
37
|
const [zeroScale, setZeroScale] = useState<boolean>(visualConfig.zeroScale);
|
|
38
|
+
const [background, setBackground] = useState<string | undefined>(visualConfig.background);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setZeroScale(visualConfig.zeroScale);
|
|
42
|
+
setBackground(visualConfig.background);
|
|
43
|
+
setResolve(toJS(visualConfig.resolve));
|
|
44
|
+
setFormat({
|
|
45
|
+
numberFormat: visualConfig.format.numberFormat,
|
|
46
|
+
timeFormat: visualConfig.format.timeFormat,
|
|
47
|
+
normalizedNumberFormat: visualConfig.format.normalizedNumberFormat,
|
|
48
|
+
});
|
|
49
|
+
}, [showVisualConfigPanel]);
|
|
28
50
|
|
|
29
51
|
return (
|
|
30
52
|
<Modal
|
|
@@ -36,22 +58,22 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
36
58
|
<div>
|
|
37
59
|
<h2 className="text-lg mb-4">{t('config.format')}</h2>
|
|
38
60
|
<p className="text-xs">
|
|
39
|
-
|
|
61
|
+
{t(`config.formatGuidesDocs`)}:{' '}
|
|
40
62
|
<a
|
|
41
63
|
target="_blank"
|
|
42
64
|
className="underline text-blue-500"
|
|
43
65
|
href="https://github.com/d3/d3-format#locale_format"
|
|
44
66
|
>
|
|
45
|
-
|
|
67
|
+
{t(`config.readHere`)}
|
|
46
68
|
</a>
|
|
47
69
|
</p>
|
|
48
70
|
{formatConfigList.map((fc) => (
|
|
49
71
|
<div className="my-2" key={fc}>
|
|
50
|
-
<label className="block text-xs font-medium leading-6
|
|
72
|
+
<label className="block text-xs font-medium leading-6">{t(`config.${fc}`)}</label>
|
|
51
73
|
<div className="mt-1">
|
|
52
74
|
<input
|
|
53
75
|
type="text"
|
|
54
|
-
className="block w-full rounded-md border-0 py-1 px-2
|
|
76
|
+
className="block w-full text-gray-700 dark:text-gray-200 rounded-md border-0 py-1 px-2 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 dark:bg-zinc-900 "
|
|
55
77
|
value={format[fc] ?? ''}
|
|
56
78
|
onChange={(e) => {
|
|
57
79
|
setFormat((f) => ({
|
|
@@ -63,9 +85,55 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
63
85
|
</div>
|
|
64
86
|
</div>
|
|
65
87
|
))}
|
|
88
|
+
<h2 className="text-lg">{t('config.background')}</h2>
|
|
89
|
+
<div className="my-2">
|
|
90
|
+
<label className="block text-xs font-medium leading-6">{t(`config.color`)}</label>
|
|
91
|
+
<div className="mt-1">
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
className="block w-full text-gray-700 dark:text-gray-200 rounded-md border-0 py-1 px-2 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 dark:bg-zinc-900 "
|
|
95
|
+
value={background ?? ''}
|
|
96
|
+
onChange={(e) => {
|
|
97
|
+
setBackground(e.target.value);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<h2 className="text-lg">{t('config.independence')}</h2>
|
|
103
|
+
<div className="my-2">
|
|
104
|
+
<div className="flex space-x-6">
|
|
105
|
+
{PositionChannelConfigList.map((pc) => (
|
|
106
|
+
<Toggle
|
|
107
|
+
label={t(`config.${pc}`)}
|
|
108
|
+
key={pc}
|
|
109
|
+
enabled={resolve[pc] ?? false}
|
|
110
|
+
onChange={(e) => {
|
|
111
|
+
setResolve((r) => ({
|
|
112
|
+
...r,
|
|
113
|
+
[pc]: e,
|
|
114
|
+
}));
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
{NonPositionChannelConfigList.map((npc) => (
|
|
119
|
+
<Toggle
|
|
120
|
+
label={t(`constant.draggable_key.${npc}`)}
|
|
121
|
+
key={npc}
|
|
122
|
+
enabled={resolve[npc] ?? false}
|
|
123
|
+
onChange={(e) => {
|
|
124
|
+
setResolve((r) => ({
|
|
125
|
+
...r,
|
|
126
|
+
[npc]: e,
|
|
127
|
+
}));
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<h2 className="text-lg">{t('config.zeroScale')}</h2>
|
|
66
134
|
<div className="my-2">
|
|
67
135
|
<Toggle
|
|
68
|
-
label=
|
|
136
|
+
label={t(`config.zeroScale`)}
|
|
69
137
|
enabled={zeroScale}
|
|
70
138
|
onChange={(en) => {
|
|
71
139
|
setZeroScale(en);
|
|
@@ -80,8 +148,10 @@ const VisualConfigPanel: React.FC = (props) => {
|
|
|
80
148
|
runInAction(() => {
|
|
81
149
|
vizStore.setVisualConfig('format', format);
|
|
82
150
|
vizStore.setVisualConfig('zeroScale', zeroScale);
|
|
151
|
+
vizStore.setVisualConfig('background', background);
|
|
152
|
+
vizStore.setVisualConfig('resolve', resolve);
|
|
83
153
|
commonStore.setShowVisualConfigPanel(false);
|
|
84
|
-
})
|
|
154
|
+
});
|
|
85
155
|
}}
|
|
86
156
|
/>
|
|
87
157
|
<DefaultButton
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IDataQueryPayload, IDataQueryWorkflowStep, IFilterFiledSimple, IRow } from "../interfaces";
|
|
2
|
+
import { applyFilter, applySort, applyViewQuery, transformDataService } from "../services";
|
|
3
|
+
|
|
4
|
+
export const dataQueryClient = async (
|
|
5
|
+
rawData: IRow[],
|
|
6
|
+
workflow: IDataQueryWorkflowStep[],
|
|
7
|
+
offset?: number,
|
|
8
|
+
limit?: number,
|
|
9
|
+
): Promise<IRow[]> => {
|
|
10
|
+
let res = rawData;
|
|
11
|
+
for await (const step of workflow) {
|
|
12
|
+
switch (step.type) {
|
|
13
|
+
case 'filter': {
|
|
14
|
+
res = await applyFilter(res, step.filters.map(filter => {
|
|
15
|
+
const res: IFilterFiledSimple = {
|
|
16
|
+
fid: filter.fid,
|
|
17
|
+
rule: null,
|
|
18
|
+
};
|
|
19
|
+
if (filter.rule.type === 'one of') {
|
|
20
|
+
res.rule = {
|
|
21
|
+
type: 'one of',
|
|
22
|
+
value: new Set(filter.rule.value),
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
res.rule = filter.rule;
|
|
26
|
+
}
|
|
27
|
+
return res;
|
|
28
|
+
}).filter(Boolean));
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'transform': {
|
|
32
|
+
res = await transformDataService(res, step.transform);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'view': {
|
|
36
|
+
for await (const job of step.query) {
|
|
37
|
+
res = await applyViewQuery(res, job);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'sort': {
|
|
42
|
+
res = await applySort(res, step.by, step.sort);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
default: {
|
|
46
|
+
// @ts-expect-error - runtime check
|
|
47
|
+
console.warn(new Error(`Unknown step type: ${step.type}`));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return res.slice(offset ?? 0, limit ? ((offset ?? 0) + limit) : undefined);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const getComputation = (rawData: IRow[]) => (payload: IDataQueryPayload) => dataQueryClient(rawData, payload.workflow, payload.offset, payload.limit)
|