From 7146b2153caf05482bba6028c6b056426dd09ca8 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 7 Jan 2025 10:37:57 +0800 Subject: [PATCH] refactor: update vchartspec utils --- .../src/pages/ChartDialogueQA/qaPairRag.tsx | 4 +- .../__tests__/unit/vchartSpec/bar.test.ts | 182 ++++---- .../__tests__/unit/vchartSpec/util.test.ts | 41 +- packages/vmind/src/atom/VChartSpec/index.ts | 2 +- packages/vmind/src/atom/VChartSpec/utils.ts | 391 ++++++++++++------ packages/vmind/src/types/atom.ts | 12 +- 6 files changed, 367 insertions(+), 265 deletions(-) diff --git a/packages/vmind/__tests__/experiment/src/pages/ChartDialogueQA/qaPairRag.tsx b/packages/vmind/__tests__/experiment/src/pages/ChartDialogueQA/qaPairRag.tsx index 34bb8a7..e0d65be 100644 --- a/packages/vmind/__tests__/experiment/src/pages/ChartDialogueQA/qaPairRag.tsx +++ b/packages/vmind/__tests__/experiment/src/pages/ChartDialogueQA/qaPairRag.tsx @@ -160,9 +160,7 @@ export function QARag() { { spec: spec, appendSpec: { - leafSpec: dslRes, - parentKeyPath, - aliasKeyPath + spec: dslRes } }, true diff --git a/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts b/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts index 866bd09..ae61515 100644 --- a/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts +++ b/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts @@ -123,7 +123,7 @@ const spec = { describe('mergeAppendSpec of barchart', () => { it('should reduce duplicated code', () => { const newSpec = mergeAppendSpec(merge({}, spec), { - leafSpec: { + spec: { scales: [ { domain: [ @@ -133,19 +133,19 @@ describe('mergeAppendSpec of barchart', () => { ] } ] - }, - parentKeyPath: 'scales' + } }); expect(newSpec).toEqual( mergeAppendSpec(spec, { - parentKeyPath: 'scales[0]', - leafSpec: { - domain: [ - { - fields: ['yourFieldName'] - } - ] + spec: { + 'scales[0]': { + domain: [ + { + fields: ['yourFieldName'] + } + ] + } } }) ); @@ -153,10 +153,9 @@ describe('mergeAppendSpec of barchart', () => { it('should parse complicated path', () => { const append = { - leafSpec: { + spec: { 'scales[0].domain[0].fields': 'yourFieldName' - }, - parentKeyPath: 'scales[0].domain[0].fields' + } }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); @@ -172,9 +171,9 @@ describe('mergeAppendSpec of barchart', () => { ]); }); - it('should parse complicated path of `axes[0].label.style.lineWidth`', () => { + it('should parse complicated path of axes', () => { const append = { - leafSpec: { + spec: { axes: [ { label: { @@ -184,10 +183,8 @@ describe('mergeAppendSpec of barchart', () => { } } ] - }, - parentKeyPath: 'axes[0].label.style.lineWidth' + } }; - const { newSpec } = mergeAppendSpec(merge({}, spec), append); expect(newSpec.axes).toEqual([ @@ -199,75 +196,112 @@ describe('mergeAppendSpec of barchart', () => { } } ]); - }); - it('should parse complicated path when `parentKeyPath` > spec ', () => { - const append = { - leafSpec: { - grid: { - style: { - strokeOpacity: 1 + const append1 = { + spec: { + axes: { + label: { + style: { + lineWidth: 2 + } } } - }, - parentKeyPath: 'axes[0].grid.style' + } }; - const { newSpec } = mergeAppendSpec(merge({}, spec), append); + const { newSpec: newSpec1 } = mergeAppendSpec(merge({}, spec), append1); - expect(newSpec.axes).toEqual([ + expect(newSpec1.axes).toEqual([ { - grid: { + label: { style: { - strokeOpacity: 1 + lineWidth: 2 } } } ]); - const { newSpec: newSpec2 } = mergeAppendSpec(merge({}, newSpec), { - aliasKeyPath: 'xAxis', - parentKeyPath: 'axes', - leafSpec: { - axes: [ - { - title: { - text: '城市' + const append2 = { + spec: { + yAxis: { + label: { + style: { + lineWidth: 2 } } - ] + } } - }); + }; + const { newSpec: newSpec2 } = mergeAppendSpec(merge({}, spec), append2); expect(newSpec2.axes).toEqual([ { - grid: { + _alias_name: 'yAxis', + orient: 'left', + label: { style: { - strokeOpacity: 1 + lineWidth: 2 } } - }, + } + ]); + + const append3 = { + spec: { + 'yAxis[0]': { + label: { + style: { + lineWidth: 2 + } + } + } + } + }; + + const { newSpec: newSpec3 } = mergeAppendSpec(merge({}, spec), append3); + + expect(newSpec3.axes).toEqual([ { - title: { - text: '城市' - }, - _alias_name: 'xAxis', - orient: 'bottom' + _alias_name: 'yAxis', + orient: 'left', + label: { + style: { + lineWidth: 2 + } + } + } + ]); + + const append4 = { + spec: { + 'yAxis[0].label.style.lineWidth': 2 + } + }; + + const { newSpec: newSpec4 } = mergeAppendSpec(merge({}, spec), append4); + + expect(newSpec4.axes).toEqual([ + { + _alias_name: 'yAxis', + orient: 'left', + label: { + style: { + lineWidth: 2 + } + } } ]); }); it('should handle function', () => { const append = { - aliasKeyPath: 'xAxis.label.style.fill', - leafSpec: { + spec: { bar: { style: { fill: "(datum, index) => index === 0 ? 'red' : null" } } - }, - parentKeyPath: 'bar' + } }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); @@ -279,11 +313,9 @@ describe('mergeAppendSpec of barchart', () => { it('should not not add series when has only one series', () => { const append = { - leafSpec: { + spec: { 'series[0].extensionMark[0].style.size': 10 - }, - parentKeyPath: 'series[0].extensionMark[0].style.size', - aliasKeyPath: 'bar.extensionMark[0].style.size' + } }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); @@ -298,10 +330,9 @@ describe('mergeAppendSpec of barchart', () => { it('should not not add series when has only one series when alias is empty', () => { const append = { - leafSpec: { + spec: { 'series[0].extensionMark[0].style.size': 10 - }, - parentKeyPath: 'series[0].extensionMark[0].style.size' + } }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); @@ -316,7 +347,7 @@ describe('mergeAppendSpec of barchart', () => { it('should not not add series when has only one series in bar chart', () => { const append = { - leafSpec: { + spec: { series: [ { label: { @@ -324,10 +355,7 @@ describe('mergeAppendSpec of barchart', () => { } } ] - }, - - parentKeyPath: 'series', - aliasKeyPath: 'bar' + } }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); @@ -339,12 +367,14 @@ describe('mergeAppendSpec of barchart', () => { it('should contain all spec when spec has more than one path', () => { const append = { - leafSpec: { - mark: { - maxLineCount: 20 - }, - dimension: { - maxLineCount: 20 + spec: { + tooltip: { + mark: { + maxLineCount: 20 + }, + dimension: { + maxLineCount: 20 + } } }, parentKeyPath: 'tooltip', @@ -352,14 +382,12 @@ describe('mergeAppendSpec of barchart', () => { }; const { newSpec } = mergeAppendSpec(merge({}, spec), append); - expect(newSpec.tooltip).toEqual(append.leafSpec); + expect(newSpec.tooltip).toEqual(append.spec.tooltip); }); it('should not create legends array when only one legend', () => { const { newSpec } = mergeAppendSpec(merge({}, spec), { - parentKeyPath: 'legends[0]', - aliasKeyPath: '', - leafSpec: { + spec: { 'legends[0]': { orient: 'left' } @@ -371,9 +399,7 @@ describe('mergeAppendSpec of barchart', () => { orient: 'left' }); const { newSpec: newSpec2 } = mergeAppendSpec(merge({}, spec), { - parentKeyPath: 'legends', - aliasKeyPath: '', - leafSpec: { + spec: { 'legends[0]': { orient: 'left' } diff --git a/packages/vmind/__tests__/unit/vchartSpec/util.test.ts b/packages/vmind/__tests__/unit/vchartSpec/util.test.ts index 643a4ec..656aa8c 100644 --- a/packages/vmind/__tests__/unit/vchartSpec/util.test.ts +++ b/packages/vmind/__tests__/unit/vchartSpec/util.test.ts @@ -98,44 +98,7 @@ describe('convertFunctionString', () => { describe('removeUnneedArrayInSpec', () => { it('should remove unneed array in spec', () => { - expect( - removeUnneedArrayInSpec( - { - legends: [{ orient: 'left' }] - }, - 'legends', - 'legends' - ) - ).toEqual({ orient: 'left' }); - - expect( - removeUnneedArrayInSpec( - { - legends: [{ orient: 'left' }] - }, - 'legends', - 'legends[0]' - ) - ).toEqual({ orient: 'left' }); - - expect( - removeUnneedArrayInSpec( - { - 'legends[0]': { orient: 'left' } - }, - 'legends', - 'legends[0]' - ) - ).toEqual({ orient: 'left' }); - - expect( - removeUnneedArrayInSpec( - { - 'legends[0]': { orient: 'left' } - }, - 'legends', - 'legends' - ) - ).toEqual({ orient: 'left' }); + expect(removeUnneedArrayInSpec([{ orient: 'left' }], 'legends', 'legends')).toEqual({ orient: 'left' }); + expect(removeUnneedArrayInSpec({ orient: 'left' }, 'legends', 'legends[0]')).toEqual({ orient: 'left' }); }); }); diff --git a/packages/vmind/src/atom/VChartSpec/index.ts b/packages/vmind/src/atom/VChartSpec/index.ts index 96e2678..4795341 100644 --- a/packages/vmind/src/atom/VChartSpec/index.ts +++ b/packages/vmind/src/atom/VChartSpec/index.ts @@ -30,7 +30,7 @@ export class VChartSpec extends BaseAtom { return this.context; } - if (appendSpec && 'leafSpec' in appendSpec) { + if (appendSpec && 'spec' in appendSpec) { const { newSpec, code } = mergeAppendSpec(this.context.spec, appendSpec); this.context.appendCode = code; diff --git a/packages/vmind/src/atom/VChartSpec/utils.ts b/packages/vmind/src/atom/VChartSpec/utils.ts index f18d381..d70a176 100644 --- a/packages/vmind/src/atom/VChartSpec/utils.ts +++ b/packages/vmind/src/atom/VChartSpec/utils.ts @@ -122,10 +122,11 @@ export const aliasByComponentType: Record< string, { isArray?: boolean; - aliasMap: Record boolean }>; + aliasMap?: Record boolean }>; } > = { axes: { + isArray: true, aliasMap: { xAxis: { appendSpec: { orient: 'bottom' } @@ -208,152 +209,271 @@ export const aliasByComponentType: Record< series: { isArray: true, aliasMap: { - line: { - appendSpec: { type: 'line' } + lineSeries: { + appendSpec: { + type: 'line' + } }, - area: { - appendSpec: { type: 'area' } + areaSeries: { + appendSpec: { + type: 'area' + } }, - bar: { - appendSpec: { type: 'bar' } + barSeries: { + appendSpec: { + type: 'bar' + } }, - bar3d: { - appendSpec: { type: 'bar3d' } + bar3dSeries: { + appendSpec: { + type: 'bar3d' + } }, - pie: { - appendSpec: { type: 'pie' } + pieSeries: { + appendSpec: { + type: 'pie' + } }, - pie3d: { - appendSpec: { type: 'pie3d' } + pie3dSeries: { + appendSpec: { + type: 'pie3d' + } }, - scatter: { - appendSpec: { type: 'scatter' } + scatterSeries: { + appendSpec: { + type: 'scatter' + } }, - funnel: { - appendSpec: { type: 'funnel' } + funnelSeries: { + appendSpec: { + type: 'funnel' + } }, - funnel3d: { - appendSpec: { type: 'funnel3d' } + funnel3dSeries: { + appendSpec: { + type: 'funnel3d' + } }, - map: { - appendSpec: { type: 'map' } + mapSeries: { + appendSpec: { + type: 'map' + } }, - radar: { - appendSpec: { type: 'radar' } + radarSeries: { + appendSpec: { + type: 'radar' + } }, - wordCloud: { - appendSpec: { type: 'wordCloud' } + wordCloudSeries: { + appendSpec: { + type: 'wordCloud' + } }, - wordCloud3d: { - appendSpec: { type: 'wordCloud3d' } + wordCloud3dSeries: { + appendSpec: { + type: 'wordCloud3d' + } }, - heatmap: { - appendSpec: { type: 'heatmap' } + heatmapSeries: { + appendSpec: { + type: 'heatmap' + } }, - treemap: { - appendSpec: { type: 'treemap' } + treemapSeries: { + appendSpec: { + type: 'treemap' + } }, - gauge: { - appendSpec: { type: 'gauge' } + gaugeSeries: { + appendSpec: { + type: 'gauge' + } }, - rangeColumn: { - appendSpec: { type: 'rangeColumn' } + rangeColumnSeries: { + appendSpec: { + type: 'rangeColumn' + } }, - rangeColumn3d: { - appendSpec: { type: 'rangeColumn3d' } + rangeColumn3dSeries: { + appendSpec: { + type: 'rangeColumn3d' + } }, - rangeArea: { - appendSpec: { type: 'rangeArea' } + rangeAreaSeries: { + appendSpec: { + type: 'rangeArea' + } }, - dot: { - appendSpec: { type: 'dot' } + dotSeries: { + appendSpec: { + type: 'dot' + } }, - geo: { - appendSpec: { type: 'geo' } + geoSeries: { + appendSpec: { + type: 'geo' + } }, - - link: { - appendSpec: { type: 'link' } + linkSeries: { + appendSpec: { + type: 'link' + } }, - - rose: { + roseSeries: { appendSpec: { type: 'rose' } }, - circularProgress: { + circularProgressSeries: { appendSpec: { type: 'circularProgress' } }, - linearProgress: { appendSpec: { type: 'linearProgress' } }, - boxPlot: { appendSpec: { type: 'boxPlot' } }, - sankey: { appendSpec: { type: 'sankey' } }, - gaugePointer: { appendSpec: { type: 'gaugePointer' } }, - sunburst: { appendSpec: { type: 'sunburst' } }, - circlePacking: { appendSpec: { type: 'circlePacking' } }, - waterfall: { appendSpec: { type: 'waterfall' } }, - correlation: { appendSpec: { type: 'correlation' } }, - liquid: { appendSpec: { type: 'liquid' } }, - venn: { appendSpec: { type: 'venn' } }, - mosaic: { appendSpec: { type: 'mosaic' } } + linearProgressSeries: { + appendSpec: { + type: 'linearProgress' + } + }, + boxPlotSeries: { + appendSpec: { + type: 'boxPlot' + } + }, + sankeySeries: { + appendSpec: { + type: 'sankey' + } + }, + gaugePointerSeries: { + appendSpec: { + type: 'gaugePointer' + } + }, + sunburstSeries: { + appendSpec: { + type: 'sunburst' + } + }, + circlePackingSeries: { + appendSpec: { + type: 'circlePacking' + } + }, + waterfallSeries: { + appendSpec: { + type: 'waterfall' + } + }, + correlationSeries: { + appendSpec: { + type: 'correlation' + } + }, + liquidSeries: { + appendSpec: { + type: 'liquid' + } + }, + vennSeries: { + appendSpec: { + type: 'venn' + } + }, + mosaicSeries: { + appendSpec: { + type: 'mosaic' + } + } } + }, + + scales: { + isArray: true + }, + customMark: { + isArray: true + }, + background: { + isArray: false + }, + player: { + isArray: false + }, + crosshair: {}, + region: { + isArray: true + }, + title: {}, + markLine: {}, + markArea: {}, + markPoint: {}, + seriesStyle: { + isArray: true + }, + tooltip: { + isArray: false + }, + brush: { + isArray: false } }; export const removeUnneedArrayInSpec = (leafSpec: any, compKey: string, parentKeyPath: string) => { - return leafSpec[compKey] - ? isArray(leafSpec[compKey]) - ? leafSpec[compKey][0] - : leafSpec[compKey] - : parentKeyPath in leafSpec - ? leafSpec[parentKeyPath] - : `${compKey}[0]` in leafSpec - ? leafSpec[`${compKey}[0]`] - : leafSpec; + if (compKey === parentKeyPath) { + return isArray(leafSpec) ? leafSpec[0] : leafSpec; + } + + return leafSpec; +}; + +export const findComponentNameByAlias = (alias: string) => { + if (aliasByComponentType[alias]) { + return alias; + } + + const comp = Object.keys(aliasByComponentType).find(key => { + return !!aliasByComponentType[key]?.aliasMap?.[alias]; + }); + + if (comp) { + return comp; + } + + return alias; }; const ALIAS_NAME_KEY = '_alias_name'; -export const parseAliasOfPath = (parentKeyPath: string, aliasKeyPath: string, chartSpec: any, leafSpec: any) => { - const aliasName = aliasKeyPath ? aliasKeyPath.split('.')[0] : null; +export const parseAliasOfPath = (parentKeyPath: string, compKey: string, chartSpec: any, leafSpec: any) => { const subPaths = parentKeyPath.split('.'); - const compKey = subPaths[0].replace(/\[\d\]/, ''); const aliasOptions = aliasByComponentType[compKey]; - const isValidAlias = aliasOptions && aliasName && !!aliasOptions.aliasMap[aliasName]; + const aliasName = subPaths[0].replace(/\[\d\]/, ''); + const isValidAlias = aliasOptions && aliasName && !!aliasOptions.aliasMap?.[aliasName]; + let newLeafSpec = leafSpec; - if (!isValidAlias) { - if (chartSpec[compKey]) { - // 组件配置没有固定为数组类型的时候 - if (isArray(chartSpec[compKey])) { - if (subPaths[0] === compKey) { - subPaths[0] = `${compKey}[0]`; - } - return { - parentKeyPath: subPaths.join('.'), - leafSpec: removeUnneedArrayInSpec(leafSpec, compKey, parentKeyPath) - }; - } + if (subPaths.length === 1) { + newLeafSpec = isArray(leafSpec) ? leafSpec[0] : leafSpec; + } + const isTargetArray = (chartSpec[compKey] && isArray(chartSpec[compKey])) || aliasOptions?.isArray; - if (subPaths[0] !== compKey) { - subPaths[0] = compKey; - } - return { - parentKeyPath: subPaths.join('.'), - leafSpec: removeUnneedArrayInSpec(leafSpec, compKey, parentKeyPath) - }; - } else if (aliasOptions && aliasOptions.isArray) { - // 像系列这种只支持数组类型的,需要扩展成数组 - if (subPaths[0] === compKey) { - subPaths[0] = `${compKey}[0]`; - } - return { - parentKeyPath: subPaths.join('.'), - leafSpec: removeUnneedArrayInSpec(leafSpec, compKey, parentKeyPath) - }; + if (/\[\d\]/.exec(subPaths[0])) { + // 路径中包含了序号 + if (!isTargetArray) { + subPaths[0] = compKey; + } else { + subPaths[0] = subPaths[0].replace(aliasName, compKey); + } + } else { + // 路径中没包含序号 + if (isTargetArray) { + subPaths[0] = `${compKey}[0]`; + } else { + subPaths[0] = subPaths[0].replace(aliasName, compKey); } + } - return { parentKeyPath }; + if (!isValidAlias) { + return { parentKeyPath: subPaths.join('.'), leafSpec: newLeafSpec }; } const appendSpec = { ...aliasOptions.aliasMap[aliasName].appendSpec, [ALIAS_NAME_KEY]: aliasName }; @@ -520,45 +640,48 @@ export const convertFunctionString = (spec: any): any => { }; export const mergeAppendSpec = (prevSpec: any, appendSpec: AppendSpecInfo) => { - const { aliasKeyPath = '' } = appendSpec; - let { parentKeyPath = '', leafSpec } = appendSpec; - let newSpec = merge({}, prevSpec); + const { spec } = appendSpec; + const newSpec = merge({}, prevSpec); - if (parentKeyPath) { - if (parentKeyPath.startsWith('series') && newSpec.type !== 'common' && !newSpec.series) { - leafSpec = removeUnneedArrayInSpec(leafSpec, 'series', parentKeyPath); + if (isPlainObject(spec)) { + Object.keys(spec).forEach(key => { + let parentKeyPath = key; + let leafSpec = (spec as any)[key]; + const aliasName = parentKeyPath.split('.')[0].replace(/\[\d\]/, ''); + const compKey = findComponentNameByAlias(aliasName); - parentKeyPath = parentKeyPath.indexOf('.') > 0 ? parentKeyPath.slice(parentKeyPath.indexOf('.') + 1) : ''; - } else { - const aliasResult = parseAliasOfPath(parentKeyPath, aliasKeyPath, newSpec, leafSpec); + if (compKey.startsWith('series') && newSpec.type !== 'common' && !newSpec.series) { + leafSpec = removeUnneedArrayInSpec((spec as any)[key], 'series', key); - if (aliasResult.appendSpec && aliasResult.appendPath) { - set(newSpec, aliasResult.appendPath, aliasResult.appendSpec); - } + parentKeyPath = parentKeyPath.indexOf('.') > 0 ? parentKeyPath.slice(parentKeyPath.indexOf('.') + 1) : ''; + } else { + const aliasResult = parseAliasOfPath(parentKeyPath, compKey, newSpec, leafSpec); - if (isValid(aliasResult.parentKeyPath)) { - parentKeyPath = aliasResult.parentKeyPath; - } + if (aliasResult.appendSpec && aliasResult.appendPath) { + set(newSpec, aliasResult.appendPath, aliasResult.appendSpec); + } - if (isValid(aliasResult.leafSpec)) { - leafSpec = aliasResult.leafSpec; - } + if (isValid(aliasResult.parentKeyPath)) { + parentKeyPath = aliasResult.parentKeyPath; + } - leafSpec = convertFunctionString( - reduceDuplicatedPath( - parentKeyPath, - aliasResult.aliasName ? reduceDuplicatedPath(aliasResult.aliasName, leafSpec) : leafSpec - ) - ); - } + if (isValid(aliasResult.leafSpec)) { + leafSpec = aliasResult.leafSpec; + } - if (parentKeyPath) { - set(newSpec, parentKeyPath, leafSpec); - } else { - merge(newSpec, leafSpec); - } - } else { - newSpec = merge(newSpec, leafSpec); + leafSpec = convertFunctionString( + reduceDuplicatedPath( + parentKeyPath, + aliasResult.aliasName ? reduceDuplicatedPath(aliasResult.aliasName, leafSpec) : leafSpec + ) + ); + } + if (parentKeyPath) { + set(newSpec, parentKeyPath, leafSpec); + } else { + merge(newSpec, leafSpec); + } + }); } return { diff --git a/packages/vmind/src/types/atom.ts b/packages/vmind/src/types/atom.ts index 8331928..19bfbd7 100644 --- a/packages/vmind/src/types/atom.ts +++ b/packages/vmind/src/types/atom.ts @@ -181,17 +181,9 @@ export interface ChartQAExtractionCtx extends BaseContext { export interface AppendSpecInfo { /** - * 大模型返回的叶子节点的配置内容 + * 大模型返回的配置内容 */ - leafSpec: any; - /** - * 配置父路径 - */ - parentKeyPath: string; - /** - * 父路径的别名 - */ - aliasKeyPath?: string; + spec: any; } export interface VChartSpecCtx extends BaseContext {