diff --git a/packages/generate-vchart/__tests__/transformers/comparativeFunnel.test.ts b/packages/generate-vchart/__tests__/transformers/comparativeFunnel.test.ts new file mode 100644 index 00000000..e57b0ebc --- /dev/null +++ b/packages/generate-vchart/__tests__/transformers/comparativeFunnel.test.ts @@ -0,0 +1,171 @@ +import { generateChart } from '../../src'; + +const MOCK_DATA_TABLE = [ + { group: 'A', name: 'Category1', value: 10 }, + { group: 'A', name: 'Category2', value: 15 }, + { group: 'B', name: 'Category1', value: 20 }, + { group: 'B', name: 'Category2', value: 25 } +]; + +const layout = { + type: 'grid', + col: 3, + row: 3, + colWidth: [ + { + index: 1, + size: 120 + } + ], + elements: [ + { + modelId: 'title', + col: 0, + row: 0, + colSpan: 3 + }, + { + modelId: 'legend', + col: 0, + row: 1, + colSpan: 3 + }, + { + modelId: 'left', + col: 0, + row: 2 + }, + { + modelId: 'right', + col: 2, + row: 2 + } + ] +}; + +const region = [ + { + id: 'left' + }, + { + id: 'right' + } +]; + +describe('generate comparative funnel chart', () => { + it('should generate comparative funnel chart with dataTable', () => { + const { spec } = generateChart('comparativeFunnel', { + dataTable: MOCK_DATA_TABLE, + cell: { + x: 'name', + y: 'value', + category: 'group' + }, + spec: {} + }); + expect(spec.type).toBe('common'); + expect(spec.layout).toEqual(layout); + expect(spec.region).toEqual(region); + expect(spec.data).toEqual( + Object.entries( + MOCK_DATA_TABLE.reduce((acc, curr) => { + if (!acc[curr.group]) { + acc[curr.group] = []; + } + acc[curr.group].push(curr); + return acc; + }, {} as any) + ).map(([group, data]) => ({ + id: group, + values: data + })) + ); + const serialize = (obj: any) => JSON.parse(JSON.stringify(obj)); + expect(serialize(spec.series)).toEqual( + serialize([ + { + type: 'funnel', + dataIndex: 0, + regionIndex: 0, + isTransform: true, + gap: 2, + maxSize: '60%', + shape: 'rect', + funnelAlign: 'right', + categoryField: 'name', + valueField: 'value', + heightRatio: 1.5, + funnel: { + style: { + fill: { field: 'group', scale: 'color' }, + cornerRadius: 4 + } + }, + transform: { + style: { + fill: { field: 'group', scale: 'color' }, + fillOpacity: 0.1 + } + }, + outerLabel: { + visible: true, + line: { visible: false }, + style: { + fontSize: 24, + fontWeight: 'bold', + fill: 'black', + limit: Infinity + } + }, + extensionMark: [ + { + type: 'text', + dataIndex: 0, + style: { + fontSize: 24, + fill: 'grey', + textAlign: 'center' + } + } + ] + }, + { + type: 'funnel', + dataIndex: 1, + regionIndex: 1, + isTransform: true, + gap: 2, + maxSize: '60%', + shape: 'rect', + funnelAlign: 'left', + categoryField: 'name', + valueField: 'value', + heightRatio: 1.5, + funnel: { + style: { + fill: { field: 'group', scale: 'color' }, + cornerRadius: 4 + } + }, + transform: { + style: { + fill: { field: 'group', scale: 'color' }, + fillOpacity: 0.1 + } + }, + outerLabel: { + visible: true, + line: { visible: false }, + style: { + fontSize: 24, + fontWeight: 'bold', + fill: 'black', + limit: Infinity + } + }, + extensionMark: [] + } + ]) + ); + }); +}); diff --git a/packages/generate-vchart/__tests__/transformers/heatmap.test.ts b/packages/generate-vchart/__tests__/transformers/heatmap.test.ts index ecf9578a..c7fcc233 100644 --- a/packages/generate-vchart/__tests__/transformers/heatmap.test.ts +++ b/packages/generate-vchart/__tests__/transformers/heatmap.test.ts @@ -48,6 +48,11 @@ describe('generateChart', () => { grid: { visible: false }, + label: { + style: { + angle: 90 + } + }, domainLine: { visible: false } @@ -261,6 +266,11 @@ describe('generateChart', () => { grid: { visible: false }, + label: { + style: { + angle: 90 + } + }, domainLine: { visible: false }, diff --git a/packages/generate-vchart/src/pipeline.ts b/packages/generate-vchart/src/pipeline.ts index 80dea648..1565cd8a 100644 --- a/packages/generate-vchart/src/pipeline.ts +++ b/packages/generate-vchart/src/pipeline.ts @@ -30,6 +30,7 @@ const { pipelineWaterfall, pipelineWordCloud, pipelineBidirectionalBar, + pipelineComparativeFunnel, addSimpleComponents, theme } = allTransformers; @@ -70,7 +71,8 @@ const pipelineMap: { gauge: { type: 'gauge', aliasName: 'Gauge Chart', pipline: pipelineGauge }, heatmap: { type: 'heatmap', aliasName: 'Basic Heat Map', pipline: pipelineBasicHeatMap }, venn: { type: 'venn', aliasName: 'Venn Chart', pipline: pipelineVenn }, - bidirectionalBar: { type: 'common', aliasName: 'Bidirectional Bar Chart', pipline: pipelineBidirectionalBar } + bidirectionalBar: { type: 'common', aliasName: 'Bidirectional Bar Chart', pipline: pipelineBidirectionalBar }, + comparativeFunnel: { type: 'common', aliasName: 'Comparative Funnel Chart', pipline: pipelineComparativeFunnel } }; export const findPipelineByType = (type: string) => { diff --git a/packages/generate-vchart/src/transformers/comparativeFunnel.ts b/packages/generate-vchart/src/transformers/comparativeFunnel.ts new file mode 100644 index 00000000..2886de73 --- /dev/null +++ b/packages/generate-vchart/src/transformers/comparativeFunnel.ts @@ -0,0 +1,133 @@ +import { array } from '@visactor/vutils'; +import { DataItem, GenerateChartInput } from '../types'; +import { data, discreteLegend, formatXFields } from './common'; + +const title = (context: GenerateChartInput) => { + const { spec } = context; + const { title } = spec; + spec.title = { + ...title, + id: 'title' + }; + return { spec }; +}; + +const layout = (context: GenerateChartInput) => { + const { spec } = context; + spec.layout = { + type: 'grid', + col: 3, + row: 3, + colWidth: [{ index: 1, size: 120 }], + elements: [ + { modelId: 'title', col: 0, row: 0, colSpan: 3 }, + { modelId: 'legend', col: 0, row: 1, colSpan: 3 }, + { modelId: 'left', col: 0, row: 2 }, + { modelId: 'right', col: 2, row: 2 } + ] + }; + spec.region = [{ id: 'left' }, { id: 'right' }]; + return { spec }; +}; + +const comparativeFunnelSeries = (context: GenerateChartInput) => { + const { spec, cell } = context; + const generateSeries = (index: number, align: 'left' | 'right') => { + return { + type: 'funnel', + dataIndex: index, + regionIndex: index, + isTransform: true, + gap: 2, + maxSize: '60%', + shape: 'rect', + funnelAlign: align, + categoryField: cell.x, + valueField: cell.y, + heightRatio: 1.5, + funnel: { + style: { + fill: { field: cell.category, scale: 'color' }, + cornerRadius: 4 + } + }, + transform: { + style: { + fill: { field: cell.category, scale: 'color' }, + fillOpacity: 0.1 + } + }, + outerLabel: { + visible: true, + line: { visible: false }, + formatMethod: (data: any, datum: DataItem) => datum[array(cell.y)[0]], + style: { + fontSize: 24, + fontWeight: 'bold', + fill: 'black', + limit: Infinity + } + }, + extensionMark: [ + index === 0 + ? { + type: 'text', + dataIndex: 0, + style: { + text: (data: any) => data[array(cell.x)[0]], + fontSize: 24, + fill: 'grey', + textAlign: 'center', + x: (data: any, ctx: any) => { + const { vchart } = ctx; + return vchart.getCurrentSize().width / 2 - 10; + }, + y: (data: DataItem, ctx: any) => { + const { getPoints } = ctx; + const [tl, tr, br, bl] = getPoints(data); + return (tl.y + bl.y) / 2; + } + } + } + : null + ].filter(Boolean) + }; + }; + spec.series = [generateSeries(0, 'right'), generateSeries(1, 'left')]; + return { spec }; +}; + +const comparativeFunnelLegend = (context: GenerateChartInput) => { + const { spec, cell } = context; + spec.seriesField = cell.category; + spec.legends = array(spec.legends).map(legend => ({ + ...legend, + id: 'legend', + orient: 'top' + })); + return { spec }; +}; + +const comparativeFunnelData = (context: GenerateChartInput) => { + const { spec, dataTable } = context; + const mp = {}; + dataTable.forEach(item => { + const { group } = item; + if (!mp[group]) { + mp[group] = []; + } + mp[group].push(item); + }); + spec.data = Object.entries(mp).map(([group, data]) => ({ id: group, values: data })); + return { spec }; +}; + +export const pipelineComparativeFunnel = [ + title, + formatXFields, + layout, + comparativeFunnelData, + comparativeFunnelSeries, + discreteLegend, + comparativeFunnelLegend +]; diff --git a/packages/generate-vchart/src/transformers/heatmap.ts b/packages/generate-vchart/src/transformers/heatmap.ts index def87a73..80742630 100644 --- a/packages/generate-vchart/src/transformers/heatmap.ts +++ b/packages/generate-vchart/src/transformers/heatmap.ts @@ -74,7 +74,12 @@ export const basicHeatMapAxes = (context: GenerateChartInput) => { } }, userConfig: { - type: 'band' + type: 'band', + label: { + style: { + angle: 90 + } + } }, filters: [axis => axis.orient === 'bottom', axis => axis.orient === 'top'] }, diff --git a/packages/generate-vchart/src/transformers/index.ts b/packages/generate-vchart/src/transformers/index.ts index 19b2a14f..8223a806 100644 --- a/packages/generate-vchart/src/transformers/index.ts +++ b/packages/generate-vchart/src/transformers/index.ts @@ -29,3 +29,4 @@ export * from './waterfall'; export * from './wordcloud'; export * from './bidirectionalBar'; +export * from './comparativeFunnel'; diff --git a/packages/vmind/src/atom/chartGenerator/rule/index.ts b/packages/vmind/src/atom/chartGenerator/rule/index.ts index b006c936..1ba9073e 100644 --- a/packages/vmind/src/atom/chartGenerator/rule/index.ts +++ b/packages/vmind/src/atom/chartGenerator/rule/index.ts @@ -89,6 +89,23 @@ const formatDataTable = (simpleVChartSpec: SimpleVChartSpec, data: DataTable) => finalData.reverse(); return finalData; } + // 热力图特殊处理 + else if (type === 'heatmap') { + // 拿到所有的'name',并设置值为1 + const finalData = Array.from( + new Set(data.map(item => item.name).concat(data.map(item => item.name1))) + .keys() + .map(key => ({ name: key, name1: key, value: 1 })) + ); + // 模型识别数据可能不全,尽可能补全数据 + for (const item of data) { + const obj = { name: item.name1, name1: item.name, value: item.value as number }; + if (finalData.findIndex(i => i.name === item.name && i.name1 === item.name1) === -1) { + finalData.push(item as typeof obj, obj); + } + } + return finalData; + } return data; }; @@ -101,6 +118,10 @@ export const getContextBySimpleVChartSpec = (simpleVChartSpec: SimpleVChartSpec) simpleVChartSpec, data ?? originalSeries?.reduce((acc, cur) => { + // 对比漏斗图,group字段可能不会在data中,特殊处理 + if (type === 'comparativeFunnel' && 'group' in cur) { + cur.data?.forEach(item => (item.group = cur.group as string)); + } acc.push(...cur.data); return acc; }, []) @@ -171,8 +192,19 @@ export const getContextBySimpleVChartSpec = (simpleVChartSpec: SimpleVChartSpec) cell.x = 'value'; cell.y = 'value1'; } + if (chartType === 'heatmap') { + cell.x = 'name'; + // 热图有两个维度数据,因此新增name1字段 + cell.y = 'name1'; + cell.size = 'value'; + } + if (chartType === 'comparativeFunnel') { + cell.x = 'name'; + cell.y = 'value'; + cell.category = 'group'; + } - const fieldInfo = ['name', 'value', 'group', 'value1'].reduce((res, field) => { + const fieldInfo = ['name', 'value', 'group', 'value1', 'name1'].reduce((res, field) => { if (firstDatum && field in firstDatum) { res.push({ fieldName: field, diff --git a/packages/vmind/src/atom/imageReader/interface.ts b/packages/vmind/src/atom/imageReader/interface.ts index 9a388193..d30cef66 100644 --- a/packages/vmind/src/atom/imageReader/interface.ts +++ b/packages/vmind/src/atom/imageReader/interface.ts @@ -30,7 +30,8 @@ export interface SimpleVChartSpec { | 'liquid' | 'venn' | 'mosaic' - | 'bidirectionalBar'; + | 'bidirectionalBar' + | 'comparativeFunnel'; /** * "none" - 无坐标系 * "rect" - 直角坐标系 diff --git a/packages/vmind/src/atom/imageReader/prompt.ts b/packages/vmind/src/atom/imageReader/prompt.ts index 06d430ca..74d83d5f 100644 --- a/packages/vmind/src/atom/imageReader/prompt.ts +++ b/packages/vmind/src/atom/imageReader/prompt.ts @@ -16,13 +16,16 @@ When executing the task, you need to meet the following requirements: 6. If all series in a composite chart are unidirectional bar charts, return the type as 'bar'; if the image is symmetrically distributed on both sides, must return the type as 'bidirectionalBar' 7. You should pay attention to distinguish radar chart and rose chart 8. If the type is treemap, the background color of each block needs to be used as the group value -9. For the type property of each element object in the axes field, it must be 'band' or 'linear' +9. If the type is heatmap, you need to return all correlation coefficient values, set the two dimensions of each correlation coefficient to 'name' and 'name1', with the value as 'value' +10. For the type property of each element object in the axes field, it must be 'band' or 'linear' +11. If the type is linearProgress, the metric value must be between 0 and 1 +12. If the type is 'funnel' and consists of two funnel charts, return the type as 'comparativeFunnel' # Answer \`\`\` { /** 图表的类型 */ - type: "common"|"area"|"line"|"bar"|"rangeColumn"|"rangeArea"|"map"|"pie"|"radar"|"rose"|"scatter"|"sequence"|"circularProgress"|"linearProgress"|"wordCloud"|"funnel"|"waterfall"|"boxPlot"|"gauge"|"sankey"|"treemap"|"sunburst"|"circlePacking"|"heatmap"|"liquid"|"venn"|"mosaic"|"bidirectionalBar"; + type: "common"|"area"|"line"|"bar"|"rangeColumn"|"rangeArea"|"map"|"pie"|"radar"|"rose"|"scatter"|"sequence"|"circularProgress"|"linearProgress"|"wordCloud"|"funnel"|"waterfall"|"boxPlot"|"gauge"|"sankey"|"treemap"|"sunburst"|"circlePacking"|"heatmap"|"liquid"|"venn"|"mosaic"|"bidirectionalBar"|"comparativeFunnel"; /** * "none" - 无坐标系 * "rect" - 直角坐标系