@kanaries/graphic-walker 0.3.10 → 0.3.12
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/components/appRoot.d.ts +5 -4
- package/dist/graphic-walker.es.js +16100 -15941
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +120 -120
- 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 +1 -2
- package/src/components/appRoot.tsx +65 -7
- package/src/index.tsx +3 -3
- package/src/interfaces.ts +83 -1
- 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
|
@@ -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>)
|