@kanaries/graphic-walker 0.3.9 → 0.3.10

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/index.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import React from "react";
2
2
  import { IGWProps } from "./App";
3
+ import type { IGWHandler } from "./interfaces";
3
4
  import "./empty_sheet.css";
4
- export declare const ShadowDomContext: React.Context<{
5
- root: ShadowRoot | null;
6
- }>;
7
- export type { IGWProps };
8
- export { embedGraphicWalker } from './vanilla';
9
- export declare const GraphicWalker: React.FC<IGWProps>;
5
+ export declare const GraphicWalker: React.MemoExoticComponent<React.ForwardRefExoticComponent<Pick<IGWProps & React.RefAttributes<IGWHandler>, "key" | keyof IGWProps> & React.RefAttributes<IGWHandler>>>;
10
6
  export { default as PureRenderer } from './renderer/pureRenderer';
7
+ export { embedGraphicWalker } from './vanilla';
8
+ export type { IGWProps };
@@ -191,3 +191,24 @@ export declare enum ISegmentKey {
191
191
  export type IThemeKey = 'vega' | 'g2';
192
192
  export type IDarkMode = 'media' | 'light' | 'dark';
193
193
  export type VegaGlobalConfig = VgConfig | VlConfig;
194
+ export interface IChartExportResult<T extends 'svg' | 'data-url' = 'svg' | 'data-url'> {
195
+ mode: T;
196
+ title: string;
197
+ nCols: number;
198
+ nRows: number;
199
+ charts: {
200
+ colIndex: number;
201
+ rowIndex: number;
202
+ width: number;
203
+ height: number;
204
+ data: string;
205
+ }[];
206
+ }
207
+ interface IExportChart {
208
+ <T extends Extract<IChartExportResult['mode'], 'svg'>>(mode?: T): Promise<IChartExportResult<T>>;
209
+ <T extends IChartExportResult['mode']>(mode: T): Promise<IChartExportResult<T>>;
210
+ }
211
+ export interface IGWHandler {
212
+ exportChart: IExportChart;
213
+ }
214
+ export {};
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { IDarkMode, IRow, IThemeKey, DraggableFieldState, IVisualConfig } from '../interfaces';
3
3
  import type { IReactVegaHandler } from '../vis/react-vega';
4
4
  interface IPureRendererProps {
5
+ name?: string;
5
6
  themeKey?: IThemeKey;
6
7
  dark?: IDarkMode;
7
8
  rawData?: IRow[];
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { IReactVegaHandler } from '../vis/react-vega';
3
3
  import { DeepReadonly, DraggableFieldState, IDarkMode, IRow, IThemeKey, IVisualConfig } from '../interfaces';
4
4
  interface SpecRendererProps {
5
+ name?: string;
5
6
  themeKey?: IThemeKey;
6
7
  dark?: IDarkMode;
7
8
  data: IRow[];
@@ -0,0 +1,10 @@
1
+ import React, { HTMLAttributes } from "react";
2
+ export declare const ShadowDomContext: React.Context<{
3
+ root: ShadowRoot | null;
4
+ }>;
5
+ interface IShadowDomProps extends HTMLAttributes<HTMLDivElement> {
6
+ onMount?: (shadowRoot: ShadowRoot) => void;
7
+ onUnmount?: () => void;
8
+ }
9
+ export declare const ShadowDom: React.FC<IShadowDomProps>;
10
+ export {};
@@ -0,0 +1,10 @@
1
+ import { type ForwardedRef, type MutableRefObject } from "react";
2
+ import type { View } from "vega";
3
+ import type { IReactVegaHandler } from "../vis/react-vega";
4
+ export declare const useVegaExportApi: (name: string | undefined, viewsRef: MutableRefObject<{
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
9
+ view: View;
10
+ }[]>, ref: ForwardedRef<IReactVegaHandler>) => void;
@@ -7,6 +7,7 @@ export interface IReactVegaHandler {
7
7
  downloadPNG: (filename?: string) => Promise<string[]>;
8
8
  }
9
9
  interface ReactVegaProps {
10
+ name?: string;
10
11
  rows: Readonly<IViewField[]>;
11
12
  columns: Readonly<IViewField[]>;
12
13
  dataSource: IRow[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanaries/graphic-walker",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "scripts": {
5
5
  "dev:front_end": "vite --host",
6
6
  "dev": "npm run dev:front_end",
@@ -37,7 +37,7 @@
37
37
  "@headlessui/react": "^1.7.12",
38
38
  "@heroicons/react": "^2.0.8",
39
39
  "@kanaries/graphic-walker": "0.3.9",
40
- "@kanaries/react-beautiful-dnd": "0.0.1",
40
+ "@kanaries/react-beautiful-dnd": "0.0.2",
41
41
  "@kanaries/web-data-loader": "^0.1.7",
42
42
  "autoprefixer": "^10.3.5",
43
43
  "i18next": "^21.9.1",
@@ -0,0 +1,32 @@
1
+ import React, { createContext, forwardRef, useImperativeHandle, type ForwardedRef, useContext } from "react";
2
+ import type { IGWHandler } from "../interfaces";
3
+
4
+ const AppRootContext = createContext<ForwardedRef<IGWHandler>>(null!);
5
+
6
+ export const useAppRootContext = () => {
7
+ return useContext(AppRootContext);
8
+ };
9
+
10
+ const AppRoot = forwardRef<IGWHandler, { children: any }>(({ children }, ref) => {
11
+ useImperativeHandle(ref, () => {
12
+ return {
13
+ exportChart: (async mode => {
14
+ return {
15
+ mode,
16
+ title: '',
17
+ nCols: 0,
18
+ nRows: 0,
19
+ charts: [],
20
+ };
21
+ }) as IGWHandler['exportChart'],
22
+ };
23
+ }, []);
24
+
25
+ return (
26
+ <AppRootContext.Provider value={ref}>
27
+ {children}
28
+ </AppRootContext.Provider>
29
+ );
30
+ });
31
+
32
+ export default AppRoot;
@@ -2,7 +2,7 @@ import React, { memo, ReactNode, useContext, useEffect, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import styled from "styled-components";
4
4
  import type { IDarkMode } from "../interfaces";
5
- import { ShadowDomContext } from "..";
5
+ import { ShadowDomContext } from "../shadow-dom";
6
6
  import { useCurrentMediaTheme } from "../utils/media";
7
7
 
8
8
  export interface CalloutProps {
@@ -2,7 +2,7 @@ import React, { memo, useContext, useEffect, useMemo, useRef, useState } from "r
2
2
  import { createPortal } from "react-dom";
3
3
  import styled from "styled-components";
4
4
  import type { IDarkMode } from "../interfaces";
5
- import { ShadowDomContext } from "..";
5
+ import { ShadowDomContext } from "../shadow-dom";
6
6
  import { useCurrentMediaTheme } from "../utils/media";
7
7
 
8
8
  export interface TooltipProps {
package/src/index.tsx CHANGED
@@ -1,55 +1,40 @@
1
- import React, { createContext, useEffect, useRef, useState } from "react";
2
- import { StyleSheetManager } from "styled-components";
3
- import root from "react-shadow";
1
+ import React, { forwardRef } from "react";
4
2
  import { DOM } from "@kanaries/react-beautiful-dnd";
5
3
  import { observer } from "mobx-react-lite";
6
4
  import App, { IGWProps } from "./App";
7
5
  import { StoreWrapper } from "./store/index";
8
6
  import { FieldsContextWrapper } from "./fields/fieldsContext";
7
+ import { ShadowDom } from "./shadow-dom";
8
+ import AppRoot from "./components/appRoot";
9
+ import type { IGWHandler } from "./interfaces";
9
10
 
10
11
  import "./empty_sheet.css";
11
- import tailwindStyle from "tailwindcss/tailwind.css?inline";
12
- import style from "./index.css?inline";
13
12
 
14
- export const ShadowDomContext = createContext<{ root: ShadowRoot | null }>({ root: null });
15
- export type { IGWProps }
16
- export { embedGraphicWalker } from './vanilla';
17
-
18
- export const GraphicWalker: React.FC<IGWProps> = observer((props) => {
19
- const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
20
- const rootRef = useRef<HTMLDivElement>(null);
13
+ export const GraphicWalker = observer(forwardRef<IGWHandler, IGWProps>((props, ref) => {
21
14
  const { storeRef } = props;
22
15
 
23
- useEffect(() => {
24
- if (rootRef.current) {
25
- const shadowRoot = rootRef.current.shadowRoot!;
26
- setShadowRoot(shadowRoot);
27
- DOM.setBody(shadowRoot);
28
- DOM.setHead(shadowRoot);
29
- return () => {
30
- DOM.setBody(document.body);
31
- DOM.setHead(document.head);
32
- };
33
- }
34
- }, []);
16
+ const handleMount = (shadowRoot: ShadowRoot) => {
17
+ DOM.setBody(shadowRoot);
18
+ DOM.setHead(shadowRoot);
19
+ };
20
+ const handleUnmount = () => {
21
+ DOM.setBody(document.body);
22
+ DOM.setHead(document.head);
23
+ };
35
24
 
36
25
  return (
37
26
  <StoreWrapper keepAlive={props.keepAlive} storeRef={storeRef}>
38
- <root.div mode="open" ref={rootRef}>
39
- <style>{tailwindStyle}</style>
40
- <style>{style}</style>
41
- {shadowRoot && (
42
- <StyleSheetManager target={shadowRoot}>
43
- <FieldsContextWrapper>
44
- <ShadowDomContext.Provider value={{ root: shadowRoot }}>
45
- <App {...props} />
46
- </ShadowDomContext.Provider>
47
- </FieldsContextWrapper>
48
- </StyleSheetManager>
49
- )}
50
- </root.div>
27
+ <AppRoot ref={ref}>
28
+ <ShadowDom onMount={handleMount} onUnmount={handleUnmount}>
29
+ <FieldsContextWrapper>
30
+ <App {...props} />
31
+ </FieldsContextWrapper>
32
+ </ShadowDom>
33
+ </AppRoot>
51
34
  </StoreWrapper>
52
35
  );
53
- });
36
+ }));
54
37
 
55
38
  export { default as PureRenderer } from './renderer/pureRenderer';
39
+ export { embedGraphicWalker } from './vanilla';
40
+ export type { IGWProps };
package/src/interfaces.ts CHANGED
@@ -223,4 +223,27 @@ export enum ISegmentKey {
223
223
  export type IThemeKey = 'vega' | 'g2';
224
224
  export type IDarkMode = 'media' | 'light' | 'dark';
225
225
 
226
- export type VegaGlobalConfig = VgConfig | VlConfig;
226
+ export type VegaGlobalConfig = VgConfig | VlConfig;
227
+
228
+ export interface IChartExportResult<T extends 'svg' | 'data-url' = 'svg' | 'data-url'> {
229
+ mode: T;
230
+ title: string;
231
+ nCols: number;
232
+ nRows: number;
233
+ charts: {
234
+ colIndex: number;
235
+ rowIndex: number;
236
+ width: number;
237
+ height: number;
238
+ data: string;
239
+ }[];
240
+ }
241
+
242
+ interface IExportChart {
243
+ <T extends Extract<IChartExportResult['mode'], 'svg'>>(mode?: T): Promise<IChartExportResult<T>>;
244
+ <T extends IChartExportResult['mode']>(mode: T): Promise<IChartExportResult<T>>;
245
+ }
246
+
247
+ export interface IGWHandler {
248
+ exportChart: IExportChart;
249
+ }
@@ -30,8 +30,16 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
30
30
  const taskId = ++taskIdRef.current;
31
31
  setComputing(true);
32
32
  applyFilter(data, filters)
33
- .then((data) => transformDataService(data, allFields))
33
+ .then((data) => {
34
+ if (viewDimensions.length === 0 && viewMeasures.length === 0) {
35
+ return data;
36
+ }
37
+ return transformDataService(data, allFields);
38
+ })
34
39
  .then((d) => {
40
+ if (viewDimensions.length === 0 && viewMeasures.length === 0) {
41
+ return data;
42
+ }
35
43
  // setViewData(d);
36
44
  const dims = viewDimensions;
37
45
  const meas = viewMeasures;
@@ -20,7 +20,8 @@ interface RendererProps {
20
20
  const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, ref) {
21
21
  const { themeKey, dark } = props;
22
22
  const { vizStore, commonStore } = useGlobalStore();
23
- const { allFields, viewFilters, viewDimensions, viewMeasures, visualConfig, draggableFieldState } = vizStore;
23
+ const { allFields, viewFilters, viewDimensions, viewMeasures, visualConfig, draggableFieldState, visList, visIndex } = vizStore;
24
+ const chart = visList[visIndex];
24
25
  const { currentDataset } = commonStore;
25
26
  const { dataSource } = currentDataset;
26
27
 
@@ -83,6 +84,7 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
83
84
 
84
85
  return (
85
86
  <SpecRenderer
87
+ name={chart?.name}
86
88
  loading={waiting}
87
89
  data={viewData}
88
90
  ref={ref}
@@ -2,6 +2,8 @@ import React, { useState, useEffect, forwardRef, useMemo, useRef } from 'react';
2
2
  import { unstable_batchedUpdates } from 'react-dom';
3
3
  import { toJS } from 'mobx';
4
4
  import { observer } from 'mobx-react-lite';
5
+ import { ShadowDom } from '../shadow-dom';
6
+ import AppRoot from '../components/appRoot';
5
7
  import type { IDarkMode, IViewField, IRow, IThemeKey, DraggableFieldState, IVisualConfig } from '../interfaces';
6
8
  import type { IReactVegaHandler } from '../vis/react-vega';
7
9
  import SpecRenderer from './specRenderer';
@@ -9,6 +11,7 @@ import { useRenderer } from './hooks';
9
11
 
10
12
 
11
13
  interface IPureRendererProps {
14
+ name?: string;
12
15
  themeKey?: IThemeKey;
13
16
  dark?: IDarkMode;
14
17
  rawData?: IRow[];
@@ -22,6 +25,7 @@ interface IPureRendererProps {
22
25
  */
23
26
  const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function PureRenderer (props, ref) {
24
27
  const {
28
+ name,
25
29
  themeKey,
26
30
  dark,
27
31
  rawData,
@@ -75,15 +79,22 @@ const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function
75
79
  }, [waiting]);
76
80
 
77
81
  return (
78
- <SpecRenderer
79
- loading={waiting}
80
- data={viewData}
81
- ref={ref}
82
- themeKey={themeKey}
83
- dark={dark}
84
- draggableFieldState={visualState}
85
- visualConfig={visualConfig}
86
- />
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>
87
98
  );
88
99
  });
89
100
 
@@ -10,6 +10,7 @@ import { useCurrentMediaTheme } from '../utils/media';
10
10
  import { builtInThemes } from '../vis/theme';
11
11
 
12
12
  interface SpecRendererProps {
13
+ name?: string;
13
14
  themeKey?: IThemeKey;
14
15
  dark?: IDarkMode;
15
16
  data: IRow[];
@@ -24,7 +25,7 @@ interface SpecRendererProps {
24
25
  * This is a pure component, which means it will not depend on any global state.
25
26
  */
26
27
  const SpecRenderer = forwardRef<IReactVegaHandler, SpecRendererProps>(function (
27
- { themeKey, dark, data, loading, draggableFieldState, visualConfig, onGeomClick, onChartResize },
28
+ { name, themeKey, dark, data, loading, draggableFieldState, visualConfig, onGeomClick, onChartResize },
28
29
  ref
29
30
  ) {
30
31
  // const { draggableFieldState, visualConfig } = vizStore;
@@ -123,6 +124,7 @@ const SpecRenderer = forwardRef<IReactVegaHandler, SpecRendererProps>(function (
123
124
  >
124
125
  {loading && <LoadingLayer />}
125
126
  <ReactVega
127
+ name={name}
126
128
  vegaConfig={vegaConfig}
127
129
  // format={format}
128
130
  layoutMode={size.mode}
@@ -0,0 +1,48 @@
1
+ import React, { HTMLAttributes, createContext, useEffect, useRef, useState } from "react";
2
+ import { StyleSheetManager } from "styled-components";
3
+ import root from "react-shadow";
4
+
5
+ import tailwindStyle from "tailwindcss/tailwind.css?inline";
6
+ import style from "./index.css?inline";
7
+
8
+ export const ShadowDomContext = createContext<{ root: ShadowRoot | null }>({ root: null });
9
+
10
+ interface IShadowDomProps extends HTMLAttributes<HTMLDivElement> {
11
+ onMount?: (shadowRoot: ShadowRoot) => void;
12
+ onUnmount?: () => void;
13
+ }
14
+
15
+ export const ShadowDom: React.FC<IShadowDomProps> = function ShadowDom ({ onMount, onUnmount, children, ...attrs }) {
16
+ const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
17
+ const rootRef = useRef<HTMLDivElement>(null);
18
+
19
+ const onMountRef = useRef(onMount);
20
+ onMountRef.current = onMount;
21
+ const onUnmountRef = useRef(onUnmount);
22
+ onUnmountRef.current = onUnmount;
23
+
24
+ useEffect(() => {
25
+ if (rootRef.current) {
26
+ const shadowRoot = rootRef.current.shadowRoot!;
27
+ setShadowRoot(shadowRoot);
28
+ onMountRef.current?.(shadowRoot);
29
+ return () => {
30
+ onUnmountRef.current?.();
31
+ };
32
+ }
33
+ }, []);
34
+
35
+ return (
36
+ <root.div {...attrs} mode="open" ref={rootRef}>
37
+ <style>{tailwindStyle}</style>
38
+ <style>{style}</style>
39
+ {shadowRoot && (
40
+ <StyleSheetManager target={shadowRoot}>
41
+ <ShadowDomContext.Provider value={{ root: shadowRoot }}>
42
+ {children}
43
+ </ShadowDomContext.Provider>
44
+ </StyleSheetManager>
45
+ )}
46
+ </root.div>
47
+ );
48
+ };
@@ -0,0 +1,102 @@
1
+ import { useImperativeHandle, type ForwardedRef, type MutableRefObject, useEffect } from "react";
2
+ import type { View } from "vega";
3
+ import { useAppRootContext } from "../components/appRoot";
4
+ import type { IReactVegaHandler } from "../vis/react-vega";
5
+ import type { IChartExportResult } from "../interfaces";
6
+
7
+
8
+ export const useVegaExportApi = (name: string | undefined, viewsRef: MutableRefObject<{ x: number; y: number; w: number; h: number; view: View }[]>, ref: ForwardedRef<IReactVegaHandler>) => {
9
+ const renderHandle = {
10
+ getSVGData() {
11
+ return Promise.all(viewsRef.current.map(item => item.view.toSVG()));
12
+ },
13
+ async getCanvasData() {
14
+ const canvases = await Promise.all(viewsRef.current.map(item => item.view.toCanvas(2)));
15
+ return canvases.map(canvas => canvas.toDataURL('image/png', 1));
16
+ },
17
+ async downloadSVG(filename = `gw chart ${Date.now() % 1_000_000}`.padStart(6, '0')) {
18
+ const data = await Promise.all(viewsRef.current.map(item => item.view.toSVG()));
19
+ const files: string[] = [];
20
+ for (let i = 0; i < data.length; i += 1) {
21
+ const d = data[i];
22
+ const file = new File([d], `${filename}${data.length > 1 ? `_${i + 1}` : ''}.svg`);
23
+ const url = URL.createObjectURL(file);
24
+ const a = document.createElement('a');
25
+ a.download = file.name;
26
+ a.href = url;
27
+ a.click();
28
+ requestAnimationFrame(() => {
29
+ URL.revokeObjectURL(url);
30
+ });
31
+ }
32
+ return files;
33
+ },
34
+ async downloadPNG(filename = `gw chart ${Date.now() % 1_000_000}`.padStart(6, '0')) {
35
+ const canvases = await Promise.all(viewsRef.current.map(item => item.view.toCanvas(2)));
36
+ const data = canvases.map(canvas => canvas.toDataURL('image/png', 1));
37
+ const files: string[] = [];
38
+ for (let i = 0; i < data.length; i += 1) {
39
+ const d = data[i];
40
+ const a = document.createElement('a');
41
+ a.download = `${filename}${data.length > 1 ? `_${i + 1}` : ''}.png`;
42
+ a.href = d.replace(/^data:image\/[^;]/, 'data:application/octet-stream');
43
+ a.click();
44
+ }
45
+ return files;
46
+ },
47
+ };
48
+
49
+ useImperativeHandle(ref, () => renderHandle);
50
+
51
+ const appRef = useAppRootContext();
52
+
53
+ useEffect(() => {
54
+ if (appRef && 'current' in appRef && appRef.current) {
55
+ appRef.current.exportChart = (async (mode: IChartExportResult['mode'] = 'svg') => {
56
+ const res: IChartExportResult = {
57
+ mode,
58
+ title: name || 'untitled',
59
+ nCols: viewsRef.current.map(item => item.x).reduce((a, b) => Math.max(a, b), 0) + 1,
60
+ nRows: viewsRef.current.map(item => item.y).reduce((a, b) => Math.max(a, b), 0) + 1,
61
+ charts: viewsRef.current.map(item => ({
62
+ rowIndex: item.y,
63
+ colIndex: item.x,
64
+ width: item.w,
65
+ height: item.h,
66
+ data: '',
67
+ })),
68
+ };
69
+ if (mode === 'data-url') {
70
+ const imgData = await renderHandle.getCanvasData();
71
+ if (imgData) {
72
+ for (let i = 0; i < imgData.length; i += 1) {
73
+ res.charts[i].data = imgData[i];
74
+ }
75
+ }
76
+ } else if (mode === 'svg') {
77
+ const svgData = await renderHandle.getSVGData();
78
+ if (svgData) {
79
+ for (let i = 0; i < svgData.length; i += 1) {
80
+ res.charts[i].data = svgData[i];
81
+ }
82
+ }
83
+ }
84
+ return res;
85
+ }) as typeof appRef.current.exportChart;
86
+ }
87
+ });
88
+
89
+ useEffect(() => {
90
+ return () => {
91
+ if (appRef && 'current' in appRef && appRef.current) {
92
+ appRef.current.exportChart = async mode => ({
93
+ mode,
94
+ title: '',
95
+ nCols: 0,
96
+ nRows: 0,
97
+ charts: [],
98
+ });
99
+ }
100
+ };
101
+ }, []);
102
+ };
@@ -1,10 +1,11 @@
1
- import React, { useEffect, useState, useMemo, forwardRef, useImperativeHandle, useRef } from 'react';
1
+ 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
5
  import type { ScenegraphEvent, View } from 'vega';
6
6
  import styled from 'styled-components';
7
7
 
8
+ import { useVegaExportApi } from '../utils/vegaApiExport';
8
9
  import { IViewField, IRow, IStackMode, VegaGlobalConfig } from '../interfaces';
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import { getVegaTimeFormatRules } from './temporalFormat';
@@ -25,6 +26,7 @@ export interface IReactVegaHandler {
25
26
  downloadPNG: (filename?: string) => Promise<string[]>;
26
27
  }
27
28
  interface ReactVegaProps {
29
+ name?: string;
28
30
  rows: Readonly<IViewField[]>;
29
31
  columns: Readonly<IViewField[]>;
30
32
  dataSource: IRow[];
@@ -73,6 +75,7 @@ interface ParamStoreEntry {
73
75
 
74
76
  const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVega (props, ref) {
75
77
  const {
78
+ name,
76
79
  dataSource = [],
77
80
  rows = [],
78
81
  columns = [],
@@ -150,7 +153,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
150
153
  })
151
154
  }, [rowRepeatFields, colRepeatFields])
152
155
 
153
- const vegaRefs = useRef<View[]>([]);
156
+ const vegaRefs = useRef<{ x: number; y: number; w: number; h: number; view: View }[]>([]);
154
157
 
155
158
  useEffect(() => {
156
159
  vegaRefs.current = [];
@@ -218,7 +221,13 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
218
221
 
219
222
  if (viewPlaceholders.length > 0 && viewPlaceholders[0].current) {
220
223
  embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
221
- vegaRefs.current = [res.view];
224
+ vegaRefs.current = [{
225
+ w: res.view.container()?.clientWidth ?? res.view.width(),
226
+ h: res.view.container()?.clientHeight ?? res.view.height(),
227
+ x: 0,
228
+ y: 0,
229
+ view: res.view,
230
+ }];
222
231
  try {
223
232
  res.view.addEventListener('click', (e) => {
224
233
  click$.next(e);
@@ -250,6 +259,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
250
259
  subscriptions.push(throttledParamStore$.subscribe(cb));
251
260
  };
252
261
  let index = 0;
262
+ vegaRefs.current = new Array(rowRepeatFields.length * colRepeatFields.length);
253
263
  for (let i = 0; i < rowRepeatFields.length; i++) {
254
264
  for (let j = 0; j < colRepeatFields.length; j++, index++) {
255
265
  const sourceId = index;
@@ -282,8 +292,15 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
282
292
  ans.params = commonSpec.params;
283
293
  }
284
294
  if (node) {
295
+ const id = index;
285
296
  embed(node, ans, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(i18n.language), config: vegaConfig }).then(res => {
286
- vegaRefs.current.push(res.view);
297
+ vegaRefs.current[id] = {
298
+ w: res.view.container()?.clientWidth ?? res.view.width(),
299
+ h: res.view.container()?.clientHeight ?? res.view.height(),
300
+ x: j,
301
+ y: i,
302
+ view: res.view,
303
+ };
287
304
  const paramStores = (res.vgSpec.data?.map(d => d.name) ?? []).filter(
288
305
  name => [BRUSH_SIGNAL_NAME, POINT_SIGNAL_NAME].map(p => `${p}_store`).includes(name)
289
306
  ).map(name => name.replace(/_store$/, ''));
@@ -374,45 +391,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
374
391
  text
375
392
  ]);
376
393
 
377
- useImperativeHandle(ref, () => ({
378
- getSVGData() {
379
- return Promise.all(vegaRefs.current.map(view => view.toSVG()));
380
- },
381
- async getCanvasData() {
382
- const canvases = await Promise.all(vegaRefs.current.map(view => view.toCanvas()));
383
- return canvases.map(canvas => canvas.toDataURL('image/png'));
384
- },
385
- async downloadSVG(filename = `gw chart ${Date.now() % 1_000_000}`.padStart(6, '0')) {
386
- const data = await Promise.all(vegaRefs.current.map(view => view.toSVG()));
387
- const files: string[] = [];
388
- for (let i = 0; i < data.length; i += 1) {
389
- const d = data[i];
390
- const file = new File([d], `${filename}${data.length > 1 ? `_${i + 1}` : ''}.svg`);
391
- const url = URL.createObjectURL(file);
392
- const a = document.createElement('a');
393
- a.download = file.name;
394
- a.href = url;
395
- a.click();
396
- requestAnimationFrame(() => {
397
- URL.revokeObjectURL(url);
398
- });
399
- }
400
- return files;
401
- },
402
- async downloadPNG(filename = `gw chart ${Date.now() % 1_000_000}`.padStart(6, '0')) {
403
- const canvases = await Promise.all(vegaRefs.current.map(view => view.toCanvas(2)));
404
- const data = canvases.map(canvas => canvas.toDataURL('image/png', 1));
405
- const files: string[] = [];
406
- for (let i = 0; i < data.length; i += 1) {
407
- const d = data[i];
408
- const a = document.createElement('a');
409
- a.download = `${filename}${data.length > 1 ? `_${i + 1}` : ''}.png`;
410
- a.href = d.replace(/^data:image\/[^;]/, 'data:application/octet-stream');
411
- a.click();
412
- }
413
- return files;
414
- },
415
- }));
394
+ useVegaExportApi(name, vegaRefs, ref);
416
395
 
417
396
  return <CanvaContainer rowSize={Math.max(rowRepeatFields.length, 1)} colSize={Math.max(colRepeatFields.length, 1)}>
418
397
  {/* <div ref={container}></div> */}