@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/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
  }
@@ -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
@@ -83,9 +83,12 @@
83
83
  "variance": "Variance"
84
84
  },
85
85
  "filter_type": {
86
- "one_of": "One-Of",
86
+ "one_of": "Value set",
87
+ "one_of_desc": "Select values specified",
87
88
  "range": "Range",
88
- "temporal_range": "Date Range"
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": "Edit Rule",
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": {
@@ -82,9 +82,12 @@
82
82
  "variance": "分散"
83
83
  },
84
84
  "filter_type": {
85
- "one_of": "1つ以上",
85
+ "one_of": "値のセット",
86
+ "one_of_desc": "指定された値を選択します",
86
87
  "range": "範囲",
87
- "temporal_range": "日付範囲"
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": {
@@ -84,8 +84,11 @@
84
84
  },
85
85
  "filter_type": {
86
86
  "one_of": "按值筛选",
87
+ "one_of_desc": "选择指定的值",
87
88
  "range": "按范围筛选",
88
- "temporal_range": "按日期范围筛选"
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
- "cencel": "取消"
213
+ "cancel": "取消"
207
214
  }
208
215
  }
@@ -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);
@@ -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 AppRoot from '../components/appRoot';
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
- <AppRoot>
83
- <ShadowDom>
84
- <div className="relative">
85
- <SpecRenderer
86
- name={name}
87
- loading={waiting}
88
- data={viewData}
89
- ref={ref}
90
- themeKey={themeKey}
91
- dark={dark}
92
- draggableFieldState={visualState}
93
- visualConfig={visualConfig}
94
- />
95
- </div>
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 = (name: string | undefined, viewsRef: MutableRefObject<{ x: number; y: number; w: number; h: number; view: View }[]>, ref: ForwardedRef<IReactVegaHandler>) => {
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
- if (appRef && 'current' in appRef && appRef.current) {
55
- appRef.current.exportChart = (async (mode: IChartExportResult['mode'] = 'svg') => {
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 appRef.current.exportChart;
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 && 'current' in appRef && appRef.current) {
92
- appRef.current.exportChart = async mode => ({
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
  }, []);
@@ -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, View } from 'vega';
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<{ x: number; y: number; w: number; h: number; view: View }[]>([]);
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: res.view.container()?.clientWidth ?? res.view.width(),
226
- h: res.view.container()?.clientHeight ?? res.view.height(),
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: res.view.container()?.clientWidth ?? res.view.width(),
299
- h: res.view.container()?.clientHeight ?? res.view.height(),
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
- useVegaExportApi(name, vegaRefs, ref);
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>)