diff --git a/.vscode/launch.json b/.vscode/launch.json index 22bdbd0d0e..82c66bad97 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,45 +1,53 @@ { - "version": "0.1.0", - "configurations": [ - { - "name": "Node.js - Debug Current File", - "type": "node", - "request": "launch", - "program": "${file}" - }, - { - "name": "generate documents", - "port": 9229, - "program": "${workspaceFolder}/docs/libs/template-parse/build.js", - "args": ["--env", "dev", "--watch"], - "request": "launch", - "skipFiles": ["/**"], - "type": "pwa-node" - }, - { - "name": "Debug Jest-Electron Current File", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}/packages/vchart", - "program": "${workspaceFolder}/packages/vchart/node_modules/jest/bin/jest.js", - "args": [ - "${file}", - "--watch" - ], - "env": { - "DEBUG_MODE": "1" - }, - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - }, - { - "name": "unit test", - "type": "pwa-node", - "request": "launch", - "program": "${workspaceFolder}/packages/vchart/node_modules/jest/bin/jest.js", - "args": ["${file}"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - } - ] -} + "version": "0.1.0", + "configurations": [ + { + "name": "Node.js - Debug Current File", + "type": "node", + "request": "launch", + "program": "${file}" + }, + { + "name": "generate documents", + "port": 9229, + "program": "${workspaceFolder}/docs/libs/template-parse/build.js", + "args": [ + "--env", + "dev", + "--watch" + ], + "request": "launch", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + }, + { + "name": "Debug Jest-Electron Current File", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}/packages/vchart", + "program": "${workspaceFolder}/packages/vchart/node_modules/jest/bin/jest.js", + "args": [ + "${file}", + "--watch" + ], + "env": { + "DEBUG_MODE": "1" + }, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "unit test", + "type": "pwa-node", + "request": "launch", + "program": "${workspaceFolder}/packages/vchart/node_modules/jest/bin/jest.js", + "args": [ + "${file}" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} \ No newline at end of file diff --git a/packages/vchart/__tests__/unit/core/vchart-event.test.ts b/packages/vchart/__tests__/unit/core/vchart-event.test.ts index 993e3c13d8..39ec0a0efe 100644 --- a/packages/vchart/__tests__/unit/core/vchart-event.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart-event.test.ts @@ -1,6 +1,13 @@ import { FederatedEvent } from '@visactor/vrender-core'; -import type { IBarChartSpec } from '../../../src'; -import { default as VChart } from '../../../src'; +import VChart, { + type BaseEventParams, + type IBarChartSpec, + type IChart, + type ICommonChartSpec, + type IMark, + type IMarkGraphic, + type ISeries +} from '../../../src'; import { createDiv, removeDom } from '../../util/dom'; describe('vchart event test', () => { @@ -192,4 +199,212 @@ describe('vchart event test', () => { expect(handleTooltipRelease).toBeCalledTimes(1); }); + + it('should keep series element-active triggers isolated in common chart', () => { + const commonContainer = createDiv(); + const commonDom = createDiv(commonContainer); + commonDom.id = 'common-container'; + commonContainer.style.position = 'fixed'; + commonContainer.style.width = '500px'; + commonContainer.style.height = '500px'; + commonContainer.style.top = '0px'; + commonContainer.style.left = '0px'; + + const commonChart = new VChart( + { + type: 'common', + data: [ + { + id: 'barData', + values: [ + { x: 'Mon', y: 10 }, + { x: 'Tue', y: 12 } + ] + }, + { + id: 'lineData', + values: [ + { x: 'Mon', y: 8 }, + { x: 'Tue', y: 15 } + ] + } + ], + series: [ + { + type: 'line', + dataId: 'lineData', + xField: 'x', + yField: 'y', + line: { + state: { + active: { + lineWidth: 4 + } + } + }, + interactions: [ + { + type: 'element-active', + trigger: 'click' + } + ] + }, + { + type: 'bar', + dataId: 'barData', + xField: 'x', + yField: 'y', + bar: { + state: { + active: { + stroke: '#000', + lineWidth: 2 + } + } + }, + interactions: [ + { + type: 'element-active', + trigger: 'pointerover' + } + ] + } + ], + axes: [{ orient: 'left' }, { orient: 'bottom', type: 'band' }] + } as ICommonChartSpec, + { + dom: commonDom, + animation: false + } + ); + + commonChart.renderSync(); + + try { + const chart = commonChart.getChart() as IChart; + const barSeries = chart.getAllSeries().find((series: ISeries) => series.type === 'bar'); + expect(barSeries).toBeDefined(); + if (!barSeries) { + throw new Error('Expected bar series to exist'); + } + + const barMark = barSeries.getMarks().find((mark: IMark) => mark.name === 'bar'); + expect(barMark).toBeDefined(); + if (!barMark) { + throw new Error('Expected bar mark to exist'); + } + + const barGraphic = barMark.getGraphics()[0] as IMarkGraphic; + + chart.getEvent().emit('pointerover', { item: barGraphic } as unknown as BaseEventParams); + expect(barGraphic.hasState('active')).toBe(true); + + chart.getEvent().emit('pointerout', { item: barGraphic } as unknown as BaseEventParams); + expect(barGraphic.hasState('active')).toBe(false); + + chart.getEvent().emit('click', { item: barGraphic } as unknown as BaseEventParams); + expect(barGraphic.hasState('active')).toBe(false); + } finally { + commonChart.release(); + removeDom(commonContainer); + } + }); + + it('should merge only identical interaction triggers in common chart', () => { + const commonContainer = createDiv(); + const commonDom = createDiv(commonContainer); + + const createSpec = ( + lineTrigger: 'click' | 'pointerover', + barTrigger: 'click' | 'pointerover' + ): ICommonChartSpec => ({ + type: 'common', + data: [ + { + id: 'barData', + values: [ + { x: 'Mon', y: 10 }, + { x: 'Tue', y: 12 } + ] + }, + { + id: 'lineData', + values: [ + { x: 'Mon', y: 8 }, + { x: 'Tue', y: 15 } + ] + } + ], + series: [ + { + type: 'line', + dataId: 'lineData', + xField: 'x', + yField: 'y', + line: { + state: { + active: { + lineWidth: 4 + } + } + }, + interactions: [ + { + type: 'element-active', + trigger: lineTrigger + } + ] + }, + { + type: 'bar', + dataId: 'barData', + xField: 'x', + yField: 'y', + bar: { + state: { + active: { + stroke: '#000', + lineWidth: 2 + } + } + }, + interactions: [ + { + type: 'element-active', + trigger: barTrigger + } + ] + } + ], + axes: [{ orient: 'left' }, { orient: 'bottom', type: 'band' }] + }); + + const getActiveTriggerCount = (spec: ICommonChartSpec) => { + const chart = new VChart(spec, { + dom: commonDom, + animation: false + }); + + chart.renderSync(); + + try { + const activeTriggers = (( + chart.getChart() as unknown as { + _interaction: { _triggerMapByState: Map }; + } + )._interaction._triggerMapByState.get('active') ?? []) as unknown[]; + + return activeTriggers.length; + } finally { + chart.release(); + } + }; + + try { + expect(getActiveTriggerCount(createSpec('pointerover', 'pointerover'))).toBe(1); + expect(getActiveTriggerCount(createSpec('click', 'pointerover'))).toBe(2); + } finally { + removeDom(commonContainer); + } + }); }); diff --git a/packages/vchart/__tests__/unit/theme/radar.test.ts b/packages/vchart/__tests__/unit/theme/radar.test.ts new file mode 100644 index 0000000000..7e59b6271a --- /dev/null +++ b/packages/vchart/__tests__/unit/theme/radar.test.ts @@ -0,0 +1,91 @@ +import { VChart } from '../../../src/vchart-all'; +import type { IRadarChartSpec } from '../../../src'; +import type { ITheme } from '../../../src/theme'; +import { ThemeManager } from '../../../src/theme'; +import { createCanvas, removeDom } from '../../util/dom'; + +describe('radar theme test', () => { + const themeName = 'radar-theme-area-visible'; + let canvasDom: HTMLCanvasElement; + let vchart: VChart | undefined; + + beforeEach(() => { + canvasDom = createCanvas(); + canvasDom.style.position = 'relative'; + canvasDom.style.width = '500px'; + canvasDom.style.height = '500px'; + canvasDom.width = 500; + canvasDom.height = 500; + }); + + afterEach(() => { + vchart?.release(); + removeDom(canvasDom); + ThemeManager.setCurrentTheme('light'); + ThemeManager.removeTheme(themeName); + }); + + it('should preserve radar theme area visibility when chart area only provides style', async () => { + const theme: ITheme = { + chart: { + radar: { + series: { + radar: { + area: { + visible: true, + style: { + fillOpacity: 0.1, + lineDash: [4, 2] + } + } + } + } + } + } + }; + + ThemeManager.registerTheme(themeName, theme); + ThemeManager.setCurrentTheme(themeName); + + vchart = new VChart( + { + type: 'radar', + data: [ + { + id: 'radarData', + values: [ + { key: 'Strength', value: 2 }, + { key: 'Speed', value: 3 }, + { key: 'Shooting', value: 3 } + ] + } + ], + categoryField: 'key', + valueField: 'value', + animation: false, + tooltip: { + visible: false + }, + area: { + style: { + fillOpacity: 0.1, + lineDash: [2, 2] + } + } + } as IRadarChartSpec, + { + renderCanvas: canvasDom, + background: 'yellow', + autoFit: true, + animation: false + } + ); + + await vchart.renderAsync(); + + const series = vchart.getChart()?.getAllSeries()?.[0]; + + expect(series?.getSpec()?.area?.visible).toBe(true); + expect(series?.getSpec()?.area?.style?.lineDash).toEqual([2, 2]); + }); +}); diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index f4932deee6..656d59e689 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -261,7 +261,11 @@ export class BaseChart extends CompilableBase implements I const series = this.getAllSeries(); const mergedTriggers: Partial[] = []; - const mergedTriggersMarks: Record> = {}; + const groupedTriggers: Array<{ + regionId: number; + config: Partial; + trigger: Partial; + }> = []; series.forEach(s => { const triggers = s.getInteractionTriggers(); @@ -270,15 +274,19 @@ export class BaseChart extends CompilableBase implements I const regionId = s.getRegion().id; triggers.forEach(({ trigger, marks }) => { - const interactionId = `${regionId}-${trigger.type}`; + const sameTrigger = groupedTriggers.find(item => item.regionId === regionId && isEqual(item.config, trigger)); - if (mergedTriggersMarks[interactionId]) { - marks.forEach(m => { - mergedTriggersMarks[interactionId].marks.push(m); - }); + if (sameTrigger) { + sameTrigger.trigger.marks.push(...marks); } else { - mergedTriggersMarks[interactionId] = { ...trigger, marks }; - mergedTriggers.push(mergedTriggersMarks[interactionId]); + const mergedTrigger = { ...trigger, marks: [...marks] }; + + groupedTriggers.push({ + regionId, + config: { ...trigger }, + trigger: mergedTrigger + }); + mergedTriggers.push(mergedTrigger); } }); } diff --git a/packages/vchart/src/chart/radar/radar-transformer.ts b/packages/vchart/src/chart/radar/radar-transformer.ts index 42d331025b..57d4a48316 100644 --- a/packages/vchart/src/chart/radar/radar-transformer.ts +++ b/packages/vchart/src/chart/radar/radar-transformer.ts @@ -8,13 +8,14 @@ export class RadarChartSpecTransformer< > extends RoseLikeChartSpecTransformer { protected _getDefaultSeriesSpec(spec: any): any { const series = super._getDefaultSeriesSpec(spec); + const areaTheme = this._option.getTheme?.('series', 'radar', 'area'); series.line = spec.line; series.point = spec.point; series.stack = spec.stack; series.percent = spec.percent; series.area = mergeSpec( { - visible: false + visible: areaTheme?.visible ?? false }, spec.area ); diff --git a/packages/vchart/src/interaction/triggers/element-active.ts b/packages/vchart/src/interaction/triggers/element-active.ts index cfdd9837fd..02a450693d 100644 --- a/packages/vchart/src/interaction/triggers/element-active.ts +++ b/packages/vchart/src/interaction/triggers/element-active.ts @@ -56,7 +56,7 @@ export class ElementActive extends BaseTrigger implements const statedGraphics = interaction.getStatedGraphics(this); const g = graphic ?? statedGraphics?.[0]; - if (g && statedGraphics.includes(g)) { + if (g && statedGraphics?.includes(g)) { g.removeState(state); interaction.setStatedGraphics( this,