From f82b6a9d38fa9ca98e20c50c0c4f46a9f1726d77 Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Mon, 31 Jul 2023 23:51:23 +0800 Subject: [PATCH] fix(bi): g2plot render wrong when fields contain `.` (#2363) * fix: g2plot render wrong when fields contains `.` * fix: build * fix: test * fix: dual axes bug * chore: add comment * fix: code style --- .../src/client/Settings.tsx | 2 +- .../client/__tests__/chart-library.test.ts | 11 +- .../src/client/block/ChartConfigure.tsx | 7 +- .../src/client/chart/antd/antd.ts | 10 + .../src/client/chart/antd/index.ts | 4 + .../src/client/chart/antd/statistic.ts | 64 +++++ .../src/client/chart/antd/table.ts | 43 ++++ .../src/client/chart/chart.ts | 134 ++++++++++ .../src/client/chart/g2plot/bar.ts | 30 +++ .../src/client/chart/g2plot/dualAxes.ts | 97 +++++++ .../src/client/chart/g2plot/g2plot.ts | 107 ++++++++ .../src/client/chart/g2plot/index.ts | 16 ++ .../src/client/chart/g2plot/pie.ts | 52 ++++ .../src/client/chart/library.tsx | 89 +++++++ .../data-visualization/src/client/index.tsx | 14 +- .../src/client/renderer/ChartLibrary.tsx | 183 -------------- .../src/client/renderer/ChartRenderer.tsx | 14 +- .../src/client/renderer/index.ts | 2 - .../client/renderer/library/AntdLibrary.tsx | 94 ------- .../client/renderer/library/G2PlotLibrary.tsx | 236 ------------------ .../src/client/renderer/library/index.tsx | 4 - .../src/server/actions/query.ts | 14 +- yarn.lock | 5 - 23 files changed, 684 insertions(+), 548 deletions(-) create mode 100644 packages/plugins/data-visualization/src/client/chart/antd/antd.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/antd/index.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/antd/statistic.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/antd/table.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/chart.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/g2plot/bar.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/g2plot/dualAxes.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/g2plot/g2plot.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/g2plot/index.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/g2plot/pie.ts create mode 100644 packages/plugins/data-visualization/src/client/chart/library.tsx delete mode 100644 packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx delete mode 100644 packages/plugins/data-visualization/src/client/renderer/library/AntdLibrary.tsx delete mode 100644 packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx delete mode 100644 packages/plugins/data-visualization/src/client/renderer/library/index.tsx diff --git a/packages/plugins/data-visualization/src/client/Settings.tsx b/packages/plugins/data-visualization/src/client/Settings.tsx index c91cd4c88..96efb6840 100644 --- a/packages/plugins/data-visualization/src/client/Settings.tsx +++ b/packages/plugins/data-visualization/src/client/Settings.tsx @@ -5,7 +5,7 @@ import { Button, Card } from 'antd'; import cls from 'classnames'; import React, { useContext } from 'react'; import { useChartsTranslation } from './locale'; -import { ChartLibraryContext, useToggleChartLibrary } from './renderer'; +import { ChartLibraryContext, useToggleChartLibrary } from './chart/library'; export const Settings = () => { const { t } = useChartsTranslation(); diff --git a/packages/plugins/data-visualization/src/client/__tests__/chart-library.test.ts b/packages/plugins/data-visualization/src/client/__tests__/chart-library.test.ts index 653e65070..0ef816948 100644 --- a/packages/plugins/data-visualization/src/client/__tests__/chart-library.test.ts +++ b/packages/plugins/data-visualization/src/client/__tests__/chart-library.test.ts @@ -1,5 +1,6 @@ +import { Chart } from '../chart/chart'; import { FieldOption } from '../hooks'; -import { infer } from '../renderer'; +const chart = new Chart('test', 'Test', null); describe('library', () => { describe('auto infer', () => { @@ -37,7 +38,7 @@ describe('library', () => { ] as FieldOption[]; test('1 measure, 1 dimension', () => { - const { xField, yField } = infer(fields, { + const { xField, yField } = chart.infer(fields, { measures: [{ field: ['price'] }], dimensions: [{ field: ['title'] }], }); @@ -46,7 +47,7 @@ describe('library', () => { }); test('1 measure, 2 dimensions with date', () => { - const { xField, yField, seriesField } = infer(fields, { + const { xField, yField, seriesField } = chart.infer(fields, { measures: [{ field: ['price'] }], dimensions: [{ field: ['title'] }, { field: ['createdAt'] }], }); @@ -56,7 +57,7 @@ describe('library', () => { }); test('1 measure, 2 dimensions without date', () => { - const { xField, yField, seriesField } = infer(fields, { + const { xField, yField, seriesField } = chart.infer(fields, { measures: [{ field: ['price'] }], dimensions: [{ field: ['title'] }, { field: ['name'] }], }); @@ -66,7 +67,7 @@ describe('library', () => { }); test('2 measures, 1 dimension', () => { - const { xField, yField, yFields } = infer(fields, { + const { xField, yField, yFields } = chart.infer(fields, { measures: [{ field: ['price'] }, { field: ['count'] }], dimensions: [{ field: ['title'] }], }); diff --git a/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx b/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx index 087186f53..b80a39573 100644 --- a/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx +++ b/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx @@ -26,9 +26,10 @@ import { useTransformers, } from '../hooks'; import { useChartsTranslation } from '../locale'; -import { ChartRenderer, ChartRendererContext, useChartTypes, useCharts, useDefaultChartType } from '../renderer'; +import { ChartRenderer, ChartRendererContext } from '../renderer'; import { createRendererSchema, getField, getSelectedFields } from '../utils'; import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; +import { useChartTypes, useCharts, useDefaultChartType } from '../chart/library'; const { Paragraph, Text } = Typography; export type ChartConfigCurrent = { @@ -98,7 +99,7 @@ export const ChartConfigure: React.FC<{ } const query = form.values.query; const selectedFields = getSelectedFields(fields, query); - const { general, advanced } = init(selectedFields, query); + const { general, advanced } = chart.init(selectedFields, query); if (general || overwrite) { form.values.config.general = general; } @@ -364,7 +365,7 @@ ChartConfigure.Config = function Config() { const charts = useCharts(); const getChartFields = useChartFields(fields); const getReference = (chartType: string) => { - const reference = charts[chartType]?.reference; + const reference = charts[chartType]?.getReference?.(); if (!reference) return ''; const { title, link } = reference; return ( diff --git a/packages/plugins/data-visualization/src/client/chart/antd/antd.ts b/packages/plugins/data-visualization/src/client/chart/antd/antd.ts new file mode 100644 index 000000000..c48fac244 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/antd/antd.ts @@ -0,0 +1,10 @@ +import { Chart } from '../chart'; + +export class AntdChart extends Chart { + getReference() { + return { + title: this.title, + link: `https://ant.design/components/${this.name}`, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/antd/index.ts b/packages/plugins/data-visualization/src/client/chart/antd/index.ts new file mode 100644 index 000000000..9c75ae6e6 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/antd/index.ts @@ -0,0 +1,4 @@ +import { Statistic } from './statistic'; +import { Table } from './table'; + +export default [new Statistic(), new Table()]; diff --git a/packages/plugins/data-visualization/src/client/chart/antd/statistic.ts b/packages/plugins/data-visualization/src/client/chart/antd/statistic.ts new file mode 100644 index 000000000..993e62148 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/antd/statistic.ts @@ -0,0 +1,64 @@ +import { ISchema } from '@formily/react'; +import { AntdChart } from './antd'; +import { Statistic as AntdStatistic } from 'antd'; +import { lang } from '../../locale'; +import { FieldOption } from '../../hooks'; +import { QueryProps } from '../../renderer'; +import { RenderProps } from '../chart'; + +export class Statistic extends AntdChart { + schema: ISchema = { + type: 'object', + properties: { + field: { + title: lang('Field'), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + title: { + title: lang('Title'), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + }, + }; + + constructor() { + super('statistic', 'Statistic', AntdStatistic); + } + + init( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) { + const { yField } = this.infer(fields, { measures, dimensions }); + return { + general: { + field: yField?.value, + title: yField?.label, + }, + }; + } + + getProps({ data, fieldProps, general, advanced }: RenderProps) { + const record = data[0] || {}; + const field = general?.field; + const props = fieldProps[field]; + return { + value: record[field], + formatter: props?.transformer, + ...general, + ...advanced, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/antd/table.ts b/packages/plugins/data-visualization/src/client/chart/antd/table.ts new file mode 100644 index 000000000..51f033fa5 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/antd/table.ts @@ -0,0 +1,43 @@ +import { RenderProps } from '../chart'; +import { AntdChart } from './antd'; +import { Table as AntdTable } from 'antd'; + +export class Table extends AntdChart { + constructor() { + super('table', 'Table', AntdTable); + } + + getProps({ data, fieldProps, general, advanced }: RenderProps) { + const columns = data.length + ? Object.keys(data[0]).map((item) => ({ + title: fieldProps[item]?.label || item, + dataIndex: item, + key: item, + })) + : []; + const dataSource = data.map((item: any) => { + Object.keys(item).map((key: string) => { + const props = fieldProps[key]; + if (props?.transformer) { + item[key] = props.transformer(item[key]); + } + }); + return item; + }); + const pageSize = advanced?.pagination?.pageSize || 10; + return { + bordered: true, + size: 'middle', + pagination: + dataSource.length < pageSize + ? false + : { + pageSize, + }, + dataSource, + columns, + ...general, + ...advanced, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/chart.ts b/packages/plugins/data-visualization/src/client/chart/chart.ts new file mode 100644 index 000000000..77dbcf15c --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/chart.ts @@ -0,0 +1,134 @@ +import React from 'react'; +import { FieldOption } from '../hooks'; +import { QueryProps } from '../renderer'; +import { parseField } from '../utils'; +import { ISchema } from '@formily/react'; + +export type RenderProps = { + data: any[]; + general: any; + advanced: any; + fieldProps: { + [field: string]: FieldOption & { + transformer: (val: any) => string; + }; + }; +}; + +export interface ChartType { + name: string; + title: string; + component: React.FC; + schema: ISchema; + infer: ( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) => { + xField: FieldOption; + yField: FieldOption; + seriesField: FieldOption; + yFields: FieldOption[]; + }; + init?: ( + fields: FieldOption[], + query: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) => { + general?: any; + advanced?: any; + }; + /** + * getProps + * Accept the information that the chart component needs to render, + * process it and return the props of the chart component. + */ + getProps: (props: RenderProps) => any; + getReference?: () => { + title: string; + link: string; + }; + render: (props: RenderProps) => React.FC; +} + +export class Chart implements ChartType { + name: string; + title: string; + component: React.FC; + schema = {}; + + constructor(name: string, title: string, component: React.FC) { + this.name = name; + this.title = title; + this.component = component; + } + + infer( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) { + let xField: FieldOption; + let yField: FieldOption; + let seriesField: FieldOption; + let yFields: FieldOption[]; + const getField = (fields: FieldOption[], selected: { field: string | string[]; alias?: string }) => { + if (selected.alias) { + return fields.find((f) => f.value === selected.alias); + } + const { alias } = parseField(selected.field); + return fields.find((f) => f.value === alias); + }; + if (measures?.length) { + yField = getField(fields, measures[0]); + yFields = measures.map((m) => getField(fields, m)); + } + if (dimensions) { + if (dimensions.length === 1) { + xField = getField(fields, dimensions[0]); + } else if (dimensions.length > 1) { + // If there is a time field, it is used as the x-axis field by default. + let xIndex: number; + dimensions.forEach((d, i) => { + const field = getField(fields, d); + if (['date', 'time', 'datetime'].includes(field?.type)) { + xField = field; + xIndex = i; + } + }); + if (xIndex) { + // If there is a time field, the other field is used as the series field by default. + const index = xIndex === 0 ? 1 : 0; + seriesField = getField(fields, dimensions[index]); + } else { + xField = getField(fields, dimensions[0]); + seriesField = getField(fields, dimensions[1]); + } + } + } + return { xField, yField, seriesField, yFields }; + } + + getProps(props: RenderProps) { + return props; + } + + render({ data, general, advanced, fieldProps }: RenderProps) { + return () => + React.createElement(this.component, { + ...this.getProps({ data, general, advanced, fieldProps }), + }); + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/g2plot/bar.ts b/packages/plugins/data-visualization/src/client/chart/g2plot/bar.ts new file mode 100644 index 000000000..f28b8eccc --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/g2plot/bar.ts @@ -0,0 +1,30 @@ +import { FieldOption } from '../../hooks'; +import { QueryProps } from '../../renderer'; +import { Bar as G2PlotBar } from '@ant-design/plots'; +import { G2PlotChart } from './g2plot'; + +export class Bar extends G2PlotChart { + constructor() { + super('bar', 'Bar Chart', G2PlotBar); + } + + init( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) { + const { xField, yField, seriesField } = this.infer(fields, { measures, dimensions }); + return { + general: { + xField: yField?.value, + yField: xField?.value, + seriesField: seriesField?.value, + }, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/g2plot/dualAxes.ts b/packages/plugins/data-visualization/src/client/chart/g2plot/dualAxes.ts new file mode 100644 index 000000000..90cdadc9f --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/g2plot/dualAxes.ts @@ -0,0 +1,97 @@ +import { ISchema } from '@formily/react'; +import { G2PlotChart } from './g2plot'; +import { RenderProps } from '../chart'; +import React from 'react'; +import { DualAxes as G2DualAxes } from '@ant-design/plots'; +import { FieldOption } from '../../hooks'; +import { QueryProps } from '../../renderer'; + +export class DualAxes extends G2PlotChart { + schema: ISchema = { + type: 'object', + properties: { + xField: { + title: '{{t("xField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + yField: { + title: '{{t("yField")}}', + type: 'array', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems', + items: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + input: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + 'x-component-props': { + style: { + minWidth: '200px', + }, + }, + required: true, + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + properties: { + add: { + type: 'void', + title: '{{t("Add")}}', + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + }; + + constructor() { + super('dualAxes', 'Dual Axes Chart', G2DualAxes); + } + + init( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) { + const { xField, yFields } = this.infer(fields, { measures, dimensions }); + return { + general: { + xField: xField?.value, + yField: yFields?.map((f) => f.value).slice(0, 2) || [], + }, + }; + } + + render({ data, general, advanced, fieldProps }: RenderProps) { + const props = this.getProps({ data, general, advanced, fieldProps }); + const { data: _data } = props; + return () => + React.createElement(this.component, { + ...props, + data: [_data, _data], + }); + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/g2plot/g2plot.ts b/packages/plugins/data-visualization/src/client/chart/g2plot/g2plot.ts new file mode 100644 index 000000000..8c1cbcc47 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/g2plot/g2plot.ts @@ -0,0 +1,107 @@ +import { Chart, RenderProps } from '../chart'; +import { FieldOption } from '../../hooks'; +import { QueryProps } from '../../renderer'; +import { ISchema } from '@formily/react'; + +export class G2PlotChart extends Chart { + schema: ISchema = { + type: 'object', + properties: { + xField: { + title: '{{t("xField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + yField: { + title: '{{t("yField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + seriesField: { + title: '{{t("seriesField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + }, + }, + }; + + init( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ): { + general?: any; + advanced?: any; + } { + const { xField, yField, seriesField } = this.infer(fields, { measures, dimensions }); + return { + general: { + xField: xField?.value, + yField: yField?.value, + seriesField: seriesField?.value, + }, + }; + } + + getProps({ data, general, advanced, fieldProps }: RenderProps) { + const meta = {}; + // Some charts render wrong when the field name contains a dot in G2Plot + const replace = (key: string) => key.replace(/\./g, '_'); + Object.entries(fieldProps).forEach(([key, props]) => { + if (key.includes('.')) { + key = replace(key); + } + meta[key] = { + formatter: props.transformer, + alias: props.label, + }; + }); + general = Object.entries(general).reduce((obj, [key, value]) => { + obj[key] = value; + if (key.includes('Field')) { + if (Array.isArray(value)) { + obj[key] = value.map((v) => (v.includes('.') ? replace(v) : v)); + } else if (typeof value === 'string' && value.includes('.')) { + obj[key] = replace(value); + } + } + return obj; + }, {}); + return { + data: data.map((item) => { + const obj = {}; + Object.entries(item).forEach(([key, value]) => { + if (key.includes('.')) { + key = replace(key); + } + obj[key] = value; + }); + return obj; + }), + meta, + animation: false, + ...general, + ...advanced, + }; + } + + getReference() { + return { + title: this.title, + link: `https://g2plot.antv.antgroup.com/api/plots/${this.name}`, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/g2plot/index.ts b/packages/plugins/data-visualization/src/client/chart/g2plot/index.ts new file mode 100644 index 000000000..576d51cf0 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/g2plot/index.ts @@ -0,0 +1,16 @@ +import { Area, Column, Line, Scatter } from '@ant-design/plots'; +import { Chart } from '../chart'; +import { Bar } from './bar'; +import { Pie } from './pie'; +import { DualAxes } from './dualAxes'; +import { G2PlotChart } from './g2plot'; + +export default [ + new G2PlotChart('line', 'Line Chart', Line), + new G2PlotChart('area', 'Area Chart', Area), + new G2PlotChart('column', 'Column Chart', Column), + new Bar(), + new Pie(), + new DualAxes(), + new G2PlotChart('scatter', 'Scatter Chart', Scatter), +]; diff --git a/packages/plugins/data-visualization/src/client/chart/g2plot/pie.ts b/packages/plugins/data-visualization/src/client/chart/g2plot/pie.ts new file mode 100644 index 000000000..fcfccabf5 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/g2plot/pie.ts @@ -0,0 +1,52 @@ +import { ISchema } from '@formily/react'; +import { G2PlotChart } from './g2plot'; +import { Pie as G2Pie } from '@ant-design/plots'; +import { FieldOption } from '../../hooks'; +import { QueryProps } from '../../renderer'; + +export class Pie extends G2PlotChart { + schema: ISchema = { + type: 'object', + properties: { + angleField: { + title: '{{t("angleField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + colorField: { + title: '{{t("colorField")}}', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': '{{ useChartFields }}', + required: true, + }, + }, + }; + + constructor() { + super('pie', 'Pie Chart', G2Pie); + } + + init( + fields: FieldOption[], + { + measures, + dimensions, + }: { + measures?: QueryProps['measures']; + dimensions?: QueryProps['dimensions']; + }, + ) { + const { xField, yField } = this.infer(fields, { measures, dimensions }); + return { + general: { + colorField: xField?.value, + angleField: yField?.value, + }, + }; + } +} diff --git a/packages/plugins/data-visualization/src/client/chart/library.tsx b/packages/plugins/data-visualization/src/client/chart/library.tsx new file mode 100644 index 000000000..cef1bb675 --- /dev/null +++ b/packages/plugins/data-visualization/src/client/chart/library.tsx @@ -0,0 +1,89 @@ +import React, { createContext, useContext } from 'react'; +import { lang } from '../locale'; +import { ChartType } from './chart'; + +export type ChartLibraries = { + [library: string]: { + enabled: boolean; + charts: ChartType[]; + }; +}; + +export const ChartLibraryContext = createContext({}); + +export const useCharts = (): { + [name: string]: ChartType; +} => { + const library = useContext(ChartLibraryContext); + return Object.values(library) + .filter((l) => l.enabled) + .reduce((allCharts, l) => { + const charts = Object.values(l.charts); + return { + ...allCharts, + ...charts.reduce((all, chart) => { + return { + ...all, + [chart.name]: chart, + }; + }, {}), + }; + }, {}); +}; + +export const useChartTypes = (): { + label: string; + children: { + key: string; + label: string; + value: string; + }[]; +}[] => { + const library = useContext(ChartLibraryContext); + return Object.entries(library) + .filter(([_, l]) => l.enabled) + .reduce((charts, [name, l]) => { + const children = Object.values(l.charts).map((chart) => ({ + key: chart.name, + label: lang(chart.title), + value: chart.name, + })); + return [ + ...charts, + { + label: lang(name), + children, + }, + ]; + }, []); +}; + +export const useDefaultChartType = () => { + const chartTypes = useChartTypes(); + return chartTypes[0]?.children?.[0]?.value; +}; + +export const useToggleChartLibrary = () => { + const ctx = useContext(ChartLibraryContext); + return { + toggle: (library: string) => { + ctx[library].enabled = !ctx[library].enabled; + }, + }; +}; + +export const ChartLibraryProvider: React.FC<{ + name: string; + charts: ChartType[]; +}> = (props) => { + const { children, charts, name } = props; + const ctx = useContext(ChartLibraryContext); + const library = { + ...ctx, + [name]: { + charts, + enabled: true, + }, + }; + return {children}; +}; diff --git a/packages/plugins/data-visualization/src/client/index.tsx b/packages/plugins/data-visualization/src/client/index.tsx index 2eec05f13..988e6e757 100644 --- a/packages/plugins/data-visualization/src/client/index.tsx +++ b/packages/plugins/data-visualization/src/client/index.tsx @@ -2,10 +2,12 @@ import { Plugin, SchemaComponentOptions, SchemaInitializerContext, SchemaInitial import React, { useContext } from 'react'; import { ChartInitializers, ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer } from './block'; import { useChartsTranslation } from './locale'; -import { ChartRenderer, ChartRendererProvider, InternalLibrary } from './renderer'; -import { ChartLibraryProvider } from './renderer/ChartLibrary'; +import { ChartRenderer, ChartRendererProvider } from './renderer'; +import { ChartLibraryProvider } from './chart/library'; +import g2plot from './chart/g2plot'; +import antd from './chart/antd'; -const Chart: React.FC = (props) => { +const DataVisualization: React.FC = (props) => { const { t } = useChartsTranslation(); const initializers = useContext(SchemaInitializerContext); const children = initializers.BlockInitializers.items[0].children; @@ -29,7 +31,7 @@ const Chart: React.FC = (props) => { }} > - + {props.children} @@ -39,9 +41,11 @@ const Chart: React.FC = (props) => { class DataVisualizationPlugin extends Plugin { async load() { - this.app.addProvider(Chart); + this.app.addProvider(DataVisualization); } } export default DataVisualizationPlugin; export { ChartLibraryProvider }; +export { Chart } from './chart/chart'; +export type { ChartType } from './chart/chart'; diff --git a/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx b/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx deleted file mode 100644 index fd625ed79..000000000 --- a/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { ISchema } from '@formily/react'; -import React, { createContext, useContext } from 'react'; -import { FieldOption } from '../hooks'; -import { lang } from '../locale'; -import { parseField } from '../utils'; -import { QueryProps } from './ChartRendererProvider'; - -/** - * @params {usePropsFunc} useProps - Accept the information that the chart component needs to render, - * process it and return the props of the chart component. - */ -export type usePropsFunc = (props: { - data: any[]; - fieldProps: { - [field: string]: FieldOption & { - transformer: (val: any) => string; - }; - }; - general: any; - advanced: any; -}) => any; - -export type ChartProps = { - name: string; - component: React.FC; - schema?: ISchema; - useProps?: usePropsFunc; - // The init function is used to initialize the configuration of the chart component from the query configuration. - init?: ( - fields: FieldOption[], - query: { - measures?: QueryProps['measures']; - dimensions?: QueryProps['dimensions']; - }, - ) => { - general?: any; - advanced?: any; - }; - reference?: { - title: string; - link: string; - }; -}; - -export type Charts = { - [type: string]: ChartProps; -}; - -export type ChartLibraries = { - [library: string]: { - enabled: boolean; - charts: Charts; - }; -}; - -export const ChartLibraryContext = createContext({}); - -export const useCharts = (): Charts => { - const library = useContext(ChartLibraryContext); - return Object.values(library) - .filter((l) => l.enabled) - .reduce((charts, l) => ({ ...charts, ...l.charts }), {}); -}; - -export const useChartTypes = (): { - label: string; - children: (ChartProps & { - key: string; - label: string; - value: string; - })[]; -}[] => { - const library = useContext(ChartLibraryContext); - return Object.entries(library) - .filter(([_, l]) => l.enabled) - .reduce((charts, [name, l]) => { - const children = Object.entries(l.charts).map(([type, chart]) => ({ - ...chart, - key: type, - label: lang(chart.name), - value: type, - })); - return [ - ...charts, - { - label: lang(name), - children, - }, - ]; - }, []); -}; - -export const useDefaultChartType = () => { - const chartTypes = useChartTypes(); - return chartTypes[0]?.children?.[0]?.value; -}; - -export const useToggleChartLibrary = () => { - const ctx = useContext(ChartLibraryContext); - return { - toggle: (library: string) => { - ctx[library].enabled = !ctx[library].enabled; - }, - }; -}; - -export const ChartLibraryProvider: React.FC<{ - name: string; - charts: Charts; -}> = (props) => { - const { children, charts, name } = props; - const ctx = useContext(ChartLibraryContext); - const library = { - ...ctx, - [name]: { - charts, - enabled: true, - }, - }; - return {children}; -}; - -export const infer = ( - fields: FieldOption[], - { - measures, - dimensions, - }: { - measures?: QueryProps['measures']; - dimensions?: QueryProps['dimensions']; - }, -) => { - let xField: FieldOption; - let yField: FieldOption; - let seriesField: FieldOption; - let yFields: FieldOption[]; - const getField = (fields: FieldOption[], selected: { field: string | string[]; alias?: string }) => { - if (selected.alias) { - return fields.find((f) => f.value === selected.alias); - } - const { alias } = parseField(selected.field); - return fields.find((f) => f.value === alias); - }; - if (measures?.length) { - yField = getField(fields, measures[0]); - yFields = measures.map((m) => getField(fields, m)); - } - if (dimensions) { - if (dimensions.length === 1) { - xField = getField(fields, dimensions[0]); - } else if (dimensions.length > 1) { - // If there is a time field, it is used as the x-axis field by default. - let xIndex: number; - dimensions.forEach((d, i) => { - const field = getField(fields, d); - if (['date', 'time', 'datetime'].includes(field?.type)) { - xField = field; - xIndex = i; - } - }); - if (xIndex) { - // If there is a time field, the other field is used as the series field by default. - const index = xIndex === 0 ? 1 : 0; - seriesField = getField(fields, dimensions[index]); - } else { - xField = getField(fields, dimensions[0]); - seriesField = getField(fields, dimensions[1]); - } - } - } - return { xField, yField, seriesField, yFields }; -}; - -export const commonInit: ChartProps['init'] = (fields, { measures, dimensions }) => { - const { xField, yField, seriesField } = infer(fields, { measures, dimensions }); - return { - general: { - xField: xField?.value, - yField: yField?.value, - seriesField: seriesField?.value, - }, - }; -}; diff --git a/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx b/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx index b49d44dee..faca1fba8 100644 --- a/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx +++ b/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx @@ -14,7 +14,7 @@ import { ChartConfigContext } from '../block'; import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks'; import { useChartsTranslation } from '../locale'; import { createRendererSchema, getField } from '../utils'; -import { useCharts } from './ChartLibrary'; +import { useCharts } from '../chart/library'; import { ChartRendererContext } from './ChartRendererProvider'; const { Paragraph, Text } = Typography; @@ -32,10 +32,9 @@ export const ChartRenderer: React.FC & { const charts = useCharts(); const chart = charts[config?.chartType]; - const Component = chart?.component; const locale = api.auth.getLocale(); const transformers = useFieldTransformer(transform, locale); - const info = { + const Component = chart?.render({ data, general, advanced, @@ -47,18 +46,17 @@ export const ChartRenderer: React.FC & { } return props; }, {}), - locale, - }; - const componentProps = chart?.useProps?.(info) || info; + }); + const C = () => - Component ? ( + chart ? ( { console.error(error); }} FallbackComponent={ErrorFallback} > - + ) : ( diff --git a/packages/plugins/data-visualization/src/client/renderer/index.ts b/packages/plugins/data-visualization/src/client/renderer/index.ts index f32fc2039..ba8115370 100644 --- a/packages/plugins/data-visualization/src/client/renderer/index.ts +++ b/packages/plugins/data-visualization/src/client/renderer/index.ts @@ -1,4 +1,2 @@ -export * from './ChartLibrary'; export * from './ChartRenderer'; export * from './ChartRendererProvider'; -export * from './library'; diff --git a/packages/plugins/data-visualization/src/client/renderer/library/AntdLibrary.tsx b/packages/plugins/data-visualization/src/client/renderer/library/AntdLibrary.tsx deleted file mode 100644 index 42e036633..000000000 --- a/packages/plugins/data-visualization/src/client/renderer/library/AntdLibrary.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Statistic, Table } from 'antd'; -import { lang } from '../../locale'; -import { Charts, infer } from '../ChartLibrary'; - -export const AntdLibrary: Charts = { - statistic: { - name: lang('Statistic'), - component: Statistic, - schema: { - type: 'object', - properties: { - field: { - title: lang('Field'), - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - title: { - title: lang('Title'), - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input', - }, - }, - }, - init: (fields, { measures, dimensions }) => { - const { yField } = infer(fields, { measures, dimensions }); - return { - general: { - field: yField?.value, - title: yField?.label, - }, - }; - }, - useProps: ({ data, fieldProps, general, advanced }) => { - const record = data[0] || {}; - const field = general?.field; - const props = fieldProps[field]; - return { - value: record[field], - formatter: props?.transformer, - ...general, - ...advanced, - }; - }, - reference: { - title: lang('Statistic'), - link: 'https://ant.design/components/statistic/', - }, - }, - table: { - name: lang('Table'), - component: Table, - useProps: ({ data, fieldProps, general, advanced }) => { - const columns = data.length - ? Object.keys(data[0]).map((item) => ({ - title: fieldProps[item]?.label || item, - dataIndex: item, - key: item, - })) - : []; - const dataSource = data.map((item: any) => { - Object.keys(item).map((key: string) => { - const props = fieldProps[key]; - if (props?.transformer) { - item[key] = props.transformer(item[key]); - } - }); - return item; - }); - const pageSize = advanced?.pagination?.pageSize || 10; - return { - bordered: true, - size: 'middle', - pagination: - dataSource.length < pageSize - ? false - : { - pageSize, - }, - dataSource, - columns, - ...general, - ...advanced, - }; - }, - reference: { - title: lang('Table'), - link: 'https://ant.design/components/table/', - }, - }, -}; diff --git a/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx b/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx deleted file mode 100644 index f2b600cc4..000000000 --- a/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { Area, Bar, Column, DualAxes, Line, Pie, Scatter } from '@ant-design/plots'; -import { Charts, commonInit, infer, usePropsFunc } from '../ChartLibrary'; -const init = commonInit; - -const basicSchema = { - type: 'object', - properties: { - xField: { - title: '{{t("xField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - yField: { - title: '{{t("yField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - seriesField: { - title: '{{t("seriesField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - }, - }, -}; - -const useProps: usePropsFunc = ({ data, fieldProps, general, advanced }) => { - const meta = {}; - Object.entries(fieldProps).forEach(([key, props]) => { - meta[key] = { - formatter: props.transformer, - alias: props.label, - }; - }); - return { - data, - meta, - animation: false, - ...general, - ...advanced, - }; -}; - -export const G2PlotLibrary: Charts = { - line: { - name: 'Line Chart', - component: Line, - schema: basicSchema, - init, - useProps, - reference: { - title: 'Line Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/line', - }, - }, - area: { - name: 'Area Chart', - component: Area, - schema: basicSchema, - init, - useProps, - reference: { - title: 'Area Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/area', - }, - }, - column: { - name: 'Column Chart', - component: Column, - schema: basicSchema, - init, - useProps, - reference: { - title: 'Column Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/column', - }, - }, - bar: { - name: 'Bar Chart', - component: Bar, - schema: basicSchema, - init: (fields, { measures, dimensions }) => { - const { xField, yField, seriesField } = infer(fields, { measures, dimensions }); - return { - general: { - xField: yField?.value, - yField: xField?.value, - seriesField: seriesField?.value, - }, - }; - }, - useProps, - reference: { - title: 'Bar Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/bar', - }, - }, - pie: { - name: 'Pie Chart', - component: Pie, - schema: { - type: 'object', - properties: { - angleField: { - title: '{{t("angleField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - colorField: { - title: '{{t("colorField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - }, - }, - init: (fields, { measures, dimensions }) => { - const { xField, yField } = infer(fields, { measures, dimensions }); - return { - general: { - colorField: xField?.value, - angleField: yField?.value, - }, - }; - }, - useProps, - reference: { - title: 'Pie Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/pie', - }, - }, - dualAxes: { - name: 'Dual Axes Chart', - component: DualAxes, - useProps: ({ data, fieldProps, general, advanced }) => { - return { - ...useProps({ data, fieldProps, general, advanced }), - data: [data, data], - }; - }, - schema: { - type: 'object', - properties: { - xField: { - title: '{{t("xField")}}', - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - required: true, - }, - yField: { - title: '{{t("yField")}}', - type: 'array', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems', - items: { - type: 'void', - 'x-component': 'Space', - properties: { - sort: { - type: 'void', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems.SortHandle', - }, - input: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-reactions': '{{ useChartFields }}', - 'x-component-props': { - style: { - minWidth: '200px', - }, - }, - required: true, - }, - remove: { - type: 'void', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems.Remove', - }, - }, - }, - properties: { - add: { - type: 'void', - title: '{{t("Add")}}', - 'x-component': 'ArrayItems.Addition', - }, - }, - }, - }, - }, - init: (fields, { measures, dimensions }) => { - const { xField, yFields } = infer(fields, { measures, dimensions }); - return { - general: { - xField: xField?.value, - yField: yFields?.map((f) => f.value).slice(0, 2) || [], - }, - }; - }, - reference: { - title: 'Dual Axes Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/dual-axes', - }, - }, - // gauge: { - // name: 'Gauge Chart', - // component: Gauge, - // }, - scatter: { - name: 'Scatter Chart', - component: Scatter, - schema: basicSchema, - init, - useProps, - reference: { - title: 'Scatter Chart', - link: 'https://g2plot.antv.antgroup.com/api/plots/scatter', - }, - }, -}; diff --git a/packages/plugins/data-visualization/src/client/renderer/library/index.tsx b/packages/plugins/data-visualization/src/client/renderer/library/index.tsx deleted file mode 100644 index 0b98bf3aa..000000000 --- a/packages/plugins/data-visualization/src/client/renderer/library/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { AntdLibrary } from './AntdLibrary'; -import { G2PlotLibrary } from './G2PlotLibrary'; - -export const InternalLibrary = { ...G2PlotLibrary, ...AntdLibrary }; diff --git a/packages/plugins/data-visualization/src/server/actions/query.ts b/packages/plugins/data-visualization/src/server/actions/query.ts index afd245066..aa3013a83 100644 --- a/packages/plugins/data-visualization/src/server/actions/query.ts +++ b/packages/plugins/data-visualization/src/server/actions/query.ts @@ -251,8 +251,18 @@ export const cacheWrap = async ( }; export const query = async (ctx: Context, next: Next) => { - const { uid, collection, measures, dimensions, orders, filter, limit, sql, cache: cacheConfig, refresh } = ctx.action - .params.values as QueryParams; + const { + uid, + collection, + measures, + dimensions, + orders, + filter, + limit, + sql, + cache: cacheConfig, + refresh, + } = ctx.action.params.values as QueryParams; const roleName = ctx.state.currentRole || 'anonymous'; const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' }); if (!can && roleName !== 'root') { diff --git a/yarn.lock b/yarn.lock index 2ba78d43a..e581ff0a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20581,14 +20581,9 @@ prettier@2.2.1: version "2.2.1" resolved "https://registry.npmmirror.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" -prettier@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae" - prettier@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae" - integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g== pretty-error@^4.0.0: version "4.0.0"