@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.
Files changed (83) hide show
  1. package/dist/App.d.ts +9 -2
  2. package/dist/assets/filter.worker-f09fcd6f.js.map +1 -1
  3. package/dist/assets/sort.worker-f77540ac.js.map +1 -0
  4. package/dist/assets/transform.worker-bae8e910.js.map +1 -0
  5. package/dist/assets/{viewQuery.worker-03404216.js.map → viewQuery.worker-bdb6477c.js.map} +1 -1
  6. package/dist/components/askViz/index.d.ts +6 -0
  7. package/dist/components/askViz/schemaTransform.d.ts +2 -0
  8. package/dist/components/dataTable/index.d.ts +8 -5
  9. package/dist/components/pivotTable/store.d.ts +0 -2
  10. package/dist/components/spinner.d.ts +2 -0
  11. package/dist/computation/clientComputation.d.ts +3 -0
  12. package/dist/computation/serverComputation.d.ts +8 -0
  13. package/dist/config.d.ts +3 -1
  14. package/dist/fields/filterField/tabs.d.ts +2 -1
  15. package/dist/graphic-walker.es.js +22226 -21650
  16. package/dist/graphic-walker.es.js.map +1 -1
  17. package/dist/graphic-walker.umd.js +137 -137
  18. package/dist/graphic-walker.umd.js.map +1 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/interfaces.d.ts +93 -4
  21. package/dist/lib/execExp.d.ts +4 -4
  22. package/dist/lib/interfaces.d.ts +1 -0
  23. package/dist/lib/viewQuery.d.ts +2 -2
  24. package/dist/renderer/hooks.d.ts +8 -4
  25. package/dist/renderer/index.d.ts +2 -1
  26. package/dist/renderer/pureRenderer.d.ts +17 -1
  27. package/dist/renderer/specRenderer.d.ts +1 -0
  28. package/dist/services.d.ts +8 -5
  29. package/dist/store/commonStore.d.ts +2 -2
  30. package/dist/store/visualSpecStore.d.ts +58 -42
  31. package/dist/utils/save.d.ts +10 -2
  32. package/dist/utils/workflow.d.ts +3 -0
  33. package/dist/vis/react-vega.d.ts +3 -1
  34. package/dist/workers/sort.d.ts +2 -2
  35. package/dist/workers/transform.d.ts +5 -2
  36. package/package.json +2 -2
  37. package/src/App.tsx +46 -7
  38. package/src/components/askViz/index.tsx +92 -0
  39. package/src/components/askViz/schemaTransform.ts +38 -0
  40. package/src/components/dataTable/index.tsx +51 -11
  41. package/src/components/pivotTable/index.tsx +0 -1
  42. package/src/components/pivotTable/store.tsx +0 -16
  43. package/src/components/spinner.tsx +14 -0
  44. package/src/components/toggle.tsx +2 -2
  45. package/src/components/visualConfig/index.tsx +78 -8
  46. package/src/computation/clientComputation.ts +55 -0
  47. package/src/computation/serverComputation.ts +153 -0
  48. package/src/config.ts +15 -2
  49. package/src/dataSource/datasetConfig/index.tsx +38 -6
  50. package/src/dataSource/table.tsx +11 -2
  51. package/src/fields/filterField/filterEditDialog.tsx +11 -10
  52. package/src/fields/filterField/tabs.tsx +178 -77
  53. package/src/index.tsx +2 -0
  54. package/src/interfaces.ts +108 -5
  55. package/src/lib/execExp.ts +20 -11
  56. package/src/lib/interfaces.ts +1 -0
  57. package/src/lib/op/aggregate.ts +1 -1
  58. package/src/lib/viewQuery.ts +2 -2
  59. package/src/locales/en-US.json +11 -2
  60. package/src/locales/ja-JP.json +11 -2
  61. package/src/locales/zh-CN.json +11 -2
  62. package/src/main.tsx +1 -1
  63. package/src/renderer/hooks.ts +100 -66
  64. package/src/renderer/index.tsx +10 -6
  65. package/src/renderer/pureRenderer.tsx +40 -14
  66. package/src/renderer/specRenderer.tsx +24 -7
  67. package/src/services.ts +7 -8
  68. package/src/store/commonStore.ts +7 -7
  69. package/src/store/visualSpecStore.ts +287 -192
  70. package/src/utils/save.ts +81 -3
  71. package/src/utils/workflow.ts +148 -0
  72. package/src/vis/react-vega.tsx +21 -6
  73. package/src/vis/spec/aggregate.ts +3 -2
  74. package/src/vis/spec/stack.ts +7 -6
  75. package/src/visualSettings/index.tsx +2 -3
  76. package/src/workers/filter.worker.js +1 -1
  77. package/src/workers/sort.ts +3 -4
  78. package/src/workers/sort.worker.ts +2 -2
  79. package/src/workers/transform.ts +7 -8
  80. package/src/workers/transform.worker.js +2 -2
  81. package/src/workers/viewQuery.worker.js +2 -2
  82. package/dist/assets/sort.worker-4299a6a0.js.map +0 -1
  83. 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.16",
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": 120,
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 = dataSource;
107
+ let safeData = remoteDataSource;
80
108
  let safeMetas = rawFields;
81
109
  if (fieldKeyGuard) {
82
- const { safeData: _safeData, safeMetas: _safeMetas } = guardDataKeys(dataSource, rawFields);
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, dataSource, fieldKeyGuard]);
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 { IMutField, IRow } from '../../interfaces';
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
- metas: IMutField[];
12
- data: IRow[];
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, data, metas, onMetaChange } = props;
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, data.length - 1);
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={data.length}
178
+ total={total}
140
179
  from={from + 1}
141
180
  to={to + 1}
142
181
  onNext={() => {
143
- setPageIndex(Math.min(Math.ceil(data.length / size) - 1, pageIndex + 1));
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
- {data.slice(from, to + 1).map((row, index) => (
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);
@@ -78,7 +78,6 @@ const PivotTable: React.FC<PivotTableProps> = (props) => {
78
78
  data
79
79
  );
80
80
  const metric = buildMetricTableFromNestTree(lt, tt, data);
81
- // debugger
82
81
  unstable_batchedUpdates(() => {
83
82
  setLeftTree(lt);
84
83
  setTopTree(tt);
@@ -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 text-gray-900">{label}</span>
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
- Format guides docs:{' '}
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
- read here
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 text-gray-900">{t(`config.${fc}`)}</label>
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 text-gray-900 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"
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="zero scale"
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)