@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.
@@ -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>)