feat(data-vi): optimize (#5299)

* refactor(data-vi): add echarts

* fix: echart

* feat: echarts

* chore: add migration

* feat: update

* feat: dark theme

* feat: add configuration

* chore: update

* chore: update

* fix: bug

* chore: update

* chore: update

* fix: test

* fix: build

* chore: update

* chore: locale
This commit is contained in:
YANG QIA 2024-10-08 20:15:00 +08:00 committed by GitHub
parent bcd154453f
commit ce74a77e96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 515 additions and 329 deletions

View File

@ -19,43 +19,36 @@ describe('api', () => {
plugin.charts = new ChartGroup(); plugin.charts = new ChartGroup();
}); });
test('setGroup', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group', charts1);
expect(plugin.charts.charts.get('group')).toEqual(charts1);
const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })];
plugin.charts.setGroup('group', charts2);
expect(plugin.charts.charts.get('group')).toEqual(charts2);
});
test('addGroup', () => { test('addGroup', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })]; const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group1', charts1); plugin.charts.addGroup('group', { title: 'Group', charts: charts1 });
expect(plugin.charts.charts.get('group')).toEqual({ title: 'Group', charts: charts1 });
const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })]; const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })];
plugin.charts.addGroup('group2', charts2); try {
expect(plugin.charts.charts.get('group1')).toEqual(charts1); plugin.charts.addGroup('group', { title: 'Group2', charts: charts2 });
expect(plugin.charts.charts.get('group2')).toEqual(charts2); } catch (error) {
expect(error.message).toEqual('[data-visualization] Chart group "group" already exists');
}
}); });
test('add', () => { test('add', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })]; const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group', charts1); plugin.charts.addGroup('group', { title: 'Group', charts: charts1 });
const chart = new Chart({ name: 'test2', title: 'Test2', Component: null }); const chart = new Chart({ name: 'test2', title: 'Test2', Component: null });
plugin.charts.add('group', chart); plugin.charts.add('group', chart);
expect(plugin.charts.charts.get('group').length).toEqual(2); expect(plugin.charts.charts.get('group').charts.length).toEqual(2);
expect(plugin.charts.charts.get('group')[1].name).toEqual('test2'); expect(plugin.charts.charts.get('group').charts[1].name).toEqual('test2');
}); });
test('getChartTypes', () => { test('getChartTypes', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })]; const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group1', charts1); plugin.charts.addGroup('group1', { title: 'Group1', charts: charts1 });
const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })]; const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })];
plugin.charts.setGroup('group2', charts2); plugin.charts.addGroup('group2', { title: 'Group2', charts: charts2 });
expect(plugin.charts.getChartTypes()).toEqual([ expect(plugin.charts.getChartTypes()).toEqual([
{ {
label: 'group1', label: 'Group1',
children: [ children: [
{ {
key: 'group1.test1', key: 'group1.test1',
@ -65,7 +58,7 @@ describe('api', () => {
], ],
}, },
{ {
label: 'group2', label: 'Group2',
children: [ children: [
{ {
key: 'group2.test2', key: 'group2.test2',
@ -79,9 +72,9 @@ describe('api', () => {
test('getCharts', () => { test('getCharts', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })]; const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group1', charts1); plugin.charts.addGroup('group1', { title: 'Group1', charts: charts1 });
const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })]; const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })];
plugin.charts.setGroup('group2', charts2); plugin.charts.addGroup('group2', { title: 'Group2', charts: charts2 });
expect(plugin.charts.getCharts()).toEqual({ expect(plugin.charts.getCharts()).toEqual({
'group1.test1': charts1[0], 'group1.test1': charts1[0],
'group2.test2': charts2[0], 'group2.test2': charts2[0],
@ -90,9 +83,9 @@ describe('api', () => {
test('getChart', () => { test('getChart', () => {
const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })]; const charts1 = [new Chart({ name: 'test1', title: 'Test1', Component: null })];
plugin.charts.setGroup('group1', charts1); plugin.charts.addGroup('group1', { title: 'Group1', charts: charts1 });
const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })]; const charts2 = [new Chart({ name: 'test2', title: 'Test2', Component: null })];
plugin.charts.setGroup('group2', charts2); plugin.charts.addGroup('group2', { title: 'Group2', charts: charts2 });
expect(plugin.charts.getChart('group1.test1')).toEqual(charts1[0]); expect(plugin.charts.getChart('group1.test1')).toEqual(charts1[0]);
expect(plugin.charts.getChart('group2.test2')).toEqual(charts2[0]); expect(plugin.charts.getChart('group2.test2')).toEqual(charts2[0]);
}); });

View File

@ -17,10 +17,11 @@ export class Statistic extends AntdChart {
super({ super({
name: 'statistic', name: 'statistic',
title: 'Statistic', title: 'Statistic',
enableAdvancedConfig: true,
Component: C, Component: C,
config: [ config: [
{ {
property: 'field', configType: 'field',
name: 'field', name: 'field',
title: 'Field', title: 'Field',
required: true, required: true,

View File

@ -13,7 +13,7 @@ import { Table as AntdTable } from 'antd';
export class Table extends AntdChart { export class Table extends AntdChart {
constructor() { constructor() {
super({ name: 'table', title: 'Table', Component: AntdTable }); super({ name: 'table', title: 'Table', enableAdvancedConfig: true, Component: AntdTable });
} }
getProps({ data, fieldProps, general, advanced }: RenderProps) { getProps({ data, fieldProps, general, advanced }: RenderProps) {

View File

@ -7,12 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React from 'react'; import React, { memo } from 'react';
import { FieldOption } from '../hooks'; import { FieldOption } from '../hooks';
import { DimensionProps, MeasureProps, QueryProps } from '../renderer'; import { DimensionProps, MeasureProps, QueryProps } from '../renderer';
import { parseField } from '../utils'; import { parseField } from '../utils';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import configs, { AnySchemaProperties, Config, FieldConfigProps } from './configs'; import configs, { AnySchemaProperties, Config, ConfigType } from './configs';
import { Transformer } from '../transformers'; import { Transformer } from '../transformers';
export type RenderProps = { export type RenderProps = {
@ -31,6 +31,7 @@ export type RenderProps = {
export interface ChartType { export interface ChartType {
name: string; name: string;
title: string; title: string;
enableAdvancedConfig?: boolean;
Component: React.FC<any>; Component: React.FC<any>;
schema: ISchema; schema: ISchema;
init?: ( init?: (
@ -53,6 +54,7 @@ export interface ChartType {
export type ChartProps = { export type ChartProps = {
name: string; name: string;
title: string; title: string;
enableAdvancedConfig?: boolean;
Component: React.FC<any>; Component: React.FC<any>;
config?: Config[]; config?: Config[];
}; };
@ -60,24 +62,26 @@ export type ChartProps = {
export class Chart implements ChartType { export class Chart implements ChartType {
name: string; name: string;
title: string; title: string;
enableAdvancedConfig = false;
Component: React.FC<any>; Component: React.FC<any>;
config: Config[]; config: Config[];
configs = new Map<string, Function>(); configTypes = new Map<string, ConfigType>();
constructor({ name, title, Component, config }: ChartProps) { constructor({ name, title, enableAdvancedConfig, Component, config }: ChartProps) {
this.name = name; this.name = name;
this.title = title; this.title = title;
this.Component = Component; this.Component = memo(Component, (prev, next) => JSON.stringify(prev) === JSON.stringify(next));
this.config = config; this.config = config;
this.addConfigs(configs); this.enableAdvancedConfig = enableAdvancedConfig || false;
this.addConfigTypes(configs);
} }
/* /*
* Generate config schema according to this.config * Generate config schema according to this.config
* How to set up this.config: * How to set up this.config:
* 1. string - the config function name in config.ts * 1. string - the config function name in config.ts
* 2. object - { property: string, ...props } * 2. object - { configType: string, ...props }
* - property is the config function name in config.ts, and the other props are the arguments of the function * - sttingType is the config function name in config.ts, and the other props are the arguments of the function
* 3. object - use the object directly as the properties of the schema * 3. object - use the object directly as the properties of the schema
* 4. function - use the custom function to return the properties of the schema * 4. function - use the custom function to return the properties of the schema
*/ */
@ -85,35 +89,35 @@ export class Chart implements ChartType {
if (!this.config) { if (!this.config) {
return {}; return {};
} }
const properties = this.config.reduce((properties, conf) => { const properties = this.config.reduce((props, conf) => {
let schema: AnySchemaProperties = {}; let schema: AnySchemaProperties = {};
if (typeof conf === 'string') { if (typeof conf === 'string') {
const func = this.configs.get(conf); conf = this.configTypes.get(conf);
schema = func?.() || {}; }
} else if (typeof conf === 'function') { if (typeof conf === 'function') {
schema = conf(); schema = conf();
} else { } else {
if (conf.property) { if (conf.configType) {
const func = this.configs.get(conf.property); const func = this.configTypes.get(conf.configType as string) as Function;
schema = func?.(conf) || {}; schema = func?.(conf) || {};
} else { } else {
schema = conf as AnySchemaProperties; schema = conf as AnySchemaProperties;
} }
} }
return { return {
...properties, ...props,
...schema, ...schema,
}; };
}, {} as AnySchemaProperties); }, {} as any);
return { return {
type: 'object', type: 'object',
properties, properties,
}; };
} }
addConfigs(configs: { [key: string]: (props: FieldConfigProps) => AnySchemaProperties }) { addConfigTypes(configs: { [key: string]: ConfigType }) {
Object.entries(configs).forEach(([key, func]) => { Object.entries(configs).forEach(([key, func]) => {
this.configs.set(key, func); this.configTypes.set(key, func);
}); });
} }

View File

@ -15,48 +15,241 @@ export type FieldConfigProps = Partial<{
title: string; title: string;
required: boolean; required: boolean;
defaultValue: any; defaultValue: any;
description: string;
options: { label: string; value: any }[];
componentProps: Record<string, any>;
}>; }>;
export type AnySchemaProperties = SchemaProperties<any, any, any, any, any, any, any, any>; export type AnySchemaProperties = SchemaProperties<any, any, any, any, any, any, any, any>;
export type ConfigProps = FieldConfigProps | AnySchemaProperties | (() => AnySchemaProperties); export type ConfigType =
| (FieldConfigProps & { configType?: string })
| ((props?: FieldConfigProps) => AnySchemaProperties)
| AnySchemaProperties;
export type Config = export type Config = string | ConfigType;
| (ConfigProps & {
property?: string;
})
| string;
const selectField = ({ name, title, required, defaultValue }: FieldConfigProps) => { const field = ({ name, title, required, defaultValue, description }: FieldConfigProps) => {
return { return {
[name || 'field']: { [name]: {
title: lang(title || 'Field'), title: lang(title),
type: 'string', type: 'string',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-reactions': '{{ useChartFields }}', 'x-reactions': '{{ useChartFields }}',
required, required,
description,
default: defaultValue, default: defaultValue,
}, },
}; };
}; };
const booleanField = ({ name, title, defaultValue = false }: FieldConfigProps) => { const select = ({ name, title, required, defaultValue, options, description }: FieldConfigProps) => {
return { return {
[name || 'field']: { [name]: {
'x-content': lang(title || 'Field'), title: lang(title),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
required,
default: defaultValue,
description,
enum: options,
},
};
};
const boolean = ({ name, title, defaultValue = false, description }: FieldConfigProps) => {
return {
[name]: {
'x-content': lang(title),
type: 'boolean', type: 'boolean',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', 'x-component': 'Checkbox',
default: defaultValue, default: defaultValue,
description,
},
};
};
const radio = ({ name, title, defaultValue, options, description, componentProps }: FieldConfigProps) => {
return {
[name]: {
title: lang(title),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
...componentProps,
},
default: defaultValue,
description,
enum: options,
},
};
};
const percent = ({ name, title, defaultValue, description }: FieldConfigProps) => {
return {
[name]: {
title,
type: 'number',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
default: defaultValue,
description,
'x-component-props': {
suffix: '%',
},
},
};
};
const input = ({ name, title, required, defaultValue, description }: FieldConfigProps) => {
return {
[name]: {
title: lang(title),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
required,
default: defaultValue,
description,
}, },
}; };
}; };
export default { export default {
field: selectField, field,
booleanField, input,
xField: (props: FieldConfigProps) => selectField({ name: 'xField', title: 'xField', required: true, ...props }), boolean,
yField: (props: FieldConfigProps) => selectField({ name: 'yField', title: 'yField', required: true, ...props }), select,
seriesField: (props: FieldConfigProps) => selectField({ name: 'seriesField', title: 'seriesField', ...props }), radio,
colorField: (props: FieldConfigProps) => selectField({ name: 'colorField', title: 'colorField', ...props }), percent,
xField: {
configType: 'field',
name: 'xField',
title: 'xField',
required: true,
},
yField: {
configType: 'field',
name: 'yField',
title: 'yField',
required: true,
},
seriesField: {
configType: 'field',
name: 'seriesField',
title: 'seriesField',
},
colorField: {
configType: 'field',
name: 'colorField',
title: 'colorField',
required: true,
},
isStack: {
configType: 'boolean',
name: 'isStack',
title: 'isStack',
},
smooth: {
configType: 'boolean',
name: 'smooth',
title: 'smooth',
},
isPercent: {
configType: 'boolean',
name: 'isPercent',
title: 'isPercent',
},
isGroup: {
configType: 'boolean',
name: 'isGroup',
title: 'isGroup',
},
size: () => ({
size: {
title: lang('Size'),
type: 'object',
'x-decorator': 'FormItem',
'x-component': 'Space',
properties: {
type: {
'x-component': 'Select',
'x-component-props': {
allowClear: false,
},
default: 'ratio',
enum: [
{
label: lang('Aspect ratio'),
value: 'ratio',
},
{
label: lang('Fixed height'),
value: 'fixed',
},
],
},
fixed: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
min: 0,
addonAfter: 'px',
},
'x-reactions': [
{
dependencies: ['.type'],
fulfill: {
state: {
visible: "{{$deps[0] === 'fixed'}}",
},
},
},
],
},
ratio: {
type: 'object',
'x-component': 'Space',
'x-reactions': [
{
dependencies: ['.type'],
fulfill: {
state: {
visible: "{{$deps[0] === 'ratio'}}",
},
},
},
],
properties: {
width: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: lang('Width'),
min: 1,
},
},
colon: {
type: 'void',
'x-component': 'Text',
'x-component-props': {
children: ':',
},
},
height: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: lang('Height'),
min: 1,
},
},
},
},
},
},
}),
}; };

View File

@ -7,8 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React, { useContext, useEffect, useRef } from 'react'; import React from 'react';
import { ChartRendererContext } from '../../renderer'; import { useSetChartSize } from '../../hooks/chart';
import { useGlobalTheme } from '@nocobase/client'; import { useGlobalTheme } from '@nocobase/client';
export const getAntChart = (Component: React.FC<any>) => (props: any) => { export const getAntChart = (Component: React.FC<any>) => (props: any) => {
@ -18,31 +18,7 @@ export const getAntChart = (Component: React.FC<any>) => (props: any) => {
if (!fixedHeight && size.type === 'fixed') { if (!fixedHeight && size.type === 'fixed') {
fixedHeight = size.fixed; fixedHeight = size.fixed;
} }
const { service } = useContext(ChartRendererContext); const { chartRef, chartHeight } = useSetChartSize(size, fixedHeight);
const chartRef = useRef(null);
const [height, setHeight] = React.useState<number>(0);
useEffect(() => {
const el = chartRef.current;
if (!el || service.loading === true || fixedHeight) {
return;
}
let ratio = 0;
if (size.type === 'ratio' && size.ratio?.width && size.ratio?.height) {
ratio = size.ratio.height / size.ratio.width;
}
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
if (ratio) {
setHeight(entry.contentRect.width * ratio);
return;
}
setHeight(entry.contentRect.height);
});
});
observer.observe(el);
return () => observer.disconnect();
}, [service.loading, fixedHeight, size.type, size.ratio?.width, size.ratio?.height]);
const chartHeight = fixedHeight || height;
return ( return (
<div ref={chartRef} style={chartHeight ? { height: `${chartHeight}px` } : {}}> <div ref={chartRef} style={chartHeight ? { height: `${chartHeight}px` } : {}}>

View File

@ -1,103 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import config, { FieldConfigProps } from '../configs';
const { booleanField } = config;
import { lang } from '../../locale';
export default {
isStack: (props: FieldConfigProps) => booleanField({ name: 'isStack', title: 'isStack', ...props }),
smooth: (props: FieldConfigProps) => booleanField({ name: 'smooth', title: 'smooth', ...props }),
isPercent: (props: FieldConfigProps) => booleanField({ name: 'isPercent', title: 'isPercent', ...props }),
isGroup: (props: FieldConfigProps) => booleanField({ name: 'isGroup', title: 'isGroup', ...props }),
size: () => ({
size: {
title: lang('Size'),
type: 'object',
'x-decorator': 'FormItem',
'x-component': 'Space',
properties: {
type: {
'x-component': 'Select',
'x-component-props': {
allowClear: false,
},
default: 'ratio',
enum: [
{
label: lang('Aspect ratio'),
value: 'ratio',
},
{
label: lang('Fixed height'),
value: 'fixed',
},
],
},
fixed: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
min: 0,
addonAfter: 'px',
},
'x-reactions': [
{
dependencies: ['.type'],
fulfill: {
state: {
visible: "{{$deps[0] === 'fixed'}}",
},
},
},
],
},
ratio: {
type: 'object',
'x-component': 'Space',
'x-reactions': [
{
dependencies: ['.type'],
fulfill: {
state: {
visible: "{{$deps[0] === 'ratio'}}",
},
},
},
],
properties: {
width: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: lang('Width'),
min: 1,
},
},
colon: {
type: 'void',
'x-component': 'Text',
'x-component-props': {
children: ':',
},
},
height: {
type: 'number',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: lang('Height'),
min: 1,
},
},
},
},
},
},
}),
};

View File

@ -9,13 +9,12 @@
import { G2PlotChart } from './g2plot'; import { G2PlotChart } from './g2plot';
import { ChartType, RenderProps } from '../chart'; import { ChartType, RenderProps } from '../chart';
import React from 'react';
import { DualAxes as G2DualAxes } from '@ant-design/plots'; import { DualAxes as G2DualAxes } from '@ant-design/plots';
import lodash from 'lodash'; import lodash from 'lodash';
export class DualAxes extends G2PlotChart { export class DualAxes extends G2PlotChart {
constructor() { constructor() {
super({ name: 'dualAxes', title: 'Dual Axes Chart', Component: G2DualAxes }); super({ name: 'dualAxes', title: 'Dual axes', Component: G2DualAxes });
this.config = [ this.config = [
'xField', 'xField',
{ {

View File

@ -8,7 +8,6 @@
*/ */
import { Chart, ChartProps, ChartType, RenderProps } from '../chart'; import { Chart, ChartProps, ChartType, RenderProps } from '../chart';
import configs from './configs';
import { getAntChart } from './AntChart'; import { getAntChart } from './AntChart';
export class G2PlotChart extends Chart { export class G2PlotChart extends Chart {
@ -16,10 +15,10 @@ export class G2PlotChart extends Chart {
super({ super({
name, name,
title, title,
enableAdvancedConfig: true,
Component: getAntChart(Component), Component: getAntChart(Component),
config: ['xField', 'yField', 'seriesField', 'size', ...(config || [])], config: ['xField', 'yField', 'seriesField', 'size', ...(config || [])],
}); });
this.addConfigs(configs);
} }
init: ChartType['init'] = (fields, { measures, dimensions }) => { init: ChartType['init'] = (fields, { measures, dimensions }) => {

View File

@ -15,18 +15,20 @@ import { G2PlotChart } from './g2plot';
export default [ export default [
new G2PlotChart({ new G2PlotChart({
name: 'line', name: 'line',
title: 'Line Chart', title: 'Line',
Component: Line, Component: Line,
config: ['smooth', 'isStack'], config: ['smooth', 'isStack'],
}), }),
new G2PlotChart({ new G2PlotChart({
name: 'area', name: 'area',
title: 'Area Chart', title: 'Area',
Component: Area, Component: Area,
config: [ config: [
'smooth', 'smooth',
{ {
property: 'isStack', configType: 'boolean',
name: 'isStack',
title: 'isStack',
defaultValue: true, defaultValue: true,
}, },
'isPercent', 'isPercent',
@ -34,17 +36,17 @@ export default [
}), }),
new G2PlotChart({ new G2PlotChart({
name: 'column', name: 'column',
title: 'Column Chart', title: 'Column',
Component: Column, Component: Column,
config: ['isGroup', 'isStack', 'isPercent'], config: ['isGroup', 'isStack', 'isPercent'],
}), }),
new G2PlotChart({ new G2PlotChart({
name: 'bar', name: 'bar',
title: 'Bar Chart', title: 'Bar',
Component: Bar, Component: Bar,
config: ['isGroup', 'isStack', 'isPercent'], config: ['isGroup', 'isStack', 'isPercent'],
}), }),
new Pie(), new Pie(),
new DualAxes(), new DualAxes(),
new G2PlotChart({ name: 'scatter', title: 'Scatter Chart', Component: Scatter }), new G2PlotChart({ name: 'scatter', title: 'Scatter', Component: Scatter }),
]; ];

View File

@ -13,16 +13,16 @@ import { ChartType, RenderProps } from '../chart';
export class Pie extends G2PlotChart { export class Pie extends G2PlotChart {
constructor() { constructor() {
super({ name: 'pie', title: 'Pie Chart', Component: G2Pie }); super({ name: 'pie', title: 'Pie', Component: G2Pie });
this.config = [ this.config = [
{ {
property: 'field', configType: 'field',
name: 'angleField', name: 'angleField',
title: 'angleField', title: 'angleField',
required: true, required: true,
}, },
{ {
property: 'field', configType: 'field',
name: 'colorField', name: 'colorField',
title: 'colorField', title: 'colorField',
required: true, required: true,

View File

@ -12,28 +12,37 @@ import { ChartType } from './chart';
import DataVisualizationPlugin from '..'; import DataVisualizationPlugin from '..';
import { lang } from '../locale'; import { lang } from '../locale';
interface Group {
title: string;
charts: ChartType[];
sort?: number;
}
export class ChartGroup { export class ChartGroup {
/** /**
* @internal * @internal
*/ */
charts: Map<string, ChartType[]> = new Map(); charts: Map<string, Group> = new Map();
setGroup(name: string, charts: ChartType[]) { addGroup(name: string, group: Group) {
this.charts.set(name, charts);
}
addGroup(name: string, charts: ChartType[]) {
if (this.charts.has(name)) { if (this.charts.has(name)) {
throw new Error(`[data-visualization] Chart group "${name}" already exists`); throw new Error(`[data-visualization] Chart group "${name}" already exists`);
} }
this.setGroup(name, charts); this.charts.set(name, group);
} }
add(group: string, chart: ChartType) { add(name: string, charts: ChartType | ChartType[]) {
if (!this.charts.has(group)) { if (!this.charts.has(name)) {
this.setGroup(group, []); return;
} }
this.charts.get(group)?.push(chart); if (!Array.isArray(charts)) {
charts = [charts];
}
const group = this.charts.get(name);
this.charts.set(name, {
...group,
charts: [...group.charts, ...charts],
});
} }
/** /**
@ -48,23 +57,19 @@ export class ChartGroup {
}[]; }[];
}[] { }[] {
const result = []; const result = [];
this.charts.forEach((charts, group) => { Array.from(this.charts.entries())
const children = charts.map((chart) => ({ .sort(([, a], [, b]) => a.sort || 0 - b.sort || 0)
key: `${group}.${chart.name}`, .forEach(([group, { title, charts }]) => {
label: lang(chart.title), const children = charts.map((chart) => ({
value: `${group}.${chart.name}`, key: `${group}.${chart.name}`,
})); label: lang(chart.title),
result.push({ value: `${group}.${chart.name}`,
label: lang(group), }));
children, result.push({
label: lang(title),
children,
});
}); });
});
// Put group named "Built-in" at the first
const index = result.findIndex((item) => item.label === lang('Built-in'));
if (index > -1) {
const [item] = result.splice(index, 1);
result.unshift(item);
}
return result; return result;
} }
@ -75,7 +80,7 @@ export class ChartGroup {
[key: string]: ChartType; [key: string]: ChartType;
} { } {
const result = {}; const result = {};
this.charts.forEach((charts, group) => { this.charts.forEach(({ charts }, group) => {
charts.forEach((chart) => { charts.forEach((chart) => {
result[`${group}.${chart.name}`] = chart; result[`${group}.${chart.name}`] = chart;
}); });

View File

@ -22,7 +22,7 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography, theme } from 'antd'; import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography, theme } from 'antd';
import { cloneDeep, isEqual } from 'lodash'; import { cloneDeep, isEqual } from 'lodash';
import React, { useContext, useEffect, useMemo, useRef } from 'react'; import React, { memo, useContext, useEffect, useMemo, useRef } from 'react';
import { import {
useChartFields, useChartFields,
useCollectionOptions, useCollectionOptions,
@ -101,6 +101,7 @@ export const ChartConfigure: React.FC<{
const selectedFields = getSelectedFields(fields, query); const selectedFields = getSelectedFields(fields, query);
const { general, advanced } = chart.init(selectedFields, query); const { general, advanced } = chart.init(selectedFields, query);
if (general || overwrite) { if (general || overwrite) {
form.setInitialValuesIn('config.general', {});
form.values.config.general = general; form.values.config.general = general;
} }
if (advanced || overwrite) { if (advanced || overwrite) {
@ -142,7 +143,7 @@ export const ChartConfigure: React.FC<{
[field, visible, dataSource, collection], [field, visible, dataSource, collection],
); );
const RunButton: React.FC = () => ( const RunButton: React.FC = memo(() => (
<Button <Button
type="link" type="link"
loading={service?.loading} loading={service?.loading}
@ -165,7 +166,7 @@ export const ChartConfigure: React.FC<{
> >
{t('Run query')} {t('Run query')}
</Button> </Button>
); ));
const queryRef = useRef(null); const queryRef = useRef(null);
const configRef = useRef(null); const configRef = useRef(null);
@ -434,10 +435,11 @@ ChartConfigure.Config = function Config() {
{(form) => { {(form) => {
const chartType = form.values.config?.chartType; const chartType = form.values.config?.chartType;
const chart = charts[chartType]; const chart = charts[chartType];
const enableAdvancedConfig = chart?.enableAdvancedConfig;
const schema = chart?.schema || {}; const schema = chart?.schema || {};
return ( return (
<SchemaComponent <SchemaComponent
schema={getConfigSchema(schema)} schema={getConfigSchema(schema, enableAdvancedConfig)}
scope={{ t, chartTypes, useChartFields: getChartFields, getReference, formCollapse }} scope={{ t, chartTypes, useChartFields: getChartFields, getReference, formCollapse }}
components={{ FormItem, ArrayItems, Space, AutoComplete, FormCollapse }} components={{ FormItem, ArrayItems, Space, AutoComplete, FormCollapse }}
/> />

View File

@ -47,7 +47,7 @@ const getArraySchema = (fields = {}, extra = {}) => ({
}, },
}); });
export const getConfigSchema = (general: any): ISchema => ({ export const getConfigSchema = (general: any, enableAdvancedConfig?: boolean): ISchema => ({
type: 'void', type: 'void',
properties: { properties: {
config: { config: {
@ -115,37 +115,43 @@ export const getConfigSchema = (general: any): ISchema => ({
general, general,
}, },
}, },
[uid()]: { ...(enableAdvancedConfig
type: 'void', ? {
properties: { [uid()]: {
advanced: { type: 'void',
type: 'json', properties: {
title: '{{t("JSON config")}}', advanced: {
'x-decorator': 'FormItem', type: 'json',
'x-decorator-props': { title: '{{t("JSON config")}}',
extra: lang('Same properties set in the form above will be overwritten by this JSON config.'), 'x-decorator': 'FormItem',
}, 'x-decorator-props': {
'x-component': 'Input.JSON', extra: lang(
'x-component-props': { 'Same properties set in the form above will be overwritten by this JSON config.',
autoSize: { ),
minRows: 3, },
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 3,
},
json5: true,
},
},
}, },
json5: true,
}, },
}, reference: {
}, type: 'string',
}, 'x-reactions': {
reference: { dependencies: ['.chartType'],
type: 'string', fulfill: {
'x-reactions': { schema: {
dependencies: ['.chartType'], 'x-content': '{{ getReference($deps[0]) }}',
fulfill: { },
schema: { },
'x-content': '{{ getReference($deps[0]) }}', },
}, },
}, }
}, : {}),
},
}, },
}, },
}, },

View File

@ -0,0 +1,51 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useContext, useEffect, useRef, useState } from 'react';
import { ChartRendererContext } from '../renderer';
export const useSetChartSize = (
size: {
type: 'fixed' | 'ratio';
ratio?: {
width: number;
height: number;
};
fixed?: number;
},
fixedHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
const chartRef = useRef(null);
const { service } = useContext(ChartRendererContext);
useEffect(() => {
const el = chartRef.current;
if (!el || service.loading === true || fixedHeight) {
return;
}
let ratio = 0;
if (size.type === 'ratio' && size.ratio?.width && size.ratio?.height) {
ratio = size.ratio.height / size.ratio.width;
}
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
if (ratio) {
setHeight(entry.contentRect.width * ratio);
return;
}
setHeight(entry.contentRect.height);
});
});
observer.observe(el);
return () => observer.disconnect();
}, [service.loading, fixedHeight, size.type, size.ratio?.width, size.ratio?.height]);
const chartHeight = fixedHeight || height;
return { chartRef, chartHeight };
};

View File

@ -11,3 +11,4 @@ export * from './query';
export * from './transformer'; export * from './transformer';
export * from './useVariableOptions'; export * from './useVariableOptions';
export * from './filter'; export * from './filter';
export * from './chart';

View File

@ -39,7 +39,8 @@ class PluginDataVisualiztionClient extends Plugin {
public charts: ChartGroup = new ChartGroup(); public charts: ChartGroup = new ChartGroup();
async load() { async load() {
this.charts.setGroup('Built-in', [...g2plot, ...antd]); this.charts.addGroup('antd', { title: 'Ant Design', charts: antd });
this.charts.addGroup('ant-design-charts', { title: 'Ant Design Charts', charts: g2plot });
this.app.addComponents({ this.app.addComponents({
ChartV2BlockInitializer, ChartV2BlockInitializer,
@ -85,5 +86,6 @@ export default PluginDataVisualiztionClient;
export { Chart } from './chart/chart'; export { Chart } from './chart/chart';
export type { ChartProps, ChartType, RenderProps } from './chart/chart'; export type { ChartProps, ChartType, RenderProps } from './chart/chart';
export { ChartConfigContext } from './configure'; export { ChartConfigContext } from './configure';
export { useSetChartSize } from './hooks';
export type { FieldOption } from './hooks'; export type { FieldOption } from './hooks';
export type { QueryProps } from './renderer'; export type { QueryProps } from './renderer';

View File

@ -9,7 +9,7 @@
import { useAPIClient } from '@nocobase/client'; import { useAPIClient } from '@nocobase/client';
import { Empty, Result, Spin, Typography } from 'antd'; import { Empty, Result, Spin, Typography } from 'antd';
import React, { useContext } from 'react'; import React, { useContext, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks'; import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
import { useChartsTranslation } from '../locale'; import { useChartsTranslation } from '../locale';
@ -18,6 +18,7 @@ import { ChartRendererContext } from './ChartRendererProvider';
import { useChart } from '../chart/group'; import { useChart } from '../chart/group';
import { Schema } from '@formily/react'; import { Schema } from '@formily/react';
import { ChartRendererDesigner } from './ChartRendererDesigner'; import { ChartRendererDesigner } from './ChartRendererDesigner';
import { uid } from '@formily/shared';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;
const ErrorFallback = ({ error }) => { const ErrorFallback = ({ error }) => {
@ -50,7 +51,21 @@ export const ChartRenderer: React.FC & {
const chart = useChart(config?.chartType); const chart = useChart(config?.chartType);
const locale = api.auth.getLocale(); const locale = api.auth.getLocale();
const transformers = useFieldTransformer(transform, locale); const transformers = useFieldTransformer(transform, locale);
const chartProps = chart?.getProps({ // error key is used for resetting error boundary when config changes
const [errorKey, setErrorKey] = React.useState(uid());
useEffect(() => {
setErrorKey(uid());
}, [config]);
if (!chart) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure chart')} />;
}
if (!(data && data.length) && !service.loading) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} />;
}
const chartProps = chart.getProps({
data, data,
general, general,
advanced, advanced,
@ -64,24 +79,18 @@ export const ChartRenderer: React.FC & {
}, {}), }, {}),
}); });
const compiledProps = Schema.compile(chartProps); const compiledProps = Schema.compile(chartProps);
const C = chart?.Component; const C = chart.Component;
if (!chart) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure chart')} />;
}
if (!(data && data.length) && !service.loading) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} />;
}
return ( return (
<Spin spinning={service.loading}> <Spin spinning={service.loading}>
<ErrorBoundary <ErrorBoundary
key={errorKey}
onError={(error) => { onError={(error) => {
console.error(error); console.error(error);
}} }}
FallbackComponent={ErrorFallback} FallbackComponent={ErrorFallback}
> >
<C {...compiledProps} /> {!service.loading && <C {...compiledProps} />}
</ErrorBoundary> </ErrorBoundary>
</Spin> </Spin>
); );

View File

@ -34,19 +34,19 @@
"Type": "Type", "Type": "Type",
"Add field": "Add field", "Add field": "Add field",
"Add chart": "Add chart", "Add chart": "Add chart",
"xField": "xField", "xField": "X field",
"yField": "yField", "yField": "Y field",
"seriesField": "seriesField", "seriesField": "Series field",
"angleField": "angleField", "angleField": "Angle field",
"colorField": "colorField", "colorField": "Color field",
"Line Chart": "Line Chart", "Line": "Line",
"Area Chart": "Area Chart", "Area": "Area",
"Column Chart": "Column Chart", "Column": "Column",
"Bar Chart": "Bar Chart", "Bar": "Bar",
"Pie Chart": "Pie Chart", "Pie": "Pie",
"Dual Axes Chart": "Dual Axes Chart", "Dual axes": "Dual axes",
"Scatter Chart": "Scatter Chart", "Scatter": "Scatter",
"Gauge Chart": "Gauge Chart", "Gauge": "Gauge",
"Statistic": "Statistic", "Statistic": "Statistic",
"Currency": "Currency", "Currency": "Currency",
"Percent": "Percent", "Percent": "Percent",

View File

@ -39,14 +39,14 @@
"seriesField": "分类字段", "seriesField": "分类字段",
"angleField": "角度字段", "angleField": "角度字段",
"colorField": "颜色字段", "colorField": "颜色字段",
"Line Chart": "折线图", "Line": "折线图",
"Area Chart": "面积图", "Area": "面积图",
"Column Chart": "柱状图", "Column": "柱状图",
"Bar Chart": "条形图", "Bar": "条形图",
"Pie Chart": "饼图", "Pie": "饼图",
"Dual Axes Chart": "双轴图", "Dual axes": "双轴图",
"Scatter Chart": "散点图", "Scatter": "散点图",
"Gauge Chart": "仪表盘", "Gauge": "仪表盘",
"Statistic": "统计", "Statistic": "统计",
"Currency": "货币", "Currency": "货币",
"Percent": "百分比", "Percent": "百分比",

View File

@ -0,0 +1,43 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Repository } from '@nocobase/database';
import { Migration } from '@nocobase/server';
export default class extends Migration {
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
appVersion = '<=1.5.0-beta';
async up() {
const r = this.db.getRepository<Repository>('uiSchemas');
const items = await r.find({
filter: {
'schema.x-decorator': 'ChartRendererProvider',
},
});
await this.db.sequelize.transaction(async (transaction) => {
for (const item of items) {
const schema = item.schema;
const chartType = schema['x-decorator-props']?.config?.chartType;
if (!chartType) {
continue;
}
if (chartType.startsWith('Built-in.')) {
if (chartType === 'Built-in.statistic' || chartType === 'Built-in.table') {
schema['x-decorator-props'].config.chartType = chartType.replace('Built-in', 'antd');
} else {
schema['x-decorator-props'].config.chartType = chartType.replace('Built-in', 'ant-design-charts');
}
item.set('schema', schema);
await item.save({ transaction });
}
}
});
}
}

View File

@ -12975,6 +12975,22 @@ ecdsa-sig-formatter@1.0.11:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
echarts-for-react@^3.0.2:
version "3.0.2"
resolved "https://registry.npmmirror.com/echarts-for-react/-/echarts-for-react-3.0.2.tgz#ac5859157048a1066d4553e34b328abb24f2b7c1"
integrity sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==
dependencies:
fast-deep-equal "^3.1.3"
size-sensor "^1.0.1"
echarts@^5.5.0:
version "5.5.0"
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.5.0.tgz#c13945a7f3acdd67c134d8a9ac67e917830113ac"
integrity sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==
dependencies:
tslib "2.3.0"
zrender "5.5.0"
editions@^2.2.0: editions@^2.2.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.npmmirror.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698" resolved "https://registry.npmmirror.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698"
@ -25213,7 +25229,7 @@ string-convert@^0.2.0:
resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -25231,15 +25247,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@ -25354,7 +25361,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0" character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0" character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -25382,13 +25389,6 @@ strip-ansi@^5.1.0:
dependencies: dependencies:
ansi-regex "^4.1.0" ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1: strip-ansi@^7.0.1:
version "7.1.0" version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -26318,6 +26318,11 @@ tslib@1.9.3:
resolved "https://registry.npmmirror.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.npmmirror.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@ -27783,7 +27788,7 @@ wordwrap@^1.0.0:
resolved "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" resolved "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -27809,15 +27814,6 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@ -28170,6 +28166,13 @@ zip-stream@^4.1.0:
compress-commons "^4.1.2" compress-commons "^4.1.2"
readable-stream "^3.6.0" readable-stream "^3.6.0"
zrender@5.5.0:
version "5.5.0"
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"
integrity sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==
dependencies:
tslib "2.3.0"
zustand@^4.4.1: zustand@^4.4.1:
version "4.4.7" version "4.4.7"
resolved "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c" resolved "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c"