@kanaries/graphic-walker 0.3.11 → 0.3.13
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/assets/transform.worker-a12fb3d8.js.map +1 -0
- package/dist/components/appRoot.d.ts +5 -4
- package/dist/fields/filterField/slider.d.ts +0 -1
- package/dist/graphic-walker.es.js +24578 -24272
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +260 -128
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/interfaces.d.ts +78 -1
- package/dist/renderer/pureRenderer.d.ts +4 -12
- package/dist/utils/chartIndexControl.d.ts +7 -0
- package/dist/utils/vegaApiExport.d.ts +3 -9
- package/package.json +2 -1
- package/src/assets/kanaries-logo.svg +1 -0
- package/src/components/appRoot.tsx +65 -7
- package/src/components/modal.tsx +9 -9
- package/src/fields/filterField/filterEditDialog.tsx +33 -8
- package/src/fields/filterField/slider.tsx +127 -85
- package/src/fields/filterField/tabs.tsx +301 -184
- package/src/index.tsx +3 -3
- package/src/interfaces.ts +83 -1
- package/src/lib/execExp.ts +1 -1
- package/src/locales/en-US.json +10 -3
- package/src/locales/ja-JP.json +10 -3
- package/src/locales/zh-CN.json +10 -3
- package/src/renderer/hooks.ts +6 -0
- package/src/renderer/index.tsx +7 -0
- package/src/renderer/pureRenderer.tsx +16 -18
- package/src/utils/chartIndexControl.ts +39 -0
- package/src/utils/vegaApiExport.ts +127 -10
- package/src/vis/react-vega.tsx +31 -11
- package/src/visualSettings/index.tsx +299 -70
- package/dist/assets/transform.worker-90e4f506.js.map +0 -1
package/src/interfaces.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Config as VgConfig} from 'vega';
|
|
1
|
+
import {Config as VgConfig, View} from 'vega';
|
|
2
2
|
import {Config as VlConfig} from 'vega-lite';
|
|
3
3
|
|
|
4
4
|
export type DeepReadonly<T extends Record<keyof any, any>> = {
|
|
@@ -225,6 +225,17 @@ export type IDarkMode = 'media' | 'light' | 'dark';
|
|
|
225
225
|
|
|
226
226
|
export type VegaGlobalConfig = VgConfig | VlConfig;
|
|
227
227
|
|
|
228
|
+
export interface IVegaChartRef {
|
|
229
|
+
x: number;
|
|
230
|
+
y: number;
|
|
231
|
+
w: number;
|
|
232
|
+
h: number;
|
|
233
|
+
innerWidth: number;
|
|
234
|
+
innerHeight: number;
|
|
235
|
+
view: View;
|
|
236
|
+
canvas: HTMLCanvasElement | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
export interface IChartExportResult<T extends 'svg' | 'data-url' = 'svg' | 'data-url'> {
|
|
229
240
|
mode: T;
|
|
230
241
|
title: string;
|
|
@@ -235,8 +246,12 @@ export interface IChartExportResult<T extends 'svg' | 'data-url' = 'svg' | 'data
|
|
|
235
246
|
rowIndex: number;
|
|
236
247
|
width: number;
|
|
237
248
|
height: number;
|
|
249
|
+
canvasWidth: number;
|
|
250
|
+
canvasHeight: number;
|
|
238
251
|
data: string;
|
|
252
|
+
canvas(): HTMLCanvasElement | null;
|
|
239
253
|
}[];
|
|
254
|
+
container(): HTMLDivElement | null;
|
|
240
255
|
}
|
|
241
256
|
|
|
242
257
|
interface IExportChart {
|
|
@@ -244,6 +259,73 @@ interface IExportChart {
|
|
|
244
259
|
<T extends IChartExportResult['mode']>(mode: T): Promise<IChartExportResult<T>>;
|
|
245
260
|
}
|
|
246
261
|
|
|
262
|
+
export interface IChartListExportResult<T extends 'svg' | 'data-url' = 'svg' | 'data-url'> {
|
|
263
|
+
mode: T;
|
|
264
|
+
total: number;
|
|
265
|
+
index: number;
|
|
266
|
+
data: IChartExportResult<T>;
|
|
267
|
+
hasNext: boolean;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface IExportChartList {
|
|
271
|
+
<T extends Extract<IChartExportResult['mode'], 'svg'>>(mode?: T): AsyncGenerator<IChartListExportResult<T>, void, unknown>;
|
|
272
|
+
<T extends IChartExportResult['mode']>(mode: T): AsyncGenerator<IChartListExportResult<T>, void, unknown>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The status of the current chart.
|
|
277
|
+
* * `computing`: _GraphicWalker_ is computing the data view.
|
|
278
|
+
* * `rendering`: _GraphicWalker_ is rendering the chart.
|
|
279
|
+
* * `idle`: rendering is finished.
|
|
280
|
+
* * `error`: an error occurs during the process above.
|
|
281
|
+
*/
|
|
282
|
+
export type IRenderStatus = 'computing' | 'rendering' | 'idle' | 'error';
|
|
283
|
+
|
|
247
284
|
export interface IGWHandler {
|
|
285
|
+
/** length of the "chart" tab list */
|
|
286
|
+
chartCount: number;
|
|
287
|
+
/** current selected chart index */
|
|
288
|
+
chartIndex: number;
|
|
289
|
+
/** Switches to the specified chart */
|
|
290
|
+
openChart: (index: number) => void;
|
|
291
|
+
/**
|
|
292
|
+
* Returns the status of the current chart.
|
|
293
|
+
*
|
|
294
|
+
* It is computed by the following rules:
|
|
295
|
+
* - If _GraphicWalker_ is computing the data view, it returns `computing`.
|
|
296
|
+
* - If _GraphicWalker_ is rendering the chart, it returns `rendering`.
|
|
297
|
+
* - If rendering is finished, it returns `idle`.
|
|
298
|
+
* - If an error occurs during the process above, it returns `error`.
|
|
299
|
+
*/
|
|
300
|
+
get renderStatus(): IRenderStatus;
|
|
301
|
+
/**
|
|
302
|
+
* Registers a callback function to listen to the status change of the current chart.
|
|
303
|
+
*
|
|
304
|
+
* @param {(renderStatus: IRenderStatus) => void} cb - the callback function
|
|
305
|
+
* @returns {() => void} a dispose function to remove this callback
|
|
306
|
+
*/
|
|
307
|
+
onRenderStatusChange: (cb: (renderStatus: IRenderStatus) => void) => (() => void);
|
|
308
|
+
/**
|
|
309
|
+
* Exports the current chart.
|
|
310
|
+
*
|
|
311
|
+
* @param {IChartExportResult['mode']} [mode='svg'] - the export mode, either `svg` or `data-url`
|
|
312
|
+
*/
|
|
248
313
|
exportChart: IExportChart;
|
|
314
|
+
/**
|
|
315
|
+
* Exports all charts.
|
|
316
|
+
*
|
|
317
|
+
* @param {IChartExportResult['mode']} [mode='svg'] - the export mode, either `svg` or `data-url`
|
|
318
|
+
* @returns {AsyncGenerator<IChartListExportResult, void, unknown>} an async generator to iterate over all charts
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* for await (const chart of gwRef.current.exportChartList()) {
|
|
322
|
+
* console.log(chart);
|
|
323
|
+
* }
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
exportChartList: IExportChartList;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface IGWHandlerInsider extends IGWHandler {
|
|
330
|
+
updateRenderStatus: (renderStatus: IRenderStatus) => void;
|
|
249
331
|
}
|
package/src/lib/execExp.ts
CHANGED
|
@@ -82,7 +82,7 @@ function binCount(resKey: string, params: IExpParamter[], data: IDataFrame, binS
|
|
|
82
82
|
|
|
83
83
|
const groupSize = valueWithIndices.length / binSize;
|
|
84
84
|
|
|
85
|
-
const newValues = valueWithIndices.map(item => {
|
|
85
|
+
const newValues = valueWithIndices.sort((a, b) => a.index - b.index).map(item => {
|
|
86
86
|
let bIndex = Math.floor(item.orderIndex / groupSize);
|
|
87
87
|
if (bIndex === binSize) bIndex = binSize - 1;
|
|
88
88
|
return bIndex + 1
|
package/src/locales/en-US.json
CHANGED
|
@@ -83,9 +83,12 @@
|
|
|
83
83
|
"variance": "Variance"
|
|
84
84
|
},
|
|
85
85
|
"filter_type": {
|
|
86
|
-
"one_of": "
|
|
86
|
+
"one_of": "Value set",
|
|
87
|
+
"one_of_desc": "Select values specified",
|
|
87
88
|
"range": "Range",
|
|
88
|
-
"
|
|
89
|
+
"range_desc": "Select values in a range [start, end]",
|
|
90
|
+
"temporal_range": "Date Range",
|
|
91
|
+
"temporal_range_desc": "Select values in a date range [start, end]"
|
|
89
92
|
}
|
|
90
93
|
},
|
|
91
94
|
"App": {
|
|
@@ -165,7 +168,7 @@
|
|
|
165
168
|
"filters": {
|
|
166
169
|
"to_edit": "Edit This Rule",
|
|
167
170
|
"empty_rule": "! (empty rule)",
|
|
168
|
-
"editing": "
|
|
171
|
+
"editing": "Filter Rule Settings",
|
|
169
172
|
"form": {
|
|
170
173
|
"name": "Field",
|
|
171
174
|
"rule": "Rule"
|
|
@@ -182,6 +185,10 @@
|
|
|
182
185
|
"reverse": "Reverse Selection",
|
|
183
186
|
"confirm": "Confirm",
|
|
184
187
|
"cancel": "Cancel"
|
|
188
|
+
},
|
|
189
|
+
"range": {
|
|
190
|
+
"start_value": "Start Value",
|
|
191
|
+
"end_value": "End Value"
|
|
185
192
|
}
|
|
186
193
|
},
|
|
187
194
|
"explain": {
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -82,9 +82,12 @@
|
|
|
82
82
|
"variance": "分散"
|
|
83
83
|
},
|
|
84
84
|
"filter_type": {
|
|
85
|
-
"one_of": "
|
|
85
|
+
"one_of": "値のセット",
|
|
86
|
+
"one_of_desc": "指定された値を選択します",
|
|
86
87
|
"range": "範囲",
|
|
87
|
-
"
|
|
88
|
+
"range_desc": "範囲 [開始, 終了] 内の値を選択します",
|
|
89
|
+
"temporal_range": "日付範囲",
|
|
90
|
+
"temporal_range_desc": "日付範囲 [開始, 終了] 内の値を選択します"
|
|
88
91
|
}
|
|
89
92
|
},
|
|
90
93
|
"App": {
|
|
@@ -164,7 +167,7 @@
|
|
|
164
167
|
"filters": {
|
|
165
168
|
"to_edit": "このルールを編集",
|
|
166
169
|
"empty_rule": "! (空のルール)",
|
|
167
|
-
"editing": "
|
|
170
|
+
"editing": "フィルタールールの設定",
|
|
168
171
|
"form": {
|
|
169
172
|
"name": "フィールド",
|
|
170
173
|
"rule": "ルール"
|
|
@@ -181,6 +184,10 @@
|
|
|
181
184
|
"reverse": "選択を反転する",
|
|
182
185
|
"confirm": "確認",
|
|
183
186
|
"cancel": "キャンセル"
|
|
187
|
+
},
|
|
188
|
+
"range": {
|
|
189
|
+
"start_value": "開始値",
|
|
190
|
+
"end_value": "終了値"
|
|
184
191
|
}
|
|
185
192
|
},
|
|
186
193
|
"explain": {
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -84,8 +84,11 @@
|
|
|
84
84
|
},
|
|
85
85
|
"filter_type": {
|
|
86
86
|
"one_of": "按值筛选",
|
|
87
|
+
"one_of_desc": "选择指定的值",
|
|
87
88
|
"range": "按范围筛选",
|
|
88
|
-
"
|
|
89
|
+
"range_desc": "选择范围内的值 [开始, 结束]",
|
|
90
|
+
"temporal_range": "按日期范围筛选",
|
|
91
|
+
"temporal_range_desc": "选择日期范围内的值 [开始, 结束]"
|
|
89
92
|
}
|
|
90
93
|
},
|
|
91
94
|
"App": {
|
|
@@ -165,7 +168,7 @@
|
|
|
165
168
|
"filters": {
|
|
166
169
|
"to_edit": "编辑这条规则",
|
|
167
170
|
"empty_rule": "! (空的规则)",
|
|
168
|
-
"editing": "
|
|
171
|
+
"editing": "筛选器规则设定",
|
|
169
172
|
"form": {
|
|
170
173
|
"name": "字段",
|
|
171
174
|
"rule": "规则"
|
|
@@ -182,6 +185,10 @@
|
|
|
182
185
|
"reverse": "选择反向",
|
|
183
186
|
"confirm": "确认",
|
|
184
187
|
"cancel": "取消"
|
|
188
|
+
},
|
|
189
|
+
"range": {
|
|
190
|
+
"start_value": "起始值",
|
|
191
|
+
"end_value": "终止值"
|
|
185
192
|
}
|
|
186
193
|
},
|
|
187
194
|
"explain": {
|
|
@@ -203,6 +210,6 @@
|
|
|
203
210
|
"next": "向后",
|
|
204
211
|
"drop_field": "拖拽字段至此",
|
|
205
212
|
"confirm": "确认",
|
|
206
|
-
"
|
|
213
|
+
"cancel": "取消"
|
|
207
214
|
}
|
|
208
215
|
}
|
package/src/renderer/hooks.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { unstable_batchedUpdates } from 'react-dom';
|
|
|
3
3
|
import type { DeepReadonly, IFilterField, IRow, IViewField } from '../interfaces';
|
|
4
4
|
import { applyFilter, applyViewQuery, transformDataService } from '../services';
|
|
5
5
|
import { getMeaAggKey } from '../utils';
|
|
6
|
+
import { useAppRootContext } from '../components/appRoot';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
interface UseRendererProps {
|
|
@@ -26,8 +27,11 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
26
27
|
|
|
27
28
|
const [viewData, setViewData] = useState<IRow[]>([]);
|
|
28
29
|
|
|
30
|
+
const appRef = useAppRootContext();
|
|
31
|
+
|
|
29
32
|
useEffect(() => {
|
|
30
33
|
const taskId = ++taskIdRef.current;
|
|
34
|
+
appRef.current?.updateRenderStatus('computing');
|
|
31
35
|
setComputing(true);
|
|
32
36
|
applyFilter(data, filters)
|
|
33
37
|
.then((data) => {
|
|
@@ -53,6 +57,7 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
53
57
|
if (taskId !== taskIdRef.current) {
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
60
|
+
appRef.current?.updateRenderStatus('rendering');
|
|
56
61
|
unstable_batchedUpdates(() => {
|
|
57
62
|
setComputing(false);
|
|
58
63
|
setViewData(data);
|
|
@@ -61,6 +66,7 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
61
66
|
if (taskId !== taskIdRef.current) {
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
69
|
+
appRef.current?.updateRenderStatus('error');
|
|
64
70
|
console.error(err);
|
|
65
71
|
unstable_batchedUpdates(() => {
|
|
66
72
|
setComputing(false);
|
package/src/renderer/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { IReactVegaHandler } from '../vis/react-vega';
|
|
|
8
8
|
import { unstable_batchedUpdates } from 'react-dom';
|
|
9
9
|
import { useRenderer } from './hooks';
|
|
10
10
|
import { initEncoding, initVisualConfig } from '../store/visualSpecStore';
|
|
11
|
+
import { useChartIndexControl } from '../utils/chartIndexControl';
|
|
11
12
|
|
|
12
13
|
interface RendererProps {
|
|
13
14
|
themeKey?: IThemeKey;
|
|
@@ -60,6 +61,12 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
|
|
|
60
61
|
}
|
|
61
62
|
}, [waiting, vizStore]);
|
|
62
63
|
|
|
64
|
+
useChartIndexControl({
|
|
65
|
+
count: visList.length,
|
|
66
|
+
index: visIndex,
|
|
67
|
+
onChange: idx => vizStore.selectVisualization(idx),
|
|
68
|
+
});
|
|
69
|
+
|
|
63
70
|
const handleGeomClick = useCallback(
|
|
64
71
|
(values: any, e: any) => {
|
|
65
72
|
e.stopPropagation();
|
|
@@ -3,7 +3,7 @@ import { unstable_batchedUpdates } from 'react-dom';
|
|
|
3
3
|
import { toJS } from 'mobx';
|
|
4
4
|
import { observer } from 'mobx-react-lite';
|
|
5
5
|
import { ShadowDom } from '../shadow-dom';
|
|
6
|
-
import
|
|
6
|
+
import { withAppRoot } from '../components/appRoot';
|
|
7
7
|
import type { IDarkMode, IViewField, IRow, IThemeKey, DraggableFieldState, IVisualConfig } from '../interfaces';
|
|
8
8
|
import type { IReactVegaHandler } from '../vis/react-vega';
|
|
9
9
|
import SpecRenderer from './specRenderer';
|
|
@@ -79,23 +79,21 @@ const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function
|
|
|
79
79
|
}, [waiting]);
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</ShadowDom>
|
|
97
|
-
</AppRoot>
|
|
82
|
+
<ShadowDom>
|
|
83
|
+
<div className="relative">
|
|
84
|
+
<SpecRenderer
|
|
85
|
+
name={name}
|
|
86
|
+
loading={waiting}
|
|
87
|
+
data={viewData}
|
|
88
|
+
ref={ref}
|
|
89
|
+
themeKey={themeKey}
|
|
90
|
+
dark={dark}
|
|
91
|
+
draggableFieldState={visualState}
|
|
92
|
+
visualConfig={visualConfig}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</ShadowDom>
|
|
98
96
|
);
|
|
99
97
|
});
|
|
100
98
|
|
|
101
|
-
export default observer(PureRenderer);
|
|
99
|
+
export default observer(withAppRoot(PureRenderer));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useAppRootContext } from "../components/appRoot";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface IUseChartIndexControlOptions {
|
|
6
|
+
count: number;
|
|
7
|
+
index: number;
|
|
8
|
+
onChange: (index: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useChartIndexControl = (options: IUseChartIndexControlOptions): void => {
|
|
12
|
+
const appRef = useAppRootContext();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (appRef.current) {
|
|
16
|
+
appRef.current.chartCount = options.count;
|
|
17
|
+
appRef.current.chartIndex = options.index;
|
|
18
|
+
appRef.current.openChart = function (index: number) {
|
|
19
|
+
if (index === this.chartIndex) {
|
|
20
|
+
return;
|
|
21
|
+
} else if (Number.isInteger(index) && index >= 0 && index < this.chartCount) {
|
|
22
|
+
options.onChange(index);
|
|
23
|
+
} else {
|
|
24
|
+
console.warn(`Invalid chart index: ${index}`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
if (appRef.current) {
|
|
33
|
+
appRef.current.chartCount = 1;
|
|
34
|
+
appRef.current.chartIndex = 0;
|
|
35
|
+
appRef.current.openChart = () => {};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { useImperativeHandle, type ForwardedRef, type MutableRefObject, useEffect } from "react";
|
|
2
|
-
import type { View } from "vega";
|
|
1
|
+
import { useImperativeHandle, type ForwardedRef, type MutableRefObject, useEffect, RefObject } from "react";
|
|
3
2
|
import { useAppRootContext } from "../components/appRoot";
|
|
4
3
|
import type { IReactVegaHandler } from "../vis/react-vega";
|
|
5
|
-
import type { IChartExportResult } from "../interfaces";
|
|
4
|
+
import type { IChartExportResult, IVegaChartRef } from "../interfaces";
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
export const useVegaExportApi = (
|
|
7
|
+
export const useVegaExportApi = (
|
|
8
|
+
name: string | undefined,
|
|
9
|
+
viewsRef: MutableRefObject<IVegaChartRef[]>,
|
|
10
|
+
ref: ForwardedRef<IReactVegaHandler>,
|
|
11
|
+
renderTaskRefs: MutableRefObject<Promise<unknown>[]>,
|
|
12
|
+
containerRef: RefObject<HTMLDivElement>,
|
|
13
|
+
) => {
|
|
9
14
|
const renderHandle = {
|
|
10
15
|
getSVGData() {
|
|
11
16
|
return Promise.all(viewsRef.current.map(item => item.view.toSVG()));
|
|
@@ -47,12 +52,73 @@ export const useVegaExportApi = (name: string | undefined, viewsRef: MutableRefO
|
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
useImperativeHandle(ref, () => renderHandle);
|
|
50
|
-
|
|
55
|
+
|
|
51
56
|
const appRef = useAppRootContext();
|
|
52
57
|
|
|
53
58
|
useEffect(() => {
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const ctx = appRef.current;
|
|
60
|
+
if (ctx) {
|
|
61
|
+
Promise.all(renderTaskRefs.current).then(() => {
|
|
62
|
+
if (appRef.current) {
|
|
63
|
+
const appCtx = appRef.current;
|
|
64
|
+
if (appCtx.renderStatus !== 'rendering') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// add a short delay to wait for the canvas to be ready
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
if (appCtx.renderStatus !== 'rendering') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
appCtx.updateRenderStatus('idle');
|
|
73
|
+
}, 0);
|
|
74
|
+
}
|
|
75
|
+
}).catch(() => {
|
|
76
|
+
if (appRef.current) {
|
|
77
|
+
if (appRef.current.renderStatus !== 'rendering') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
appRef.current.updateRenderStatus('error');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
ctx.exportChart = (async (mode: IChartExportResult['mode'] = 'svg') => {
|
|
84
|
+
if (ctx.renderStatus === 'error') {
|
|
85
|
+
console.error('exportChart failed because error occurred when rendering chart.');
|
|
86
|
+
return {
|
|
87
|
+
mode,
|
|
88
|
+
title: '',
|
|
89
|
+
nCols: 0,
|
|
90
|
+
nRows: 0,
|
|
91
|
+
charts: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (ctx.renderStatus !== 'idle') {
|
|
95
|
+
let dispose = null as (() => void) | null;
|
|
96
|
+
// try to wait for a while
|
|
97
|
+
const waitForChartReady = new Promise<void>((resolve, reject) => {
|
|
98
|
+
dispose = ctx.onRenderStatusChange(status => {
|
|
99
|
+
if (status === 'error') {
|
|
100
|
+
reject(new Error('Error occurred when rendering chart'));
|
|
101
|
+
} else if (status === 'idle') {
|
|
102
|
+
resolve();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
setTimeout(() => reject(new Error('Timeout')), 10_000);
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
await waitForChartReady;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('exportChart failed:', `${error}`);
|
|
111
|
+
return {
|
|
112
|
+
mode,
|
|
113
|
+
title: '',
|
|
114
|
+
nCols: 0,
|
|
115
|
+
nRows: 0,
|
|
116
|
+
charts: [],
|
|
117
|
+
};
|
|
118
|
+
} finally {
|
|
119
|
+
dispose?.();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
56
122
|
const res: IChartExportResult = {
|
|
57
123
|
mode,
|
|
58
124
|
title: name || 'untitled',
|
|
@@ -63,8 +129,16 @@ export const useVegaExportApi = (name: string | undefined, viewsRef: MutableRefO
|
|
|
63
129
|
colIndex: item.x,
|
|
64
130
|
width: item.w,
|
|
65
131
|
height: item.h,
|
|
132
|
+
canvasWidth: item.innerWidth,
|
|
133
|
+
canvasHeight: item.innerHeight,
|
|
66
134
|
data: '',
|
|
135
|
+
canvas() {
|
|
136
|
+
return item.canvas;
|
|
137
|
+
},
|
|
67
138
|
})),
|
|
139
|
+
container() {
|
|
140
|
+
return containerRef.current;
|
|
141
|
+
},
|
|
68
142
|
};
|
|
69
143
|
if (mode === 'data-url') {
|
|
70
144
|
const imgData = await renderHandle.getCanvasData();
|
|
@@ -82,20 +156,63 @@ export const useVegaExportApi = (name: string | undefined, viewsRef: MutableRefO
|
|
|
82
156
|
}
|
|
83
157
|
}
|
|
84
158
|
return res;
|
|
85
|
-
}) as typeof
|
|
159
|
+
}) as typeof ctx.exportChart;
|
|
160
|
+
ctx.exportChartList = async function * exportChartList (mode: IChartExportResult['mode'] = 'svg') {
|
|
161
|
+
const total = ctx.chartCount;
|
|
162
|
+
const indices = new Array(total).fill(0).map((_, i) => i);
|
|
163
|
+
const currentIdx = ctx.chartIndex;
|
|
164
|
+
for await (const index of indices) {
|
|
165
|
+
ctx.openChart(index);
|
|
166
|
+
// wait for a while to make sure the correct chart is rendered
|
|
167
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
168
|
+
const chart = await ctx.exportChart(mode);
|
|
169
|
+
yield {
|
|
170
|
+
mode,
|
|
171
|
+
total,
|
|
172
|
+
index,
|
|
173
|
+
data: chart,
|
|
174
|
+
hasNext: index < total - 1,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
ctx.openChart(currentIdx);
|
|
178
|
+
};
|
|
86
179
|
}
|
|
87
180
|
});
|
|
88
181
|
|
|
89
182
|
useEffect(() => {
|
|
183
|
+
// NOTE: this is totally a cleanup function
|
|
90
184
|
return () => {
|
|
91
|
-
if (appRef
|
|
92
|
-
appRef.current.
|
|
185
|
+
if (appRef.current) {
|
|
186
|
+
appRef.current.updateRenderStatus('idle');
|
|
187
|
+
appRef.current.exportChart = async (mode: IChartExportResult['mode'] = 'svg') => ({
|
|
93
188
|
mode,
|
|
94
189
|
title: '',
|
|
95
190
|
nCols: 0,
|
|
96
191
|
nRows: 0,
|
|
97
192
|
charts: [],
|
|
193
|
+
container() {
|
|
194
|
+
return null;
|
|
195
|
+
},
|
|
98
196
|
});
|
|
197
|
+
appRef.current.exportChartList = async function * exportChartList (mode: IChartExportResult['mode'] = 'svg') {
|
|
198
|
+
yield {
|
|
199
|
+
mode,
|
|
200
|
+
total: 1,
|
|
201
|
+
completed: 0,
|
|
202
|
+
index: 0,
|
|
203
|
+
data: {
|
|
204
|
+
mode,
|
|
205
|
+
title: '',
|
|
206
|
+
nCols: 0,
|
|
207
|
+
nRows: 0,
|
|
208
|
+
charts: [],
|
|
209
|
+
container() {
|
|
210
|
+
return null;
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
hasNext: false,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
99
216
|
}
|
|
100
217
|
};
|
|
101
218
|
}, []);
|
package/src/vis/react-vega.tsx
CHANGED
|
@@ -2,11 +2,11 @@ import React, { useEffect, useState, useMemo, forwardRef, useRef } from 'react';
|
|
|
2
2
|
import embed from 'vega-embed';
|
|
3
3
|
import { Subject, Subscription } from 'rxjs'
|
|
4
4
|
import * as op from 'rxjs/operators';
|
|
5
|
-
import type { ScenegraphEvent
|
|
5
|
+
import type { ScenegraphEvent } from 'vega';
|
|
6
6
|
import styled from 'styled-components';
|
|
7
7
|
|
|
8
8
|
import { useVegaExportApi } from '../utils/vegaApiExport';
|
|
9
|
-
import { IViewField, IRow, IStackMode, VegaGlobalConfig } from '../interfaces';
|
|
9
|
+
import { IViewField, IRow, IStackMode, VegaGlobalConfig, IVegaChartRef } from '../interfaces';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import { getVegaTimeFormatRules } from './temporalFormat';
|
|
12
12
|
import { getSingleView } from './spec/view';
|
|
@@ -153,10 +153,12 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
153
153
|
})
|
|
154
154
|
}, [rowRepeatFields, colRepeatFields])
|
|
155
155
|
|
|
156
|
-
const vegaRefs = useRef<
|
|
156
|
+
const vegaRefs = useRef<IVegaChartRef[]>([]);
|
|
157
|
+
const renderTaskRefs = useRef<Promise<unknown>[]>([]);
|
|
157
158
|
|
|
158
159
|
useEffect(() => {
|
|
159
160
|
vegaRefs.current = [];
|
|
161
|
+
renderTaskRefs.current = [];
|
|
160
162
|
|
|
161
163
|
const yField = rows.length > 0 ? rows[rows.length - 1] : NULL_FIELD;
|
|
162
164
|
const xField = columns.length > 0 ? columns[columns.length - 1] : NULL_FIELD;
|
|
@@ -220,13 +222,18 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
if (viewPlaceholders.length > 0 && viewPlaceholders[0].current) {
|
|
223
|
-
embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
|
|
225
|
+
const task = embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
|
|
226
|
+
const container = res.view.container();
|
|
227
|
+
const canvas = container?.querySelector('canvas') ?? null;
|
|
224
228
|
vegaRefs.current = [{
|
|
225
|
-
w:
|
|
226
|
-
h:
|
|
229
|
+
w: container?.clientWidth ?? res.view.width(),
|
|
230
|
+
h: container?.clientHeight ?? res.view.height(),
|
|
231
|
+
innerWidth: canvas?.clientWidth ?? res.view.width(),
|
|
232
|
+
innerHeight: canvas?.clientHeight ?? res.view.height(),
|
|
227
233
|
x: 0,
|
|
228
234
|
y: 0,
|
|
229
235
|
view: res.view,
|
|
236
|
+
canvas,
|
|
230
237
|
}];
|
|
231
238
|
try {
|
|
232
239
|
res.view.addEventListener('click', (e) => {
|
|
@@ -239,6 +246,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
239
246
|
console.warn(error)
|
|
240
247
|
}
|
|
241
248
|
});
|
|
249
|
+
renderTaskRefs.current = [task];
|
|
242
250
|
}
|
|
243
251
|
} else {
|
|
244
252
|
if (layoutMode === 'fixed') {
|
|
@@ -293,13 +301,18 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
293
301
|
}
|
|
294
302
|
if (node) {
|
|
295
303
|
const id = index;
|
|
296
|
-
embed(node, ans, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
|
|
304
|
+
const task = embed(node, ans, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
|
|
305
|
+
const container = res.view.container();
|
|
306
|
+
const canvas = container?.querySelector('canvas') ?? null;
|
|
297
307
|
vegaRefs.current[id] = {
|
|
298
|
-
w:
|
|
299
|
-
h:
|
|
308
|
+
w: container?.clientWidth ?? res.view.width(),
|
|
309
|
+
h: container?.clientHeight ?? res.view.height(),
|
|
310
|
+
innerWidth: canvas?.clientWidth ?? res.view.width(),
|
|
311
|
+
innerHeight: canvas?.clientHeight ?? res.view.height(),
|
|
300
312
|
x: j,
|
|
301
313
|
y: i,
|
|
302
314
|
view: res.view,
|
|
315
|
+
canvas,
|
|
303
316
|
};
|
|
304
317
|
const paramStores = (res.vgSpec.data?.map(d => d.name) ?? []).filter(
|
|
305
318
|
name => [BRUSH_SIGNAL_NAME, POINT_SIGNAL_NAME].map(p => `${p}_store`).includes(name)
|
|
@@ -356,6 +369,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
356
369
|
console.warn(error);
|
|
357
370
|
}
|
|
358
371
|
})
|
|
372
|
+
renderTaskRefs.current.push(task);
|
|
359
373
|
}
|
|
360
374
|
}
|
|
361
375
|
}
|
|
@@ -363,6 +377,10 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
363
377
|
subscriptions.forEach(sub => sub.unsubscribe());
|
|
364
378
|
};
|
|
365
379
|
}
|
|
380
|
+
return () => {
|
|
381
|
+
vegaRefs.current = [];
|
|
382
|
+
renderTaskRefs.current = [];
|
|
383
|
+
};
|
|
366
384
|
}, [
|
|
367
385
|
dataSource,
|
|
368
386
|
allFieldIds,
|
|
@@ -391,9 +409,11 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
391
409
|
text
|
|
392
410
|
]);
|
|
393
411
|
|
|
394
|
-
|
|
412
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
413
|
+
|
|
414
|
+
useVegaExportApi(name, vegaRefs, ref, renderTaskRefs, containerRef);
|
|
395
415
|
|
|
396
|
-
return <CanvaContainer rowSize={Math.max(rowRepeatFields.length, 1)} colSize={Math.max(colRepeatFields.length, 1)}>
|
|
416
|
+
return <CanvaContainer rowSize={Math.max(rowRepeatFields.length, 1)} colSize={Math.max(colRepeatFields.length, 1)} ref={containerRef}>
|
|
397
417
|
{/* <div ref={container}></div> */}
|
|
398
418
|
{
|
|
399
419
|
viewPlaceholders.map((view, i) => <div key={i} ref={view}></div>)
|