diff --git a/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts b/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts index 9671c10..656a16d 100644 --- a/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts +++ b/packages/vmind/__tests__/unit/vchartSpec/bar.test.ts @@ -200,4 +200,99 @@ describe('mergeAppendSpec of barchart', () => { } ]); }); + + it('should parse complicated path when `parentKeyPath` > spec ', () => { + const append = { + leafSpec: { + grid: { + style: { + strokeOpacity: 1 + } + } + }, + parentKeyPath: 'axes[0].grid.style' + }; + + const { newSpec } = mergeAppendSpec(merge({}, spec), append); + + expect(newSpec.axes).toEqual([ + { + grid: { + style: { + strokeOpacity: 1 + } + } + } + ]); + + const { newSpec: newSpec2 } = mergeAppendSpec(merge({}, newSpec), { + aliasKeyPath: 'xAxis', + parentKeyPath: 'axes', + leafSpec: { + axes: [ + { + title: { + text: '城市' + } + } + ] + } + }); + + expect(newSpec2.axes).toEqual([ + { + grid: { + style: { + strokeOpacity: 1 + } + } + }, + { + title: { + text: '城市' + }, + _alias_name: 'xAxis', + orient: 'bottom' + } + ]); + }); + + it('should handle function', () => { + const append = { + aliasKeyPath: 'xAxis.label.style.fill', + leafSpec: { + bar: { + style: { + fill: "(datum, index) => index === 0 ? 'red' : null" + } + } + }, + parentKeyPath: 'bar' + }; + + const { newSpec } = mergeAppendSpec(merge({}, spec), append); + + expect(newSpec.bar.style.fill).toBeDefined(); + expect(newSpec.bar.style.fill('test', 0)).toBe('red'); + expect(newSpec.bar.style.fill('test', 1)).toBeNull(); + }); + + it('should not not add series when has only one series', () => { + const append = { + leafSpec: { + '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); + expect(newSpec.extensionMark).toEqual([ + { + style: { + size: 10 + } + } + ]); + }); }); diff --git a/packages/vmind/__tests__/unit/vchartSpec/util.test.ts b/packages/vmind/__tests__/unit/vchartSpec/util.test.ts index f5427d9..6b409b1 100644 --- a/packages/vmind/__tests__/unit/vchartSpec/util.test.ts +++ b/packages/vmind/__tests__/unit/vchartSpec/util.test.ts @@ -1,14 +1,17 @@ -import { checkDuplicatedKey } from '../../../src/atom/VChartSpec/utils'; +import { checkDuplicatedKey, convertFunctionString } from '../../../src/atom/VChartSpec/utils'; describe('checkDuplicatedKey', () => { it('should return null when not match', () => { expect(checkDuplicatedKey('a.b.c', 'd')).toBeNull(); - expect(checkDuplicatedKey('a.b.c', 'b')).toBeNull(); - - expect(checkDuplicatedKey('[0].a', 'a')).toBeNull(); }); it('should return correct result', () => { + expect(checkDuplicatedKey('[0].a', 'a')).toEqual({ + remainKeyPath: '' + }); + expect(checkDuplicatedKey('a.b.c', 'b')).toEqual({ + remainKeyPath: 'c' + }); expect(checkDuplicatedKey('[0].a', '0')).toEqual({ remainKeyPath: 'a' }); @@ -29,3 +32,66 @@ describe('checkDuplicatedKey', () => { }); }); }); + +describe('convertFunctionString', () => { + it('should convert arrow function string to function', () => { + const func1 = convertFunctionString('(datum, index) => index === 0? "red" : null'); + expect(func1({}, 0)).toBe('red'); + expect(func1({}, 1)).toBeNull(); + }); + + it('should convert function string to function', () => { + const func1 = convertFunctionString('function(datum, index) { return index === 0? "red" : null }'); + expect(func1({}, 0)).toBe('red'); + expect(func1({}, 1)).toBeNull(); + }); + + it('should not convert normal string', () => { + const str1 = convertFunctionString('a'); + expect(str1).toBe('a'); + }); + + it('should not convert normal object', () => { + const obj1 = convertFunctionString({ a: 1 }); + expect(obj1).toEqual({ a: 1 }); + }); + + it('should not convert normal array', () => { + const arr1 = convertFunctionString([1, 2, 3]); + expect(arr1).toEqual([1, 2, 3]); + }); + + it('should not convert normal number', () => { + const num1 = convertFunctionString(1); + expect(num1).toBe(1); + }); + + it('should not convert normal boolean', () => { + const bool1 = convertFunctionString(true); + expect(bool1).toBe(true); + }); + + it('should not convert normal null', () => { + const null1 = convertFunctionString(null); + expect(null1).toBeNull(); + }); + it('should not convert normal undefined', () => { + const undefined1 = convertFunctionString(undefined); + expect(undefined1).toBeUndefined(); + }); + it('should not convert normal function', () => { + const func1 = convertFunctionString(() => { + return 1; + }); + expect(func1).toBeInstanceOf(Function); + }); + it('should not convert normal arrow function', () => { + const func1 = convertFunctionString(() => 1); + expect(func1).toBeInstanceOf(Function); + }); + it('should not convert normal object with function', () => { + const spec = { a: () => 1 }; + const obj1 = convertFunctionString(spec); + expect(obj1.a).toEqual(spec.a); + }); +}); diff --git a/packages/vmind/src/atom/VChartSpec/utils.ts b/packages/vmind/src/atom/VChartSpec/utils.ts index 59ad41a..8476eae 100644 --- a/packages/vmind/src/atom/VChartSpec/utils.ts +++ b/packages/vmind/src/atom/VChartSpec/utils.ts @@ -1,4 +1,4 @@ -import { isArray, isNil, isPlainObject, merge } from '@visactor/vutils'; +import { isArray, isNil, isObject, isPlainObject, isString, isValid, merge } from '@visactor/vutils'; import type { AppendSpecInfo } from '../../types/atom'; import { set } from '../../utils/set'; @@ -288,13 +288,26 @@ export const checkDuplicatedKey = (parentPath: string, key: string) => { } } - if (parentPath.startsWith(`${key}.`)) { + if (parentPath.startsWith(`${key}`)) { + let remainKeyPath = parentPath.substring(key.length); + + if (remainKeyPath[0] === '.') { + remainKeyPath = remainKeyPath.substring(1); + } + return { - remainKeyPath: parentPath.substring(key.length + 1) + remainKeyPath }; - } else if (parentPath.startsWith(`${key}[`)) { + } else if (parentPath.includes(`.${key}`)) { + const str = `.${key}`; + let remainKeyPath = parentPath.substring(parentPath.indexOf(str) + str.length); + + if (remainKeyPath[0] === '.') { + remainKeyPath = remainKeyPath.substring(1); + } + return { - remainKeyPath: parentPath.substring(key.length) + remainKeyPath }; } @@ -321,10 +334,43 @@ export const reduceDuplicatedPath = (parentPath: string, spec: any): any => { } else if (isArray(spec) && parentPath) { const res = /^\[((\d)+)\]/.exec(parentPath); - if (res && +res[1] < spec.length) { + if (res) { const remainPath = parentPath.substring(res[0].length + 1); + const val = spec[+res[1]] ?? spec[spec.length - 1]; - return remainPath ? reduceDuplicatedPath(remainPath, spec[+res[1]]) : spec[+res[1]]; + return remainPath ? reduceDuplicatedPath(remainPath, val) : val; + } + } + + return spec; +}; + +/** + * 将大模型返回的spec中的函数字符串转换成函数 + * @param spec 转换后的spec + * @returns + */ +export const convertFunctionString = (spec: any): any => { + if (isPlainObject(spec)) { + const newSpec: any = {}; + + Object.keys(spec).forEach(key => { + newSpec[key] = convertFunctionString((spec as any)[key]); + }); + + return newSpec; + } else if (isArray(spec)) { + return spec.map(convertFunctionString); + } + + if (isString(spec)) { + if (spec.includes('=>') || spec.includes('function')) { + try { + // 将函数自浮窗转换成可执行的函数 + return new Function(`return (${spec})`)(); + } catch (e) { + return spec; + } } } @@ -332,14 +378,28 @@ export const reduceDuplicatedPath = (parentPath: string, spec: any): any => { }; export const mergeAppendSpec = (prevSpec: any, appendSpec: AppendSpecInfo) => { - const { leafSpec, parentKeyPath = '', aliasKeyPath = '' } = appendSpec; + const { aliasKeyPath = '' } = appendSpec; + let { parentKeyPath = '', leafSpec } = appendSpec; let newSpec = merge({}, prevSpec); if (parentKeyPath) { - const aliasResult = parseRealPath(parentKeyPath, aliasKeyPath, newSpec); + let aliasResult = parseRealPath(parentKeyPath, aliasKeyPath, newSpec); if (aliasResult.appendSpec && aliasResult.appendPath) { - set(newSpec, aliasResult.appendPath, aliasResult.appendSpec); + if (aliasResult.appendPath.includes('series') && !newSpec.series) { + // 系列比较特殊,默认是打平在第一层的 + leafSpec = leafSpec.series + ? isArray(leafSpec.series) + ? leafSpec.series[0] + : leafSpec.series + : parentKeyPath in leafSpec + ? leafSpec[parentKeyPath] + : leafSpec; + parentKeyPath = parentKeyPath.slice(parentKeyPath.indexOf('.') + 1); + aliasResult = { path: parentKeyPath }; + } else { + set(newSpec, aliasResult.appendPath, aliasResult.appendSpec); + } } const finalParentKeyPath = aliasResult.path ?? parentKeyPath; @@ -347,9 +407,11 @@ export const mergeAppendSpec = (prevSpec: any, appendSpec: AppendSpecInfo) => { set( newSpec, finalParentKeyPath, - reduceDuplicatedPath( - finalParentKeyPath, - aliasResult.aliasName ? reduceDuplicatedPath(aliasResult.aliasName, leafSpec) : leafSpec + convertFunctionString( + reduceDuplicatedPath( + finalParentKeyPath, + aliasResult.aliasName ? reduceDuplicatedPath(aliasResult.aliasName, leafSpec) : leafSpec + ) ) ); } else {