diff --git a/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel.ts b/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel.ts index 20810f3358..167c1e5d78 100644 --- a/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel.ts +++ b/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel.ts @@ -1,5 +1,5 @@ import type { IConversionFunnelChartSpecBase, IConversionFunnelSeriesSpecBase } from './interface'; -import { VChart, FunnelChart, PREFIX, FunnelSeries, GroupMark } from '@visactor/vchart'; +import { VChart, FunnelChart, PREFIX, FunnelSeries, GroupMark, registerMarkFilterTransform } from '@visactor/vchart'; import { DataView } from '@visactor/vdataset'; import { ConversionFunnelChartSpecTransformer } from './conversion-funnel-transformer'; import { conversionArrowTransform } from './arrow-data-transform'; @@ -42,7 +42,7 @@ export class ConversionFunnelSeries extends FunnelSeries { mark.setDataView(this._arrowData); mark.compileData(); - mark.getProduct().transform([ + mark.setTransform([ { type: 'filter', callback: datum => datum.position === 'right' @@ -55,7 +55,7 @@ export class ConversionFunnelSeries extends FunnelSeries { mark.setDataView(this._arrowData); mark.compileData(); - mark.getProduct().transform([ + mark.setTransform([ { type: 'filter', callback: datum => datum.position === 'left' @@ -67,6 +67,8 @@ export class ConversionFunnelSeries extends FunnelSeries { + registerMarkFilterTransform(); + const vchartConstructor = option?.VChart || VChart; if (vchartConstructor) { vchartConstructor.useChart([ConversionFunnelChart]); diff --git a/packages/vchart/__tests__/unit/chart/treemap.test.ts b/packages/vchart/__tests__/unit/chart/treemap.test.ts index 4de64e7871..5289199a83 100644 --- a/packages/vchart/__tests__/unit/chart/treemap.test.ts +++ b/packages/vchart/__tests__/unit/chart/treemap.test.ts @@ -100,8 +100,7 @@ describe('treemap chart test', () => { await cs.renderAsync(); const series: TreemapSeries = cs.getChart().getAllSeries()[0] as TreemapSeries; const leafMark = series.getMarkInName('leaf'); - const leafMarkProduct = leafMark?.getProduct(); - expect(leafMarkProduct?.elements.length).toBe(90); // 叶子图元 + expect(leafMark?.getGraphics().length).toBe(90); // 叶子图元 expect(series.getRawDataStatisticsByField(DEFAULT_HIERARCHY_DEPTH, true).max).toBe(2); cs.release(); }); @@ -136,10 +135,7 @@ describe('treemap chart test', () => { const leafMark = series.getMarkInName('leaf'); const nonLeafMark = series.getMarkInName('nonLeaf'); - const leafProduct = leafMark?.getProduct(); - const nonLeafProduct = nonLeafMark?.getProduct(); - - expect(leafProduct?.elements.length).toBe(86); // 叶子图元 - expect(nonLeafProduct?.elements.length).toBe(17); // 非叶子图元 + expect(leafMark?.getGraphics().length).toBe(86); // 叶子图元 + expect(nonLeafMark?.getGraphics().length).toBe(17); // 非叶子图元 }); }); diff --git a/packages/vchart/__tests__/unit/chart/word-cloud.test.ts b/packages/vchart/__tests__/unit/chart/word-cloud.test.ts index 37cef8aabe..e15241e702 100644 --- a/packages/vchart/__tests__/unit/chart/word-cloud.test.ts +++ b/packages/vchart/__tests__/unit/chart/word-cloud.test.ts @@ -197,6 +197,6 @@ describe('wordCloud chart test', () => { const marks = series.getMarks(); const wordMark = marks[1]; - expect(wordMark.getProduct()?.elements[0].getGraphicAttribute('fill')).toBe('#1664FF'); + expect(wordMark.getGraphics()[0].attribute.fill).toBe('#1664FF'); }); }); diff --git a/packages/vchart/__tests__/unit/core/vchart-event.test.ts b/packages/vchart/__tests__/unit/core/vchart-event.test.ts index aa26a758d5..ddc16aca2e 100644 --- a/packages/vchart/__tests__/unit/core/vchart-event.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart-event.test.ts @@ -1,3 +1,4 @@ +import { FederatedEvent } from '@visactor/vrender-core'; import type { IBarChartSpec } from '../../../src'; import { default as VChart } from '../../../src'; import { createDiv, removeDom } from '../../util/dom'; @@ -163,9 +164,13 @@ describe('vchart event test', () => { it('should fire pointerdown event once after updateSpecSync()', () => { const pointDowmSpy = jest.fn(); + const stage = vchart.getStage(); + const e = new FederatedEvent((stage as any).eventSystem.manager); + + e.type = 'pointerdown'; vchart.on('pointerdown', pointDowmSpy); - vchart.getCompiler().getVGrammarView().emit('pointerdown', {}); + stage.dispatchEvent(e); expect(pointDowmSpy).toBeCalledTimes(1); @@ -174,7 +179,7 @@ describe('vchart event test', () => { stack: 'percent' }); - vchart.getCompiler().getVGrammarView().emit('pointerdown', {}); + stage.dispatchEvent(e); expect(pointDowmSpy).toBeCalledTimes(2); }); diff --git a/packages/vchart/__tests__/unit/core/vchart.test.ts b/packages/vchart/__tests__/unit/core/vchart.test.ts index 11b59adb98..d51e0fba7f 100644 --- a/packages/vchart/__tests__/unit/core/vchart.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart.test.ts @@ -1,4 +1,4 @@ -import type { Group, Text } from '@visactor/vrender-core'; +import type { Group, IArc, Text } from '@visactor/vrender-core'; import type { IBarChartSpec } from '../../../src'; import { default as VChart } from '../../../src'; import { createDiv, createCanvas, removeDom } from '../../util/dom'; @@ -6,6 +6,7 @@ import type { ICommonChartSpec } from '../../../src/chart/common'; import type { IAreaSeriesSpec } from '../../../src/series/area/interface'; import type { IPoint } from '../../../src/typings'; import { polarToCartesian } from '@visactor/vutils'; +import type { IMarkGraphic } from '../../../src/mark/interface'; describe('VChart', () => { describe('render and update', () => { @@ -433,7 +434,7 @@ describe('VChart', () => { } ) as VChart; vchart.renderSync(); - const mark = vchart.getChart()!.getVGrammarView().getMarksByType('rect')[0].elements[0].getGraphicItem(); + const mark = vchart.getChart()!.getStage().getElementsByType('rect')[0] as IMarkGraphic; const point = vchart.convertDatumToPosition({ State: 'WY', 年龄段: '小于5岁', @@ -516,7 +517,8 @@ describe('VChart', () => { } ) as VChart; vchart.renderSync(); - const mark = vchart.getChart()!.getVGrammarView().getMarksByType('polygon')[0].elements[1].getGraphicItem(); + + const mark = vchart.getChart()!.getStage().getElementsByType('polygon')[1]; // @ts-ignore const centerX = (mark.attribute.points[0].x + mark.attribute.points[1].x) / 2; // @ts-ignore @@ -576,10 +578,9 @@ describe('VChart', () => { const point1 = vchart.convertDatumToPosition({ type: 'sodium', value: '2.83' }) as IPoint; const mark = vchart .getChart()! - .getVGrammarView() - .getMarksByType('arc')[0] - .elements.filter(ele => ele.groupKey === 'sodium')[0] - .getGraphicItem() as unknown as any; + .getStage() + .getElementsByType('arc') + .filter(ele => (ele as IMarkGraphic).context.groupKey === 'sodium')[0] as IArc; const markCoord = polarToCartesian( { x: mark.attribute.x as number, y: mark.attribute.y as number }, diff --git a/packages/vchart/__tests__/unit/mark/line.test.ts b/packages/vchart/__tests__/unit/mark/line.test.ts index 60eef41e4a..df4e091545 100644 --- a/packages/vchart/__tests__/unit/mark/line.test.ts +++ b/packages/vchart/__tests__/unit/mark/line.test.ts @@ -11,7 +11,7 @@ describe('Line Mark', () => { stroke: undefined } }); - expect((lineMark1 as any).getMarkConfig().enableSegments).toBe(undefined); + expect((lineMark1 as any)._segmentStyleKeys.length).toBe(0); const lineMark2 = new LineMark('line1', ctx); lineMark2.created(); @@ -29,7 +29,7 @@ describe('Line Mark', () => { } } }); - expect((lineMark2 as any).getMarkConfig().enableSegments).toBe(true); + expect((lineMark2 as any)._segmentStyleKeys).toEqual(['lineDash']); const lineMark3 = new LineMark('line1', ctx); lineMark3.created(); @@ -39,7 +39,7 @@ describe('Line Mark', () => { lineDash: undefined } }); - expect((lineMark3 as any).getMarkConfig().enableSegments).toBe(undefined); + expect((lineMark3 as any)._segmentStyleKeys.length).toBe(0); const lineMark4 = new LineMark('line1', ctx); lineMark4.created(); @@ -50,7 +50,7 @@ describe('Line Mark', () => { stroke: 'red' } }); - expect((lineMark4 as any).getMarkConfig().enableSegments).toBeUndefined(); + expect((lineMark4 as any)._segmentStyleKeys.length).toBe(0); }); // FIXME: 'fill' does not exist in type 'IMarkSpec' diff --git a/packages/vchart/__tests__/unit/theme/line.test.ts b/packages/vchart/__tests__/unit/theme/line.test.ts index 745675f806..1b18d1b3d4 100644 --- a/packages/vchart/__tests__/unit/theme/line.test.ts +++ b/packages/vchart/__tests__/unit/theme/line.test.ts @@ -137,7 +137,7 @@ describe('theme switch test', () => { await vchart.renderAsync(); // await vchart.setCurrentTheme('light'); // sepc - expect(vchart.getCompiler().getVGrammarView().background()).toBe('red'); + expect(vchart.getCompiler().getStage().background).toBe('red'); }); it('set theme in spec and theme is an object', async () => { @@ -178,7 +178,7 @@ describe('theme switch test', () => { await vchart.renderAsync(); // spec - expect(vchart.getCompiler().getVGrammarView().background()).toBe('red'); + expect(vchart.getCompiler().getStage().background).toBe('red'); expect(vchart.getCurrentThemeName()).toBe('light'); }); }); diff --git a/packages/vchart/__tests__/util/factory/compiler.ts b/packages/vchart/__tests__/util/factory/compiler.ts index e004eb1286..b0318bfa9a 100644 --- a/packages/vchart/__tests__/util/factory/compiler.ts +++ b/packages/vchart/__tests__/util/factory/compiler.ts @@ -1,26 +1,14 @@ +import { getTestStage } from './stage'; + export const getTestCompiler = () => ({ updateData: () => {}, updateState: () => {}, renderAsync: () => {}, - getVGrammarView: () => { - return { - updateLayoutTag: () => {}, - getDataById: () => {}, - getMarkById: () => {}, - getSignalById: () => {}, - signal: () => { - return { - id: () => { - return { - value: () => {} - }; - } - }; - } - }; - }, - addGrammarItem: () => {}, - addInteraction: () => {}, - removeInteraction: () => {} + getLayoutState: () => '', + updateLayoutTag: () => {}, + getStage: getTestStage, + addRootMark: () => {}, + renderNextTick: () => {}, + addGrammarItem: () => {} } as any); diff --git a/packages/vchart/__tests__/util/factory/stage.ts b/packages/vchart/__tests__/util/factory/stage.ts new file mode 100644 index 0000000000..8017c31edc --- /dev/null +++ b/packages/vchart/__tests__/util/factory/stage.ts @@ -0,0 +1,4 @@ +export const getTestStage = () => + ({ + getElementById: () => {} + } as any); diff --git a/packages/vchart/src/animation/animate-manager.ts b/packages/vchart/src/animation/animate-manager.ts index 2c5d6fcd1e..94df1d8c0b 100644 --- a/packages/vchart/src/animation/animate-manager.ts +++ b/packages/vchart/src/animation/animate-manager.ts @@ -4,7 +4,7 @@ import type { IAnimate, IAnimateState } from './interface'; // eslint-disable-next-line no-duplicate-imports import { AnimationStateEnum } from './interface'; import type { StateValueMap } from '../compile/interface/compilable-item'; -import type { IGraphic } from '@visactor/vrender-core'; +import type { IMarkGraphic } from '../mark/interface/common'; export class AnimateManager extends StateManager implements IAnimate { protected declare _stateMap: IAnimateState & StateValueMap; @@ -17,7 +17,7 @@ export class AnimateManager extends StateManager implements IAnimate { this.updateState( { animationState: { - callback: (datum: any, g: IGraphic) => g.context.diffState + callback: (datum: any, g: IMarkGraphic) => g.context.diffState } }, noRender @@ -28,7 +28,7 @@ export class AnimateManager extends StateManager implements IAnimate { this.updateState( { animationState: { - callback: (datum: any, g: IGraphic) => { + callback: (datum: any, g: IMarkGraphic) => { return g.context.diffState === 'exit' ? AnimationStateEnum.none : AnimationStateEnum.appear; } } @@ -41,7 +41,7 @@ export class AnimateManager extends StateManager implements IAnimate { this.updateState( { animationState: { - callback: (datum: any, g: IGraphic) => state + callback: (datum: any, g: IMarkGraphic) => state } }, noRender @@ -52,7 +52,7 @@ export class AnimateManager extends StateManager implements IAnimate { protected _getDefaultStateMap(): IAnimateState & StateValueMap { return { animationState: { - callback: (datum: any, g: IGraphic) => { + callback: (datum: any, g: IMarkGraphic) => { return g.context.diffState === 'exit' ? AnimationStateEnum.exit : g.context.diffState === 'update' diff --git a/packages/vchart/src/animation/config.ts b/packages/vchart/src/animation/config.ts index 5eb0ab8d68..03925682b3 100644 --- a/packages/vchart/src/animation/config.ts +++ b/packages/vchart/src/animation/config.ts @@ -5,36 +5,36 @@ import type { ILineAnimationParams, LineAppearPreset } from '../series/line/inte import { linePresetAnimation } from '../series/line/animation'; import type { MarkAnimationSpec, ICartesianGroupAnimationParams } from './interface'; import { Factory } from '../core/factory'; -import { - View, - registerScaleInAnimation, - registerScaleOutAnimation, - registerFadeInAnimation, - registerFadeOutAnimation, - registerClipInAnimation, - registerClipOutAnimation, - registerGrowAngleInAnimation, - registerGrowAngleOutAnimation, - registerGrowCenterInAnimation, - registerGrowCenterOutAnimation, - registerGrowHeightInAnimation, - registerGrowHeightOutAnimation, - registerGrowPointsInAnimation, - registerGrowPointsOutAnimation, - registerGrowPointsXInAnimation, - registerGrowPointsXOutAnimation, - registerGrowPointsYInAnimation, - registerGrowPointsYOutAnimation, - registerGrowRadiusInAnimation, - registerGrowRadiusOutAnimation, - registerGrowWidthInAnimation, - registerGrowWidthOutAnimation, - registerMoveInAnimation, - registerMoveOutAnimation, - registerRotateInAnimation, - registerRotateOutAnimation, - registerUpdateAnimation -} from '@visactor/vgrammar-core'; +// import { +// View, +// registerScaleInAnimation, +// registerScaleOutAnimation, +// registerFadeInAnimation, +// registerFadeOutAnimation, +// registerClipInAnimation, +// registerClipOutAnimation, +// registerGrowAngleInAnimation, +// registerGrowAngleOutAnimation, +// registerGrowCenterInAnimation, +// registerGrowCenterOutAnimation, +// registerGrowHeightInAnimation, +// registerGrowHeightOutAnimation, +// registerGrowPointsInAnimation, +// registerGrowPointsOutAnimation, +// registerGrowPointsXInAnimation, +// registerGrowPointsXOutAnimation, +// registerGrowPointsYInAnimation, +// registerGrowPointsYOutAnimation, +// registerGrowRadiusInAnimation, +// registerGrowRadiusOutAnimation, +// registerGrowWidthInAnimation, +// registerGrowWidthOutAnimation, +// registerMoveInAnimation, +// registerMoveOutAnimation, +// registerRotateInAnimation, +// registerRotateOutAnimation, +// registerUpdateAnimation +// } from '@visactor/vgrammar-core'; import { Direction } from '../typings/space'; export const DEFAULT_ANIMATION_CONFIG = { @@ -153,52 +153,52 @@ export const registerAreaAnimation = () => { }; export const registerVGrammarCommonAnimation = () => { - View.useRegisters([ - registerScaleInAnimation, - registerScaleOutAnimation, - registerFadeInAnimation, - registerFadeOutAnimation, - registerMoveInAnimation, - registerMoveOutAnimation, - registerRotateInAnimation, - registerRotateOutAnimation, - registerUpdateAnimation - ]); + // View.useRegisters([ + // registerScaleInAnimation, + // registerScaleOutAnimation, + // registerFadeInAnimation, + // registerFadeOutAnimation, + // registerMoveInAnimation, + // registerMoveOutAnimation, + // registerRotateInAnimation, + // registerRotateOutAnimation, + // registerUpdateAnimation + // ]); }; export const registerVGrammarRectAnimation = () => { - View.useRegisters([ - registerGrowHeightInAnimation, - registerGrowHeightOutAnimation, - registerGrowWidthInAnimation, - registerGrowWidthOutAnimation, - registerGrowCenterInAnimation, - registerGrowCenterOutAnimation - ]); + // View.useRegisters([ + // registerGrowHeightInAnimation, + // registerGrowHeightOutAnimation, + // registerGrowWidthInAnimation, + // registerGrowWidthOutAnimation, + // registerGrowCenterInAnimation, + // registerGrowCenterOutAnimation + // ]); }; export const registerVGrammarArcAnimation = () => { - View.useRegisters([ - registerGrowRadiusInAnimation, - registerGrowRadiusOutAnimation, - registerGrowAngleInAnimation, - registerGrowAngleOutAnimation - ]); + // View.useRegisters([ + // registerGrowRadiusInAnimation, + // registerGrowRadiusOutAnimation, + // registerGrowAngleInAnimation, + // registerGrowAngleOutAnimation + // ]); }; export const registerVGrammarLineOrAreaAnimation = () => { - View.useRegisters([ - registerGrowPointsInAnimation, - registerGrowPointsOutAnimation, - registerGrowPointsXInAnimation, - registerGrowPointsXOutAnimation, - registerGrowPointsYInAnimation, - registerGrowPointsYOutAnimation, - registerClipInAnimation, - registerClipOutAnimation - ]); + // View.useRegisters([ + // registerGrowPointsInAnimation, + // registerGrowPointsOutAnimation, + // registerGrowPointsXInAnimation, + // registerGrowPointsXOutAnimation, + // registerGrowPointsYInAnimation, + // registerGrowPointsYOutAnimation, + // registerClipInAnimation, + // registerClipOutAnimation + // ]); }; export const registerVGrammarPolygonAnimation = () => { - View.useRegisters([registerGrowPointsInAnimation, registerGrowPointsOutAnimation]); + // View.useRegisters([registerGrowPointsInAnimation, registerGrowPointsOutAnimation]); }; diff --git a/packages/vchart/src/animation/interface.ts b/packages/vchart/src/animation/interface.ts index 5a67c06a91..b9e32f54fb 100644 --- a/packages/vchart/src/animation/interface.ts +++ b/packages/vchart/src/animation/interface.ts @@ -169,5 +169,6 @@ export interface MarkAnimationSpec { enter?: IAnimationConfig | IAnimationConfig[]; exit?: IAnimationConfig | IAnimationConfig[]; update?: IAnimationConfig | IAnimationConfig[]; + normal?: IAnimationConfig | IAnimationConfig[]; state?: IStateAnimationConfig; } diff --git a/packages/vchart/src/chart/area/area.ts b/packages/vchart/src/chart/area/area.ts index 59e2a98fa4..2f278735d7 100644 --- a/packages/vchart/src/chart/area/area.ts +++ b/packages/vchart/src/chart/area/area.ts @@ -7,6 +7,7 @@ import { AreaChartSpecTransformer } from './area-transformer'; import { BaseChart } from '../base'; import { mixin } from '@visactor/vutils'; import { StackChartMixin } from '../stack'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class AreaChart extends BaseChart { static readonly type: string = ChartTypeEnum.area; @@ -19,6 +20,7 @@ export class AreaChart extends BaseCh mixin(AreaChart, StackChartMixin); export const registerAreaChart = () => { + registerDimensionHover(); registerAreaSeries(); Factory.registerChart(AreaChart.type, AreaChart); }; diff --git a/packages/vchart/src/chart/bar/bar.ts b/packages/vchart/src/chart/bar/bar.ts index 71b33e18fb..ea1c66d480 100644 --- a/packages/vchart/src/chart/bar/bar.ts +++ b/packages/vchart/src/chart/bar/bar.ts @@ -7,6 +7,7 @@ import { BarChartSpecTransformer } from './bar-transformer'; import { BaseChart } from '../base'; import { mixin } from '@visactor/vutils'; import { StackChartMixin } from '../stack'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class BarChart extends BaseChart { static readonly type: string = ChartTypeEnum.bar; @@ -20,6 +21,7 @@ export class BarChart extends BaseChart mixin(BarChart, StackChartMixin); export const registerBarChart = () => { + registerDimensionHover(); registerBarSeries(); Factory.registerChart(BarChart.type, BarChart); }; diff --git a/packages/vchart/src/chart/base/base-chart-transformer.ts b/packages/vchart/src/chart/base/base-chart-transformer.ts index 59aec151c7..dac7d77d85 100644 --- a/packages/vchart/src/chart/base/base-chart-transformer.ts +++ b/packages/vchart/src/chart/base/base-chart-transformer.ts @@ -287,7 +287,7 @@ export class BaseChartSpecTransformer implements IChartSpe if (cmp.type === ComponentTypeEnum.tooltip) { tooltip = cmp; } else { - otherComponents.push(cmp); + otherComponents.push(components[index]); } } } @@ -322,7 +322,11 @@ export class BaseChartSpecTransformer implements IChartSpe }); } - otherComponents.forEach(C => { + otherComponents.sort((a, b) => { + return a.createOrder - b.createOrder; + }); + + otherComponents.forEach(({ cmp: C }) => { (C.getSpecInfo ? C.getSpecInfo(chartSpec, chartSpecInfo) : getSpecInfo(chartSpec, C.specKey, C.type))?.forEach( info => { results.push(callbackfn(C, info, chartSpecInfo)); diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index 8aba1f6ac7..be86242249 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -35,7 +35,7 @@ import type { IRegion } from '../../region/interface'; import { ComponentTypeEnum } from '../../component/interface'; // eslint-disable-next-line no-duplicate-imports import type { IComponent, IComponentConstructor } from '../../component/interface'; -import type { IMark, IRectMark } from '../../mark/interface'; +import type { IMark, IMarkGraphic, IRectMark } from '../../mark/interface'; // eslint-disable-next-line no-duplicate-imports import { MarkTypeEnum } from '../../mark/interface'; import type { IEvent } from '../../event/interface'; @@ -44,7 +44,16 @@ import type { DataView } from '@visactor/vdataset'; import type { DataSet } from '@visactor/vdataset'; import { Factory } from '../../core/factory'; import { Event } from '../../event/event'; -import { isArray, isValid, createID, calcPadding, normalizeLayoutPaddingSpec, array } from '../../util'; +import { + isArray, + isValid, + createID, + calcPadding, + normalizeLayoutPaddingSpec, + array, + isCollectionMark, + getDatumOfGraphic +} from '../../util'; import { BaseModel } from '../../model/base-model'; import { BaseMark } from '../../mark/base/base-mark'; import { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT } from '../../constant/base'; @@ -52,13 +61,13 @@ import { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT } from '../../constant/base'; import type { IParserOptions } from '@visactor/vdataset'; import type { IBoundsLike, Maybe } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports -import { isFunction, isEmpty, isNil, isString, isEqual, pickWithout } from '@visactor/vutils'; +import { isFunction, isEmpty, isNil, isString, isEqual, pickWithout, isBoolean, isObject } from '@visactor/vutils'; import { getDataScheme } from '../../theme/color-scheme/util'; import { CompilableBase } from '../../compile/compilable-base'; import type { IStateInfo } from '../../compile/mark/interface'; // eslint-disable-next-line no-duplicate-imports import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; -import { ChartEvent, VGRAMMAR_HOOK_EVENT } from '../../constant/event'; +import { ChartEvent, HOOK_EVENT } from '../../constant/event'; import type { IGlobalScale } from '../../scale/interface'; import { DimensionEventEnum } from '../../event/events/dimension'; import type { ITooltip } from '../../component/tooltip/interface'; @@ -69,6 +78,9 @@ import { LayoutZIndex } from '../../constant/layout'; import type { IAxis } from '../../component/axis/interface/common'; import type { IMorphConfig } from '../../animation/spec'; import type { IGraphic } from '@visactor/vrender-core'; +import { Interaction } from '../../interaction/interaction'; +import type { IInteraction } from '../../interaction/interface/common'; +import type { IBaseTriggerOptions } from '../../interaction/interface/trigger'; export class BaseChart extends CompilableBase implements IChart { readonly type: string = 'chart'; @@ -180,6 +192,8 @@ export class BaseChart extends CompilableBase implements I // background protected _backgroundMark: IRectMark; + protected _interaction: IInteraction; + constructor(spec: T, option: IChartOption) { super(option); this._paddingSpec = normalizeLayoutPaddingSpec(spec.padding ?? option.getTheme().padding); @@ -220,6 +234,52 @@ export class BaseChart extends CompilableBase implements I // components transformer.forEachComponentInSpec(this._spec, this._createComponent.bind(this), this._option.getSpecInfo()); } + _initInteractions() { + if (this._option.disableTriggerEvent) { + return; + } + + // 创建交互 + this._interaction = new Interaction(); + + const series = this.getAllSeries(); + const mergedTriggers: Partial[] = []; + const mergedTriggersMarks: Record> = {}; + + series.forEach(s => { + const triggers = s.getInteractionTriggers(); + + if (triggers && triggers.length) { + const regionId = s.getRegion().id; + + triggers.forEach(({ trigger, marks }) => { + const interactionId = `${regionId}-${trigger.type}`; + + if (mergedTriggersMarks[interactionId]) { + marks.forEach(m => { + mergedTriggersMarks[interactionId].marks.push(m); + }); + } else { + mergedTriggersMarks[interactionId] = { ...trigger, marks }; + mergedTriggers.push(mergedTriggersMarks[interactionId]); + } + }); + } + }); + + mergedTriggers.forEach(trigger => { + const triggerInstance = Factory.createInteractionTrigger((trigger as any).type, { + ...trigger, + event: this._event, + interaction: this._interaction + }); + + if (triggerInstance) { + triggerInstance.init(); + this._interaction.addTrigger(triggerInstance); + } + }); + } init() { (this as any)._beforeInit?.(); @@ -235,6 +295,8 @@ export class BaseChart extends CompilableBase implements I // data flow start this.reDataFlow(); + + this._initInteractions(); } reDataFlow() { @@ -284,6 +346,8 @@ export class BaseChart extends CompilableBase implements I this._backgroundMark.setMarkConfig({ zIndex: LayoutZIndex.SeriesGroup - 2 }); + + this.getCompiler().addRootMark(this._backgroundMark); } protected _createRegion(constructor: IRegionConstructor, specInfo: IModelSpecInfo) { @@ -427,7 +491,7 @@ export class BaseChart extends CompilableBase implements I } layout(): void { - this._option.performanceHook?.beforeLayoutWithSceneGraph?.(); + this._option.performanceHook?.beforeLayoutWithSceneGraph?.(this._option.globalInstance); if (this.getLayoutTag()) { this._event.emit(ChartEvent.layoutStart, { chart: this, vchart: this._option.globalInstance }); @@ -440,7 +504,7 @@ export class BaseChart extends CompilableBase implements I this._event.emit(ChartEvent.layoutEnd, { chart: this, vchart: this._option.globalInstance }); } - this._option.performanceHook?.afterLayoutWithSceneGraph?.(); + this._option.performanceHook?.afterLayoutWithSceneGraph?.(this._option.globalInstance); } // 通知所有需要通知的元素 onLayout 钩子 @@ -1025,34 +1089,31 @@ export class BaseChart extends CompilableBase implements I if (!this._backgroundMark) { return; } - this._backgroundMark.compile({ context: { model: this } }); - // this._backgroundMark.getProduct()?.layout(() => { - // // console.log('region mark layout'); - // }); + this._backgroundMark.compile(); } compileRegions() { - this._option.performanceHook?.beforeRegionCompile?.(); + this._option.performanceHook?.beforeRegionCompile?.(this._option.globalInstance); this.getAllRegions().forEach(r => { r.compile(); }); - this._option.performanceHook?.afterRegionCompile?.(); + this._option.performanceHook?.afterRegionCompile?.(this._option.globalInstance); } compileSeries() { - this._option.performanceHook?.beforeSeriesCompile?.(); + this._option.performanceHook?.beforeSeriesCompile?.(this._option.globalInstance); this.getAllSeries().forEach(s => { s.compile(); }); - this._option.performanceHook?.afterSeriesCompile?.(); + this._option.performanceHook?.afterSeriesCompile?.(this._option.globalInstance); } compileComponents() { - this._option.performanceHook?.beforeComponentCompile?.(); + this._option.performanceHook?.beforeComponentCompile?.(this._option.globalInstance); this.getAllComponents().forEach(c => { c.compile(); }); - this._option.performanceHook?.afterComponentCompile?.(); + this._option.performanceHook?.afterComponentCompile?.(this._option.globalInstance); } release() { @@ -1130,7 +1191,7 @@ export class BaseChart extends CompilableBase implements I filter?: (series: ISeries, mark: IMark) => boolean, region?: IRegionQuerier ): void { - this._setStateInDatum(STATE_VALUE_ENUM.STATE_SELECTED, true, datum, filter, region); + this._setStateInDatum(STATE_VALUE_ENUM.STATE_SELECTED, datum, filter, region); } /** @@ -1144,7 +1205,7 @@ export class BaseChart extends CompilableBase implements I filter?: (series: ISeries, mark: IMark) => boolean, region?: IRegionQuerier ): void { - this._setStateInDatum(STATE_VALUE_ENUM.STATE_HOVER, true, datum, filter, region); + this._setStateInDatum(STATE_VALUE_ENUM.STATE_HOVER, datum, filter, region); } /** @@ -1153,11 +1214,7 @@ export class BaseChart extends CompilableBase implements I * @since 1.11.0 */ clearState(state: string) { - this.getAllRegions().forEach(r => { - r.interaction.clearEventElement(state, true); - r.interaction.resetInteraction(state, null); - return; - }); + this._interaction.clearByState(state); } /** @@ -1166,11 +1223,7 @@ export class BaseChart extends CompilableBase implements I * @since 1.12.4 */ clearAllStates() { - this.getAllRegions().forEach(r => { - r.interaction.clearAllEventElement(); - r.interaction.resetAllInteraction(); - return; - }); + this._interaction.clearAllStates(); } /** @@ -1197,9 +1250,9 @@ export class BaseChart extends CompilableBase implements I this._disableMarkAnimation(['exit', 'update']); const enableMarkAnimate = () => { this._enableMarkAnimation(['exit', 'update']); - this._event.off(VGRAMMAR_HOOK_EVENT.AFTER_MARK_RENDER_END, enableMarkAnimate); + this._event.off(HOOK_EVENT.AFTER_MARK_RENDER_END, enableMarkAnimate); }; - this._event.on(VGRAMMAR_HOOK_EVENT.AFTER_MARK_RENDER_END, enableMarkAnimate); + this._event.on(HOOK_EVENT.AFTER_MARK_RENDER_END, enableMarkAnimate); }); }); } @@ -1209,7 +1262,7 @@ export class BaseChart extends CompilableBase implements I marks.forEach(mark => { const product = mark.getProduct(); if (product && product.animate) { - product.animate.enableAnimationState(states); + // product.animate.enableAnimationState(states); } }); } @@ -1219,72 +1272,71 @@ export class BaseChart extends CompilableBase implements I marks.forEach(mark => { const product = mark.getProduct(); if (product && product.animate) { - product.animate.disableAnimationState(states); + // product.animate.disableAnimationState(states); } }); } protected _setStateInDatum( stateKey: string, - checkReverse: boolean, - datum: MaybeArray | null, + d: MaybeArray | null, filter?: (series: ISeries, mark: IMark) => boolean, region?: IRegionQuerier ) { - datum = datum ? array(datum) : null; - const keys = !datum ? null : Object.keys(datum[0]); + if (!d) { + this._interaction.clearByState(stateKey); + return; + } + const datum = array(d); + const keys = Object.keys(datum[0]); + const pickGraphics = [] as IMarkGraphic[]; + this.getRegionsInQuerier(region).forEach(r => { - if (!datum) { - r.interaction.clearEventElement(stateKey, true); - return; - } r.getSeries().forEach(s => { s.getMarks().forEach(m => { - if (!m.getProduct()) { + const graphics = m.getGraphics(); + if (!graphics || !graphics.length) { return; } if (!filter || (isFunction(filter) && filter(s, m))) { - const isCollect = m.getProduct().isCollectionMark(); - const elements = m.getProduct().elements; - let pickElements = [] as IGraphic[]; + const isCollect = isCollectionMark(m.type); + if (isCollect) { - pickElements = elements.filter(e => { - const elDatum = e.getDatum(); + graphics.filter(g => { + const elDatum = getDatumOfGraphic(g) as Datum[]; + // eslint-disable-next-line max-nested-callbacks, eqeqeq - (datum as Datum[]).every((d, index) => keys.every(k => d[k] == elDatum[index][k])); + const isPicked = + elDatum && (datum as Datum[]).every((d, index) => keys.every(k => d[k] == elDatum[index]?.[k])); + + if (isPicked) { + pickGraphics.push(g); + } }); } else { if (datum.length > 1) { const datumTemp = (datum as Datum[]).slice(); - pickElements = elements.filter(e => { - if (datumTemp.length === 0) { - return false; - } - const elDatum = e.getDatum(); + + graphics.forEach((g: IMarkGraphic) => { + const elDatum = getDatumOfGraphic(g) as Datum; // eslint-disable-next-line max-nested-callbacks, eqeqeq - const index = datumTemp.findIndex(d => keys.every(k => d[k] == elDatum[k])); + const index = datumTemp.findIndex(d => keys.every(k => d[k] == elDatum?.[k])); if (index >= 0) { datumTemp.splice(index, 1); - return true; + pickGraphics.push(g); } - return false; }); } else { // eslint-disable-next-line eqeqeq - const el = elements.find(e => keys.every(k => datum[0][k] == e.getDatum()[k])); - el && (pickElements = [el]); + const el = graphics.find(e => keys.every(k => datum[0][k] == (getDatumOfGraphic(e) as Datum)?.[k])); + el && pickGraphics.push(el); } } - pickElements.forEach(element => { - r.interaction.startInteraction(stateKey, element); - }); } }); }); - if (checkReverse) { - r.interaction.reverseEventElement(stateKey); - } }); + this._interaction.updateStateOfGraphics(stateKey, pickGraphics); } /** diff --git a/packages/vchart/src/chart/box-plot/box-plot.ts b/packages/vchart/src/chart/box-plot/box-plot.ts index d7905e795e..d536c0b880 100644 --- a/packages/vchart/src/chart/box-plot/box-plot.ts +++ b/packages/vchart/src/chart/box-plot/box-plot.ts @@ -5,6 +5,7 @@ import { registerBoxplotSeries } from '../../series/box-plot/box-plot'; import { Factory } from '../../core/factory'; import { BoxPlotChartSpecTransformer } from './box-plot-transformer'; import { BaseChart } from '../base'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class BoxPlotChart extends BaseChart { static readonly type: string = ChartTypeEnum.boxPlot; @@ -16,6 +17,7 @@ export class BoxPlotChart exten } export const registerBoxplotChart = () => { + registerDimensionHover(); registerBoxplotSeries(); Factory.registerChart(BoxPlotChart.type, BoxPlotChart); }; diff --git a/packages/vchart/src/chart/histogram/3d/histogram-3d.ts b/packages/vchart/src/chart/histogram/3d/histogram-3d.ts index 7c257d83e9..cf4851678e 100644 --- a/packages/vchart/src/chart/histogram/3d/histogram-3d.ts +++ b/packages/vchart/src/chart/histogram/3d/histogram-3d.ts @@ -7,6 +7,7 @@ import type { IHistogram3dChartSpec } from '../interface'; import type { AdaptiveSpec } from '../../../typings'; import { HistogramChartSpecTransformer } from '../histogram-transformer'; import { register3DPlugin } from '../../../plugin/other'; +import { registerDimensionHover } from '../../../interaction/triggers/dimension-hover'; export class Histogram3dChart extends BaseHistogramChart> { static readonly type: string = ChartTypeEnum.histogram3d; @@ -17,6 +18,7 @@ export class Histogram3dChart extends BaseHisto readonly seriesType: string = SeriesTypeEnum.bar3d; } export const registerHistogram3dChart = () => { + registerDimensionHover(); register3DPlugin(); registerBar3dSeries(); Factory.registerChart(Histogram3dChart.type, Histogram3dChart); diff --git a/packages/vchart/src/chart/histogram/histogram.ts b/packages/vchart/src/chart/histogram/histogram.ts index b222ecc6ad..e2b53e134a 100644 --- a/packages/vchart/src/chart/histogram/histogram.ts +++ b/packages/vchart/src/chart/histogram/histogram.ts @@ -5,6 +5,7 @@ import { BaseHistogramChart } from './base/base'; import { Factory } from '../../core/factory'; import type { IHistogramChartSpec } from './interface'; import { HistogramChartSpecTransformer } from './histogram-transformer'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class HistogramChart extends BaseHistogramChart { static readonly type: string = ChartTypeEnum.histogram; @@ -16,6 +17,7 @@ export class HistogramChart } export const registerHistogramChart = () => { + registerDimensionHover(); registerBarSeries(); Factory.registerChart(HistogramChart.type, HistogramChart); }; diff --git a/packages/vchart/src/chart/interface/common.ts b/packages/vchart/src/chart/interface/common.ts index b28d7ebc96..3bf1a7c6da 100644 --- a/packages/vchart/src/chart/interface/common.ts +++ b/packages/vchart/src/chart/interface/common.ts @@ -4,6 +4,7 @@ import type { IModelOption, IModelSpecInfo } from '../../model/interface'; import type { IBoundsLike } from '@visactor/vutils'; import type { ISeriesSpecInfo } from '../../series/interface'; import type { IRegionSpecInfo } from '../../region/interface'; +import type { IPerformanceHook } from '../../typings'; export interface IChartOption extends Omit { @@ -25,6 +26,10 @@ export interface IChartOption * 是否关闭交互效果 */ disableTriggerEvent?: boolean; + /** + * 性能相关的钩子 + */ + performanceHook?: IPerformanceHook; } export interface IChartSpecTransformerOption extends Partial { diff --git a/packages/vchart/src/chart/line/line.ts b/packages/vchart/src/chart/line/line.ts index d676971402..de69e76311 100644 --- a/packages/vchart/src/chart/line/line.ts +++ b/packages/vchart/src/chart/line/line.ts @@ -7,6 +7,7 @@ import { LineChartSpecTransformer } from './line-transformer'; import { BaseChart } from '../base'; import { StackChartMixin } from '../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class LineChart extends BaseChart { static readonly type: string = ChartTypeEnum.line; @@ -20,6 +21,7 @@ export class LineChart extends BaseChart { mixin(LineChart, StackChartMixin); export const registerLineChart = () => { + registerDimensionHover(); registerLineSeries(); Factory.registerChart(LineChart.type, LineChart); }; diff --git a/packages/vchart/src/chart/mosaic/mosaic.ts b/packages/vchart/src/chart/mosaic/mosaic.ts index 49417741f3..8d95c980d9 100644 --- a/packages/vchart/src/chart/mosaic/mosaic.ts +++ b/packages/vchart/src/chart/mosaic/mosaic.ts @@ -11,6 +11,7 @@ import type { IStackCacheNode, IStackCacheRoot } from '../../util/data'; import { stackMosaic, stackMosaicTotal } from '../../util/data'; import { stackSplit } from '../../data/transforms/stack-split'; import { registerDataSetInstanceTransform } from '../../data/register'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class MosaicChart extends BaseChart { static readonly type: string = ChartTypeEnum.mosaic; @@ -49,6 +50,7 @@ export class MosaicChart extends } export const registerMosaicChart = () => { + registerDimensionHover(); registerMosaicSeries(); Factory.registerChart(MosaicChart.type, MosaicChart); }; diff --git a/packages/vchart/src/chart/progress/circular/circular.ts b/packages/vchart/src/chart/progress/circular/circular.ts index f69bfd7d71..be01d5c2f2 100644 --- a/packages/vchart/src/chart/progress/circular/circular.ts +++ b/packages/vchart/src/chart/progress/circular/circular.ts @@ -8,6 +8,7 @@ import type { AdaptiveSpec } from '../../../typings'; import { BaseChart } from '../../base'; import { StackChartMixin } from '../../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../../interaction/triggers/dimension-hover'; export class CircularProgressChart extends BaseChart< AdaptiveSpec @@ -23,6 +24,7 @@ export class CircularProgressChart { + registerDimensionHover(); registerCircularProgressSeries(); Factory.registerChart(CircularProgressChart.type, CircularProgressChart); }; diff --git a/packages/vchart/src/chart/progress/linear/linear.ts b/packages/vchart/src/chart/progress/linear/linear.ts index b89633deff..961c2fcd37 100644 --- a/packages/vchart/src/chart/progress/linear/linear.ts +++ b/packages/vchart/src/chart/progress/linear/linear.ts @@ -7,6 +7,7 @@ import { LinearProgressChartSpecTransformer } from './linear-progress-transforme import { BaseChart } from '../../base'; import { StackChartMixin } from '../../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../../interaction/triggers/dimension-hover'; export class LinearProgressChart extends BaseChart { static readonly type: string = ChartTypeEnum.linearProgress; @@ -20,6 +21,7 @@ export class LinearProgressChart { + registerDimensionHover(); registerLinearProgressSeries(); Factory.registerChart(LinearProgressChart.type, LinearProgressChart); }; diff --git a/packages/vchart/src/chart/radar/radar.ts b/packages/vchart/src/chart/radar/radar.ts index 1c38fc8fa2..3c5769fd05 100644 --- a/packages/vchart/src/chart/radar/radar.ts +++ b/packages/vchart/src/chart/radar/radar.ts @@ -7,6 +7,7 @@ import { RadarChartSpecTransformer } from './radar-transformer'; import { BaseChart } from '../base'; import { StackChartMixin } from '../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class RadarChart extends BaseChart { static readonly type: string = ChartTypeEnum.radar; @@ -21,6 +22,7 @@ export class RadarChart extends BaseC mixin(RadarChart, StackChartMixin); export const registerRadarChart = () => { + registerDimensionHover(); registerRadarSeries(); Factory.registerChart(RadarChart.type, RadarChart); }; diff --git a/packages/vchart/src/chart/range-area/range-area.ts b/packages/vchart/src/chart/range-area/range-area.ts index ab3e9add72..cac72388fd 100644 --- a/packages/vchart/src/chart/range-area/range-area.ts +++ b/packages/vchart/src/chart/range-area/range-area.ts @@ -5,6 +5,7 @@ import { Factory } from '../../core/factory'; import type { IRangeAreaChartSpec } from './interface'; import { RangeAreaChartSpecTransformer } from './range-area-transformer'; import { BaseChart } from '../base'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class RangeAreaChart extends BaseChart { static readonly type: string = ChartTypeEnum.rangeArea; @@ -16,6 +17,7 @@ export class RangeAreaChart } export const registerRangeAreaChart = () => { + registerDimensionHover(); registerRangeAreaSeries(); Factory.registerChart(RangeAreaChart.type, RangeAreaChart); }; diff --git a/packages/vchart/src/chart/range-column/3d/range-column-3d.ts b/packages/vchart/src/chart/range-column/3d/range-column-3d.ts index 7deec305aa..d0bd1dc684 100644 --- a/packages/vchart/src/chart/range-column/3d/range-column-3d.ts +++ b/packages/vchart/src/chart/range-column/3d/range-column-3d.ts @@ -6,6 +6,7 @@ import { registerRangeColumn3dSeries } from '../../../series/range-column/3d/ran import { RangeColumn3dChartSpecTransformer } from './range-column-3d-transformer'; import { BaseChart } from '../../base'; import { register3DPlugin } from '../../../plugin/other'; +import { registerDimensionHover } from '../../../interaction/triggers/dimension-hover'; export class RangeColumn3dChart extends BaseChart { static readonly type: string = ChartTypeEnum.rangeColumn3d; @@ -18,6 +19,7 @@ export class RangeColumn3dChart { + registerDimensionHover(); register3DPlugin(); registerRangeColumn3dSeries(); Factory.registerChart(RangeColumn3dChart.type, RangeColumn3dChart); diff --git a/packages/vchart/src/chart/range-column/range-column.ts b/packages/vchart/src/chart/range-column/range-column.ts index 0b7ad88a94..8274908513 100644 --- a/packages/vchart/src/chart/range-column/range-column.ts +++ b/packages/vchart/src/chart/range-column/range-column.ts @@ -5,6 +5,7 @@ import { Factory } from '../../core/factory'; import { registerRangeColumnSeries } from '../../series/range-column/range-column'; import { RangeColumnChartSpecTransformer } from './range-column-transformer'; import { BaseChart } from '../base'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class RangeColumnChart extends BaseChart { static readonly type: string = ChartTypeEnum.rangeColumn; @@ -16,6 +17,7 @@ export class RangeColumnChart { + registerDimensionHover(); registerRangeColumnSeries(); Factory.registerChart(RangeColumnChart.type, RangeColumnChart); }; diff --git a/packages/vchart/src/chart/rose/rose.ts b/packages/vchart/src/chart/rose/rose.ts index 0d346ee3dd..c4519a39cd 100644 --- a/packages/vchart/src/chart/rose/rose.ts +++ b/packages/vchart/src/chart/rose/rose.ts @@ -7,6 +7,7 @@ import { RoseChartSpecTransformer } from './rose-transformer'; import { BaseChart } from '../base'; import { StackChartMixin } from '../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class RoseChart extends BaseChart { static readonly type: string = ChartTypeEnum.rose; @@ -20,6 +21,7 @@ export class RoseChart extends BaseCh mixin(RoseChart, StackChartMixin); export const registerRoseChart = () => { + registerDimensionHover(); registerRoseSeries(); Factory.registerChart(RoseChart.type, RoseChart); }; diff --git a/packages/vchart/src/chart/sankey/sankey.ts b/packages/vchart/src/chart/sankey/sankey.ts index 80f8e3f5c0..a1de063707 100644 --- a/packages/vchart/src/chart/sankey/sankey.ts +++ b/packages/vchart/src/chart/sankey/sankey.ts @@ -7,10 +7,11 @@ import { Factory } from '../../core/factory'; import { SankeyChartSpecTransformer } from './sankey-transformer'; import type { Datum, MaybeArray } from '../../typings/common'; import type { ISeries } from '../../series/interface'; -import type { IMark } from '../../mark/interface/common'; +import type { IMark, IMarkGraphic } from '../../mark/interface/common'; import type { IRegionQuerier } from '../../typings/params'; import { isArray, isFunction } from '@visactor/vutils'; import { loadScrollbar } from '@visactor/vrender-components'; +import { getDatumOfGraphic } from '../../util'; export class SankeyChart extends BaseChart { static readonly type: string = ChartTypeEnum.sankey; @@ -22,37 +23,43 @@ export class SankeyChart extends protected _setStateInDatum( stateKey: string, - checkReverse: boolean, datum: MaybeArray | null, filter?: (series: ISeries, mark: IMark) => boolean, region?: IRegionQuerier ) { + const activeDatum = (isArray(datum) ? (datum as Datum[])[0] : datum) as Datum; + if (!activeDatum) { + this._interaction.clearByState(stateKey); + return; + } + let activeNodeOrLink: IMarkGraphic = null; + let activeMark: IMark = null; + let activeSeries: ISeries = null; // 桑基图暂时只支持单选 - const activeDatum = isArray(datum) ? datum[0] : datum; const keys = !activeDatum ? null : Object.keys(activeDatum); this.getRegionsInQuerier(region).forEach(r => { - if (!activeDatum) { - r.interaction.clearEventElement(stateKey, true); + if (activeNodeOrLink) { return; } - let hasPick = false; - r.getSeries().forEach(s => { - let activeNodeOrLink = null; + r.getSeries().forEach(s => { + if (activeNodeOrLink) { + return; + } s.getMarksWithoutRoot().forEach(m => { - if (m.type === 'text') { + if (m.type === 'text' || activeNodeOrLink) { return; } let pickElement = null; - const mark = m.getProduct(); - if (!mark) { + const graphics = m.getGraphics(); + if (!graphics || !graphics.length) { return; } if (!filter || (isFunction(filter) && filter(s, m))) { - pickElement = mark.elements.find((e: any) => + pickElement = graphics.find((e: any) => keys.every(k => { - let datum = e.getDatum()?.datum; + let datum = (getDatumOfGraphic(e) as Datum)?.datum; if (isArray(datum)) { // data of link @@ -65,23 +72,20 @@ export class SankeyChart extends ); } if (pickElement) { - hasPick = true; - r.interaction.startInteraction(stateKey, pickElement); - - if (mark.id().includes('node') || mark.id().includes('link')) { + if (m.getProductId().includes('node') || m.getProductId().includes('link')) { activeNodeOrLink = pickElement; + activeMark = m; + activeSeries = s; } } }); - - if (activeNodeOrLink) { - (s as any)._handleEmphasisElement?.({ item: activeNodeOrLink }); - } }); - if (checkReverse && hasPick) { - r.interaction.reverseEventElement(stateKey); - } }); + + if (activeNodeOrLink) { + this._interaction.updateStateOfGraphics(stateKey, [activeNodeOrLink]); + (activeSeries as any)._handleEmphasisElement?.({ item: activeNodeOrLink, mark: activeMark }); + } } } diff --git a/packages/vchart/src/chart/scatter/scatter.ts b/packages/vchart/src/chart/scatter/scatter.ts index 1ea982929f..85715a81a2 100644 --- a/packages/vchart/src/chart/scatter/scatter.ts +++ b/packages/vchart/src/chart/scatter/scatter.ts @@ -7,6 +7,7 @@ import { ScatterChartSpecTransformer } from './scatter-transformer'; import { BaseChart } from '../base'; import { StackChartMixin } from '../stack'; import { mixin } from '@visactor/vutils'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class ScatterChart extends BaseChart { static readonly type: string = ChartTypeEnum.scatter; @@ -21,6 +22,7 @@ export class ScatterChart exten mixin(ScatterChart, StackChartMixin); export const registerScatterChart = () => { + registerDimensionHover(); registerScatterSeries(); Factory.registerChart(ScatterChart.type, ScatterChart); }; diff --git a/packages/vchart/src/chart/waterfall/waterfall.ts b/packages/vchart/src/chart/waterfall/waterfall.ts index ca12448510..2cbf3fdb97 100644 --- a/packages/vchart/src/chart/waterfall/waterfall.ts +++ b/packages/vchart/src/chart/waterfall/waterfall.ts @@ -6,6 +6,7 @@ import { registerWaterfallSeries } from '../../series/waterfall/waterfall'; import { Factory } from '../../core/factory'; import type { AdaptiveSpec } from '../../typings'; import { WaterfallChartSpecTransformer } from './waterfall-transformer'; +import { registerDimensionHover } from '../../interaction/triggers/dimension-hover'; export class WaterfallChart extends BarChart< AdaptiveSpec @@ -20,6 +21,7 @@ export class WaterfallChart } export const registerWaterfallChart = () => { + registerDimensionHover(); registerWaterfallSeries(); Factory.registerChart(WaterfallChart.type, WaterfallChart); }; diff --git a/packages/vchart/src/compile/compiler.ts b/packages/vchart/src/compile/compiler.ts index 55905fafcb..e49fdf0696 100644 --- a/packages/vchart/src/compile/compiler.ts +++ b/packages/vchart/src/compile/compiler.ts @@ -1,11 +1,8 @@ import { ChartEvent, Event_Source_Type } from './../constant/event'; -import type { IElement, InteractionSpec } from '@visactor/vgrammar-core'; import type { CompilerListenerParameters, ICompiler, ICompilerModel, - IGrammarItem, - IProductMap, IRenderContainer, IRenderOption } from './interface'; @@ -16,17 +13,18 @@ import { isMobileLikeMode, isTrueBrowser } from '../util/env'; import { isString } from '../util/type'; import type { IBoundsLike } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports -import { isNil, isObject, isValid, Logger, LoggerLevel } from '@visactor/vutils'; +import { isObject, isValid, Logger, LoggerLevel } from '@visactor/vutils'; import type { EventSourceType } from '../event/interface'; import type { IChart } from '../chart/interface'; -import { createGroup, Stage, vglobal } from '@visactor/vrender-core'; +import { createGroup, Stage, vglobal, waitForAllSubLayers } from '@visactor/vrender-core'; import type { IColor, IEventTarget, IGroup, IStage } from '@visactor/vrender-core'; import type { IMorphConfig } from '../animation/spec'; -import type { IVChart } from '../core/interface'; -import type { IMark } from '../mark/interface'; +import type { IVChart, IVChartRenderOption } from '../core/interface'; +import { type IGraphicContext, type IMark, type IMarkGraphic } from '../mark/interface'; import { Factory } from '../core/factory'; import type { Gesture } from '@visactor/vrender-kits'; -import { hasCommited } from './util'; +import { findMarkGraphic, getDatumOfGraphic } from '../util/mark'; +import { diffMarks, findSimpleMarks, traverseGroupMark } from './util'; type EventListener = { type: string; @@ -34,6 +32,18 @@ type EventListener = { }; export class Compiler implements ICompiler { + /** + * 更新后缓存的mark + */ + private _cachedMarks: IMark[]; + + private _progressiveMarks?: IMark[]; + + /** + * 增量渲染相关的raf id + */ + private _progressiveRafId?: number; + protected _rootMarks: IMark[] = []; protected _stage: IStage; @@ -63,17 +73,6 @@ export class Compiler implements ICompiler { /** 布局阶段 */ private _layoutState?: LayoutState; - protected _model: ICompilerModel = { - [GrammarType.signal]: {}, - [GrammarType.data]: {}, - [GrammarType.mark]: {} - }; - - protected _interactions: (InteractionSpec & { seriesId?: number; regionId?: number })[]; - getModel() { - return this._model; - } - private _compileChart: IChart = null; constructor(container: IRenderContainer, option: IRenderOption) { @@ -111,12 +110,21 @@ export class Compiler implements ICompiler { this._option?.onError?.(...args); }); } - const { autoRefreshDpr, dpr, mode, gestureConfig, interactive, clickInterval, autoPreventDefault, options3d } = - this._option; - + const { + autoRefreshDpr, + dpr, + mode, + gestureConfig, + interactive, + clickInterval, + autoPreventDefault, + options3d, + background + } = this._option; this._stage = this._option.stage ?? (new Stage({ + background, width: this._width, height: this._height, container: this._container.dom ?? null, @@ -146,6 +154,17 @@ export class Compiler implements ICompiler { if (options3d?.enable) { this._stage.set3dOptions(options3d); } + // 之前vgrammar 设置了一些默认配置 + (this._stage as any).setTheme({ + symbol: { + shape: 'circle', + size: 8 + }, + text: { + fontSize: 14, + fill: '#000000' + } + }); const group = createGroup({ x: 0, @@ -200,6 +219,10 @@ export class Compiler implements ICompiler { this._layoutState = LayoutState.before; } + protected handleLayoutEnd = () => { + this._compileChart?.getEvent()?.emit(ChartEvent.afterMarkLayoutEnd, { chart: this._compileChart }); + }; + protected handleStageRender = () => { this._compileChart?.getEvent()?.emit(ChartEvent.afterRender, { chart: this._compileChart }); }; @@ -218,40 +241,7 @@ export class Compiler implements ICompiler { } } - protected compileInteractions() { - // this._view.removeAllInteractions(); - // if (this._interactions?.length) { - // const regionCombindInteractions = {}; - // this._interactions.forEach(interaction => { - // if (interaction.regionId) { - // const interactionId = `${interaction.regionId}-${interaction.type}-${interaction.id ?? ''}`; - // const spec = regionCombindInteractions[interactionId]; - // if (spec) { - // regionCombindInteractions[interactionId] = { - // ...spec, - // ...interaction, - // selector: [...spec.selector, ...(interaction as any).selector] - // }; - // } else { - // regionCombindInteractions[interactionId] = interaction; - // } - // } else { - // this._view.interaction(interaction.type, interaction); - // } - // }); - // Object.keys(regionCombindInteractions).forEach(key => { - // const interaction = this._view.interaction(regionCombindInteractions[key].type, regionCombindInteractions[key]); - // if (this._compileChart) { - // const region = this._compileChart.getRegionsInIds([regionCombindInteractions[key].regionId])[0]; - // if (region) { - // region.interaction.addVgrammarInteraction(interaction.getStartState(), interaction); - // } - // } - // }); - // } - } - - compile(ctx: { chart: IChart; vChart: IVChart }, option: any) { + compile(ctx: { chart: IChart; vChart: IVChart }, option?: IVChartRenderOption) { if (this._released) { return; } @@ -262,11 +252,14 @@ export class Compiler implements ICompiler { return; } + if (option?.actionSource !== 'render' && this._cachedMarks) { + this.reuseOrMorphing(option.morphConfig); + // 清除缓存 + this._cachedMarks = null; + } + chart.compile(); chart.afterCompile(); - this.updateDepend(); - - this.compileInteractions(); } protected clearNextRender() { if (this._nextRafId) { @@ -279,12 +272,11 @@ export class Compiler implements ICompiler { return false; } - clear(ctx: { chart: IChart; vChart: IVChart }, removeGraphicItems: boolean = false) { + clear(ctx: { chart: IChart; vChart: IVChart }) { const { chart } = ctx; this.clearNextRender(); chart.clear(); - this.releaseGrammar(removeGraphicItems); } renderNextTick(morphConfig?: IMorphConfig): void { @@ -301,30 +293,89 @@ export class Compiler implements ICompiler { }) as unknown as number; } + protected _commitedAll() { + return this._rootMarks.some(mark => { + return traverseGroupMark(mark, m => m.commit()); + }); + } + protected _hasCommitedMark() { - return this._rootMarks.some(hasCommited); + return this._rootMarks.some(mark => { + return traverseGroupMark(mark, m => m.isCommited(), null, null, true); + }); } - renderMarks(morphConfig?: IMorphConfig) { + private _handleAfterNextRender = () => { + if (this._stage && !this._option.disableDirtyBounds) { + this._stage.enableDirtyBounds(); + } + }; + + private _doRender() { + if (this._stage) { + // 全量渲染的时候先关闭dirty bounds 提升性能 + this._stage.disableDirtyBounds(); + this._stage.afterNextRender(this._handleAfterNextRender); + + this._stage.render(); + } + } + + renderMarks() { if (!this._hasCommitedMark()) { return; } + this.clearProgressive(); + // 更新所有的mark this._rootMarks.forEach(mark => { mark.render(); }); - this._layoutState = LayoutState.layouting; - this._compileChart?.onLayout(); - this._layoutState = LayoutState.reevaluate; + if (this._layoutState === LayoutState.before) { + // 需要更新布局 + this._layoutState = LayoutState.layouting; + this._compileChart?.onLayout(); + this._layoutState = LayoutState.reevaluate; - if (this._hasCommitedMark()) { - // 第二次更新所有的mark - this._rootMarks.forEach(mark => { - mark.render(); - }); + if (this._hasCommitedMark()) { + // 第二次更新所有的mark + this._rootMarks.forEach(mark => { + mark.render(); + }); + } + this.handleLayoutEnd(); } + + this.findProgressiveMarks(); + + this._doRender(); + this.doPreProgressive(); + } + + reuseOrMorphing(morphConfig: IMorphConfig = {}) { + const { reuse = true, morph = true, morphAll = false, animation = {}, enableExitAnimation = false } = morphConfig; + const newMarks = findSimpleMarks(this._rootMarks); + const { update, exit } = diffMarks(this._cachedMarks, newMarks, { morph, morphAll, reuse }); + + update.forEach(({ prev, next }) => { + const enableMarkMorphConfig = + prev.every(mark => mark.getMarkConfig().morph) && next.every(mark => mark.getMarkConfig().morph); + + if ((enableExitAnimation && morph) || morphAll) { + // todo morphing + } else if (reuse && prev.length === 1 && next.length === 1 && prev[0].type === next[0].type) { + next[0].reuse(prev[0]); + } + }); + + // todo 离场元素执行exit动画 + exit.forEach(({ prev }) => { + prev.forEach(m => { + m.removeProduct(); + }); + }); } render(morphConfig?: IMorphConfig) { @@ -345,9 +396,9 @@ export class Compiler implements ICompiler { this._rootGroup.setAttributes({ width: this._width, height: this._height }); } - this.renderMarks(morphConfig); + this.renderMarks(); if (this.clearNextRender()) { - this.renderMarks(morphConfig); + this.renderMarks(); } } @@ -379,10 +430,12 @@ export class Compiler implements ICompiler { if (hasChange) { this._stage.resize(width, height); - } - // todo resize - if (reRender) { - this.render({ morph: false }); + this._commitedAll(); + + // todo resize + if (reRender) { + this.render({ morph: false }); + } } } @@ -416,10 +469,19 @@ export class Compiler implements ICompiler { return; } if (source === Event_Source_Type.chart) { - const wrappedCallback = function (event: any, element: IElement | null) { - const context = element?.mark?.getContext() ?? {}; - const modelId = isValid(context.modelId) ? context.modelId : null; + const rootGroup = this.getRootGroup(); + const wrappedCallback = function (event: any) { + const graphic = event.target; + let markGraphic: IMarkGraphic = null; + + if (isValid(graphic.context)) { + markGraphic = graphic; + } else { + markGraphic = findMarkGraphic(rootGroup, graphic); + } + const context = (markGraphic?.context ?? {}) as Partial; const markId = isValid(context.markId) ? context.markId : null; + const modelId = isValid(context.modelId) ? context.modelId : null; const modelUserId = isValid(context.modelUserId) ? context.modelUserId : null; const markUserId = isValid(context.markUserId) ? context.markUserId : null; @@ -427,8 +489,8 @@ export class Compiler implements ICompiler { event, type, source, - item: element, - datum: element?.getDatum?.() || null, + item: markGraphic, + datum: getDatumOfGraphic(markGraphic), // 是否要区分图元 markId, modelId, markUserId, @@ -525,7 +587,7 @@ export class Compiler implements ICompiler { this.releaseEvent(); this._option = this._container = null as any; // vgrammar release - this._releaseModel(); + this.releaseGrammar(true); if (this._stage !== this._option?.stage) { // don't release the stage created by outside @@ -543,24 +605,36 @@ export class Compiler implements ICompiler { * @param removeGraphicItems 是否删除场景元素,在同步渲染,并且无动画时,必须设置为true,否则有绘图残留 */ releaseGrammar(removeGraphicItems: boolean = false) { - this._releaseModel(); + // this._releaseModel(); if (removeGraphicItems) { - // this._view?.removeAllGraphicItems(); + // 彻底删除图形 + this._rootMarks.forEach(g => { + traverseGroupMark( + g, + m => { + m.removeProduct(); + }, + null, + true + ); + }); + } else { + this._cachedMarks = findSimpleMarks(this._rootMarks); } - // this._view?.removeAllGrammars(); + this._rootMarks = []; } - protected _releaseModel() { - // 释放model - Object.keys(this._model).forEach(type => { - Object.values((this._model as any)[type] as IProductMap).forEach(grammarItemMap => { - Object.values(grammarItemMap).forEach((item: IGrammarItem) => { - item.removeProduct(true); // 保留 vgrammar 语法元素,下面一起清空 - }); - }); - (this._model as any)[type] = {}; - }); - } + // protected _releaseModel() { + // // 释放model + // Object.keys(this._model).forEach(type => { + // Object.values((this._model as any)[type] as IProductMap).forEach(grammarItemMap => { + // Object.values(grammarItemMap).forEach((item: IGrammarItem) => { + // item.removeProduct(true); // 保留 vgrammar 语法元素,下面一起清空 + // }); + // }); + // (this._model as any)[type] = {}; + // }); + // } addRootMark(mark: IMark) { if (!this._rootMarks.includes(mark)) { @@ -572,80 +646,92 @@ export class Compiler implements ICompiler { return this._rootMarks; } - /** 添加语法元素 */ - addGrammarItem(grammarItem: IGrammarItem) { - if (isNil(grammarItem)) { - return; - } - const id = grammarItem.getProductId(); - const type = grammarItem.grammarType; - if (isNil(this._model[type][id])) { - this._model[type][id] = {}; + removeRootMark(mark: IMark) { + const index = this._rootMarks.findIndex(m => m === mark); + + if (index >= 0) { + this._rootMarks.splice(index, 1); + + return true; } - this._model[type][id][grammarItem.id] = grammarItem; + return false; } - /** 删除语法元素 */ - removeGrammarItem(grammarItem: IGrammarItem, reserveVGrammarModel?: boolean) { - if (isNil(grammarItem)) { - return; - } - const id = grammarItem.getProductId(); - const type = grammarItem.grammarType; - const map = this._model[type][id]; - if (isValid(map)) { - delete map[grammarItem.id]; - if (Object.keys(map).length === 0) { - delete this._model[type][id]; - } - } - if (!reserveVGrammarModel) { - // todo this._view?.removeGrammar(product); + private _getGlobalThis() { + return isTrueBrowser(this._option.mode) ? globalThis : this.getStage()?.window; + } + + private _combineIncrementalLayers() { + if (this._stage) { + waitForAllSubLayers(this._stage).then(() => { + // stage might be null in current tick + if (this._stage) { + this._stage.defaultLayer.combineSubLayer(); + } + }); } } - addInteraction(interaction: InteractionSpec & { seriesId?: number; regionId?: number }) { - if (!this._interactions) { - this._interactions = []; + private findProgressiveMarks() { + const marks: IMark[] = []; + + this._rootMarks.forEach(mark => { + traverseGroupMark(mark, m => { + if (m.isProgressive()) { + marks.push(m); + } + }); + }); + + if (!marks.length) { + this._progressiveMarks = null; + return null; } - this._interactions.push(interaction); + this._progressiveMarks = marks; + + this._combineIncrementalLayers(); + + return marks; } - removeInteraction(seriesId: number) { - if (!this._interactions) { - return; + private doPreProgressive() { + if (this._progressiveMarks && this._progressiveMarks.some(mark => mark.isDoingProgressive())) { + const raf = vglobal.getRequestAnimationFrame(); + this._progressiveRafId = raf(this.handleProgressiveFrame); + } else if (this._progressiveMarks && this._progressiveMarks.every(mark => mark.canAnimateAfterProgressive())) { + // todo this.animate.animate(); + } else if (this._progressiveMarks) { + this._progressiveMarks = null; } - - this._interactions = this._interactions.filter(entry => entry.seriesId !== seriesId); } - /** 更新语法元素间的依赖关系,返回是否全部成功更新 */ - updateDepend(): boolean { - // 全局更新依赖 - // Object.values(this._model).forEach(productMap => { - // Object.values(productMap).forEach(grammarItemMap => { - // const grammarItems = Object.values(grammarItemMap) as IGrammarItem[]; + /** 监听frame事件,更新增量元素的mark */ + private handleProgressiveFrame = () => { + if (this._progressiveMarks.length) { + this._progressiveMarks.forEach(mark => { + if (mark.isDoingProgressive()) { + mark.renderProgressive(); + } + }); + } - // // 获取编译产物的依赖项 - // const dependList = grammarItems - // .reduce((depend, item) => { - // if (item.getDepend().length > 0) { - // return depend.concat(item.getDepend()); - // } - // return depend; - // }, [] as IGrammarItem[]) - // .filter(grammarItem => !!grammarItem) - // .map(grammarItem => grammarItem.getProductId()); + this.doPreProgressive(); + }; - // // 更新依赖 - // grammarItems[0].depend(dependList); - // }); - // }); - return true; - } + /** 清除 */ + private clearProgressive() { + if (this._progressiveRafId) { + const cancelRaf = vglobal.getCancelAnimationFrame(); + cancelRaf(this._progressiveRafId); + } - private _getGlobalThis() { - return isTrueBrowser(this._option.mode) ? globalThis : this.getStage()?.window; + if (this._progressiveMarks && this._progressiveMarks.length) { + this._progressiveMarks.forEach(entry => { + entry.clearProgressive(); + }); + + this._progressiveMarks = null; + } } } diff --git a/packages/vchart/src/compile/data/compilable-data.ts b/packages/vchart/src/compile/data/compilable-data.ts index fc3dbb10f9..a2a85d98fe 100644 --- a/packages/vchart/src/compile/data/compilable-data.ts +++ b/packages/vchart/src/compile/data/compilable-data.ts @@ -41,7 +41,14 @@ export class CompilableData extends GrammarItem implements ICompilableData { this._data = dataView; } + removeProduct() { + this._product = null; + this._prevProduct = null; + this._compiledProductId = null; + } + release() { + this.removeProduct(); super.release(); this._data = null; } diff --git a/packages/vchart/src/compile/grammar-item.ts b/packages/vchart/src/compile/grammar-item.ts index 733d37f377..e208c14dd0 100644 --- a/packages/vchart/src/compile/grammar-item.ts +++ b/packages/vchart/src/compile/grammar-item.ts @@ -1,5 +1,4 @@ import type { Maybe } from '../typings'; -import { isValid } from '@visactor/vutils'; import { createID } from '../util/id'; import { CompilableBase } from './compilable-base'; import type { @@ -9,7 +8,6 @@ import type { GrammarItemInitOption, ITransformSpec } from './interface'; -import type { IGroup } from '@visactor/vrender-core'; import { Factory } from '../core/factory'; /** 可以直接编译为一个 VGrammar 语法元素的类的统一基类 */ @@ -21,19 +19,9 @@ export abstract class GrammarItem extends CompilableBase implements IGrammarItem /** id */ readonly id: number = createID(); - protected _product: Maybe; + protected _product: Maybe; /** 获取编译产物 */ - getProduct() { - if (isValid(this._product)) { - return this._product; - } - const stage = this.getStage(); - const id = this.getProductId(); - if (isValid(id) && isValid(stage)) { - this._product = stage.getElementById(id) as unknown as IGroup; // 更新product - } - return this._product; - } + abstract getProduct(): Maybe; /** 已经编译完成的产物的 name */ protected _compiledProductId: string = null; @@ -44,60 +32,14 @@ export abstract class GrammarItem extends CompilableBase implements IGrammarItem return this._compiledProductId ?? this.generateProductId(); } - /** 该语法元素依赖于哪些语法元素 */ - protected _depend: IGrammarItem[] = []; - getDepend() { - return this._depend; - } - setDepend(...depend: IGrammarItem[]) { - this._depend = depend; - } - /** 编译入口(尽量不重写这个方法) */ compile(option?: GrammarItemCompileOption): void { this._compileProduct(option); - this._afterCompile(option); } /** 编译主过程 */ protected abstract _compileProduct(option?: GrammarItemCompileOption): void; - - /** 编译后的逻辑 */ - protected _afterCompile(option?: GrammarItemCompileOption) { - if (isValid(this._product)) { - this.getCompiler()?.addGrammarItem(this); - } - } - - /** 更新语法元素的依赖,返回是否全部成功更新 */ - updateDepend(): boolean { - if (isValid(this._product)) { - const depend = this.getDepend() - .map(item => item.getProduct()) - .filter(isValid); - // 更新依赖 - this._product.depend(depend); - return depend.length === this.getDepend().length; - } - return false; - } - - release() { - this.removeProduct(); - super.release(); - this._depend = []; - } - - /** - * 删除编译产物 - * @param reserveVGrammarModel 是否保留 view 中的语法元素 - */ - removeProduct(reserveVGrammarModel?: boolean) { - const compiler = this.getCompiler(); - compiler.removeGrammarItem(this, reserveVGrammarModel); - this._product = null; - this._compiledProductId = null; - } + abstract removeProduct(): void; protected _transform: ITransformSpec[]; setTransform(transform: ITransformSpec[]) { diff --git a/packages/vchart/src/compile/interface/compilable-item.ts b/packages/vchart/src/compile/interface/compilable-item.ts index 0a617fffea..f9d4eb8cf3 100644 --- a/packages/vchart/src/compile/interface/compilable-item.ts +++ b/packages/vchart/src/compile/interface/compilable-item.ts @@ -1,20 +1,18 @@ -import type { InteractionSpec } from '@visactor/vgrammar-core'; -import type { IPerformanceHook, StringOrNumber } from '../../typings'; +import type { StringOrNumber } from '../../typings'; import type { IColor, IGroup, IStage } from '@visactor/vrender-core'; import type { IChart } from '../../chart/interface/chart'; -import type { IVChart } from '../../core/interface'; +import type { IVChart, IVChartRenderOption } from '../../core/interface'; import type { IMorphConfig } from '../../animation/spec'; import type { IBoundsLike } from '@visactor/vutils'; import type { EventSourceType, EventType } from '../../event/interface'; -import type { IMark } from '../../mark/interface'; +import type { IMark, IMarkGraphic } from '../../mark/interface'; import type { LayoutState } from '../interface/compiler'; export type CompilerListenerParameters = { type: EventType; event: Event; source: EventSourceType; - // FIXME: 这里 item 应当为场景树的 Item 类型 - item: any | null; + item: IMarkGraphic | null; datum: any | null; markId: number | null; modelId: number | null; @@ -36,10 +34,9 @@ export type ICompilerModel = Record>; export interface ICompiler { isInited?: boolean; - getModel: () => ICompilerModel; getCanvas: () => HTMLCanvasElement | undefined; getStage: () => IStage | undefined; - compile: (ctx: { chart: IChart; vChart: IVChart }, option: any) => void; + compile: (ctx: { chart: IChart; vChart: IVChart }, option?: IVChartRenderOption) => void; clear: (ctx: { chart: IChart; vChart: IVChart }, removeGraphicItems?: boolean) => void; renderNextTick: (morphConfig?: IMorphConfig) => void; render: (morphConfig?: IMorphConfig) => void; @@ -60,13 +57,11 @@ export interface ICompiler { ) => void; release: () => void; releaseGrammar: (removeGraphicItems: boolean) => void; - addGrammarItem: (grammarItem: IGrammarItem) => void; - removeGrammarItem: (grammarItem: IGrammarItem, reserveVGrammarModel?: boolean) => void; - addInteraction: (interaction: InteractionSpec & { seriesId?: number; regionId?: number }) => void; - removeInteraction: (seriesId: number) => void; - updateDepend: (items?: IGrammarItem[]) => boolean; + // addGrammarItem: (grammarItem: IGrammarItem) => void; + // removeGrammarItem: (grammarItem: IGrammarItem, reserveVGrammarModel?: boolean) => void; addRootMark: (mark: IMark) => any; + removeRootMark: (mark: IMark) => any; getRootMarks: () => IMark[]; updateLayoutTag: () => void; @@ -100,13 +95,10 @@ export interface ICompilable { export interface ICompilableInitOption { /** 编译对象 应当由外部提供 */ getCompiler: () => ICompiler; - /** 性能测试钩子 */ - performanceHook?: IPerformanceHook; } export enum GrammarType { data = 'data', - signal = 'signal', mark = 'mark' } @@ -124,13 +116,7 @@ export interface IGrammarItem extends ICompilable { /** 获取语法元素名称 */ getProductId: () => string; /** 删除已编译的语法元素 */ - removeProduct: (reserveVGrammarModel?: boolean) => void; - /** 获取该语法元素依赖的语法元素 */ - getDepend: () => IGrammarItem[]; - /** 设置该语法元素依赖的语法元素 */ - setDepend: (...depend: IGrammarItem[]) => void; - /** 更新语法元素的依赖,返回是否全部成功更新 */ - updateDepend: () => boolean; + removeProduct: () => void; // transform setTransform: (transform: ITransformSpec[]) => void; } diff --git a/packages/vchart/src/compile/interface/compiler.ts b/packages/vchart/src/compile/interface/compiler.ts index 407cb06706..3b8091aa3b 100644 --- a/packages/vchart/src/compile/interface/compiler.ts +++ b/packages/vchart/src/compile/interface/compiler.ts @@ -1,6 +1,5 @@ -import type { Hooks } from '@visactor/vgrammar-core'; import type { IColor, IStageParams, IStage, ILayer, IOption3D } from '@visactor/vrender-core'; -import type { RenderMode } from '../../typings/spec/common'; +import type { IPerformanceHook, RenderMode } from '../../typings/spec/common'; import type { IBoundsLike } from '@visactor/vutils'; import type { StringOrNumber } from '../../typings'; @@ -196,7 +195,7 @@ export interface IRenderOption { * @deprecated * 请使用 hooks 代替 */ - performanceHook?: Hooks; + performanceHook?: IPerformanceHook; /** * 3d配置 diff --git a/packages/vchart/src/compile/mark/compilable-mark.ts b/packages/vchart/src/compile/mark/compilable-mark.ts index 9f3d80792b..de20f940fc 100644 --- a/packages/vchart/src/compile/mark/compilable-mark.ts +++ b/packages/vchart/src/compile/mark/compilable-mark.ts @@ -3,7 +3,7 @@ import { GrammarItem } from '../grammar-item'; import type { Maybe, StringOrNumber } from '../../typings'; import { isNil, isValid } from '@visactor/vutils'; import { LayoutZIndex } from '../../constant/layout'; -import type { IMarkStateStyle, MarkType } from '../../mark/interface'; +import { MarkTypeEnum, type IMarkStateStyle, type MarkType } from '../../mark/interface'; import type { IModel } from '../../model/interface'; import { MarkStateManager } from './mark-state-manager'; import type { @@ -177,7 +177,9 @@ export abstract class CompilableMark extends GrammarItem implements ICompilableM } protected declare _product: Maybe; - declare getProduct: () => Maybe; + getProduct() { + return this._product; + } /** 初始化 mark data */ protected initMarkData(option: ICompilableInitOption) { @@ -191,6 +193,8 @@ export abstract class CompilableMark extends GrammarItem implements ICompilableM } protected _compileProduct(option?: IMarkCompileOption) { + this.commit(); + this._context = option?.context; const product = this.getProduct(); // 处理 visible 为 false 的情况 if (!this.getVisible()) { @@ -199,23 +203,25 @@ export abstract class CompilableMark extends GrammarItem implements ICompilableM } return; } else if (isValid(product)) { - return; // 每个mark只执行一次编译 + if (option.group && product.parent !== option.group) { + option.group.appendChild(product); + } + } else { + const compiler = this.getCompiler(); + if (!compiler.isInited) { + return; + } + this._initProduct(option?.group); } - const compiler = this.getCompiler(); - if (!compiler.isInited) { - return; - } - this._initProduct(option?.group); if (isNil(this._product)) { return; } - this.commit(); this.compileData(); this.compileState(); this.compileEncode(); // todo this.compileAnimation(); - this.compileContext(option?.context); + // this.compileContext(option?.context); // this.compileTransform(); } @@ -230,16 +236,24 @@ export abstract class CompilableMark extends GrammarItem implements ICompilableM // 声明语法元素 const id = this.getProductId(); - this._product = createGroup({}); - - this._product.id = id; + this._product = createGroup( + this.type !== MarkTypeEnum.group + ? { + pickable: false, + zIndex: this._markConfig.zIndex, + overflow: this._markConfig.overflow + } + : {} + ); + // todo 为了和之前的版本兼容,这里暂时设置成name + this._product.name = id; // todo (group ?? this.getCompiler()?.getRootGroup()).appendChild(this._product); - if (this.name && this._product) { - this._product.name = this.name; - } + // if (this.name && this._product) { + // this._product.name = this.name; + // } this._compiledProductId = id; } @@ -404,8 +418,20 @@ export abstract class CompilableMark extends GrammarItem implements ICompilableM resumeAnimationByState(state?: string) { // return this.getProduct()?.animate?.resumeAnimationByState(state); } + + removeProduct() { + if (this._product && this._product.parent) { + this._product.parent.removeChild(this._product); + } + this._product = null; + this._compiledProductId = null; + } release() { - super.release(); + // if (this._product && this._product.parent) { + // this._product.parent.removeChild(this._product); + // } + this.state.release(); + super.release(); } } diff --git a/packages/vchart/src/compile/mark/interface.ts b/packages/vchart/src/compile/mark/interface.ts index 6df5b29f1f..d723fc2853 100644 --- a/packages/vchart/src/compile/mark/interface.ts +++ b/packages/vchart/src/compile/mark/interface.ts @@ -1,4 +1,4 @@ -import type { IMarkStateStyle, MarkType } from '../../mark/interface'; +import type { IMark, IMarkStateStyle, MarkType } from '../../mark/interface'; import type { IModel } from '../../model/interface'; import type { GrammarItemCompileOption, GrammarItemInitOption, IGrammarItem } from '../interface'; import type { DataView } from '@visactor/vdataset'; @@ -31,6 +31,10 @@ export interface IMarkConfig { */ // largeChunkMode?: 'sequential' | 'mod'; support3d?: boolean; + /** + * 象形图,给图形设置名称 + */ + graphicName?: string | ((g: IMarkConfig) => string); /** * enable global morphing animation of the mark */ @@ -175,7 +179,17 @@ export interface IStateInfo { /** 筛选 item */ items?: any[] | null | undefined; /** 筛选函数 */ - filter?: ((datum: any, options: Record) => boolean) | null | undefined; + filter?: + | (( + datum: any, + options: { + mark?: IMark; + type?: string; + renderNode?: IGraphic; + } + ) => boolean) + | null + | undefined; cache?: { [key: string]: { [key: string]: boolean; @@ -232,7 +246,12 @@ export enum STATE_VALUE_ENUM { // todo: 2.0考虑优化 STATE_SANKEY_EMPHASIS = 'selected', - STATE_SANKEY_EMPHASIS_REVERSE = 'blur' + STATE_SANKEY_EMPHASIS_REVERSE = 'blur', + + STATE_HIGHLIGHT = 'highlight', + STATE_BLUR = 'blur', + + STATE_ACTIVE = 'active' } export enum STATE_VALUE_ENUM_REVERSE { diff --git a/packages/vchart/src/compile/mark/mark-state-manager.ts b/packages/vchart/src/compile/mark/mark-state-manager.ts index b3627633ba..38891fca65 100644 --- a/packages/vchart/src/compile/mark/mark-state-manager.ts +++ b/packages/vchart/src/compile/mark/mark-state-manager.ts @@ -1,20 +1,18 @@ import { isContinuous } from '@visactor/vscale'; import { isArray, isObject, isValid } from '@visactor/vutils'; import { PREFIX } from '../../constant/base'; -import type { MarkTypeEnum } from '../../mark/interface'; +import type { IMark, IMarkGraphic, MarkTypeEnum } from '../../mark/interface'; import { isMultiDatumMark } from '../../mark/utils/common'; import type { Datum, StringOrNumber } from '../../typings'; -import type { CompilableMark } from './compilable-mark'; import type { IMarkStateManager, IStateInfo, StateValue } from './interface'; import { stateInDefaultEnum } from './util'; import type { ICompilableInitOption } from '../interface/compilable-item'; -import type { IGraphic } from '@visactor/vrender-core'; import { StateManager } from '../state-manager'; /** mark state 管理器 */ export class MarkStateManager extends StateManager implements IMarkStateManager { /** 相关 mark */ - protected _mark: CompilableMark; + protected _mark: IMark; // state info:state 种类信息 private _stateInfoList: IStateInfo[] = []; @@ -22,7 +20,7 @@ export class MarkStateManager extends StateManager implements IMarkStateManager return this._stateInfoList; } - constructor(options: ICompilableInitOption, mark: CompilableMark) { + constructor(options: ICompilableInitOption, mark: IMark) { super(options); this._mark = mark; } @@ -129,7 +127,7 @@ export class MarkStateManager extends StateManager implements IMarkStateManager return !this._mark || isMultiDatumMark(this._mark.type as MarkTypeEnum); } - checkOneState(renderNode: IGraphic, datum: Datum[], state: IStateInfo): 'in' | 'out' | 'skip' { + checkOneState(renderNode: IMarkGraphic, datum: Datum[], state: IStateInfo): 'in' | 'out' | 'skip' { let inState = false; let stateChecked = false; // 如果有 state.datum 那么判断是否与datum的所有值相等 @@ -168,7 +166,7 @@ export class MarkStateManager extends StateManager implements IMarkStateManager } // TODO:renderNode - checkState(renderNode: IGraphic, datum: Datum[]): StateValue[] { + checkState(renderNode: IMarkGraphic, datum: Datum[]): StateValue[] { // 由于存在多个 stateManager,需要额外返回 state 的优先级 // 交互状态不要删除,并且交互状态优先级统一为10 const result: [StateValue, number][] = ((renderNode.context.states as string[]) ?? []) diff --git a/packages/vchart/src/compile/util.ts b/packages/vchart/src/compile/util.ts index 02a812ec04..7734ebb8c6 100644 --- a/packages/vchart/src/compile/util.ts +++ b/packages/vchart/src/compile/util.ts @@ -1,8 +1,12 @@ +import { isNil } from '@visactor/vutils'; +import type { IMorphConfig } from '../animation/spec'; +import type { IGroupMark, IMark, IMarkRaw } from '../mark/interface'; import { MarkTypeEnum } from '../mark/interface'; +import { groupData } from '../mark/utils/common'; +import type { DiffResult } from '../typings/common'; import type { RenderMode } from '../typings/spec'; // eslint-disable-next-line no-duplicate-imports import { RenderModeEnum } from '../typings/spec/common'; -import type { ICompilableMark } from './mark'; // TODO: feishu => lark export function toRenderMode(mode: RenderMode): any { @@ -28,14 +32,180 @@ export function toRenderMode(mode: RenderMode): any { return 'browser'; } -export function hasCommited(group: ICompilableMark) { - if (group.type === MarkTypeEnum.group) { - if (group.isCommited()) { - return true; +export function traverseGroupMark( + group: IMark, + apply: (mark: IMark) => T, + filter?: (mark: IMark) => boolean, + leafFirst?: boolean, + stop?: boolean +): T | undefined { + const traverse = (mark: IMark): T | undefined => { + if (!leafFirst) { + if (mark && (!filter || filter(mark))) { + const res = apply.call(null, mark); + + if (stop && res) { + return res; + } + } } - return group.getMarks().some(hasCommited); - } + if (mark.type === MarkTypeEnum.group) { + const children: IMark[] = (mark as IGroupMark).getMarks(); + + if (children) { + for (let i = 0; i < children.length; i++) { + const res = traverse(children[i]); + + if (res && stop) { + return res; + } + } + } + } + + if (leafFirst) { + if (mark && (!filter || filter(mark))) { + const res = apply.call(null, mark); + + if (res && stop) { + return res; + } + } + } + + return undefined; + }; + + return traverse(group); +} + +export function findSimpleMarks(groups: IMark[]): IMark[] { + const marks: IMark[] = []; + + groups.forEach(g => { + traverseGroupMark(g, (m: IMark) => { + if (m.type !== MarkTypeEnum.group && m.type !== MarkTypeEnum.component) { + marks.push(m); + } + }); + }); + + return marks; +} + +export function diffUpdateByGroup( + prev: IMark[], + next: IMark[], + prevKey: (datum: IMark) => string, + nextKey: (datum: IMark) => string +) { + const prevGroup = groupData(prev, datum => prevKey(datum as IMark)); + const nextGroup = groupData(next, datum => nextKey(datum as IMark)); + + let prevAfterDiff = prev; + let nextAfterDiff = next; + const update: { prev: IMark[]; next: IMark[] }[] = []; + nextGroup.keys.forEach(key => { + if (!isNil(key)) { + const prevKeyData = prevGroup.data.get(key); + const nextKeyData = nextGroup.data.get(key); + if (prevKeyData && nextKeyData) { + update.push({ prev: prevKeyData, next: nextKeyData }); + prevAfterDiff = prevAfterDiff.filter(datum => !prevKeyData.includes(datum)); + nextAfterDiff = nextAfterDiff.filter(datum => !nextKeyData.includes(datum)); + } + } + }); + return { + prev: prevAfterDiff, + next: nextAfterDiff, + update + }; +} + +export function diffMarks( + prevMarks: IMark[], + nextMarks: IMark[], + runningConfig: IMorphConfig +): DiffResult { + const diffResult: DiffResult = { + enter: [], + exit: [], + update: [] + }; + + let prevDiffMarks: IMark[] = []; + let nextDiffMarks: IMark[] = []; + + // filter out marks & specs which will not morph + prevMarks.forEach(mark => { + if ((runningConfig.morph && mark.getMarkConfig().morph) || runningConfig.morphAll || runningConfig.reuse) { + prevDiffMarks.push(mark); + } else { + diffResult.exit.push({ prev: [mark] }); + } + }); + nextMarks.forEach(mark => { + if ((runningConfig.morph && mark.getMarkConfig().morph) || runningConfig.morphAll || runningConfig.reuse) { + nextDiffMarks.push(mark); + } else { + diffResult.enter.push({ next: [mark] }); + } + }); + + // 1. match by custom key + const keyDiffResult = diffUpdateByGroup( + prevDiffMarks, + nextDiffMarks, + mark => mark.getMarkConfig().morphKey, + mark => mark.getMarkConfig().morphKey + ); + prevDiffMarks = keyDiffResult.prev; + nextDiffMarks = keyDiffResult.next; + diffResult.update = diffResult.update.concat(keyDiffResult.update); + + // 2. match by name + const nameDiffResult = diffUpdateByGroup( + prevDiffMarks, + nextDiffMarks, + mark => `${mark.getUserId()}`, + mark => `${mark.getUserId()}` + ); + prevDiffMarks = nameDiffResult.prev; + nextDiffMarks = nameDiffResult.next; + diffResult.update = diffResult.update.concat(nameDiffResult.update); + + // 3. match by index + + // FIXME: mark index cannot be get before executing, index is decided by remove/order for now + const prevParentGroup = groupData(prevDiffMarks, mark => mark.group?.id?.()); + const nextParentGroup = groupData(nextDiffMarks, mark => mark.group?.id?.()); + + Object.keys(nextParentGroup).forEach(groupName => { + const prevChildren = prevParentGroup.data.get(groupName); + const nextChildren = nextParentGroup.data.get(groupName); + if (prevChildren && nextChildren) { + for (let i = 0; i < Math.max(prevChildren.length, nextChildren.length); i += 1) { + const prevChild = prevChildren[i]; + const nextChild = nextChildren[i]; + if (prevChild && nextChild) { + diffResult.update.push({ prev: [prevChild], next: [nextChild] }); + } else if (prevChild) { + diffResult.exit.push({ prev: [prevChild] }); + } else if (nextChild) { + diffResult.enter.push({ next: [nextChild] }); + } + } + + prevDiffMarks = prevDiffMarks.filter(mark => !prevChildren.includes(mark)); + nextDiffMarks = nextDiffMarks.filter(mark => !nextChildren.includes(mark)); + } + }); + + // 4. handle unmatched marks + prevDiffMarks.forEach(mark => diffResult.exit.push({ prev: [mark] })); + nextDiffMarks.forEach(mark => diffResult.enter.push({ next: [mark] })); - return group.isCommited(); + return diffResult; } diff --git a/packages/vchart/src/component/axis/base-axis.ts b/packages/vchart/src/component/axis/base-axis.ts index 933df5a624..e83afa7239 100644 --- a/packages/vchart/src/component/axis/base-axis.ts +++ b/packages/vchart/src/component/axis/base-axis.ts @@ -135,10 +135,7 @@ export abstract class AxisComponent { - return (datum: Datum, index: number) => { - const style = spec.grid.style(datum.datum?.rawValue, index, datum.datum); - return transformToGraphic(mergeSpec({}, this._theme.grid?.style, style)); - }; + ? (datum: Datum, index: number) => { + const style = spec.grid.style(datum.datum?.rawValue, index, datum.datum); + return transformToGraphic(mergeSpec({}, this._theme.grid?.style, style)); } : transformToGraphic(spec.grid.style), subGrid: @@ -722,7 +717,7 @@ export abstract class AxisComponent extends BaseComponent i // 用brushName做分组管理的原因是: 如果有多个brush, 某个图元A brush内, 但在B brush外, 该图元state会被B误变成out of brush。 但其实该图元只有在A brush外才能被判断out of brush // 用dict做存储因为方便查找和删减对应图元 - protected _inBrushElementsMap: { [brushName: string]: { [elementKey: string]: IElement } } = {}; - protected _outOfBrushElementsMap: { [elementKey: string]: IElement } = {}; - protected _linkedInBrushElementsMap: { [brushName: string]: { [elementKey: string]: IElement } } = {}; - protected _linkedOutOfBrushElementsMap: { [elementKey: string]: IElement } = {}; + protected _inBrushElementsMap: { [brushName: string]: { [elementKey: string]: IMarkGraphic } } = {}; + protected _outOfBrushElementsMap: { [elementKey: string]: IMarkGraphic } = {}; + protected _linkedInBrushElementsMap: { [brushName: string]: { [elementKey: string]: IMarkGraphic } } = {}; + protected _linkedOutOfBrushElementsMap: { [elementKey: string]: IMarkGraphic } = {}; private _needInitOutState: boolean = true; private _cacheInteractiveRangeAttrs: BrushInteractiveRangeAttr[] = []; @@ -119,24 +118,24 @@ export class Brush extends BaseComponent i this._initNeedOperatedItem(); } - protected _extendDataInBrush(elementsMap: { [brushName: string]: { [elementKey: string]: IElement } }) { + protected _extendDataInBrush(elementsMap: { [brushName: string]: { [elementKey: string]: IMarkGraphic } }) { const data = []; for (const brushName in elementsMap) { for (const elementKey in elementsMap[brushName]) { data.push({ - ...elementsMap[brushName][elementKey]?.data?.[0] + ...elementsMap[brushName][elementKey].context?.data?.[0] }); } } return data; } - protected _extendDatumOutOfBrush(elementsMap: { [elementKey: string]: IElement }) { + protected _extendDatumOutOfBrush(elementsMap: { [elementKey: string]: IMarkGraphic }) { const data = []; for (const elementKey in elementsMap) { // 图例筛选后, elementKey未更新, 导致data可能为null // FIXME: brush透出的map维护逻辑有待优化 - data.push(elementsMap[elementKey].data?.[0]); + data.push(elementsMap[elementKey].context?.data?.[0]); } return data; } @@ -310,14 +309,13 @@ export class Brush extends BaseComponent i private _reconfigItem(operateMask: IPolygon, region: IRegion) { // 遍历图元, 更新状态 this._itemMap[region.id].forEach((mark: IMark) => { - const grammarMark = mark.getProduct(); - if (!grammarMark || !grammarMark.elements || !grammarMark.elements.length) { + const graphics = mark.getGraphics(); + + if (!graphics || !graphics.length) { return; } - const elements = grammarMark.elements; - elements.forEach((el: IElement) => { - const graphicItem = el.getGraphicItem(); - const elementKey = mark.id + '_' + el.key; + graphics.forEach((graphicItem: IMarkGraphic) => { + const elementKey = mark.id + '_' + graphicItem.context.key; // 判断逻辑: // 应该被置为inBrush状态的图元: // before: 在out brush elment map, 即不在任何brush中 @@ -327,19 +325,19 @@ export class Brush extends BaseComponent i // before: 在当前brush 的 in brush element map中, 即在当前brush中 // now: 不在当前brush中 if (this._outOfBrushElementsMap?.[elementKey] && this._isBrushContainItem(operateMask, graphicItem)) { - el.addState(IN_BRUSH_STATE); + graphicItem.addState(IN_BRUSH_STATE, true); if (!this._inBrushElementsMap[operateMask?.name]) { this._inBrushElementsMap[operateMask?.name] = {}; } - this._inBrushElementsMap[operateMask?.name][elementKey] = el; + this._inBrushElementsMap[operateMask?.name][elementKey] = graphicItem; delete this._outOfBrushElementsMap[elementKey]; } else if ( this._inBrushElementsMap?.[operateMask?.name]?.[elementKey] && !this._isBrushContainItem(operateMask, graphicItem) ) { - el.removeState(IN_BRUSH_STATE); - el.addState(OUT_BRUSH_STATE); - this._outOfBrushElementsMap[elementKey] = el; + graphicItem.removeState(IN_BRUSH_STATE); + graphicItem.addState(OUT_BRUSH_STATE, true); + this._outOfBrushElementsMap[elementKey] = graphicItem; delete this._inBrushElementsMap[operateMask.name][elementKey]; } graphicItem.setAttribute('pickable', !this._needDisablePickable); @@ -358,14 +356,14 @@ export class Brush extends BaseComponent i const regionOffsetY = sRegionLayoutPos.y - regionLayoutPos.y; this._linkedItemMap[s.id].forEach((mark: IMark) => { - const grammarMark = mark.getProduct(); - if (!grammarMark || !grammarMark.elements || !grammarMark.elements.length) { + const graphics = mark.getGraphics(); + if (!graphics || !graphics.length) { return; } - const elements = grammarMark.elements; - elements.forEach((el: IElement) => { - const graphicItem = el.getGraphicItem(); - const elementKey = mark.id + '_' + el.key; + graphics.forEach((graphicItem: IMarkGraphic) => { + const { key } = graphicItem.context; + + const elementKey = mark.id + '_' + key; // 判断逻辑: // 应该被置为inBrush状态的图元: // before: 在out brush elment map, 即不在任何brush中 @@ -378,19 +376,19 @@ export class Brush extends BaseComponent i this._linkedOutOfBrushElementsMap?.[elementKey] && this._isBrushContainItem(operateMask, graphicItem, { dx: regionOffsetX, dy: regionOffsetY }) ) { - el.addState(IN_BRUSH_STATE); + graphicItem.addState(IN_BRUSH_STATE, true); if (!this._linkedInBrushElementsMap[operateMask?.name]) { this._linkedInBrushElementsMap[operateMask?.name] = {}; } - this._linkedInBrushElementsMap[operateMask?.name][elementKey] = el; + this._linkedInBrushElementsMap[operateMask?.name][elementKey] = graphicItem; delete this._linkedOutOfBrushElementsMap[elementKey]; } else if ( this._linkedInBrushElementsMap?.[operateMask?.name]?.[elementKey] && !this._isBrushContainItem(operateMask, graphicItem, { dx: regionOffsetX, dy: regionOffsetY }) ) { - el.removeState(IN_BRUSH_STATE); - el.addState(OUT_BRUSH_STATE); - this._linkedOutOfBrushElementsMap[elementKey] = el; + graphicItem.removeState(IN_BRUSH_STATE); + graphicItem.addState(OUT_BRUSH_STATE, true); + this._linkedOutOfBrushElementsMap[elementKey] = graphicItem; } graphicItem.setAttribute('pickable', !this._needDisablePickable); }); @@ -399,7 +397,7 @@ export class Brush extends BaseComponent i }); } - private _isBrushContainItem(brushMask: IPolygon, item: IGraphic, linkedOffset?: { dx: number; dy: number }) { + private _isBrushContainItem(brushMask: IPolygon, item: IMarkGraphic, linkedOffset?: { dx: number; dy: number }) { if (!brushMask?.globalTransMatrix || !brushMask?.attribute?.points) { return false; } @@ -650,18 +648,17 @@ export class Brush extends BaseComponent i this._option.getAllSeries().forEach((s: ISeries) => { s.getMarksWithoutRoot().forEach((mark: IMark) => { - const grammarMark = mark.getProduct(); - if (!grammarMark || !grammarMark.elements || !grammarMark.elements.length) { + const graphics = mark.getGraphics(); + if (!graphics || !graphics.length) { return; } - const elements = grammarMark.elements; - elements.forEach((el: IElement) => { - const elementKey = mark.id + '_' + el.key; - el.removeState(IN_BRUSH_STATE); - el.removeState(OUT_BRUSH_STATE); - el.addState(stateName); - this._outOfBrushElementsMap[elementKey] = el; - this._linkedOutOfBrushElementsMap[elementKey] = el; + graphics.forEach((g: IMarkGraphic) => { + const elementKey = mark.id + '_' + g.context.key; + g.removeState(IN_BRUSH_STATE); + g.removeState(OUT_BRUSH_STATE); + g.addState(stateName, true); + this._outOfBrushElementsMap[elementKey] = g; + this._linkedOutOfBrushElementsMap[elementKey] = g; }); }); }); diff --git a/packages/vchart/src/component/custom-mark/custom-mark.ts b/packages/vchart/src/component/custom-mark/custom-mark.ts index b4dfa9d185..34d1e77ec2 100644 --- a/packages/vchart/src/component/custom-mark/custom-mark.ts +++ b/packages/vchart/src/component/custom-mark/custom-mark.ts @@ -116,9 +116,6 @@ export class CustomMark extends BaseComponent> { mark.setAnimationConfig(config); } - if (options.depend && options.depend.length) { - mark.setDepend(...options.depend); - } if (isNil(parentMark)) { this._marks.addMark(mark); } else if (parentMark) { @@ -196,7 +193,7 @@ export class CustomMark extends BaseComponent> { const product = mark.getProduct(); if (product) { - bounds.union(product.getBounds()); + bounds.union(product.AABBBounds); } }); diff --git a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts index 33100e7ae0..4432864e97 100644 --- a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts +++ b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts @@ -777,7 +777,7 @@ export abstract class DataFilterBaseComponent { (g).getMarks().forEach(m => { - this.initMarkStyleWithSpec(m, this._spec[m.name]); + this.initMarkStyleWithSpec(m, (this._spec as any)[m.name]); }); }); } diff --git a/packages/vchart/src/component/indicator/indicator.ts b/packages/vchart/src/component/indicator/indicator.ts index 7200a87cea..8924569155 100644 --- a/packages/vchart/src/component/indicator/indicator.ts +++ b/packages/vchart/src/component/indicator/indicator.ts @@ -5,7 +5,7 @@ import { ComponentTypeEnum } from '../interface/type'; import { BaseComponent } from '../base/base-component'; import type { IRegion } from '../../region/interface'; import type { IIndicator, IIndicatorItemSpec, IIndicatorSpec } from './interface'; -import type { Maybe } from '../../typings'; +import type { Datum, Maybe } from '../../typings'; import { mergeSpec } from '@visactor/vutils-extension'; import { transformIndicatorStyle } from '../../util/style'; import { getActualNumValue } from '../../util/space'; @@ -23,6 +23,8 @@ import { Factory } from '../../core/factory'; // eslint-disable-next-line no-duplicate-imports import type { IRichTextCharacter } from '@visactor/vrender-core'; import { getSpecInfo } from '../util'; +import { getDatumOfGraphic } from '../../util/mark'; +import type { IMarkGraphic } from '../../mark/interface'; export class Indicator extends BaseComponent implements IIndicator { static type = ComponentTypeEnum.indicator; @@ -88,35 +90,33 @@ export class Indicator extends BaseComponent implem return; } - // const view = this.getCompiler()?.getVGrammarView(); - - // if (!view) { - // return; - // } - - // if (this._spec.trigger === 'hover') { - // view.addEventListener('element-highlight:start', (params: any) => { - // if (this.isRelativeModel(params.options.regionId)) { - // this.updateDatum(params.elements[0].getDatum()); - // } - // }); - // view.addEventListener('element-highlight:reset', (params: any) => { - // if (this.isRelativeModel(params.options.regionId)) { - // this.updateDatum(null); - // } - // }); - // } else { - // view.addEventListener('element-select:start', (params: any) => { - // if (this.isRelativeModel(params.options.regionId)) { - // this.updateDatum(params.elements[0].getDatum()); - // } - // }); - // view.addEventListener('element-select:reset', (params: any) => { - // if (this.isRelativeModel(params.options.regionId)) { - // this.updateDatum(null); - // } - // }); - // } + if (this._spec.trigger === 'hover') { + this.event.on('element-highlight:start', (params: any) => { + const g = params.graphics[0]; + + if (this.isRelativeModel(g)) { + this.updateDatum(getDatumOfGraphic(g) as Datum[]); + } + }); + this.event.on('element-highlight:reset', (params: any) => { + if (this._activeDatum) { + this.updateDatum(null); + } + }); + } else { + this.event.on('element-select:start', (params: any) => { + const g = params.graphics[0]; + + if (this.isRelativeModel(g)) { + this.updateDatum(getDatumOfGraphic(g) as Datum[]); + } + }); + this.event.on('element-select:reset', (params: any) => { + if (this._activeDatum) { + this.updateDatum(null); + } + }); + } } updateDatum(datum: any) { @@ -262,8 +262,8 @@ export class Indicator extends BaseComponent implem return Math.min(width / 2, height / 2); } - private isRelativeModel(regionId: number) { - return this._regions.some(region => region.id === regionId); + private isRelativeModel(g: IMarkGraphic) { + return this._regions.some(region => !!region.getSeriesInId(g.context.modelId)); } protected _getNeedClearVRenderComponents(): IGraphic[] { diff --git a/packages/vchart/src/component/label/base-label.ts b/packages/vchart/src/component/label/base-label.ts index 16e0892048..9dca42d98d 100644 --- a/packages/vchart/src/component/label/base-label.ts +++ b/packages/vchart/src/component/label/base-label.ts @@ -6,15 +6,16 @@ import type { IRegion } from '../../region/interface'; import type { IModelRenderOption } from '../../model/interface'; import { LayoutZIndex } from '../../constant/layout'; import type { ILabelSpec } from './interface'; -import type { IHoverSpec, ISelectSpec } from '../../interaction/interface'; -import type { LooseFunction } from '@visactor/vutils'; -import { array, isEqual } from '@visactor/vutils'; -import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { IHoverSpec, ISelectSpec } from '../../interaction/interface/spec'; +import { array, isEqual, isNil, isPlainObject } from '@visactor/vutils'; +import type { IGraphic } from '@visactor/vrender-core'; import type { IComponentMark } from '../../mark/interface/mark'; import type { ICompilableMark } from '../../compile/mark/interface'; import { DiffState } from '../../mark/interface/enum'; import type { Datum } from '../../typings/common'; import { MarkTypeEnum } from '../../mark/interface/type'; +import type { IMark } from '../../mark/interface/common'; +import { getActualColor } from '../../core'; export abstract class BaseLabelComponent extends BaseComponent { static type = ComponentTypeEnum.label; @@ -74,12 +75,16 @@ export abstract class BaseLabelComponent extends BaseComponent { if (markType === MarkTypeEnum.symbol || markType === MarkTypeEnum.cell) { return 'symbol'; } + + return ''; } - _setTransformOfComponent(labelComponent: IComponentMark, baseMark: ICompilableMark | ICompilableMark[]) { + _setTransformOfComponent(labelComponent: IComponentMark, baseMark: IMark | IMark[]) { labelComponent.setAttributeTransform(({ labelStyle, size, itemEncoder }) => { - const labelStyleRes = labelStyle(); - const dataLabels = array(baseMark).map(mark => { + const regionSize = size(); + const defaultFill = getActualColor({ type: 'palette', key: 'secondaryFontColor' }, this.getColorScheme()); + const dataLabels = array(baseMark).map((mark: IMark, labelIndex: number) => { + const labelStyleRes = labelStyle(labelIndex); const labelData: any[] = []; const graphics = (mark as any).getGraphics(); @@ -87,20 +92,44 @@ export abstract class BaseLabelComponent extends BaseComponent { return; } - graphics.forEach((g: IGraphic) => { - const { data, diffState } = g.context; + if (labelStyleRes.data && labelStyleRes.data.length) { + labelStyleRes.data.forEach((d: Datum, index: number) => { + if (graphics[index]) { + const formattedDatum = itemEncoder(d, { labelIndex }); + + if (isNil(formattedDatum.fill)) { + formattedDatum.fill = defaultFill; + } + + labelData.push(formattedDatum); + } + }); + } else { + graphics.forEach((g: IGraphic) => { + const { data, diffState } = g.context; + + if (diffState !== DiffState.exit) { + data.forEach((datum: Datum, {}) => { + const formattedDatum = itemEncoder(datum, { labelIndex }); - if (diffState !== DiffState.exit) { - data.forEach((datum: Datum) => { - labelData.push({ - data: datum, - ...itemEncoder(datum) + if (isNil(formattedDatum.fill)) { + formattedDatum.fill = defaultFill; + } + + labelData.push(formattedDatum); }); - }); - } - }); + } + }); + } + + if (isPlainObject(labelStyleRes.overlap) && isNil(labelStyleRes.overlap.size)) { + labelStyleRes.overlap.size = { + ...regionSize + }; + } return { + smartInvert: false, // 之前在vgrammar 中设置的默认值 baseMarkGroupName: mark.getProductId(), getBaseMarks: () => graphics, ...labelStyleRes, @@ -111,7 +140,7 @@ export abstract class BaseLabelComponent extends BaseComponent { return { dataLabels, - size: size() + size: regionSize }; }); } diff --git a/packages/vchart/src/component/label/interface.ts b/packages/vchart/src/component/label/interface.ts index b138779753..de7628b89f 100644 --- a/packages/vchart/src/component/label/interface.ts +++ b/packages/vchart/src/component/label/interface.ts @@ -1,13 +1,12 @@ import type { BaseLabelAttrs } from '@visactor/vrender-components'; import type { ConvertToMarkStyleSpec, Datum, IComposedTextMarkSpec, IFormatMethod, ITextMarkSpec } from '../../typings'; import type { IComponentSpec } from '../base/interface'; -import type { ILabelMark } from '../../mark/interface'; +import type { ILabelMark, IMark } from '../../mark/interface'; import type { ISeries } from '../../series/interface'; -import type { ICompilableMark } from '../../compile/mark/interface'; import type { IRegion } from '../../region/interface'; export interface ILabelInfo { - baseMark: ICompilableMark; + baseMark: IMark; labelMark: ILabelMark; series: ISeries; labelSpec: TransformedLabelSpec; diff --git a/packages/vchart/src/component/label/label.ts b/packages/vchart/src/component/label/label.ts index 5edf9cecaf..174a915991 100644 --- a/packages/vchart/src/component/label/label.ts +++ b/packages/vchart/src/component/label/label.ts @@ -4,10 +4,11 @@ import { ComponentTypeEnum } from '../interface/type'; import type { IRegion } from '../../region/interface'; import type { IModelInitOption, IModelSpecInfo } from '../../model/interface'; import { STACK_FIELD_TOTAL_BOTTOM, STACK_FIELD_TOTAL_TOP } from '../../constant/data'; -import { ChartEvent, VGRAMMAR_HOOK_EVENT } from '../../constant/event'; +import { ChartEvent, HOOK_EVENT } from '../../constant/event'; import { AttributeLevel } from '../../constant/attribute'; import { LayoutZIndex } from '../../constant/layout'; -import { DiffState, type IComponentMark, type ILabelMark } from '../../mark/interface'; +import type { IMark } from '../../mark/interface'; +import { type IComponentMark, type ILabelMark } from '../../mark/interface'; import { MarkTypeEnum } from '../../mark/interface/type'; import { mergeSpec } from '@visactor/vutils-extension'; import { eachSeries } from '../../util/model'; @@ -30,7 +31,6 @@ import { LabelSpecTransformer } from './label-transformer'; import type { IGraphic, IGroup } from '@visactor/vrender-core'; import type { DataLabelAttrs } from '@visactor/vrender-components'; import { DataLabel } from '@visactor/vrender-components'; -import type { ICompilableMark } from '../../compile/mark'; export class Label extends BaseLabelComponent { static type = ComponentTypeEnum.label; @@ -95,29 +95,39 @@ export class Label extends BaseLabelComponent { reInit(spec?: T) { super.reInit(spec); + + if (this._labelInfoMap) { + this._labelInfoMap.forEach(labelInfos => { + labelInfos.forEach(({ labelMark }) => { + labelMark.release(); + }); + }); + this._labelInfoMap.clear(); + } this._labelInfoMap && this._labelInfoMap.clear(); this._initTextMark(); + this._initLabelComponent(); this._initTextMarkStyle(); } initEvent() { this.event.on(ChartEvent.dataZoomChange, () => { this._labelComponentMap.forEach((info, component) => { - const graphicItem = component.getProduct().getGroupGraphicItem(); + const graphicItem = component.getComponent(); if (graphicItem) { graphicItem.disableAnimation(); } }); - this.event.on(VGRAMMAR_HOOK_EVENT.AFTER_MARK_RENDER_END, enableAnimation); + this.event.on(HOOK_EVENT.AFTER_MARK_RENDER_END, enableAnimation); }); const enableAnimation = () => { this._labelComponentMap.forEach((info, component) => { - const graphicItem = component.getProduct().getGroupGraphicItem(); + const graphicItem = component.getComponent(); if (graphicItem) { graphicItem.enableAnimation(); } }); - this.event.off(VGRAMMAR_HOOK_EVENT.AFTER_MARK_RENDER_END, enableAnimation); + this.event.off(HOOK_EVENT.AFTER_MARK_RENDER_END, enableAnimation); }; } @@ -170,55 +180,96 @@ export class Label extends BaseLabelComponent { } protected _initLabelComponent() { + const removedComponents: Record = {}; + + this._labelComponentMap.forEach((labelInfo, comp) => { + removedComponents[comp.name] = comp; + }); + this._labelInfoMap.forEach((regionLabelInfo, region) => { if (this._layoutRule === 'region') { - const component = this._createMark( - { type: MarkTypeEnum.component, name: `${region.getGroupMark().name}-label-component` }, - { - componentType: 'label', - noSeparateStyle: true - }, - { - support3d: (this._spec as any).support3d - } - ); + let isNew = false; + const labelName = `${region.getGroupMark().name}-label-component`; + let component = removedComponents[labelName]; + + if (!component) { + isNew = true; + component = this._createMark( + { type: MarkTypeEnum.component, name: labelName }, + { + componentType: 'label', + noSeparateStyle: true + }, + { + support3d: (this._spec as any).support3d + } + ); + } if (component) { - component.setSkipBeforeLayouted(true); + if (isNew) { + component.setSkipBeforeLayouted(true); + this._marks.addMark(component); + + // 当任意主图元数据更新的时候,都需要触发label的更新 + regionLabelInfo.forEach(labelInfo => { + labelInfo.baseMark.getData()?.addRelatedMark(component); + }); + } if (regionLabelInfo[0] && isValid(regionLabelInfo[0].labelSpec.zIndex)) { component.setMarkConfig({ zIndex: regionLabelInfo[0].labelSpec.zIndex }); } - this._marks.addMark(component); this._labelComponentMap.set(component as IComponentMark, () => { return this._labelInfoMap.get(region); }); + removedComponents[labelName] = null; } } else { regionLabelInfo.forEach((labelInfo, i) => { - const component = this._createMark( - { type: MarkTypeEnum.component, name: `${labelInfo.labelMark.name}-component` }, - { - componentType: 'label', - noSeparateStyle: true - }, - { - support3d: labelInfo.baseMark.getMarkConfig().support3d - } - ); + let isNew = false; + const labelName = `${labelInfo.labelMark.name}-component`; + let component = removedComponents[labelName]; + if (!component) { + isNew = true; + component = this._createMark( + { type: MarkTypeEnum.component, name: labelName }, + { + componentType: 'label', + noSeparateStyle: true + }, + { + support3d: labelInfo.baseMark.getMarkConfig().support3d + } + ); + } if (component) { if (isValid(labelInfo.labelSpec.zIndex)) { component.setMarkConfig({ zIndex: labelInfo.labelSpec.zIndex }); } + if (isNew) { + component.setSkipBeforeLayouted(true); - component.setSkipBeforeLayouted(true); - this._marks.addMark(component); + this._marks.addMark(component); + // 当主图元数据更新的时候,都需要触发label的更新 + labelInfo.baseMark.getData()?.addRelatedMark(component); + } this._labelComponentMap.set(component as IComponentMark, () => { return this._labelInfoMap.get(region)[i]; }); + removedComponents[labelName] = null; } }); } }); + + Object.keys(removedComponents).forEach(name => { + const comp = removedComponents[name]; + + if (comp) { + comp.release(); // todo 是否要上报 + this._labelComponentMap.delete(comp); + } + }); } protected _initTextMarkStyle() { @@ -231,7 +282,7 @@ export class Label extends BaseLabelComponent { this._labelInfoMap.forEach(labelInfos => { labelInfos.forEach(info => { const { labelMark, labelSpec, series } = info; - this.initMarkStyleWithSpec(labelMark, labelSpec, undefined); + this.initMarkStyleWithSpec(labelMark, labelSpec); if (isFunction(labelSpec?.getStyleHandler)) { const styleHandler = labelSpec.getStyleHandler(series); styleHandler?.call(series, labelMark, labelSpec); @@ -248,9 +299,9 @@ export class Label extends BaseLabelComponent { this._labelComponentMap.forEach((labelInfoCb, labelComponent) => { const labelInfo = labelInfoCb(); if (isArray(labelInfo)) { - this._updateMultiLabelAttribute(labelInfo, labelComponent); + this._updateMultiLabelAttribute(labelInfo as ILabelInfo[], labelComponent); } else { - this._updateSingleLabelAttribute(labelInfo, labelComponent); + this._updateSingleLabelAttribute(labelInfo as ILabelInfo, labelComponent); } }); } @@ -270,30 +321,28 @@ export class Label extends BaseLabelComponent { protected _updateLabelComponentAttribute( labelComponent: IComponentMark, - baseMark: ICompilableMark | ICompilableMark[], + baseMark: IMark | IMark[], labelInfos: ILabelInfo[] ) { - const dependCmp = this._option.getComponentsByType('totalLabel'); + const totalLabels = this._option.getComponentsByType('totalLabel'); labelComponent.setMarkConfig({ interactive: false }); - const { labelInfo } = labelComponent.getContext(); - - const { labelSpec, labelMark, series } = labelInfo; - const rule = labelMark.getRule(); - const interactive = this._interactiveConfig(labelSpec); - /** arc label When setting the centerOffset of the spec, the label also needs to be offset accordingly, and the centerOffset is not in the labelSpec */ - const centerOffset = (this._spec as any)?.centerOffset ?? 0; - (labelComponent.stateStyle as any).normal = { - labelStyle: () => { + labelStyle: (labelIndex: number) => { + const labelInfo = labelInfos[labelIndex]; + const { labelSpec, labelMark, series } = labelInfo; + const rule = labelMark.getRule(); + const interactive = this._interactiveConfig(labelSpec); + /** arc label When setting the centerOffset of the spec, the label also needs to be offset accordingly, and the centerOffset is not in the labelSpec */ + const centerOffset = (this._spec as any)?.centerOffset ?? 0; let spec = mergeSpec( { textStyle: { pickable: labelSpec.interactive === true, ...labelSpec.style }, overlap: { - avoidMarks: dependCmp.map(cmp => cmp.getMarks()[0].getProductId()) + avoidMarks: totalLabels.map(cmp => cmp.getMarks()[0].getProductId()) } }, defaultLabelConfig(rule, labelInfo), @@ -327,11 +376,16 @@ export class Label extends BaseLabelComponent { return spec; }, size: () => { - return labelInfo.series.getRegion().getLayoutRect(); + return labelInfos[0].series.getRegion().getLayoutRect(); }, - itemEncoder: (datum: Datum) => { - return labelInfo && !labelMark.skipEncode - ? textAttribute(labelInfo, datum, labelSpec.formatMethod, labelSpec.formatter) + itemEncoder: (datum: Datum, { labelIndex }: { labelIndex: number }) => { + return labelInfos[labelIndex] && !labelInfos[labelIndex].labelMark.skipEncode + ? textAttribute( + labelInfos[labelIndex], + datum, + labelInfos[labelIndex].labelSpec.formatMethod, + labelInfos[labelIndex].labelSpec.formatter + ) : {}; } }; @@ -348,14 +402,14 @@ export class Label extends BaseLabelComponent { } else { group = labelInfo.series.getRegion().getGroupMark().getProduct() as IGroup; } - m.compile({ group, context: { model: this, labelInfo } }); + m.compile({ group }); }); } getVRenderComponents() { const labels: any[] = []; this._labelComponentMap.forEach((infoFunc, component) => { - const graphicItem = component.getProduct().getGroupGraphicItem(); + const graphicItem = component.getComponent(); if (graphicItem) { labels.push(graphicItem); } @@ -370,7 +424,7 @@ export class Label extends BaseLabelComponent { if (vrenderDataLabel) { const labelIndex = vrenderDataLabel.getChildren().indexOf(vrenderLabel as any); this._labelComponentMap.forEach((infoFunc, component) => { - const graphicItem = component.getProduct().getGroupGraphicItem(); + const graphicItem = component.getComponent(); if (graphicItem === vrenderDataLabel) { labelInfo = array(infoFunc())[labelIndex]; } @@ -386,5 +440,5 @@ export const registerLabel = () => { }); registerLabelMark(); registerComponentMark(); - Factory.registerComponent(Label.type, Label, true); + Factory.registerComponent(Label.type, Label, true, Infinity); // 标签逐渐最后创建 }; diff --git a/packages/vchart/src/component/label/total-label.ts b/packages/vchart/src/component/label/total-label.ts index 93c081a0bb..9ceb1648ea 100644 --- a/packages/vchart/src/component/label/total-label.ts +++ b/packages/vchart/src/component/label/total-label.ts @@ -100,7 +100,7 @@ export class TotalLabel extends BaseLabelComponent { protected _initLabelComponent() { const series = this._getSeries(); const component = this._createMark( - { type: MarkTypeEnum.component, name: `${series.name}-total-label-component` }, + { type: MarkTypeEnum.component, name: `${series.name ?? series.type}-total-label-component` }, { componentType: 'label', noSeparateStyle: true @@ -111,6 +111,7 @@ export class TotalLabel extends BaseLabelComponent { ); if (component) { this._marks.addMark(component); + series.getData().addRelatedMark(component); } } @@ -180,7 +181,7 @@ export class TotalLabel extends BaseLabelComponent { getVRenderComponents() { const labels: any[] = []; this.getMarks().forEach(m => { - const graphicItem = m.getProduct().getGroupGraphicItem(); + const graphicItem = (m as IComponentMark).getComponent(); if (graphicItem) { labels.push(graphicItem); } diff --git a/packages/vchart/src/component/label/util.ts b/packages/vchart/src/component/label/util.ts index 68336951ba..f14e511322 100644 --- a/packages/vchart/src/component/label/util.ts +++ b/packages/vchart/src/component/label/util.ts @@ -86,11 +86,12 @@ export function symbolLabel(labelInfo: ILabelInfo) { const position = uniformLabelPosition(labelSpec.position) ?? defaultPosition; // encode overlap config - let overlap; + let overlap: OverlapAttrs | boolean; if (labelSpec.overlap === false) { overlap = false; } else { overlap = { + // clampForce: true, strategy: (labelSpec.overlap as OverlapAttrs)?.strategy ?? symbolLabelOverlapStrategy(), avoidBaseMark: position !== 'center' }; @@ -103,6 +104,7 @@ export function lineDataLabel(labelInfo: ILabelInfo) { const result = symbolLabel(labelInfo); if (!isBoolean(result.overlap)) { result.overlap.avoidBaseMark = false; + result.overlap.clampForce = false; } return result; } @@ -211,6 +213,7 @@ export function pointLabel(labelInfo: ILabelInfo) { overlap = false; } else { overlap = { + clampForce: false, avoidBaseMark: false }; } @@ -363,7 +366,14 @@ export function LineLabel(labelInfo: ILabelInfo) { [DEFAULT_DATA_SERIES_FIELD]: series.getSeriesKeys()[0] } ]; - return { position: labelSpec.position ?? 'end', data }; + return { + position: labelSpec.position ?? 'end', + data, + overlap: { + avoidBaseMark: false, + clampForce: false + } + }; } export function sankeyLabel(labelInfo: ILabelInfo) { diff --git a/packages/vchart/src/component/map-label/component.ts b/packages/vchart/src/component/map-label/component.ts index 1a3ae6490c..d997584fac 100644 --- a/packages/vchart/src/component/map-label/component.ts +++ b/packages/vchart/src/component/map-label/component.ts @@ -25,6 +25,8 @@ import type { IModel, IModelSpecInfo } from '../../model/interface'; import { Factory } from '../../core/factory'; import { TransformLevel } from '../../data/initialize'; import { getSpecInfo } from '../util'; +import { getDatumOfGraphic } from '../../util'; +import type { IMarkGraphic } from '../../mark/interface'; export class MapLabelComponent extends BaseComponent { static type = ComponentTypeEnum.mapLabel; @@ -120,31 +122,27 @@ export class MapLabelComponent extends BaseComponent { return; } - const view = this.getCompiler()?.getVGrammarView(); - - if (!view) { - return; - } - if (trigger === 'hover') { - view.addEventListener('element-highlight:start', (params: any) => { - if (this._isRelativeSeries(params.options.seriesId)) { - this._updateDatum(params.elements[0].getDatum()); + this.event.on('element-highlight:start', (params: any) => { + const g = params.graphics[0]; + if (this._isRelativeSeries(g)) { + this._updateDatum(getDatumOfGraphic(g) as Datum[]); } }); - view.addEventListener('element-highlight:reset', (params: any) => { - if (this._isRelativeSeries(params.options.seriesId)) { + this.event.on('element-highlight:reset', (params: any) => { + if (this._activeDatum) { this._updateDatum(null); } }); } else if (trigger === 'click') { - view.addEventListener('element-select:start', (params: any) => { - if (this._isRelativeSeries(params.options.seriesId)) { - this._updateDatum(params.elements[0].getDatum()); + this.event.on('element-select:start', (params: any) => { + const g = params.graphics[0]; + if (this._isRelativeSeries(g)) { + this._updateDatum(getDatumOfGraphic(g) as Datum[]); } }); - view.addEventListener('elementSelectReset', (params: any) => { - if (this._isRelativeSeries(params.options.seriesId)) { + this.event.on('elementSelectReset', (params: any) => { + if (this._activeDatum) { this._updateDatum([]); } }); @@ -429,8 +427,8 @@ export class MapLabelComponent extends BaseComponent { return model?.id === id; } - private _isRelativeSeries(model: IModel) { - return model?.id === this._series.id; + private _isRelativeSeries(g: IMarkGraphic) { + return g.context?.modelId === this._series.id; } onRender(ctx: any): void { diff --git a/packages/vchart/src/component/tooltip/processor/mark-tooltip.ts b/packages/vchart/src/component/tooltip/processor/mark-tooltip.ts index f73fa0b458..1697392f52 100644 --- a/packages/vchart/src/component/tooltip/processor/mark-tooltip.ts +++ b/packages/vchart/src/component/tooltip/processor/mark-tooltip.ts @@ -7,6 +7,7 @@ import type { ISeries } from '../../../series/interface'; import { IContainPointMode } from '@visactor/vrender-core'; import type { IDimensionData } from '../../../event/events/dimension/interface'; import type { Label } from '../../label'; +import { getDatumOfGraphic } from '../../../util/mark'; export class MarkTooltipProcessor extends BaseTooltipProcessor { activeType: TooltipActiveType = 'mark'; @@ -39,7 +40,7 @@ export class MarkTooltipProcessor extends BaseTooltipProcessor { g && g.containsPoint(point.x, point.y, IContainPointMode.GLOBAL, g.stage.getPickerService()) ) { - tooltipData[0].datum.push(g.context.data); + tooltipData[0].datum.push(getDatumOfGraphic(g)); } }); }); diff --git a/packages/vchart/src/component/tooltip/utils/show-tooltip.ts b/packages/vchart/src/component/tooltip/utils/show-tooltip.ts index 359aebaecf..698dad65f3 100644 --- a/packages/vchart/src/component/tooltip/utils/show-tooltip.ts +++ b/packages/vchart/src/component/tooltip/utils/show-tooltip.ts @@ -1,8 +1,7 @@ -import { Direction, type Datum, type IPoint, type IShowTooltipOption, type TooltipActiveType } from '../../../typings'; +import { type Datum, type IPoint, type IShowTooltipOption, type TooltipActiveType } from '../../../typings'; import type { ICartesianSeries, IPolarSeries, ISeries } from '../../../series/interface'; import { SeriesTypeEnum } from '../../../series/interface/type'; import type { PieSeries } from '../../../series/pie/pie'; -import type { TooltipHandlerParams } from '../interface'; import { Event_Source_Type } from '../../../constant/event'; import { getElementAbsolutePosition, isArray, isValid, isNil } from '@visactor/vutils'; import type { IDimensionData, IDimensionInfo } from '../../../event/events/dimension/interface'; @@ -11,6 +10,7 @@ import type { IRegion } from '../../../region'; import type { Tooltip } from '../tooltip'; import type { IComponentOption } from '../../interface'; import { isDiscrete } from '@visactor/vscale'; +import type { TooltipHandlerParams } from '../interface/common'; const getDataArrayFromFieldArray = (fields: string[], datum?: Datum) => isValid(datum) ? fields.map(f => datum[f]) : undefined; @@ -145,8 +145,7 @@ export function showTooltip(datum: Datum, options: IShowTooltipOption, component x: markInfoList.reduce((sum, info) => sum + info.pos.x, 0) / markInfoList.length, y: markInfoList.reduce((sum, info) => sum + info.pos.y, 0) / markInfoList.length // 位置求平均 }), - item: undefined, - itemMap: new Map() + item: undefined }; component.processor.dimension.showTooltip(mockDimensionInfo, mockParams, false); @@ -189,8 +188,7 @@ export function showTooltip(datum: Datum, options: IShowTooltipOption, component model: info.series, source: Event_Source_Type.chart, event: getMockEvent(info.pos), - item: undefined, - itemMap: new Map() + item: undefined } as any; component.processor.mark.showTooltip( diff --git a/packages/vchart/src/constant/event.ts b/packages/vchart/src/constant/event.ts index 87cff18dae..1b20f30e70 100644 --- a/packages/vchart/src/constant/event.ts +++ b/packages/vchart/src/constant/event.ts @@ -1,4 +1,75 @@ -export { HOOK_EVENT as VGRAMMAR_HOOK_EVENT } from '@visactor/vgrammar-core'; +export enum HOOK_EVENT { + BEFORE_EVALUATE_DATA = 'beforeEvaluateData', + AFTER_EVALUATE_DATA = 'afterEvaluateData', + + BEFORE_EVALUATE_SCALE = 'beforeEvaluateScale', + AFTER_EVALUATE_SCALE = 'afterEvaluateScale', + + BEFORE_PARSE_VIEW = 'beforeParseView', + AFTER_PARSE_VIEW = 'afterParseView', + + BEFORE_TRANSFORM = 'beforeTransform', + AFTER_TRANSFORM = 'afterTransform', + + BEFORE_CREATE_VRENDER_STAGE = 'beforeCreateVRenderStage', + AFTER_CREATE_VRENDER_STAGE = 'afterCreateVRenderStage', + + BEFORE_CREATE_VRENDER_LAYER = 'beforeCreateVRenderLayer', + AFTER_CREATE_VRENDER_LAYER = 'afterCreateVRenderLayer', + + BEFORE_STAGE_RESIZE = 'beforeStageResize', + AFTER_STAGE_RESIZE = 'afterStageResize', + + BEFORE_VRENDER_DRAW = 'beforeVRenderDraw', + AFTER_VRENDER_DRAW = 'afterVRenderDraw', + + BEFORE_MARK_JOIN = 'beforeMarkJoin', + AFTER_MARK_JOIN = 'afterMarkJoin', + BEFORE_MARK_UPDATE = 'beforeMarkUpdate', + AFTER_MARK_UPDATE = 'afterMarkUpdate', + BEFORE_MARK_STATE = 'beforeMarkState', + AFTER_MARK_STATE = 'afterMarkState', + BEFORE_MARK_ENCODE = 'beforeMarkEncode', + AFTER_MARK_ENCODE = 'afterMarkEncode', + + BEFORE_DO_LAYOUT = 'beforeDoLayout', + AFTER_DO_LAYOUT = 'afterDoLayout', + + BEFORE_MARK_LAYOUT_END = 'beforeMarkLayoutEnd', + AFTER_MARK_LAYOUT_END = 'afterMarkLayoutEnd', + + BEFORE_DO_RENDER = 'beforeDoRender', + AFTER_DO_RENDER = 'afterDoRender', + + BEFORE_MARK_RENDER_END = 'beforeMarkRenderEnd', + AFTER_MARK_RENDER_END = 'afterMarkRenderEnd', + + BEFORE_CREATE_VRENDER_MARK = 'beforeCreateVRenderMark', + AFTER_CREATE_VRENDER_MARK = 'afterCreateVRenderMark', + + BEFORE_ADD_VRENDER_MARK = 'beforeAddVRenderMark', + AFTER_ADD_VRENDER_MARK = 'afterAddVRenderMark', + + AFTER_VRENDER_NEXT_RENDER = 'afterVRenderNextRender', + + BEFORE_ELEMENT_UPDATE_DATA = 'beforeElementUpdateData', + AFTER_ELEMENT_UPDATE_DATA = 'afterElementUpdateData', + + BEFORE_ELEMENT_STATE = 'beforeElementState', + AFTER_ELEMENT_STATE = 'afterElementState', + + BEFORE_ELEMENT_ENCODE = 'beforeElementEncode', + AFTER_ELEMENT_ENCODE = 'afterElementEncode', + + ANIMATION_START = 'animationStart', + ANIMATION_END = 'animationEnd', + + ELEMENT_ANIMATION_START = 'elementAnimationStart', + ELEMENT_ANIMATION_END = 'elementAnimationEnd', + + ALL_ANIMATION_START = 'allAnimationStart', + ALL_ANIMATION_END = 'allAnimationEnd' +} export const BASE_EVENTS = [ 'pointerdown', @@ -111,7 +182,8 @@ export enum ChartEvent { afterResize = 'afterResize', afterRender = 'afterRender', // layout - afterLayout = 'afterLayout' + afterLayout = 'afterLayout', + afterMarkLayoutEnd = 'afterMarkLayoutEnd' } export enum Event_Source_Type { diff --git a/packages/vchart/src/core/factory.ts b/packages/vchart/src/core/factory.ts index 70599f7419..bcbec18e3c 100644 --- a/packages/vchart/src/core/factory.ts +++ b/packages/vchart/src/core/factory.ts @@ -21,6 +21,7 @@ import type { IComponentPluginConstructor } from '../plugin/components/interface import type { IGraphic } from '@visactor/vrender-core'; import type { IStageEventPlugin, VRenderComponentOptions } from './interface'; import type { MarkAnimationSpec } from '../animation/interface'; +import type { IBaseTriggerOptions, ITriggerConstructor } from '../interaction/interface/trigger'; export class Factory { private static _charts: { [key: string]: IChartConstructor } = {}; @@ -29,6 +30,7 @@ export class Factory { [key: string]: { cmp: IComponentConstructor; alwaysCheck?: boolean; + createOrder: number; }; } = {}; private static _graphicComponents: Record IGraphic> = {}; @@ -64,8 +66,8 @@ export class Factory { static registerSeries(key: string, series: ISeriesConstructor) { Factory._series[key] = series; } - static registerComponent(key: string, cmp: IComponentConstructor, alwaysCheck?: boolean) { - Factory._components[key] = { cmp, alwaysCheck }; + static registerComponent(key: string, cmp: IComponentConstructor, alwaysCheck?: boolean, createOrder?: number) { + Factory._components[key] = { cmp, alwaysCheck, createOrder: createOrder ?? 0 }; } static registerGraphicComponent(key: string, creator: (attrs: any, options?: VRenderComponentOptions) => IGraphic) { @@ -275,4 +277,23 @@ export class Factory { static getStageEventPlugin = (type: string) => { return Factory._stageEventPlugins[type]; }; + + private static _interactionTriggers: Record = {}; + + static registerInteractionTrigger = (interactionType: string, interaction: ITriggerConstructor) => { + Factory._interactionTriggers[interactionType] = interaction; + }; + + static createInteractionTrigger(interactionType: string, options?: IBaseTriggerOptions) { + const Ctor = Factory._interactionTriggers[interactionType]; + if (!Ctor) { + return null; + } + + return new Ctor(options); + } + + static hasInteractionTrigger(interactionType: string) { + return !!Factory._interactionTriggers[interactionType]; + } } diff --git a/packages/vchart/src/core/interface.ts b/packages/vchart/src/core/interface.ts index e252ddcfdc..6a582f7938 100644 --- a/packages/vchart/src/core/interface.ts +++ b/packages/vchart/src/core/interface.ts @@ -15,7 +15,7 @@ import type { } from '../typings'; import type { IMorphConfig } from '../animation/spec'; import type { IBoundsLike } from '@visactor/vutils'; -import type { EventCallback, EventParams, EventQuery, EventType } from '../event/interface'; +import type { EventCallback, EventQuery, EventType, ExtendEventParam } from '../event/interface'; import type { IMark } from '../mark/interface'; import type { ISeries } from '../series/interface/series'; import type { ITheme } from '../theme/interface'; @@ -205,9 +205,9 @@ export interface IVChart { /** * 事件监听 */ - on: ((eType: EventType, handler: EventCallback) => void) & - ((eType: EventType, query: EventQuery, handler: EventCallback) => void); - off: (eType: EventType, handler?: EventCallback) => void; + on: ((eType: EventType, handler: EventCallback) => void) & + ((eType: EventType, query: EventQuery, handler: EventCallback) => void); + off: (eType: EventType, handler?: EventCallback) => void; /** * 更新或设置图元状态。 diff --git a/packages/vchart/src/core/vchart.ts b/packages/vchart/src/core/vchart.ts index caa98ab2b8..583a2f342d 100644 --- a/packages/vchart/src/core/vchart.ts +++ b/packages/vchart/src/core/vchart.ts @@ -1,7 +1,7 @@ import type { ISeries } from '../series/interface/series'; import { arrayParser } from '../data/parser/array'; import type { ILayoutConstructor, LayoutCallBack } from '../layout/interface'; -import type { IDataValues, IMarkStateSpec, IInitOption, IPerformanceHook } from '../typings/spec/common'; +import type { IDataValues, IMarkStateSpec, IInitOption } from '../typings/spec/common'; // eslint-disable-next-line no-duplicate-imports import { RenderModeEnum } from '../typings/spec/common'; import type { ISeriesConstructor } from '../series/interface'; @@ -19,10 +19,10 @@ import type { IComponentConstructor } from '../component/interface'; import { ComponentTypeEnum } from '../component/interface/type'; import type { EventCallback, - EventParams, EventParamsDefinition, EventQuery, EventType, + ExtendEventParam, IEvent, IEventDispatcher } from '../event/interface'; @@ -110,14 +110,13 @@ import { mergeTheme, preprocessTheme } from '../util/theme'; import { darkTheme, registerTheme } from '../theme/builtin'; import type { IChartPluginService } from '../plugin/chart/interface'; import { ChartPluginService } from '../plugin/chart/plugin-service'; -import { - registerElementHighlight as registerHoverInteraction, - registerElementSelect as registerSelectInteraction -} from '../interaction'; import type { IIndicator } from '../component/indicator'; import type { IGeoCoordinate } from '../component/geo'; import { getSVGSource } from '../series/pictogram/svg-source'; import { registerGesturePlugin } from '../plugin/other'; +import { registerElementHighlight } from '../interaction/triggers/element-highlight'; +import { registerElementSelect } from '../interaction/triggers/element-select'; +import { registerDimensionHover } from '../interaction/triggers/dimension-hover'; export class VChart implements IVChart { readonly id = createID(); @@ -414,15 +413,6 @@ export class VChart implements IVChart { // 桑基图默认记载滚动条组件 pluginList.push('scrollbar'); } - // hook增加图表实例参数 - const performanceHook = { ...(restOptions.performanceHook || {}) }; - (Object.keys(performanceHook) as (keyof IPerformanceHook)[]).forEach(hookKey => { - // @ts-ignore - restOptions.performanceHook[hookKey] = (...args) => { - // @ts-ignore - performanceHook[hookKey](...args, this as any); - }; - }); this._compiler = new Compiler( { @@ -652,15 +642,15 @@ export class VChart implements IVChart { return undefined; } if (isFunction(updateSpecResult)) { - updateSpecResult = updateSpecResult(); + updateSpecResult = (updateSpecResult as () => IUpdateSpecResult)(); } - if (updateSpecResult.reAnimate) { + if ((updateSpecResult as IUpdateSpecResult).reAnimate) { this.stopAnimation(); this._updateAnimateState(true); } - this._reCompile(updateSpecResult); + this._reCompile(updateSpecResult as IUpdateSpecResult); if (sync) { return this._renderSync(option); } @@ -699,7 +689,7 @@ export class VChart implements IVChart { if (updateResult.reMake) { // 如果不需要动画,那么释放item,避免元素残留 - this._compiler?.releaseGrammar(this._option?.animation === false || this._spec?.animation === false); + this._compiler?.releaseGrammar(); // chart 内部事件 模块自己必须删除 // 内部模块删除事件时,调用了event Dispatcher.release() 导致用户事件被一起删除 // 外部事件现在需要重新添加 @@ -712,15 +702,12 @@ export class VChart implements IVChart { if (updateResult.reCompile) { // recompile // 清除之前的所有 compile 内容 - this._compiler?.clear( - { chart: this._chart, vChart: this }, - this._option?.animation === false || this._spec?.animation === false - ); + this._compiler?.clear({ chart: this._chart, vChart: this }); // TODO: 释放事件? vgrammar 的 view 应该不需要释放,响应的stage也没有释放,所以事件可以不绑定 // 重新绑定事件 // TODO: 释放XX? // 重新compile - this._compiler?.compile({ chart: this._chart, vChart: this }, {}); + this._compiler?.compile({ chart: this._chart, vChart: this }); } if (updateResult.reSize) { const { width, height } = this.getCurrentSize(); @@ -760,9 +747,9 @@ export class VChart implements IVChart { } // compile - this._option.performanceHook?.beforeCompileToVGrammar?.(); - this._compiler.compile({ chart: this._chart, vChart: this }, { performanceHook: this._option.performanceHook }); - this._option.performanceHook?.afterCompileToVGrammar?.(); + this._option.performanceHook?.beforeCompileToVGrammar?.(this); + this._compiler.compile({ chart: this._chart, vChart: this }, option); + this._option.performanceHook?.afterCompileToVGrammar?.(this); return true; } @@ -1262,9 +1249,9 @@ export class VChart implements IVChart { // 插件生命周期 this._chartPluginApply('onBeforeResize', width, height); - this._option.performanceHook?.beforeResizeWithUpdate?.(); + this._option.performanceHook?.beforeResizeWithUpdate?.(this); this._chart.onResize(width, height, false); - this._option.performanceHook?.afterResizeWithUpdate?.(); + this._option.performanceHook?.afterResizeWithUpdate?.(this); return true; } @@ -1304,9 +1291,13 @@ export class VChart implements IVChart { } // 事件相关方法 - on(eType: EventType, handler: EventCallback): void; - on(eType: EventType, query: EventQuery, handler: EventCallback): void; - on(eType: EventType, query: EventQuery | EventCallback, handler?: EventCallback): void { + on(eType: EventType, handler: EventCallback): void; + on(eType: EventType, query: EventQuery, handler: EventCallback): void; + on( + eType: EventType, + query: EventQuery | EventCallback, + handler?: EventCallback + ): void { if (!this._userEvents) { // userEvents正常情况下有默认值,如果!userEvents,说明此时chart被release了,就可以终止流程 return; @@ -1314,11 +1305,14 @@ export class VChart implements IVChart { this._userEvents.push({ eType, query: typeof query === 'function' ? null : query, - handler: typeof query === 'function' ? query : handler + handler: + typeof query === 'function' + ? (query as EventCallback) + : (handler as EventCallback) }); this._event?.on(eType as any, query as any, handler as any); } - off(eType: EventType, handler?: EventCallback): void { + off(eType: EventType, handler?: EventCallback): void { if (!this._userEvents || this._userEvents.length === 0) { return; } @@ -1900,17 +1894,17 @@ export class VChart implements IVChart { /** 停止正在进行的所有动画 */ stopAnimation() { - this._compiler?.getVGrammarView()?.animate?.stop(); + // this._compiler?.getVGrammarView()?.animate?.stop(); } /** 暂停正在进行的所有动画 */ pauseAnimation() { - this._compiler?.getVGrammarView()?.animate?.pause(); + // this._compiler?.getVGrammarView()?.animate?.pause(); } /** 恢复暂停时正在进行的所有动画 */ resumeAnimation() { - this._compiler?.getVGrammarView()?.animate?.resume(); + // this._compiler?.getVGrammarView()?.animate?.resume(); } // TODO: 后续需要考虑滚动场景 @@ -2167,7 +2161,6 @@ export class VChart implements IVChart { mode: this._getMode(), modeParams: this._option.modeParams, getCompiler: () => this._compiler, - performanceHook: this._option.performanceHook, viewBox: this._viewBox, animation: this._option.animation, getTheme: () => this._currentTheme ?? {}, @@ -2190,8 +2183,9 @@ export const registerVChartCore = () => { // install essential vgrammar transform registerGesturePlugin(); // install default interaction - registerHoverInteraction(); - registerSelectInteraction(); + registerElementHighlight(); + registerElementSelect(); + registerDimensionHover(); // install default theme registerTheme(darkTheme.name, darkTheme); // set default logger level to Level.error diff --git a/packages/vchart/src/event/event-dispatcher.ts b/packages/vchart/src/event/event-dispatcher.ts index 124c071742..f9f603a9e8 100644 --- a/packages/vchart/src/event/event-dispatcher.ts +++ b/packages/vchart/src/event/event-dispatcher.ts @@ -1,6 +1,6 @@ import { Bubble } from './bubble'; import { isValid, debounce, throttle, get, isFunction } from '@visactor/vutils'; -import { BASE_EVENTS, Event_Bubble_Level, Event_Source_Type, VGRAMMAR_HOOK_EVENT } from '../constant/event'; +import { BASE_EVENTS, Event_Bubble_Level, Event_Source_Type, HOOK_EVENT } from '../constant/event'; import type { EventType, EventQuery, @@ -19,9 +19,11 @@ import type { VChart } from '../core/vchart'; import type { CompilerListenerParameters } from '../compile/interface'; import type { Compiler } from '../compile/compiler'; import type { StringOrNumber } from '../typings'; -import type { IElement } from '@visactor/vgrammar-core'; import type { IComponent } from '../component/interface'; -import { Factory as VGrammarFactory } from '@visactor/vgrammar-core'; +import { Factory } from '../core/factory'; +import type { IMarkGraphic } from '../mark/interface'; +import { IMark } from '../mark/interface'; +import { getDatumOfGraphic } from '../util'; const componentTypeMap: Record = { cartesianAxis: 'axis', @@ -269,11 +271,14 @@ export class EventDispatcher implements IEventDispatcher { params: EventParamsDefinition[Evt] ): EventParamsDefinition[Evt] { // 如果针对于 mark 做了筛选,则事件参数转为筛选器制定的父级 mark - if (filter.markName && params.mark && (params as BaseEventParams).itemMap) { - const markId = params.mark.getProductId(); - const item = (params as BaseEventParams).itemMap.get(markId); - const datum = item?.getDatum(); - return { ...params, item, datum }; + if (filter.markName && params.mark) { + const markGraphic = params.mark.getGraphics?.()?.[0]; + + return { + ...params, + item: markGraphic, + datum: getDatumOfGraphic(markGraphic) + }; } return { ...params }; } @@ -286,22 +291,6 @@ export class EventDispatcher implements IEventDispatcher { const model = (isValid(listenerParams.modelId) && chart?.getModelById(listenerParams.modelId)) || undefined; const mark = (isValid(listenerParams.markId) && chart?.getMarkById(listenerParams.markId)) || null; - // FIXME: 这里操作的应当是场景树结构,与 vgrammar 结构无关 - // 遍历取到所有父级的 mark 以支持子元素响应父元素事件 - const itemMap = new Map(); - let targetMark: any = listenerParams.item?.mark; - if (targetMark && isValid(targetMark.id())) { - itemMap.set(targetMark.id(), listenerParams.item); - } - while (targetMark?.elements) { - const id = targetMark.id(); - // 由于父级的 markName 可能重复,因此只取最近的父级 mark - if (isValid(id) && !itemMap.has(id)) { - itemMap.set(id, targetMark.elements[0]); - } - targetMark = targetMark.group; - } - const node = get(listenerParams.event, 'target'); let datum = listenerParams.datum; @@ -314,11 +303,10 @@ export class EventDispatcher implements IEventDispatcher { item: listenerParams.item, source: listenerParams.source, datum, - itemMap, chart, model, mark: mark ?? undefined, - node: get(listenerParams.event, 'target') + node: node }; this.dispatch(listenerParams.type, params); }; @@ -329,19 +317,19 @@ export class EventDispatcher implements IEventDispatcher { private _onDelegateInteractionEvent = (listenerParams: CompilerListenerParameters) => { const chart = this.globalInstance.getChart(); const event = listenerParams.event; - let items: IElement[] = null; + let graphics: IMarkGraphic[] = null; - if ((event as any).elements) { - items = (event as any).elements; + if ((event as any).graphics) { + graphics = (event as any).graphics; } const params: InteractionEventParam = { event: listenerParams.event, chart, - items, + graphics, datums: - items && - items.map(item => { - return item.getDatum(); + graphics && + graphics.map(g => { + return getDatumOfGraphic(g); }) }; this.dispatch(listenerParams.type, params); @@ -467,7 +455,7 @@ export class EventDispatcher implements IEventDispatcher { } private _isValidEvent(eType: string) { - return BASE_EVENTS.includes(eType) || (Object.values(VGRAMMAR_HOOK_EVENT) as string[]).includes(eType); + return BASE_EVENTS.includes(eType) || (Object.values(HOOK_EVENT) as string[]).includes(eType); } private _isInteractionEvent(eType: string) { @@ -476,7 +464,7 @@ export class EventDispatcher implements IEventDispatcher { return ( eType && ((interactionType = eType.split(':')[0]), interactionType) && - VGrammarFactory.hasInteraction(interactionType) + Factory.hasInteractionTrigger(interactionType) ); } } diff --git a/packages/vchart/src/event/interface.ts b/packages/vchart/src/event/interface.ts index 9fd9bd18ab..622c773cd6 100644 --- a/packages/vchart/src/event/interface.ts +++ b/packages/vchart/src/event/interface.ts @@ -1,11 +1,10 @@ import type { IGraphic } from '@visactor/vrender-core'; -import type { IElement } from '@visactor/vgrammar-core'; import type { IChart } from '../chart/interface'; import type { IModel } from '../model/interface'; -import type { IMark, MarkType } from '../mark/interface'; +import type { IMark, IMarkGraphic, MarkType } from '../mark/interface'; import type { DimensionEventParams } from './events/dimension/interface'; import type { Datum, IPoint, StringOrNumber } from '../typings'; -import type { ChartEvent, Event_Bubble_Level, Event_Source_Type, VGRAMMAR_HOOK_EVENT } from '../constant/event'; +import type { ChartEvent, Event_Bubble_Level, Event_Source_Type, HOOK_EVENT } from '../constant/event'; import type { SeriesType } from '../series/interface'; import type { TooltipEventParams } from '../component/tooltip/interface/event'; import type { ILayoutItem } from '../layout/interface'; @@ -59,7 +58,7 @@ export type EventType = | 'pinchend' | 'swipe' | keyof typeof ChartEvent - | keyof typeof VGRAMMAR_HOOK_EVENT + | keyof typeof HOOK_EVENT | string; export type EventBubbleLevel = keyof typeof Event_Bubble_Level; @@ -180,10 +179,9 @@ export type BaseEventParams = EventParams & { * 事件对象 */ event: SuperEvent; - item: IElement; + item: IMarkGraphic; datum: Datum; source: EventSourceType; - itemMap: Map; }; export type EventCallback = (params: Params) => boolean | void; @@ -214,10 +212,9 @@ export type EventHandler = { export type ExtendEventParam = EventParams & { event?: Event; - item?: any; + item?: IMarkGraphic; datum?: Datum; source?: EventSourceType; - itemMap?: Map; }; export type LayoutEventParam = { @@ -237,7 +234,7 @@ export type ZoomEventParam = ExtendEventParam & { }; export type InteractionEventParam = { - items?: IElement[]; + graphics?: IMarkGraphic[]; datums?: Datum[]; } & Partial; diff --git a/packages/vchart/src/index-harmony-simple.ts b/packages/vchart/src/index-harmony-simple.ts index a819f5a96b..5acc98c3b1 100644 --- a/packages/vchart/src/index-harmony-simple.ts +++ b/packages/vchart/src/index-harmony-simple.ts @@ -56,9 +56,10 @@ import { registerMapLabel } from './component/map-label'; import { registerGridLayout } from './layout/grid-layout/grid-layout'; import { registerPoptip } from './component/poptip'; import { registerCanvasTooltipHandler } from './plugin/components/tooltip-handler'; -// import { registerElementHighlight, registerElementSelect } from '@visactor/vgrammar-core'; import { DefaultTicker } from '@visactor/vrender-core'; import { registerAnimate } from './plugin/other'; +import { registerElementSelect } from './interaction/triggers/element-select'; +import { registerElementHighlight } from './interaction/triggers/element-highlight'; VChart.useRegisters([ registerLineChart, @@ -135,10 +136,10 @@ export { registerMapLabel, registerPoptip, // layout - registerGridLayout + registerGridLayout, // vgrammar interactions, - // registerElementHighlight, - // registerElementSelect + registerElementHighlight, + registerElementSelect }; export default VChart; diff --git a/packages/vchart/src/index-harmony.ts b/packages/vchart/src/index-harmony.ts index 6b475c022f..677028bf49 100644 --- a/packages/vchart/src/index-harmony.ts +++ b/packages/vchart/src/index-harmony.ts @@ -56,9 +56,10 @@ import { registerMapLabel } from './component/map-label'; import { registerGridLayout } from './layout/grid-layout/grid-layout'; import { registerPoptip } from './component/poptip'; import { registerCanvasTooltipHandler } from './plugin/components/tooltip-handler'; -import { registerElementHighlight, registerElementSelect } from '@visactor/vgrammar-core'; import { DefaultTicker } from '@visactor/vrender-core'; import { registerAnimate } from './plugin/other'; +import { registerElementHighlight } from './interaction/triggers/element-highlight'; +import { registerElementSelect } from './interaction/triggers/element-select'; VChart.useRegisters([ registerAnimate, diff --git a/packages/vchart/src/index-lark.ts b/packages/vchart/src/index-lark.ts index 483870f744..d2f47badd0 100644 --- a/packages/vchart/src/index-lark.ts +++ b/packages/vchart/src/index-lark.ts @@ -57,8 +57,8 @@ import { registerGridLayout } from './layout/grid-layout/grid-layout'; import { registerPoptip } from './component/poptip'; import { registerCanvasTooltipHandler } from './plugin/components/tooltip-handler'; import { registerFormatPlugin } from './plugin/chart/formatter'; - -import { registerElementHighlight, registerElementSelect } from '@visactor/vgrammar-core'; +import { registerElementHighlight } from './interaction/triggers/element-highlight'; +import { registerElementSelect } from './interaction/triggers/element-select'; VChart.useRegisters([ // charts diff --git a/packages/vchart/src/index-wx-simple.ts b/packages/vchart/src/index-wx-simple.ts index d4c589cf9a..c454b93256 100644 --- a/packages/vchart/src/index-wx-simple.ts +++ b/packages/vchart/src/index-wx-simple.ts @@ -17,7 +17,8 @@ import { registerCartesianLinearAxis } from './component/axis/cartesian/linear-a import { registerWXEnv } from './env'; import { registerCanvasTooltipHandler } from './plugin/components/tooltip-handler'; import { registerFormatPlugin } from './plugin/chart/formatter'; -import { registerElementHighlight, registerElementSelect } from '@visactor/vgrammar-core'; +import { registerElementHighlight } from './interaction/triggers/element-highlight'; +import { registerElementSelect } from './interaction/triggers/element-select'; export * from './core'; diff --git a/packages/vchart/src/index-wx.ts b/packages/vchart/src/index-wx.ts index 5a6bbff004..931f9ba949 100644 --- a/packages/vchart/src/index-wx.ts +++ b/packages/vchart/src/index-wx.ts @@ -56,7 +56,8 @@ import { registerMapLabel } from './component/map-label'; import { registerGridLayout } from './layout/grid-layout/grid-layout'; import { registerPoptip } from './component/poptip'; import { registerCanvasTooltipHandler } from './plugin/components/tooltip-handler'; -import { registerElementHighlight, registerElementSelect } from '@visactor/vgrammar-core'; +import { registerElementHighlight } from './interaction/triggers/element-highlight'; +import { registerElementSelect } from './interaction/triggers/element-select'; VChart.useRegisters([ // charts diff --git a/packages/vchart/src/interaction/config.ts b/packages/vchart/src/interaction/config.ts index b1f7c91681..31d383e2e2 100644 --- a/packages/vchart/src/interaction/config.ts +++ b/packages/vchart/src/interaction/config.ts @@ -1,6 +1,9 @@ +import { mergeSpec } from '@visactor/vutils-extension'; import type { RenderMode } from '../typings/spec/common'; import { RenderModeEnum } from '../typings/spec/common'; import { isMiniAppLikeMode, isMobileLikeMode } from '../util'; +import type { IHoverSpec, ISelectSpec } from './interface/spec'; +import { isBoolean, isObject } from '@visactor/vutils'; export function getDefaultInteractionConfigByMode(mode: RenderMode) { if (mode === RenderModeEnum['desktop-browser'] || mode === RenderModeEnum['desktop-miniApp']) { @@ -33,3 +36,28 @@ export function getDefaultInteractionConfigByMode(mode: RenderMode) { return null; } + +export const parseHoverSelect = (mode: RenderMode, hoverSpec: IHoverSpec, selectSpec: ISelectSpec) => { + const defaultConfig = getDefaultInteractionConfigByMode(mode); + let finalHoverSpec = { ...defaultConfig?.hover }; + let finalSelectSpec: ISelectSpec = { ...defaultConfig?.select }; + + if (isBoolean(hoverSpec)) { + finalHoverSpec.enable = hoverSpec as boolean; + } else if (isObject(hoverSpec)) { + finalHoverSpec.enable = true; + finalHoverSpec = mergeSpec(finalHoverSpec, hoverSpec); + } + + if (isBoolean(selectSpec)) { + finalSelectSpec.enable = selectSpec as boolean; + } else if (isObject(selectSpec)) { + finalSelectSpec.enable = true; + finalSelectSpec = mergeSpec(finalSelectSpec, selectSpec); + } + + return { + select: finalSelectSpec, + hover: finalHoverSpec + }; +}; diff --git a/packages/vchart/src/interaction/dimension-trigger.ts b/packages/vchart/src/interaction/dimension-trigger.ts deleted file mode 100644 index bf3647e92b..0000000000 --- a/packages/vchart/src/interaction/dimension-trigger.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { isEmpty, isArray } from '@visactor/vutils'; -import type { IElement } from '@visactor/vgrammar-core'; -import type { DimensionEventParams } from '../event/events/dimension/interface'; -// eslint-disable-next-line no-duplicate-imports -import { DimensionEventEnum } from '../event/events/dimension/interface'; -import type { IMark } from '../mark/interface'; -import type { EventCallback, EventParams, IEvent } from '../event/interface'; -import type { IHoverSpec, IInteraction, ISelectSpec, ITrigger, ITriggerOption } from './interface'; -import type { RenderMode } from '../typings/spec'; -import { MarkSet } from '../mark/mark-set'; -import { STATE_VALUE_ENUM } from '../compile/mark/interface'; - -export class DimensionTrigger implements ITrigger { - // 事件 - readonly event: IEvent; - - protected readonly interaction: IInteraction; - - protected _option: ITriggerOption; - protected _marks: MarkSet = new MarkSet(); - protected _markReverse: MarkSet = new MarkSet(); - - private _hover: IHoverSpec; - get hover() { - return this._hover; - } - private _select: ISelectSpec; - get select() { - return this._select; - } - - constructor(option: ITriggerOption) { - this._option = option; - this.event = this._option.model.getOption().getChart().getEvent(); // new Event(option.eventDispatcher, option.mode); - this.interaction = option.interaction; - this.initConfig(option.mode); - } - - setStateKeys(fields: string[]): void { - // do nothing - } - - registerMark(mark: IMark): void { - // do nothing - if (!isEmpty(mark.stateStyle[STATE_VALUE_ENUM.STATE_DIMENSION_HOVER])) { - this._marks.addMark(mark); - } - if (!isEmpty(mark.stateStyle[STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE])) { - this._markReverse.addMark(mark); - } - } - - init(): void { - this.initEvent(); - } - - release(): void { - this.releaseEvent(); - } - - // event - protected initEvent() { - const event = this.event; - event.on(DimensionEventEnum.dimensionHover, this.onHover as EventCallback); - } - - protected releaseEvent(): void { - this.event.release(); - } - - private initConfig(mode: RenderMode): void { - // do nothing - } - - protected getEventElement(params: DimensionEventParams, reverse: boolean = false) { - // items 修改遍历方法从 mark - - const items: IElement[] = []; - params.dimensionInfo.forEach(df => { - df.data.forEach(dd => { - const seriesMark = (reverse ? this._markReverse : this._marks) - .getMarks() - .filter(m => m.model === dd.series && m.getVisible()); - - seriesMark.forEach(m => { - const markProduct = m.getProduct(); - if (!markProduct || !markProduct.elements) { - return; - } - - const elements = markProduct.elements.filter(e => { - const datum = e.getDatum(); - let c; - if (isArray(datum)) { - c = datum.every((oneData, i) => oneData === dd.datum[i]); - } else { - c = dd.datum.some(dd_d => dd_d === datum); - } - return reverse ? !c : c; - }); - items.push(...elements); - }); - }); - }); - - return items; - } - - private onHover = (params: DimensionEventParams) => { - switch (params.action) { - case 'enter': - // clear last hover - // eslint-disable-next-line no-case-declarations - const lastHover = this.interaction.getEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); - lastHover.forEach(e => this.interaction.addEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE, e)); - this.interaction.clearEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, false); - // add new - const elements = this.getEventElement(params); - elements.forEach(el => this.interaction.addEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, el)); - this.interaction.reverseEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); - break; - case 'leave': - // clear all - this.interaction.clearEventElement(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, true); - params = null; - break; - case 'click': - case 'move': - default: - break; - } - }; -} diff --git a/packages/vchart/src/interaction/index.ts b/packages/vchart/src/interaction/index.ts index d1c5e934c0..7911c4321f 100644 --- a/packages/vchart/src/interaction/index.ts +++ b/packages/vchart/src/interaction/index.ts @@ -1,10 +1,9 @@ -export { - registerElementActive, - registerElementHighlight, - registerElementSelect, - registerElementActiveByLegend, - registerElementHighlightByLegend, - registerElementHighlightByName, - registerElementHighlightByGroup, - registerElementHighlightByKey -} from '@visactor/vgrammar-core'; +export { registerElementHighlight } from './triggers/element-highlight'; +export { registerElementSelect } from './triggers/element-select'; +export { registerDimensionHover } from './triggers/dimension-hover'; +export { registerElementActive } from './triggers/element-active'; +export { registerElementActiveByLegend } from './triggers/element-active-by-legend'; +export { registerElementHighlightByGroup } from './triggers/element-highlight-by-group'; +export { registerElementHighlightByKey } from './triggers/element-highlight-by-key'; +export { registerElementHighlightByLegend } from './triggers/element-highlight-by-legend'; +export { registerElementHighlightByName } from './triggers/element-highlight-by-name'; diff --git a/packages/vchart/src/interaction/interaction.ts b/packages/vchart/src/interaction/interaction.ts index f33a695130..4d0f97c333 100644 --- a/packages/vchart/src/interaction/interaction.ts +++ b/packages/vchart/src/interaction/interaction.ts @@ -1,35 +1,10 @@ -import { isEmpty } from '@visactor/vutils'; import type { StateValue } from '../compile/mark'; -import type { IElement } from '@visactor/vgrammar-core'; -import type { BaseEventParams } from '../event/interface'; -import type { IMark } from '../mark/interface'; -import type { IInteraction } from './interface'; -import type { IInteraction as IVGrammarInteraction } from '@visactor/vgrammar-core'; - -import { stateToReverse } from '../compile/mark/util'; +import type { IMarkGraphic } from '../mark/interface'; +import type { IInteraction } from './interface/common'; +import type { ITrigger } from './interface/trigger'; export class Interaction implements IInteraction { - // 数据 - private _stateMarks: Map = new Map(); - // active - private _stateElements: Map = new Map(); - - private _vgrammarInteractions: Map = new Map(); - addVgrammarInteraction(state: StateValue, i: IVGrammarInteraction) { - if (!state) { - return; - } - - if (!this._vgrammarInteractions.get(state)) { - !this._vgrammarInteractions.set(state, [i]); - } else { - this._vgrammarInteractions.get(state).push(i); - } - } - - static markStateEnable(mark: IMark, state: string) { - return !isEmpty(mark.stateStyle[state]); - } + private _stateGraphicsByTrigger: Map = new Map(); private _disableTriggerEvent: boolean = false; @@ -37,201 +12,261 @@ export class Interaction implements IInteraction { this._disableTriggerEvent = disable; } - registerMark(state: StateValue, mark: IMark): void { - if (!this._stateMarks.has(state)) { - this._stateMarks.set(state, []); - } - this._stateMarks.get(state)?.push(mark); - } + private _triggerMapByState: Map = new Map(); + addTrigger(trigger: ITrigger) { + if (trigger) { + const startState = trigger.getStartState(); + const resetState = trigger.getResetState(); - getStateMark(state: StateValue): IMark[] | null { - return this._stateMarks.get(state); - } + [startState, resetState].forEach(state => { + if (state) { + const stateTrigger = this._triggerMapByState.get(state); - filterEventMark(params: BaseEventParams, state: StateValue): boolean { - return !!(params.mark && this._stateMarks.get(state)?.includes(params.mark)); + if (stateTrigger) { + !stateTrigger.includes(trigger) && stateTrigger.push(trigger); + } else { + this._triggerMapByState.set(state, [trigger]); + } + } + }); + } } - getEventElement(stateValue: StateValue) { - return this._stateElements.get(stateValue) ?? []; + setStatedGraphics(trigger: ITrigger, graphics: IMarkGraphic[]) { + this._stateGraphicsByTrigger.set(trigger, graphics); } - getEventElementData(stateValue: StateValue) { - return this.getEventElement(stateValue).map(e => e.getDatum()); + getStatedGraphics(trigger: ITrigger) { + return this._stateGraphicsByTrigger.get(trigger); } - exchangeEventElement(stateValue: StateValue, element: IElement) { + updateStates( + trigger: ITrigger, + newStatedGraphics: IMarkGraphic[], + prevStatedGraphics?: IMarkGraphic[], + state?: string, + reverseState?: string + ) { if (this._disableTriggerEvent) { return; } - // reverse - const reState = stateToReverse(stateValue); - this._stateElements.get(stateValue)?.forEach(e => { - e.removeState(stateValue); - if (reState) { - this.addEventElement(reState, e); - } - }); - if (!element.getStates().includes(stateValue)) { - element.addState(stateValue); - if (reState) { - element.removeState(reState); - } - } - this._stateElements.set(stateValue, [element]); - } - removeEventElement(stateValue: StateValue, element: IElement) { - if (this._disableTriggerEvent) { - return; + if (!newStatedGraphics || !newStatedGraphics.length) { + return null; } - element.removeState(stateValue); - const list = this._stateElements.get(stateValue)?.filter(e => e !== element) ?? []; - this._stateElements.set(stateValue, list); - // reverse - const reState = stateToReverse(stateValue); - if (reState) { - if (list.length === 0) { - // clear reverse - this.clearEventElement(reState, false); + if (state && reverseState) { + if (prevStatedGraphics && prevStatedGraphics.length) { + // toggle + this.toggleReverseStateOfGraphics(trigger, newStatedGraphics, prevStatedGraphics, reverseState); + this.toggleStateOfGraphics(trigger, newStatedGraphics, prevStatedGraphics, state); + } else { + // update all the elements + this.addBothStateOfGraphics(trigger, newStatedGraphics, state, reverseState); + } + } else if (state) { + if (prevStatedGraphics && prevStatedGraphics.length) { + this.toggleStateOfGraphics(trigger, newStatedGraphics, prevStatedGraphics, state); } else { - // add reverse to element - this.addEventElement(reState, element); + this.addStateOfGraphics(trigger, newStatedGraphics, state); } } + + return newStatedGraphics; } - addEventElement(stateValue: StateValue, element: IElement) { - if (this._disableTriggerEvent) { - return; - } - if (!element.getStates().includes(stateValue)) { - element.addState(stateValue); - } - const list = this._stateElements.get(stateValue) ?? []; - list.push(element); - this._stateElements.set(stateValue, list); + protected toggleReverseStateOfGraphics( + trigger: ITrigger, + newStatedGraphics: IMarkGraphic[], + prevStatedGraphics: IMarkGraphic[], + reverseState: string + ) { + const markIdByState = trigger.getMarkIdByState(); + + prevStatedGraphics.forEach(g => { + const hasReverse = + reverseState && markIdByState[reverseState] && markIdByState[reverseState].includes(g.context.markId); + + if (hasReverse) { + g.addState(reverseState, true); + } + }); + + newStatedGraphics.forEach(g => { + const hasReverse = + reverseState && markIdByState[reverseState] && markIdByState[reverseState].includes(g.context.markId); + + if (hasReverse) { + g.removeState(reverseState); + } + }); } - clearEventElement(stateValue: StateValue, clearReverse: boolean) { - if (this._disableTriggerEvent) { - return; - } - this._stateElements.get(stateValue)?.forEach(e => { - e.removeState(stateValue); + protected toggleStateOfGraphics( + trigger: ITrigger, + newStatedGraphics: IMarkGraphic[], + prevStatedGraphics: IMarkGraphic[], + state: string + ) { + const markIdByState = trigger.getMarkIdByState(); + + prevStatedGraphics.forEach(g => { + const hasState = state && markIdByState[state] && markIdByState[state].includes(g.context.markId); + + if (hasState) { + g.removeState(state); + } }); - this._stateElements.set(stateValue, []); - if (clearReverse) { - const reState = stateToReverse(stateValue); - if (reState) { - this.clearEventElement(reState, false); + newStatedGraphics.forEach(g => { + const hasState = state && markIdByState[state] && markIdByState[state].includes(g.context.markId); + + if (hasState) { + g.addState(state, true); } - } + }); } - clearAllEventElement() { - if (this._disableTriggerEvent) { - return; - } - for (const [stateValue, elements] of this._stateElements) { - elements.forEach(e => { - e.clearStates(); + protected addBothStateOfGraphics( + trigger: ITrigger, + statedGraphics: IMarkGraphic[], + state: string, + reverseState: string + ) { + const marks = trigger.getMarks(); + const markIdByState = trigger.getMarkIdByState(); + + marks.forEach(m => { + const hasReverse = reverseState && markIdByState[reverseState] && markIdByState[reverseState].includes(m.id); + const hasState = state && markIdByState[state] && markIdByState[state].includes(m.id); + + if (!hasReverse && !hasState) { + return; + } + + m.getGraphics()?.forEach(g => { + const isStated = statedGraphics && statedGraphics.includes(g); + + if (isStated) { + if (hasState) { + g.addState(state, true); + } + } else { + if (hasReverse) { + g.addState(reverseState, true); + } + } }); - this._stateElements.set(stateValue, []); - } + }); } - /** - * 激活交互元素时 进行反选 - * 需要先将元素添加到已交互状态再使用此方法反选 - * @param stateValue - * @param activeElement - * @returns - */ - reverseEventElement(stateValue: StateValue) { + protected addStateOfGraphics(trigger: ITrigger, statedGraphics: IMarkGraphic[], state: string) { + const marks = trigger.getMarks(); + const markIdByState = trigger.getMarkIdByState(); + + marks.forEach(mark => { + const hasState = state && markIdByState[state] && markIdByState[state].includes(mark.id); + + if (!hasState) { + return; + } + + mark.getGraphics()?.forEach(g => { + const isStated = statedGraphics && statedGraphics.includes(g); + + if (isStated) { + if (hasState) { + g.addState(state, true); + } + } + }); + }); + } + + clearAllStatesOfTrigger(trigger: ITrigger, state?: string, reverseState?: string) { if (this._disableTriggerEvent) { return; } - // TODO:直接加默认后缀?or再增加一个map? - const state = stateToReverse(stateValue); - if (!state) { - return; - } - const marks = this.getStateMark(state); - if (!marks) { - return; - } - const activeElements = this.getEventElement(stateValue); - if (!activeElements.length) { + + const statedGraphics = this.getStatedGraphics(trigger); + + if (!statedGraphics || !statedGraphics.length) { return; } - const currentReverse = this.getEventElement(state); - if (!currentReverse.length) { - // all - // for performance array.include - // FIXME: 也许并没有太大必要 - if (activeElements.length === 1) { - marks.forEach(m => { - m.getProduct() - .elements.filter(e => e !== activeElements[0]) - .forEach(e => { - this.addEventElement(state, e); + const marks = trigger.getMarks(); + const markIdByState = trigger.getMarkIdByState(); + + marks.forEach(mark => { + if (mark) { + const graphics = mark.getGraphics(); + + if (graphics && graphics.length) { + if (reverseState && markIdByState[reverseState] && markIdByState[reverseState].includes(mark.id)) { + graphics.forEach(g => { + g.removeState(reverseState); }); - }); - } else { - marks.forEach(m => { - m.getProduct() - .elements.filter(e => !activeElements.includes(e)) - .forEach(e => { - this.addEventElement(state, e); + } + + if (state && markIdByState[state] && markIdByState[state].includes(mark.id)) { + graphics.forEach(g => { + if (statedGraphics.includes(g)) { + g.removeState(state); + } }); - }); + } + } } - } + }); } - /** - * hover/select 交互通过 vgrammar 代理 - * @param stateValue - * @param activeElement - * @returns - */ - startInteraction(stateValue: StateValue, element: IElement) { - const interactions = this._vgrammarInteractions.get(stateValue); - if (interactions) { - interactions.forEach(vgInteraction => { - vgInteraction.start(element); - }); + clearAllStates() { + if (this._disableTriggerEvent) { + return; } + + this._triggerMapByState.forEach((triggers, state) => { + triggers.forEach(trigger => { + this.clearAllStatesOfTrigger(trigger, state, trigger.getResetState()); + }); + }); } - /** - * hover/select 交互通过 vgrammar 代理 - * @param stateValue - * @param activeElement - * @returns - */ - resetInteraction(stateValue: StateValue, element: IElement) { - const interactions = this._vgrammarInteractions.get(stateValue); - if (interactions) { - interactions.forEach(vgInteraction => { - vgInteraction.reset(element); + clearByState(stateValue: string) { + if (this._disableTriggerEvent) { + return; + } + + const triggers = this._triggerMapByState.get(stateValue); + + if (triggers && triggers.length) { + triggers.forEach(t => { + this.clearAllStatesOfTrigger(t, stateValue, t.getResetState()); + + // 更新缓存 + this.setStatedGraphics(t, []); }); } } - /** - * 清空所有通过 vgrammar 代理的交互 - * @returns - */ - resetAllInteraction() { - for (const [stateValue, interactions] of this._vgrammarInteractions) { - if (interactions) { - interactions.forEach(vgInteraction => { - vgInteraction.reset(null); + updateStateOfGraphics(stateValue: string, markGraphics: IMarkGraphic[]) { + if (this._disableTriggerEvent) { + return; + } + const triggers = this._triggerMapByState.get(stateValue); + + if (triggers && triggers.length) { + triggers.forEach(t => { + const newStatedGraphics = markGraphics.filter(mg => { + return t.getMarks().some(m => { + const graphics = m && m.getGraphics(); + + return graphics && graphics.includes(mg); + }); }); - } + + this.updateStates(t, newStatedGraphics, this.getStatedGraphics(t), t.getStartState(), t.getResetState()); + + this.setStatedGraphics(t, newStatedGraphics); + }); } } } diff --git a/packages/vchart/src/interaction/interface/common.ts b/packages/vchart/src/interaction/interface/common.ts new file mode 100644 index 0000000000..068e16884e --- /dev/null +++ b/packages/vchart/src/interaction/interface/common.ts @@ -0,0 +1,30 @@ +import type { IMarkGraphic } from '../../mark/interface'; +import type { RenderMode } from '../../typings/spec/common'; +import type { IEventDispatcher } from '../../event/interface'; +import type { IModel } from '../../model/interface'; +import type { ITrigger } from './trigger'; + +export interface IInteraction { + setDisableActiveEffect: (disable: boolean) => void; + addTrigger: (trigger: ITrigger) => void; + setStatedGraphics: (trigger: ITrigger, graphics: IMarkGraphic[]) => void; + getStatedGraphics: (trigger: ITrigger) => IMarkGraphic[]; + updateStates: ( + trigger: ITrigger, + newStatedGraphics: IMarkGraphic[], + prevStatedGraphics?: IMarkGraphic[], + state?: string, + reverseState?: string + ) => IMarkGraphic[]; + clearAllStates: () => void; + clearAllStatesOfTrigger: (trigger: ITrigger, state?: string, reverseState?: string) => void; + clearByState: (stateValue: string) => any; + updateStateOfGraphics: (stateValue: string, markGraphics: IMarkGraphic[]) => void; +} + +export interface ITriggerOption { + mode: RenderMode; + interaction: IInteraction; + eventDispatcher: IEventDispatcher; + model: IModel; +} diff --git a/packages/vchart/src/interaction/interface.ts b/packages/vchart/src/interaction/interface/spec.ts similarity index 59% rename from packages/vchart/src/interaction/interface.ts rename to packages/vchart/src/interaction/interface/spec.ts index 13db40d750..7f8852748a 100644 --- a/packages/vchart/src/interaction/interface.ts +++ b/packages/vchart/src/interaction/interface/spec.ts @@ -1,58 +1,13 @@ +import type { EventType } from '../../event/interface'; +import type { StringOrNumber } from '../../typings/common'; import type { - ElementActiveByLegendSpec, - ElementActiveSpec, - ElementHighlightByGroupSpec, - ElementHighlightByKeySpec, - ElementHighlightByLegendSpec, - ElementHighlightByNameSpec, - ElementHighlightSpec, - ElementSelectSpec, - IElement, - IInteraction as IVGrammarInteraction -} from '@visactor/vgrammar-core'; -import type { IMark } from '../mark/interface'; -import type { RenderMode } from '../typings/spec/common'; -import type { BaseEventParams, IEventDispatcher, EventType } from '../event/interface'; -import type { IModel } from '../model/interface'; -import type { StateValue } from '../compile/mark/interface'; -import type { StringOrNumber } from '../typings/common'; - -export interface IInteraction { - registerMark: (state: StateValue, mark: IMark) => void; - filterEventMark: (params: BaseEventParams, state: StateValue) => boolean; - getStateMark: (state: StateValue) => IMark[] | null; - - getEventElement: (stateValue: StateValue) => IElement[]; - getEventElementData: (stateValue: StateValue) => any[]; - addEventElement: (stateValue: StateValue, element: IElement) => void; - removeEventElement: (stateValue: StateValue, elements: IElement) => void; - exchangeEventElement: (stateValue: StateValue, elements: IElement) => void; - clearEventElement: (stateValue: StateValue, clearReverse: boolean) => void; - reverseEventElement: (stateValue: StateValue) => void; - clearAllEventElement: () => void; - - setDisableActiveEffect: (disable: boolean) => void; - addVgrammarInteraction: (state: StateValue, i: IVGrammarInteraction) => void; - startInteraction: (state: StateValue, element: IElement) => void; - resetInteraction: (state: StateValue, element: IElement) => void; - resetAllInteraction: () => void; -} - -export interface ITrigger { - init: () => void; - setStateKeys: (fields: string[]) => void; - registerMark: (mark: IMark) => void; - release: () => void; - hover?: IHoverSpec; - select?: ISelectSpec; -} - -export interface ITriggerOption { - mode: RenderMode; - interaction: IInteraction; - eventDispatcher: IEventDispatcher; - model: IModel; -} + IElementActiveByLegendOptions, + IElementActiveOptions, + IElementHighlightByLegendOptions, + IElementHighlightByNameOptions, + IElementHighlightOptions, + IElementSelectOptions +} from './trigger'; export interface IBaseInteractionSpec { /** @@ -111,7 +66,12 @@ export interface ISelectSpec extends IBaseInteractionSpec { * 将触发元素的状态设置为激活状态,当开启这个交互的时候,可以在系列图元通过 `state.active` 设置激活状态下的视觉编码 */ export type IElementActiveSpec = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-active' + */ + type: 'element-active'; + }; /** * 图元选中相关交互,当开启这个交互的时候, @@ -119,39 +79,74 @@ export type IElementActiveSpec = IBaseInteractionSpec & * 可以在系列图元通过 `state.selected_reverse` 设置非激活状态下的视觉编码 */ export type IElementSelectSpec = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-select' + */ + type: 'element-select'; + }; /** * 图元高亮交互配置,当开启这个交互的时候, * 可以在系列图元通过 `state.highlight` 设置激活状态下的视觉编码 * 可以在系列图元通过 `state.blur` 设置非激活状态下的视觉编码 */ export type IElementHighlightSpec = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-highlight' + */ + type: 'element-highlight'; + }; /** * 将触发元素以及和触发元素具有相同key的元素状态设置为高亮状态,其他元素的状态设置为失焦状态;一般需要配合系列的dataKey 配置使用 */ export type IElementHighlightByKeySpec = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-highlight-by-key' + */ + type: 'element-highlight-by-key'; + }; /** * 将触发元素以及和触发元素具有相同分组值(groupKey)的元素状态设置为高亮状态,其他元素的状态设置为失焦状态 */ export type IElementHighlightByGroup = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-highlight-by-group' + */ + type: 'element-highlight-by-group'; + }; /** * 根据图例激活图元,默认触发事件为图例的 `legendItemHover`和`legendItemUnHover`事件 */ export type IElementActiveByLegend = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为 'element-active-by-legend' + */ + type: 'element-active-by-legend'; + }; /** * 根据图例高亮图元,默认触发事件为图例的 `legendItemHover`和`legendItemUnHover`事件 */ export type IElementHighlightByLegend = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为'element-highlight-by-legend' + */ + type: 'element-highlight-by-legend'; + }; /** * 根据图例高亮图元,默认触发事件为图例的 `legendItemHover`和`legendItemUnHover`事件 */ export type IElementHighlightByName = IBaseInteractionSpec & - Pick; + Pick & { + /** + * 设置交互的类型为'element-highlight-by-name' + */ + type: 'element-highlight-by-name'; + }; /** * 元素选中交互,将所有相同名称的元素的状态设置为选中状态;注意该交互不建议和默认的select交互同时使用(象形图除外) @@ -179,7 +174,8 @@ export type IInteractionItemSpec = | IElementHighlightByGroup | IElementActiveByLegend | IElementHighlightByLegend - | IElementHighlightByName; + | IElementHighlightByName + | ICustomInteraction; /** * 申明图表交互相关配置 diff --git a/packages/vchart/src/interaction/interface/trigger.ts b/packages/vchart/src/interaction/interface/trigger.ts new file mode 100644 index 0000000000..3228be8b4b --- /dev/null +++ b/packages/vchart/src/interaction/interface/trigger.ts @@ -0,0 +1,186 @@ +import type { GraphicEventType } from '@visactor/vrender-core'; +import type { IMark, IMarkGraphic } from '../../mark/interface/common'; +import type { RenderMode } from '../../typings/spec/common'; +import type { IInteraction } from './common'; +import type { BaseEventParams } from '../../event/interface'; + +export type ITriggerEventHandler = (e: any, markGraphic?: IMarkGraphic) => void; + +export interface ITrigger { + readonly options: TriggerOptions; + readonly type: string; + registerMark: (mark: IMark | IMark[]) => void; + release: () => void; + + init: () => void; + start: (g: any) => void; + reset: (g?: IMarkGraphic) => void; + getStartState: () => string; + getResetState: () => string; + updateMarkIdByState: (states: string[]) => void; + getMarkIdByState: () => Record; + getMarks: () => IMark[]; + getMarksByState: (state: string) => IMark[]; +} + +export interface IBaseTriggerOptions { + type?: string; + /** + * 需要处理状态的所有图元 + */ + marks?: IMark[]; + mode?: RenderMode; + event: { + on: (eType: string, callback: ITriggerEventHandler) => void; + off: (eType: string, callback?: ITriggerEventHandler) => void; + emit: (eType: string, params: any) => void; + }; + interaction: IInteraction; + + id?: string; + + shouldStart?: (e: any) => boolean; + + shouldUpdate?: (e: any) => boolean; + + shouldEnd?: (e: any) => boolean; + + shouldReset?: (e: any) => boolean; + + onStart?: (e: any) => boolean; + + onUpdate?: (e: any) => boolean; + + onEnd?: (e: any) => boolean; + + onReset?: (e: any) => boolean; +} + +export type IElementSelectTriggerOff = GraphicEventType | string | 'empty' | 'none' | number; + +export interface IElementSelectOptions extends IBaseTriggerOptions { + /** + * the trigger event name + */ + trigger?: GraphicEventType | GraphicEventType[]; + /** + * the selected state name + */ + state?: string; + /** + * the non-selected state name + */ + reverseState?: string; + /** + * the reset trigger event name + */ + triggerOff?: IElementSelectTriggerOff | IElementSelectTriggerOff[]; + /** + * whether or not support multiple selected + */ + isMultiple?: boolean; +} + +export interface IDimensionHoverOptions extends IBaseTriggerOptions { + /** + * the selected state name + */ + state?: string; + /** + * the non-selected state name + */ + reverseState?: string; + + trigger?: string; +} + +export interface IElementActiveOptions extends IBaseTriggerOptions { + /** + * the trigger event name + */ + trigger?: GraphicEventType | GraphicEventType[]; + /** + * the reset trigger event name + */ + triggerOff?: GraphicEventType | GraphicEventType[] | 'none'; + /** + * the active state name + */ + state?: string; +} + +export interface IElementHighlightOptions extends IBaseTriggerOptions { + /** + * the trigger event name + */ + trigger?: GraphicEventType; + /** + * the reset trigger event name + */ + triggerOff?: GraphicEventType | 'none'; + /** + * the highlight state name + */ + highlightState?: string; + /** + * the blur state name + */ + blurState?: string; +} + +export interface IElementFilterOptions { + /** + * the filter type of element + */ + filterType?: 'key' | 'groupKey'; + /** + * the field to be filtered + */ + filterField?: string; +} + +/** + * the interaction to set the active state of specified marks trigger by legend + */ +export interface IElementActiveByLegendOptions extends IBaseTriggerOptions, IElementFilterOptions { + /** + * the active state name + */ + state?: string; +} + +/** + * the interaction to set the active state of specified marks trigger by legend + */ +export interface IElementHighlightByLegendOptions extends IBaseTriggerOptions, IElementFilterOptions { + /** + * the highlight state name + */ + highlightState?: string; + /** + * the blur state name + */ + blurState?: string; +} + +export interface IElementHighlightByNameOptions extends IElementHighlightByLegendOptions { + graphicName?: string | string[]; + /** + * the trigger event name + */ + trigger?: GraphicEventType; + /** + * the reset trigger event name + */ + triggerOff?: GraphicEventType | 'none'; + + parseData?: (e: BaseEventParams) => any; +} + +export type IElementHighlightByGraphicNameOptions = IElementHighlightOptions; + +export interface ITriggerConstructor { + readonly type: string; + + new (options?: T): ITrigger; +} diff --git a/packages/vchart/src/interaction/triggers/base.ts b/packages/vchart/src/interaction/triggers/base.ts new file mode 100644 index 0000000000..e3ba92a4ac --- /dev/null +++ b/packages/vchart/src/interaction/triggers/base.ts @@ -0,0 +1,137 @@ +import { isArray } from '@visactor/vutils'; +import type { IBaseTriggerOptions, ITrigger, ITriggerEventHandler } from '../interface/trigger'; +import type { IMark, IMarkGraphic } from '../../mark/interface/common'; +import { MarkSet } from '../../mark/mark-set'; +import { groupMarksByState } from './util'; + +export abstract class BaseTrigger implements ITrigger { + options: T; + type: string; + + protected _markSet: MarkSet = new MarkSet(); + + protected _markIdByState: Record; + + constructor(options: T) { + this.options = options; + + if (options.marks && options.marks.length) { + options.marks.forEach(m => { + this.registerMark(m); + }); + } + } + + getMarks(): IMark[] { + return this._markSet.getMarks(); + } + + getMarksByState(state: string): IMark[] { + if (this._markIdByState && this._markSet) { + const markIds = this._markIdByState[state]; + + return markIds ? this.getMarks().filter(mark => mark && markIds.includes(mark.id)) : []; + } + + return []; + } + + registerMark(mark: IMark | IMark[]) { + if (isArray(mark)) { + mark.forEach(m => { + this._markSet.addMark(m); + }); + } else { + this._markSet.addMark(mark); + } + } + + updateMarkIdByState(states: string[]) { + this._markIdByState = groupMarksByState(this.getMarks(), states); + } + + getMarkIdByState() { + return this._markIdByState ?? {}; + } + + isGraphicInStateMark(g: IMarkGraphic, state: string) { + const markIdByState = this.getMarkIdByState(); + + return markIdByState && markIdByState[state] && markIdByState[state].includes(g.context?.markId); + } + + isGraphicInMark(g: IMarkGraphic) { + return !!this._markSet.getMarkInId(g.context?.markId); + } + + protected abstract getEvents(): Array<{ type: string | string[]; handler: ITriggerEventHandler }>; + + getStartState(): string { + return null; + } + + getResetState(): string { + return null; + } + + init() { + const events = this.getEvents(); + + if (events && this.options.event) { + events.forEach(evt => { + if (evt.type && evt.handler) { + if (isArray(evt.type)) { + evt.type.forEach(evtType => { + evtType && evtType !== 'none' && this.options.event.on(evtType, evt.handler); + }); + } else { + evt.type !== 'none' && this.options.event.on(evt.type, evt.handler); + } + } + }); + } + } + + release() { + // unbind events + const events = this.getEvents(); + + if (events && this.options.event) { + (events ?? []).forEach(evt => { + if (evt.type && evt.handler) { + if (isArray(evt.type)) { + evt.type.forEach(evtType => { + evtType && evtType !== 'none' && this.options.event.on(evtType, evt.handler); + }); + } else { + evt.type !== 'none' && this.options.event.off(evt.type, evt.handler); + } + } + }); + } + } + + start(g: IMarkGraphic | string) { + // do nothing + } + + reset(g?: IMarkGraphic) { + // do nothing + } + + protected dispatchEvent(type: 'start' | 'reset' | 'update' | 'end', params: any) { + if (this.options.event) { + this.options.event.emit(`${this.type}:${type}`, params); + + if (type === 'start' && this.options.onStart) { + this.options.onStart(params); + } else if (type === 'reset' && this.options.onReset) { + this.options.onReset(params); + } else if (type === 'update' && this.options.onUpdate) { + this.options.onUpdate(params); + } else if (type === 'end' && this.options.onEnd) { + this.options.onEnd(params); + } + } + } +} diff --git a/packages/vchart/src/interaction/triggers/dimension-hover.ts b/packages/vchart/src/interaction/triggers/dimension-hover.ts new file mode 100644 index 0000000000..58cd8d6430 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/dimension-hover.ts @@ -0,0 +1,131 @@ +import { isArray } from '@visactor/vutils'; +import type { DimensionEventParams } from '../../event/events/dimension/interface'; +// eslint-disable-next-line no-duplicate-imports +import { DimensionEventEnum } from '../../event/events/dimension/interface'; +import type { IMarkGraphic } from '../../mark/interface'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import { getDatumOfGraphic } from '../../util'; +import type { ISeries } from '../../series'; +import type { IDimensionHoverOptions, ITrigger } from '../interface/trigger'; +import { BaseTrigger } from './base'; +import { Factory } from '../../core/factory'; +import { TRIGGER_TYPE_ENUM } from './enum'; + +const defaultOptions: Partial = { + state: STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, + reverseState: STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE, + trigger: DimensionEventEnum.dimensionHover +}; + +export class DimensionHover extends BaseTrigger implements ITrigger { + static type: string = TRIGGER_TYPE_ENUM.DIMENSION_HOVER; + type: string = TRIGGER_TYPE_ENUM.DIMENSION_HOVER; + + static defaultOptions = defaultOptions; + protected _resetType: ('view' | 'self' | 'timeout')[] = []; + + protected _statedGraphics?: IMarkGraphic[]; + + constructor(options?: IDimensionHoverOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + + // this._marks = view.getMarksBySelector(this.options.selector); + this.updateMarkIdByState([this.options.state, this.options.reverseState]); + } + + getStartState(): string { + return this.options.state; + } + + getResetState(): string { + return this.options.reverseState; + } + + protected getEvents() { + const trigger = this.options.trigger; + + return [ + { + type: trigger, + handler: this.handleStart + } + ]; + } + + resetAll = () => { + const { state, reverseState, interaction } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + + if (statedGraphics && statedGraphics.length) { + interaction.clearAllStatesOfTrigger(this, state, reverseState); + this.dispatchEvent('reset', { graphics: statedGraphics, options: this.options }); + + interaction.setStatedGraphics(this, []); + } + }; + + protected getStatedGraphics(params: DimensionEventParams, reverse: boolean = false) { + const marks = this.getMarksByState(reverse ? this.options.reverseState : this.options.state); + + const items: IMarkGraphic[] = []; + params.dimensionInfo.forEach(df => { + df.data.forEach(dd => { + const seriesMark = marks.filter(m => (m.model as unknown as ISeries) === dd.series && m.getVisible()); + + seriesMark.forEach(m => { + const graphics = m.getGraphics(); + if (!graphics || !graphics.length) { + return; + } + + const elements = graphics.filter(g => { + const datum = getDatumOfGraphic(g); + let c; + if (isArray(datum)) { + c = datum.every((oneData, i) => oneData === dd.datum[i]); + } else { + c = dd.datum.some(dd_d => dd_d === datum); + } + return reverse ? !c : c; + }); + items.push(...elements); + }); + }); + }); + + return items; + } + + handleStart = (params: DimensionEventParams) => { + const interaction = this.options.interaction; + + switch (params.action) { + case 'enter': + const newStated = this.getStatedGraphics(params); + interaction.updateStates( + this, + this.getStatedGraphics(params), + interaction.getStatedGraphics(this), + this.options.state, + this.options.reverseState + ); + + interaction.setStatedGraphics(this, newStated); + + break; + case 'leave': + interaction.clearAllStatesOfTrigger(this, this.options.state, this.options.reverseState); + interaction.setStatedGraphics(this, []); + break; + case 'click': + case 'move': + default: + break; + } + }; +} + +export const registerDimensionHover = () => { + Factory.registerInteractionTrigger(DimensionHover.type, DimensionHover); +}; diff --git a/packages/vchart/src/interaction/triggers/element-active-by-legend.ts b/packages/vchart/src/interaction/triggers/element-active-by-legend.ts new file mode 100644 index 0000000000..160461fb2c --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-active-by-legend.ts @@ -0,0 +1,109 @@ +import { Factory } from '../../core/factory'; +import { BaseTrigger } from './base'; +import type { IElementActiveByLegendOptions, ITrigger, ITriggerEventHandler } from '../interface/trigger'; +import { ChartEvent } from '../../constant/event'; +import { isNil } from '@visactor/vutils'; +import { generateFilterValue } from './util'; +import type { IMarkGraphic } from '../../mark/interface'; +import type { BaseEventParams } from '../../core'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; + +const type = 'element-active-by-legend'; +const defaultOptions: Partial = { + state: STATE_VALUE_ENUM.STATE_ACTIVE, + filterType: 'groupKey' +}; + +export class ElementActiveByLegend + extends BaseTrigger + implements ITrigger +{ + static type: string = type; + type: string = type; + + static defaultOptions = defaultOptions; + + constructor(options?: IElementActiveByLegendOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + + this.updateMarkIdByState([this.options.state]); + } + protected getEvents(): Array<{ type: string | string[]; handler: ITriggerEventHandler }> { + return [ + { + type: ChartEvent.legendItemHover, + handler: this.handleStart + }, + { + type: ChartEvent.legendItemUnHover, + handler: this.handleReset + } + ]; + } + + getStartState(): string { + return this.options.state; + } + start(itemKey: string) { + if (isNil(itemKey)) { + return; + } + const { interaction, state } = this.options; + const filterValue = generateFilterValue(this.options); + const statedGraphics = interaction.getStatedGraphics(this); + const newStatedGraphics: IMarkGraphic[] = []; + + this.getMarks().forEach(m => { + m.getGraphics()?.forEach(g => { + if (filterValue(g) === itemKey) { + newStatedGraphics.push(g); + } + }); + }); + + interaction.updateStates(this, newStatedGraphics, statedGraphics, state); + interaction.setStatedGraphics(this, newStatedGraphics); + } + + resetAll() { + const { interaction, state } = this.options; + + interaction.clearAllStatesOfTrigger(this, state); + + interaction.setStatedGraphics(this, []); + } + + reset(g?: IMarkGraphic) { + const { state, interaction } = this.options; + + if (g) { + const statedGraphics = interaction.getStatedGraphics(this); + if (statedGraphics && statedGraphics.includes(g)) { + g.removeState(state); + interaction.setStatedGraphics( + this, + statedGraphics.filter(sg => sg !== g) + ); + } + } else { + this.resetAll(); + } + } + + handleStart = (e: BaseEventParams) => { + const event = e.event; + + if (event) { + this.start(event.detail?.data?.id); + } + }; + + handleReset = (e: BaseEventParams) => { + this.resetAll(); + }; +} + +export const registerElementActiveByLegend = () => { + Factory.registerInteractionTrigger(type, ElementActiveByLegend); +}; diff --git a/packages/vchart/src/interaction/triggers/element-active.ts b/packages/vchart/src/interaction/triggers/element-active.ts new file mode 100644 index 0000000000..cfdd9837fd --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-active.ts @@ -0,0 +1,79 @@ +import { Factory } from '../../core/factory'; +import { BaseTrigger } from './base'; +import type { IElementActiveOptions, ITrigger, ITriggerEventHandler } from '../interface/trigger'; +import { TRIGGER_TYPE_ENUM } from './enum'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import type { IMarkGraphic } from '../../mark/interface'; +import type { BaseEventParams } from '../../event/interface'; + +const defaultOptions: Partial = { + state: STATE_VALUE_ENUM.STATE_ACTIVE, + trigger: 'pointerover', + triggerOff: 'pointerout' +}; + +export class ElementActive extends BaseTrigger implements ITrigger { + static type: string = TRIGGER_TYPE_ENUM.ELEMENT_ACTIVE; + type: string = TRIGGER_TYPE_ENUM.ELEMENT_ACTIVE; + + static defaultOptions = defaultOptions; + + constructor(options?: IElementActiveOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + + this.updateMarkIdByState([this.options.state]); + } + protected getEvents(): Array<{ type: string | string[]; handler: ITriggerEventHandler }> { + return [ + { + type: this.options.trigger, + handler: this.handleStart + }, + { type: this.options.triggerOff, handler: this.handleReset } + ]; + } + + getStartState(): string { + return this.options.state; + } + + start(g: IMarkGraphic) { + if (g) { + const { state, interaction } = this.options; + + if (this.isGraphicInStateMark(g, state)) { + g.addState(state, true); + + interaction.setStatedGraphics(this, [g]); + } + } + } + + reset(graphic?: IMarkGraphic) { + const { interaction, state } = this.options; + + const statedGraphics = interaction.getStatedGraphics(this); + const g = graphic ?? statedGraphics?.[0]; + + if (g && statedGraphics.includes(g)) { + g.removeState(state); + interaction.setStatedGraphics( + this, + statedGraphics.filter(sg => sg !== g) + ); + } + } + + handleStart = (e: BaseEventParams) => { + this.start(e.item); + }; + + handleReset = (e: BaseEventParams) => { + this.reset(e.item); + }; +} + +export const registerElementActive = () => { + Factory.registerInteractionTrigger(ElementActive.type, ElementActive); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-graphic-name.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-graphic-name.ts new file mode 100644 index 0000000000..4794169b2a --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-graphic-name.ts @@ -0,0 +1,69 @@ +import { Factory } from '../../core/factory'; +import { ElementHighlight } from './element-highlight'; +import type { BaseEventParams } from '../../event/interface'; +import { isNil } from '@visactor/vutils'; +import type { IMarkGraphic } from '../../mark/interface/common'; + +const type = 'element-highlight-by-graphic-name'; + +export class ElementHighlightByGraphicName extends ElementHighlight { + static type: string = type; + type: string = type; + + protected _filterByName(e: BaseEventParams) { + const name = e?.node?.name; + return !!name; + } + + protected _parseTargetKey(e: BaseEventParams) { + return e.node.name; + } + + start(itemKey: any) { + if (isNil(itemKey)) { + return; + } + + const { interaction, highlightState, blurState } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + const newStatedGraphics: IMarkGraphic[] = []; + + this.getMarks().forEach(m => { + m.getGraphics()?.forEach(g => { + if (g.name === itemKey) { + newStatedGraphics.push(g); + } + }); + }); + + interaction.updateStates(this, newStatedGraphics, statedGraphics, highlightState, blurState); + interaction.setStatedGraphics(this, newStatedGraphics); + } + + reset() { + const { highlightState, blurState, interaction } = this.options; + + interaction.clearAllStatesOfTrigger(this, highlightState, blurState); + interaction.setStatedGraphics(this, []); + } + + handleStart = (e: BaseEventParams) => { + if (e && e.item && this.isGraphicInMark(e.item)) { + const shouldStart = this.options.shouldStart ? this.options.shouldStart(e) : this._filterByName(e); + if (shouldStart) { + const itemKey = this._parseTargetKey(e); + this.start(itemKey); + } + } + }; + + handleReset = (e: BaseEventParams) => { + if (e && e.item && this.isGraphicInMark(e.item)) { + this.reset(); + } + }; +} + +export const registerElementHighlightByGraphicName = () => { + Factory.registerInteractionTrigger(ElementHighlightByGraphicName.type, ElementHighlightByGraphicName); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts new file mode 100644 index 0000000000..2d24d8ac79 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts @@ -0,0 +1,117 @@ +import { Factory } from '../../core/factory'; +import { BaseTrigger } from './base'; +import type { IElementHighlightOptions, ITrigger, ITriggerEventHandler } from '../interface/trigger'; +import type { IMarkGraphic } from '../../mark/interface'; +import { isNil } from '@visactor/vutils'; +import type { BaseEventParams } from '../../core'; +import { highlightDefaultOptions } from './util'; + +const type = 'element-highlight-by-group'; + +export class ElementHighlightByGroup + extends BaseTrigger + implements ITrigger +{ + static type: string = type; + type: string = type; + + static defaultOptions = highlightDefaultOptions; + constructor(options?: IElementHighlightOptions) { + super(options); + this.options = Object.assign({}, highlightDefaultOptions, options); + + this.updateMarkIdByState([this.options.highlightState, this.options.blurState]); + } + + getStartState(): string { + return this.options.highlightState; + } + + getResetState(): string { + return this.options.blurState; + } + + protected getEvents(): Array<{ type: string | string[]; handler: ITriggerEventHandler }> { + return [ + { + type: this.options.trigger, + handler: this.handleStart + }, + { type: this.options.triggerOff, handler: this.handleReset } + ]; + } + + resetAll() { + const { interaction, highlightState, blurState } = this.options; + + interaction.clearAllStatesOfTrigger(this, highlightState, blurState); + + interaction.setStatedGraphics(this, []); + } + + protected _getHightlightKey(g: IMarkGraphic) { + return g.context?.groupKey; + } + + start(g: IMarkGraphic) { + if (g && this.isGraphicInMark(g)) { + const highlightKey = this._getHightlightKey(g); + + if (isNil(highlightKey)) { + return; + } + + const { interaction, highlightState, blurState } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + const newStatedGraphics: IMarkGraphic[] = []; + + this.getMarks().forEach(m => { + m.getGraphics()?.forEach(g => { + if (this._getHightlightKey(g) === highlightKey) { + newStatedGraphics.push(g); + } + }); + }); + + interaction.updateStates(this, newStatedGraphics, statedGraphics, highlightState, blurState); + interaction.setStatedGraphics(this, newStatedGraphics); + } + } + + reset(g?: IMarkGraphic) { + if (g) { + if (this.isGraphicInMark(g)) { + const { interaction } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + + // todo 升级vrender 版本切换成数组 + g.removeState(this.options.highlightState); + g.removeState(this.options.blurState); + + interaction.setStatedGraphics( + this, + statedGraphics.filter(sg => sg !== g) + ); + } + } else { + this.resetAll(); + } + } + + handleStart = (e: BaseEventParams) => { + this.start(e.item); + }; + + handleReset = (e: BaseEventParams) => { + const g = e.item; + const hasActiveElement = g && this.isGraphicInMark(g); + + if (hasActiveElement) { + this.resetAll(); + } + }; +} + +export const registerElementHighlightByGroup = () => { + Factory.registerInteractionTrigger(type, ElementHighlightByGroup); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-key.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-key.ts new file mode 100644 index 0000000000..afd81c147f --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-key.ts @@ -0,0 +1,19 @@ +import { Factory } from '../../core/factory'; +import type { IElementHighlightOptions, ITrigger } from '../interface/trigger'; +import { ElementHighlightByGroup } from './element-highlight-by-group'; +import type { IMarkGraphic } from '../../mark/interface/common'; + +const type = 'element-highlight-by-key'; + +export class ElementHighlightByKey extends ElementHighlightByGroup implements ITrigger { + static type: string = type; + type: string = type; + + protected _getHightlightKey(g: IMarkGraphic) { + return g.context?.key; + } +} + +export const registerElementHighlightByKey = () => { + Factory.registerInteractionTrigger(type, ElementHighlightByKey); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts new file mode 100644 index 0000000000..291ef7c576 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts @@ -0,0 +1,115 @@ +import { Factory } from '../../core/factory'; +import { BaseTrigger } from './base'; +import type { IElementHighlightByLegendOptions, ITrigger, ITriggerEventHandler } from '../interface/trigger'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import { ChartEvent } from '../../constant/event'; +import { generateFilterValue } from './util'; +import type { IMarkGraphic } from '../../mark/interface/common'; +import type { BaseEventParams } from '../../event/interface'; + +const type = 'element-highlight-by-legend'; +const defaultOptions: Partial = { + highlightState: STATE_VALUE_ENUM.STATE_HIGHLIGHT, + blurState: STATE_VALUE_ENUM.STATE_BLUR, + filterType: 'groupKey' +}; + +export class ElementHighlightByLegend + extends BaseTrigger + implements ITrigger +{ + static type: string = type; + type: string = type; + + static defaultOptions = defaultOptions; + constructor(options?: IElementHighlightByLegendOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + this.updateMarkIdByState([this.options.highlightState, this.options.blurState]); + } + + getStartState(): string { + return this.options.highlightState; + } + + getResetState(): string { + return this.options.blurState; + } + + protected getEvents(): Array<{ type: string | string[]; handler: ITriggerEventHandler }> { + return [ + { + type: ChartEvent.legendItemHover, + handler: this.handleStart + }, + { + type: ChartEvent.legendItemUnHover, + handler: this.handleReset + } + ]; + } + + start(itemKey: any) { + if (itemKey) { + const filterValue = generateFilterValue(this.options as IElementHighlightByLegendOptions); + + const { interaction, highlightState, blurState } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + const newStatedGraphics: IMarkGraphic[] = []; + + this.getMarks().forEach(m => { + m.getGraphics()?.forEach(g => { + if (filterValue(g) === itemKey) { + newStatedGraphics.push(g); + } + }); + }); + + interaction.updateStates(this, newStatedGraphics, statedGraphics, highlightState, blurState); + interaction.setStatedGraphics(this, newStatedGraphics); + } + } + + resetAll() { + const { interaction, highlightState, blurState } = this.options; + + interaction.clearAllStatesOfTrigger(this, highlightState, blurState); + + interaction.setStatedGraphics(this, []); + } + + reset(g?: IMarkGraphic) { + const { highlightState, blurState, interaction } = this.options; + + if (g) { + const statedGraphics = interaction.getStatedGraphics(this); + if (statedGraphics && statedGraphics.includes(g)) { + // todo 升级 vrender版本,切换成数组 + g.removeState(highlightState); + g.removeState(blurState); + interaction.setStatedGraphics( + this, + statedGraphics.filter(sg => sg !== g) + ); + } + } else { + this.resetAll(); + } + } + + handleStart = (e: BaseEventParams) => { + const event = e.event; + + if (event) { + this.start(event.detail?.data?.id); + } + }; + + handleReset = (e: BaseEventParams) => { + this.resetAll(); + }; +} + +export const registerElementHighlightByLegend = () => { + Factory.registerInteractionTrigger(type, ElementHighlightByLegend); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts new file mode 100644 index 0000000000..782c5759ea --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts @@ -0,0 +1,131 @@ +import { Factory } from '../../core/factory'; +import { BaseTrigger } from './base'; +import type { IElementHighlightByNameOptions, ITrigger } from '../interface/trigger'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import type { BaseEventParams } from '../../event/interface'; +import { array } from '@visactor/vutils'; +import type { IMarkGraphic } from '../../mark/interface'; +import { generateFilterValue } from './util'; + +const type = 'element-highlight-by-name'; +const defaultOptions: Partial = { + highlightState: STATE_VALUE_ENUM.STATE_HIGHLIGHT, + blurState: STATE_VALUE_ENUM.STATE_BLUR, + filterType: 'groupKey' +}; + +export class ElementHighlightByName + extends BaseTrigger + implements ITrigger +{ + static type: string = type; + type: string = type; + + static defaultOptions = defaultOptions; + constructor(options?: IElementHighlightByNameOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + this.updateMarkIdByState([this.options.highlightState, this.options.blurState]); + } + + getStartState(): string { + return this.options.highlightState; + } + + getResetState(): string { + return this.options.blurState; + } + + protected getEvents() { + return [ + { + type: this.options.trigger, + handler: this.handleStart + }, + { type: this.options.triggerOff, handler: this.handleReset } + ]; + } + + protected _filterByName(e: BaseEventParams) { + const names = array(this.options.graphicName); + + return e?.node?.name && names.includes(e.node.name); + } + + protected _parseTargetKey(e: BaseEventParams) { + return this.options.parseData + ? this.options.parseData(e) + : e.node.type === 'text' + ? (e.node.attribute as any).text + : null; + } + + start(itemKey: any) { + if (itemKey) { + const filterValue = generateFilterValue(this.options as IElementHighlightByNameOptions); + + const { interaction, highlightState, blurState } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + const newStatedGraphics: IMarkGraphic[] = []; + + this.getMarks().forEach(m => { + m.getGraphics()?.forEach(g => { + if (filterValue(g) === itemKey) { + newStatedGraphics.push(g); + } + }); + }); + + interaction.updateStates(this, newStatedGraphics, statedGraphics, highlightState, blurState); + interaction.setStatedGraphics(this, newStatedGraphics); + } + } + + resetAll() { + const { interaction, highlightState, blurState } = this.options; + + interaction.clearAllStatesOfTrigger(this, highlightState, blurState); + + interaction.setStatedGraphics(this, []); + } + + reset(g?: IMarkGraphic) { + const { highlightState, blurState, interaction } = this.options; + + if (g) { + const statedGraphics = interaction.getStatedGraphics(this); + if (statedGraphics && statedGraphics.includes(g)) { + // todo 升级 vrender版本,切换成数组 + g.removeState(highlightState); + g.removeState(blurState); + interaction.setStatedGraphics( + this, + statedGraphics.filter(sg => sg !== g) + ); + } + } else { + this.resetAll(); + } + } + + handleStart = (e: BaseEventParams) => { + const shoudStart = this.options.shouldStart ? this.options.shouldStart(e) : this._filterByName(e); + + if (shoudStart) { + const itemKey = this._parseTargetKey(e); + this.start(itemKey); + } + }; + + handleReset = (e: BaseEventParams) => { + const shoudReset = this.options.shouldReset ? this.options.shouldReset(e) : this._filterByName(e); + + if (shoudReset) { + this.resetAll(); + } + }; +} + +export const registerElementHighlightByName = () => { + Factory.registerInteractionTrigger(type, ElementHighlightByName); +}; diff --git a/packages/vchart/src/interaction/triggers/element-highlight.ts b/packages/vchart/src/interaction/triggers/element-highlight.ts new file mode 100644 index 0000000000..107517b7f0 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-highlight.ts @@ -0,0 +1,143 @@ +import { isString } from '@visactor/vutils'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import type { IElementHighlightOptions, ITrigger } from '../interface/trigger'; +import type { IMarkGraphic } from '../../mark/interface/common'; +import { BaseTrigger } from './base'; +import { Factory } from '../../core/factory'; +import type { GraphicEventType } from '@visactor/vrender-core'; +import { TRIGGER_TYPE_ENUM } from './enum'; +import type { BaseEventParams } from '../../event/interface'; + +const defaultOptions: Partial = { + highlightState: STATE_VALUE_ENUM.STATE_HIGHLIGHT, + blurState: STATE_VALUE_ENUM.STATE_BLUR, + trigger: 'pointerover', + triggerOff: 'pointerout' +}; + +export class ElementHighlight + extends BaseTrigger + implements ITrigger +{ + static type: string = TRIGGER_TYPE_ENUM.ELEMENT_HIGHLIGHT; + type: string = TRIGGER_TYPE_ENUM.ELEMENT_HIGHLIGHT; + + static defaultOptions = defaultOptions; + + protected _lastGraphic?: IMarkGraphic; + + constructor(options?: IElementHighlightOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + + this.updateMarkIdByState([this.options.highlightState, this.options.blurState]); + } + + getStartState(): string { + return this.options.highlightState; + } + + getResetState(): string { + return this.options.blurState; + } + protected _resetType?: 'view' | 'self'; + + protected getEvents() { + const triggerOff = this.options.triggerOff; + const trigger = this.options.trigger; + const events = [ + { + type: trigger, + handler: this.handleStart + } + ]; + + let eventName = triggerOff; + if (isString(triggerOff) && (triggerOff as string).includes('view:')) { + eventName = (triggerOff as string).replace('view:', '') as GraphicEventType; + this._resetType = 'view'; + } else { + this._resetType = 'self'; + } + + events.push({ type: eventName as GraphicEventType, handler: this.handleReset }); + + return events; + } + + resetAll = () => { + const { highlightState, blurState, interaction } = this.options; + + if (this._lastGraphic) { + interaction.clearAllStatesOfTrigger(this, highlightState, blurState); + + this.dispatchEvent('reset', { graphics: [this._lastGraphic], options: this.options }); + + this._lastGraphic = null; + + interaction.setStatedGraphics(this, []); + } + }; + + handleStart = (e: BaseEventParams) => { + this.start(e.item); + }; + + handleReset = (e: BaseEventParams) => { + const { interaction } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + + if (!statedGraphics || !statedGraphics.length) { + return; + } + const markGraphic = e.item; + const hasActiveElement = markGraphic && this._markSet.getMarkInId(markGraphic.context.markId); + + if (this._resetType.includes('view') && !hasActiveElement) { + this.resetAll(); + } else if (this._resetType.includes('self') && hasActiveElement) { + this.resetAll(); + } + }; + + start(markGraphic: IMarkGraphic) { + if (markGraphic && this._markSet.getMarkInId(markGraphic.context.markId)) { + const { highlightState, blurState, interaction } = this.options; + + if (this._lastGraphic === markGraphic) { + return; + } + + const newStatedGraphics = interaction.updateStates( + this, + [markGraphic], + interaction.getStatedGraphics(this), + highlightState, + blurState + ); + interaction.setStatedGraphics(this, newStatedGraphics); + + this._lastGraphic = markGraphic; + + this.dispatchEvent('start', { graphics: newStatedGraphics, options: this.options }); + } else if (this._lastGraphic && this._resetType === 'view') { + this.resetAll(); + } + } + + reset(markGraphic: IMarkGraphic) { + if (markGraphic) { + if (this._markSet.getMarkInId(markGraphic.context.markId)) { + // todo 升级 vrender版本,切换成数组 + markGraphic.removeState(this.options.highlightState); + markGraphic.removeState(this.options.blurState); + } + } else { + this.resetAll(); + } + } +} + +export const registerElementHighlight = () => { + Factory.registerInteractionTrigger(ElementHighlight.type, ElementHighlight); +}; diff --git a/packages/vchart/src/interaction/triggers/element-select-by-graphic-name.ts b/packages/vchart/src/interaction/triggers/element-select-by-graphic-name.ts new file mode 100644 index 0000000000..d9cd605456 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-select-by-graphic-name.ts @@ -0,0 +1,27 @@ +import type { IMarkGraphic } from '../../mark/interface/common'; +import { Factory } from '../../core/factory'; +import { ElementSelect } from './element-select'; + +const type = 'element-select-by-graphic-name'; + +export class ElementSelectByGraphicName extends ElementSelect { + static type: string = type; + type: string = type; + start(markGraphic: IMarkGraphic): void { + const name = markGraphic?.name; + + if (name) { + this.getMarks().forEach(mark => { + mark.getGraphics()?.forEach(g => { + if (g.name === name) { + super.start(g); + } + }); + }); + } + } +} + +export const registerElementSelectByGraphicName = () => { + Factory.registerInteractionTrigger(ElementSelectByGraphicName.type, ElementSelectByGraphicName); +}; diff --git a/packages/vchart/src/interaction/triggers/element-select.ts b/packages/vchart/src/interaction/triggers/element-select.ts new file mode 100644 index 0000000000..9414f87cb4 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/element-select.ts @@ -0,0 +1,158 @@ +import { isArray } from '@visactor/vutils'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import type { IElementSelectOptions, ITrigger } from '../interface/trigger'; +import type { IMarkGraphic } from '../../mark/interface/common'; +import { BaseTrigger } from './base'; +import { parseTriggerOffOfSelect } from './util'; +import { Factory } from '../../core/factory'; +import { TRIGGER_TYPE_ENUM } from './enum'; +import type { BaseEventParams } from '../../event/interface'; + +const defaultOptions: Partial = { + state: STATE_VALUE_ENUM.STATE_SELECTED, + trigger: 'click' +}; + +export class ElementSelect extends BaseTrigger implements ITrigger { + static type: string = TRIGGER_TYPE_ENUM.ELEMENT_SELECT; + type: string = TRIGGER_TYPE_ENUM.ELEMENT_SELECT; + + static defaultOptions = defaultOptions; + protected _resetType: ('view' | 'self' | 'timeout')[] = []; + + private _timer?: number; + + constructor(options?: IElementSelectOptions) { + super(options); + this.options = Object.assign({}, defaultOptions, options); + + this.updateMarkIdByState([this.options.state, this.options.reverseState]); + } + + getStartState(): string { + return this.options.state; + } + + getResetState(): string { + return this.options.reverseState; + } + + protected getEvents() { + const triggerOff = this.options.triggerOff; + const trigger = this.options.trigger; + + const events = [ + { + type: trigger, + handler: this.handleStart + } + ]; + + const { eventNames, resetType } = parseTriggerOffOfSelect(triggerOff); + + eventNames.forEach(evt => { + if (evt && (isArray(trigger) ? !trigger.includes(evt) : evt !== trigger)) { + events.push({ type: evt, handler: this.handleReset }); + } + }); + + this._resetType = resetType; + + return events; + } + + resetAll = () => { + const { state, reverseState, interaction } = this.options; + + const statedGraphics = interaction.getStatedGraphics(this); + + if (statedGraphics && statedGraphics.length) { + interaction.clearAllStatesOfTrigger(this, state, reverseState); + this.dispatchEvent('reset', { graphics: statedGraphics, options: this.options }); + + interaction.setStatedGraphics(this, []); + } + }; + + handleStart = (e: BaseEventParams) => { + this.start(e.item); + }; + + handleReset = (e: BaseEventParams) => { + const { interaction } = this.options; + + const statedGraphics = interaction.getStatedGraphics(this); + if (!statedGraphics || !statedGraphics.length) { + return; + } + const markGraphic = e.item; + const hasActiveElement = markGraphic && this._markSet.getMarkInId(markGraphic.context.markId); + + if (this._resetType.includes('view') && !hasActiveElement) { + this.resetAll(); + } else if (this._resetType.includes('self') && hasActiveElement) { + this.resetAll(); + } + }; + + start(markGraphic: IMarkGraphic) { + const { state, reverseState, isMultiple, interaction } = this.options; + const statedGraphics = interaction.getStatedGraphics(this); + + if (markGraphic && this._markSet.getMarkInId(markGraphic.context.markId)) { + if (markGraphic.hasState(state)) { + if (this._resetType.includes('self')) { + const newStatedGraphics = statedGraphics && statedGraphics.filter(g => g !== markGraphic); + + if (newStatedGraphics && newStatedGraphics.length) { + interaction.setStatedGraphics( + this, + interaction.updateStates(this, newStatedGraphics, statedGraphics, state, reverseState) + ); + } else { + this.resetAll(); + } + } + } else { + if (this._timer) { + clearTimeout(this._timer); + } + markGraphic.addState(state, true); + + const newStatedGraphics = this.options.interaction.updateStates( + this, + isMultiple && statedGraphics ? [...statedGraphics, markGraphic] : [markGraphic], + statedGraphics, + state, + reverseState + ); + interaction.setStatedGraphics(this, newStatedGraphics); + this.dispatchEvent('start', { graphics: newStatedGraphics, options: this.options }); + + if (this._resetType.includes('timeout')) { + this._timer = setTimeout(() => { + this.resetAll(); + }, this.options.triggerOff as number) as unknown as number; + } + } + } else if (this._resetType.includes('view') && statedGraphics && statedGraphics.length) { + this.resetAll(); + } + } + + reset(markGraphic: IMarkGraphic) { + if (markGraphic) { + if (this._markSet.getMarkInId(markGraphic.context.markId)) { + // todo 升级 vrender版本,切换成数组 + markGraphic.removeState(this.options.state); + markGraphic.removeState(this.options.reverseState); + } + } else { + this.resetAll(); + } + } +} + +export const registerElementSelect = () => { + Factory.registerInteractionTrigger(ElementSelect.type, ElementSelect); +}; diff --git a/packages/vchart/src/interaction/triggers/enum.ts b/packages/vchart/src/interaction/triggers/enum.ts new file mode 100644 index 0000000000..e3ab6e969b --- /dev/null +++ b/packages/vchart/src/interaction/triggers/enum.ts @@ -0,0 +1,8 @@ +export const enum TRIGGER_TYPE_ENUM { + DIMENSION_HOVER = 'dimension-hover', + ELEMENT_HIGHLIGHT = 'element-highlight', + ELEMENT_SELECT = 'element-select', + ELEMENT_ACTIVE = 'element-active', + ELEMENT_HIGHLIGHT_BY_GRPHIC_NAME = 'element-highlight-by-graphic-name', + ELEMENT_SELECT_BY_GRPHIC_NAME = 'element-select-by-graphic-name' +} diff --git a/packages/vchart/src/interaction/triggers/util.ts b/packages/vchart/src/interaction/triggers/util.ts new file mode 100644 index 0000000000..73248c5ab8 --- /dev/null +++ b/packages/vchart/src/interaction/triggers/util.ts @@ -0,0 +1,112 @@ +import { array, isNumber, isString } from '@visactor/vutils'; +import type { IMark, IMarkGraphic } from '../../mark/interface/common'; + +import type { IElementFilterOptions, IElementHighlightOptions, IElementSelectTriggerOff } from '../interface/trigger'; +import type { GraphicEventType } from '@visactor/vrender-core'; +import type { IBaseInteractionSpec } from '../interface/spec'; +import { getDatumOfGraphic } from '../../util'; +import type { Datum } from '../../typings'; +import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; + +export const parseTriggerOffOfSelect = (triggerOff: IElementSelectTriggerOff | IElementSelectTriggerOff[]) => { + const triggerOffArray = array(triggerOff); + const resetType: ('view' | 'self' | 'timeout')[] = []; + const eventNames: GraphicEventType[] = []; + + triggerOffArray.forEach(off => { + if (off === 'empty') { + resetType.push('view'); + } else if (isString(off) && off !== 'none') { + if ((off as string).includes('view:')) { + eventNames.push((off as string).replace('view:', '') as GraphicEventType); + + resetType.push('view'); + } else { + eventNames.push(off as GraphicEventType); + + resetType.push('self'); + } + } else if (isNumber(off)) { + resetType.push('timeout'); + } + }); + + return { + eventNames, + resetType + }; +}; + +export const groupMarksByState = (marks: IMark[], states: string[]): Record => { + if (!states || !marks) { + return null; + } + + const res: Record = {}; + + marks.forEach(mark => { + const stateStyle = mark.stateStyle; + + if (!stateStyle) { + return; + } + + states.forEach(state => { + if (state && stateStyle[state]) { + if (!res[state]) { + res[state] = []; + } + + res[state].push(mark.id); + } + }); + }); + + return res; +}; + +export const filterMarksOfInteraction = (interactionSpec: IBaseInteractionSpec, marks: IMark[]) => { + if (!marks || !marks.length) { + return []; + } + const selector: IMark[] = []; + + if (interactionSpec.markIds) { + marks.filter(mark => { + if (interactionSpec.markIds.includes(mark.getProductId())) { + selector.push(mark); + } + }); + } else if (interactionSpec.markNames) { + marks.forEach(mark => { + if (interactionSpec.markNames.includes(mark.name)) { + selector.push(mark); + } + }); + } else { + marks.forEach(mark => { + selector.push(mark); + }); + } + + return selector; +}; + +export const generateFilterValue = (options: IElementFilterOptions) => { + if (options.filterField) { + return (g: IMarkGraphic) => { + return (getDatumOfGraphic(g) as Datum)?.[options.filterField]; + }; + } + + return (el: IMarkGraphic) => { + return el.context[options.filterType]; + }; +}; + +export const highlightDefaultOptions: Partial = { + highlightState: STATE_VALUE_ENUM.STATE_HIGHLIGHT, + blurState: STATE_VALUE_ENUM.STATE_BLUR, + trigger: 'pointerover', + triggerOff: 'pointerout' +}; diff --git a/packages/vchart/src/mark/arc-3d.ts b/packages/vchart/src/mark/arc-3d.ts index ea4c18be21..77617f8ca5 100644 --- a/packages/vchart/src/mark/arc-3d.ts +++ b/packages/vchart/src/mark/arc-3d.ts @@ -17,6 +17,6 @@ export const registerArc3dMark = () => { registerVGrammarArcAnimation(); registerShadowRoot(); registerArc3d(); - Factory.createGraphicComponent(MarkTypeEnum.arc3d, createArc3d); + Factory.registerGraphicComponent(MarkTypeEnum.arc3d, createArc3d); Factory.registerMark(MarkTypeEnum.arc3d, Arc3dMark); }; diff --git a/packages/vchart/src/mark/base/base-line.ts b/packages/vchart/src/mark/base/base-line.ts index aa0fcf4df8..5aec7e5473 100644 --- a/packages/vchart/src/mark/base/base-line.ts +++ b/packages/vchart/src/mark/base/base-line.ts @@ -4,10 +4,11 @@ import type { ConvertToMarkStyleSpec, ILineLikeMarkSpec } from '../../typings/vi import type { IPointLike } from '@visactor/vutils'; import { isFunction, isNil } from '@visactor/vutils'; import { BaseMark } from './base-mark'; -import type { IMarkStyle } from '../interface'; +import type { IMarkGraphic, IMarkStyle } from '../interface'; import type { Datum } from '../../typings/common'; -import type { IGraphic, ILine, ILineGraphicAttribute } from '@visactor/vrender-core'; -import { isSegmentAttrEqual } from '../utils/common'; +import type { ILine, ILineGraphicAttribute } from '@visactor/vrender-core'; +import { isSegmentAttrEqual } from '../utils/line'; +import type { IMarkSpec } from '../../typings/spec/common'; export const LINE_SEGMENT_ATTRIBUTES = [ 'stroke', @@ -29,6 +30,11 @@ export abstract class BaseLineMark) { + this._segmentStyleKeys = []; + + super.initStyleWithSpec(spec); + } /** * @override * 之所以覆写是因为 vgrammar 侧默认都会处理 lineSegments,非常耗性能,所以需要 VChart 给一个标志位用于是否执行。 @@ -40,21 +46,19 @@ export abstract class BaseLineMark( style: Partial> | Partial>, state: StateValueType = 'normal', - level: number = 0, - stateStyle = this.stateStyle + level: number = 0 ): void { if (isNil(style)) { return; } - if (stateStyle[state] === undefined) { - stateStyle[state] = {}; + if (this.stateStyle[state] === undefined) { + this.stateStyle[state] = {}; } const ignoreAttributes = this._getIgnoreAttributes(); const segmentAttributes = this._getSegmentAttributes(); const isUserLevel = this.isUserLevel(level); - const segmentStyleKeys: string[] = []; Object.keys(style).forEach(attr => { const attrStyle = (style as any)[attr]; @@ -66,15 +70,15 @@ export abstract class BaseLineMark { @@ -139,7 +143,7 @@ export abstract class BaseLineMark any>, g: IGraphic, attrs: any = {}) { + _runPointsEncoder(newStyles: Record any>, g: IMarkGraphic, attrs: any = {}) { const data = g.context.data; const lineAttrs: any[] = []; const points: IPointLike[] = []; @@ -149,18 +153,18 @@ export abstract class BaseLineMark { + Object.keys(newStyles).forEach(attrName => { if (this._isValidPointChannel(attrName)) { - (points[index] as any)[attrName] = styles[attrName](datum); + (points[index] as any)[attrName] = newStyles[attrName](datum); } else if (this._segmentStyleKeys.includes(attrName)) { - lineAttrs[index][attrName] = styles[attrName](datum); + lineAttrs[index][attrName] = newStyles[attrName](datum); } else if (index === 0) { - commonAttrs[attrName] = styles[attrName](datum); + commonAttrs[attrName] = newStyles[attrName](datum); } }); // todo 上下文,似乎是动画相关的 - (points[index] as any).context = this._keyGetter(datum); + (points[index] as any).context = this._keyGetter(datum) ?? index; }); if (this._segmentStyleKeys && this._segmentStyleKeys.length) { @@ -184,6 +188,15 @@ export abstract class BaseLineMark any>, g: IMarkGraphic, attrs: any = {}) { + const data = g.context.data; + if (newStyles && Object.keys(newStyles).some(this._isValidPointChannel) && data && data.length) { + return this._runPointsEncoder(newStyles, g, attrs); + } + + return super._runEncoderOfGraphic(newStyles, g, attrs); + } + protected _getDataByKey(data: Datum[]) { return this._dataByGroup; } diff --git a/packages/vchart/src/mark/base/base-mark.ts b/packages/vchart/src/mark/base/base-mark.ts index 88af3759ee..373f2cf96c 100644 --- a/packages/vchart/src/mark/base/base-mark.ts +++ b/packages/vchart/src/mark/base/base-mark.ts @@ -25,7 +25,12 @@ import type { VisualScaleType, MarkInputStyle, GroupedData, - IAttrs + IAttrs, + MarkTypeEnum, + IMarkGraphic, + DiffStateValues, + ProgressiveContext, + IProgressiveTransformResult } from '../interface'; import { DiffState } from '../interface/enum'; import { GradientType, DEFAULT_GRADIENT_CONFIG } from '../../constant/gradient'; @@ -36,9 +41,9 @@ import type { ISeries } from '../../series/interface'; import { CompilableMark } from '../../compile/mark/compilable-mark'; import type { StateValueType } from '../../compile/mark'; import { array, degreeToRadian, isArray, isBoolean, isFunction, isNil, isValid } from '@visactor/vutils'; -import { curveTypeTransform, groupData, runEncoder } from '../utils'; +import { curveTypeTransform, groupData, runEncoder } from '../utils/common'; import { LayoutState } from '../../compile/interface'; -import type { IGroupGraphicAttribute, IGraphic, IGraphicAttribute } from '@visactor/vrender-core'; +import type { IGroupGraphicAttribute, IGraphicAttribute, IGroup } from '@visactor/vrender-core'; import { CustomPath2D } from '@visactor/vrender-core'; import { isStateAttrChangeable } from '../../compile/mark/util'; import { Factory } from '../../core/factory'; @@ -86,7 +91,7 @@ export class BaseMark extends CompilableMark implements I * @param key * @returns */ - initStyleWithSpec(spec: IMarkSpec, key?: string) { + initStyleWithSpec(spec: IMarkSpec) { if (!spec) { return; } @@ -108,7 +113,7 @@ export class BaseMark extends CompilableMark implements I this.setVisible(spec.visible); } // style - this._initSpecStyle(spec, this.stateStyle, key); + this._initSpecStyle(spec); } protected _transformStyleValue( @@ -151,15 +156,14 @@ export class BaseMark extends CompilableMark implements I setStyle( style: Partial>, state: StateValueType = 'normal', - level: number = 0, - stateStyle = this.stateStyle + level: number = 0 ): void { if (isNil(style)) { return; } - if (stateStyle[state] === undefined) { - stateStyle[state] = {}; + if (this.stateStyle[state] === undefined) { + this.stateStyle[state] = {}; } const isUserLevel = this.isUserLevel(level); @@ -170,9 +174,9 @@ export class BaseMark extends CompilableMark implements I return; } - attrStyle = this._filterAttribute(attr as any, attrStyle, state, level, isUserLevel, stateStyle); + attrStyle = this._filterAttribute(attr as any, attrStyle, state, level, isUserLevel); - this.setAttribute(attr as any, attrStyle, state, level, stateStyle); + this.setAttribute(attr as any, attrStyle, state, level); }); } @@ -186,8 +190,7 @@ export class BaseMark extends CompilableMark implements I style: MarkInputStyle, state: StateValueType, level: number, - isUserLevel: boolean, - stateStyle = this.stateStyle + isUserLevel: boolean ): StyleConvert { let newStyle = this._styleConvert(style); if (isUserLevel) { @@ -214,22 +217,22 @@ export class BaseMark extends CompilableMark implements I * TODO: 没有外部调用 * 设置mark样式所参考的图元 */ - setReferer(mark: IMarkRaw, styleKey?: U, state?: StateValueType, stateStyle = this.stateStyle) { + setReferer(mark: IMarkRaw, styleKey?: U, state?: StateValueType) { if (!mark) { return; } if (styleKey && state) { - const style = stateStyle[state] ?? { [styleKey]: {} }; - stateStyle[state][styleKey] = { + const style = this.stateStyle[state] ?? { [styleKey]: {} }; + this.stateStyle[state][styleKey] = { ...(style[styleKey] as unknown as any), ...{ referer: mark } }; return; } - Object.entries(stateStyle).forEach(([state, style]) => { + Object.entries(this.stateStyle).forEach(([state, style]) => { Object.entries(style).forEach(([styleKey, style]) => { - stateStyle[state][styleKey].referer = mark; + this.stateStyle[state][styleKey].referer = mark; }); }); } @@ -248,35 +251,34 @@ export class BaseMark extends CompilableMark implements I attr: U, style: MarkInputStyle, state: StateValueType = 'normal', - level: number = 0, - stateStyle = this.stateStyle + level: number = 0 ) { - if (stateStyle[state] === undefined) { - stateStyle[state] = {}; + if (this.stateStyle[state] === undefined) { + this.stateStyle[state] = {}; } - if (stateStyle[state][attr] === undefined) { + if (this.stateStyle[state][attr] === undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - stateStyle[state][attr] = { + this.stateStyle[state][attr] = { level, style, referer: undefined }; } - const attrLevel = stateStyle[state][attr]?.level; + const attrLevel = this.stateStyle[state][attr]?.level; if (isValid(attrLevel) && attrLevel <= level) { - mergeSpec(stateStyle[state][attr], { style, level }); + mergeSpec(this.stateStyle[state][attr], { style, level }); } // some attr has extension channel in VChart to make some effect if (state !== 'normal') { if (attr in this._extensionChannel) { this._extensionChannel[attr].forEach(key => { - if (stateStyle[state][key] === undefined) { + if (this.stateStyle[state][key] === undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - stateStyle[state][key as keyof T] = stateStyle.normal[key]; + this.stateStyle[state][key as keyof T] = this.stateStyle.normal[key]; } }); } @@ -369,7 +371,7 @@ export class BaseMark extends CompilableMark implements I } if (typeof stateStyle.style === 'function') { - return (datum: Datum) => stateStyle.style(datum, this._attributeContext, this.getDataView()); + return (datum: Datum) => stateStyle.style(datum, this._attributeContext, { mark: this }, this.getDataView()); } if (GradientType.includes(stateStyle.style.gradient)) { @@ -402,10 +404,10 @@ export class BaseMark extends CompilableMark implements I this.setStyle(defaultStyle, 'normal', 0); } - private _initSpecStyle(spec: IMarkSpec, stateStyle: IMarkStateStyle, key?: string) { + private _initSpecStyle(spec: IMarkSpec) { // style if (spec.style) { - this.setStyle(spec.style, 'normal', AttributeLevel.User_Mark, stateStyle); + this.setStyle(spec.style, 'normal', AttributeLevel.User_Mark); } const state = spec.state; if (state) { @@ -428,9 +430,9 @@ export class BaseMark extends CompilableMark implements I } } this.state.addStateInfo(stateInfo); - this.setStyle(style as ConvertToMarkStyleSpec, key, AttributeLevel.User_Mark, stateStyle); + this.setStyle(style as ConvertToMarkStyleSpec, key, AttributeLevel.User_Mark); } else { - this.setStyle(stateTemp, key, AttributeLevel.User_Mark, stateStyle); + this.setStyle(stateTemp, key, AttributeLevel.User_Mark); } }); } @@ -545,27 +547,61 @@ export class BaseMark extends CompilableMark implements I protected _dataByGroup: GroupedData; protected _dataByKey: GroupedData; - protected _graphicMap: Map = new Map(); - protected _graphics: IGraphic[] = []; + protected _graphicMap: Map = new Map(); + protected _graphics: IMarkGraphic[] = []; protected _keyGetter: (datum: Datum) => string; protected _groupKeyGetter: (datum: Datum) => string; + private renderContext?: { + parameters?: any; + progressive?: ProgressiveContext; + beforeTransformProgressive?: IProgressiveTransformResult; + }; + protected _getDataByKey(data: Datum[]) { - return groupData(data, (datum: Datum) => { - return `${this._groupKeyGetter(datum)}_${this._keyGetter(datum)}`; + return groupData(data, (datum: Datum, index: number) => { + return `${this._groupKeyGetter(datum)}_${this._keyGetter(datum) ?? index}`; }); } + protected _getCommonContext() { + return { + markType: this.type as MarkTypeEnum, + markId: this.id, + modelId: this.model.id, + markUserId: this._userId, + modelUserId: this.model.userId + }; + } + + reuse(mark: IMark) { + if (this.type !== mark.type) { + return; + } + this._product = mark.getProduct(); + this._graphics = mark.getGraphics(); + this._graphicMap = (mark as any)._graphicMap; + + this._graphicMap.forEach(g => { + if (this._product) { + this._product.appendChild(g); + } + + g.context = { ...g.context, ...this._getCommonContext() }; + }); + this._dataByKey = (mark as any)._dataByKey; + } + getGraphics() { return this._graphics; } - createGraphic(attrs: any = {}): IGraphic { + protected _createGraphic(attrs: any = {}): IMarkGraphic { return Factory.createGraphicComponent(this.type, attrs); } - runGroupData(data: Datum[]) { + protected _runGroupData(data: Datum[]) { this._keyGetter = isFunction(this.key) ? (this.key as (datum: Datum) => string) : isValid(this.key) @@ -580,16 +616,16 @@ export class BaseMark extends CompilableMark implements I this._dataByGroup = groupData(data, this._groupKeyGetter); } - runJoin(data: Datum[]) { + protected _runJoin(data: Datum[]) { const newGroupedData = this._getDataByKey(data); const prevGroupedData = this._dataByKey; - const newGraphics: IGraphic[] = []; + const newGraphics: IMarkGraphic[] = []; - const enterGraphics = new Set(this._graphics.filter(g => g.context.diffState === DiffState.enter)); + const enterGraphics = new Set(this._graphics.filter(g => g.context.diffState === DiffState.enter)); const callback = (key: string, newData: Datum[], prevData: Datum[]) => { - let g: IGraphic; - let diffState: DiffState; + let g: IMarkGraphic; + let diffState: DiffStateValues; if (isNil(newData)) { g = this._graphicMap.get(key); @@ -602,40 +638,35 @@ export class BaseMark extends CompilableMark implements I if (this._graphicMap.has(key)) { g = this._graphicMap.get(key); } else { - g = this.createGraphic(); - // - this._product.appendChild(g); + g = {} as IMarkGraphic; // } diffState = DiffState.enter; - if (g.diffState === DiffState.exit) { + if (g.context?.diffState === DiffState.exit) { // force element to stop exit animation if it is reentered // todo animaiton } - this._graphicMap.set(key, g); - newGraphics.push(g); + this._graphicMap.set(key, g as IMarkGraphic); + newGraphics.push(g as IMarkGraphic); } else { // update g = this._graphicMap.get(key); if (g) { diffState = DiffState.update; - newGraphics.push(g); + newGraphics.push(g as IMarkGraphic); } } if (g) { - if (!g.context) { - g.context = {}; - } - - g.context.diffState = diffState; - g.context.data = newData; - g.context.key = key; - if (newData) { - g.context.groupKey = this._groupKeyGetter(newData[0]); - } + g.context = { + ...this._getCommonContext(), + diffState, + data: newData, + key, + groupKey: newData ? this._groupKeyGetter(newData[0]) : g.context?.groupKey + }; enterGraphics.delete(g); } }; @@ -670,17 +701,19 @@ export class BaseMark extends CompilableMark implements I enterGraphics.forEach(g => { this._graphicMap.delete(g.context.key); - if (g.parent) { - g.parent.removeChild(g); + if ((g as IMarkGraphic).parent) { + (g as IMarkGraphic).parent.removeChild(g as IMarkGraphic); + } + if ((g as IMarkGraphic).release) { + (g as IMarkGraphic).release(); } - g.release(); }); this._dataByKey = newGroupedData; this._graphics = newGraphics; } - _runEncoderOfGraphic(styles: Record any>, g: IGraphic, attrs: any = {}) { + _runEncoderOfGraphic(styles: Record any>, g: IMarkGraphic, attrs: any = {}) { return runEncoder(styles, g.context.data[0], attrs); } @@ -698,7 +731,7 @@ export class BaseMark extends CompilableMark implements I return attrsByGroup; } - protected _transformGraphicAttributes(g: IGraphic, attrs: any, groupAttrs?: any) { + protected _transformGraphicAttributes(g: IMarkGraphic, attrs: any, groupAttrs?: any) { return { ...groupAttrs, ...attrs @@ -728,7 +761,7 @@ export class BaseMark extends CompilableMark implements I return { updateStyles, groupStyles }; } - protected _encoderByState = (stateName: string) => { + protected _getEncoderByState = (stateName: string) => { const style = this.stateStyle[stateName]; if (style) { @@ -743,17 +776,29 @@ export class BaseMark extends CompilableMark implements I return validEncoder; } + + return null; }; - protected _setCustomShapeOfGraphic = (g: IGraphic) => { - if (this._markConfig.setCustomizedShape) { + protected _setGraphicFromMarkConfig = (g: IMarkGraphic) => { + const { setCustomizedShape, support3d, graphicName } = this._markConfig; + + if (setCustomizedShape) { g.pathProxy = (attrs: Partial) => { - return this._markConfig.setCustomizedShape(g.context.data, attrs, new CustomPath2D()); + return setCustomizedShape(g.context.data, attrs, new CustomPath2D()); }; } + + if (graphicName) { + if (isFunction(graphicName)) { + g.name = (graphicName as (e: IMarkGraphic) => string)(g); + } else { + g.name = graphicName as string; + } + } }; - protected _setStateOfGraphic = (g: IGraphic) => { + protected _setStateOfGraphic = (g: IMarkGraphic) => { g.clearStates(); g.stateProxy = null; @@ -766,13 +811,13 @@ export class BaseMark extends CompilableMark implements I } }; - runEncoder(splitGroupEncoder?: boolean) { + protected _runEncoder(splitGroupEncoder?: boolean) { const { [STATE_VALUE_ENUM.STATE_NORMAL]: normalStyle, ...otherStateStyle } = this.stateStyle; const { groupStyles, updateStyles } = this._separateNormalStyle(normalStyle, splitGroupEncoder); const attrsByGroup = this._runGroupEncoder(groupStyles); - this._graphics.forEach(g => { + this._graphics.forEach((g, index) => { const attrs = this._runEncoderOfGraphic(updateStyles, g); // 配置的优先级高于encoder @@ -780,27 +825,40 @@ export class BaseMark extends CompilableMark implements I attrs.pickable = this._markConfig.interactive; } - g.context.attrs = attrs; - g.setAttributes(this._transformGraphicAttributes(g, attrs, attrsByGroup?.[g.context.groupKey])); + const finalAttrs = this._transformGraphicAttributes(g, attrs, attrsByGroup?.[g.context.groupKey]); + + if (!g.setAttributes) { + const mockGraphic = g; + g = this._createGraphic(finalAttrs); + this._product.appendChild(g); + g.context = mockGraphic.context; + this._graphics[index] = g; + this._graphicMap.set(g.context.key, g); + } else { + g.setAttributes(finalAttrs); + } this._setStateOfGraphic(g); - this._setCustomShapeOfGraphic(g); + this._setGraphicFromMarkConfig(g); }); } - runState() { + protected _runState() { const encoderOfState: Record any>> = {}; Object.keys(this.stateStyle).forEach(stateName => { if (stateName !== STATE_VALUE_ENUM.STATE_NORMAL) { - encoderOfState[stateName] = this._encoderByState(stateName); + encoderOfState[stateName] = this._getEncoderByState(stateName); } }); this._encoderOfState = encoderOfState; this._graphics.forEach(g => { - const newStateValues = this.state.checkState(g, g.context.data); + const prevInteractionStateValues = (g.currentStates ?? []).filter((sv: string) => { + return !this.state.getStateInfo(sv); + }); + const newStateValues = [...prevInteractionStateValues, ...this.state.checkState(g, g.context.data)]; if (this._stateSort && newStateValues.length) { newStateValues.sort(this._stateSort); @@ -814,7 +872,7 @@ export class BaseMark extends CompilableMark implements I }); } - getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}): IGroupGraphicAttribute { + protected _getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}): IGroupGraphicAttribute { const { zIndex, clip, clipPath, overflow } = this._markConfig; if (!isNil(zIndex)) { @@ -843,7 +901,30 @@ export class BaseMark extends CompilableMark implements I return attrs; } - runMainTasks() { + protected _updateAttrsOfGroup() { + const attrs = this._getAttrsFromConfig(); + + this._product?.setAttributes(attrs); + + if (this._markConfig.support3d && this._product) { + this._product.setMode('3d'); + } + } + + protected runBeforeTransform(data: Datum[]) { + const transforms = this._transform?.filter(transformSpec => { + if (transformSpec.type) { + const transform = Factory.getGrammarTransform(transformSpec.type); + return !transform?.isGraphic; + } + + return false; + }); + + return this.runTransforms(transforms, data); + } + + protected _runMainTasks() { if ( !this.getVisible() || (this.getSkipBeforeLayouted() && this.getCompiler().getLayoutState() === LayoutState.before) @@ -851,49 +932,50 @@ export class BaseMark extends CompilableMark implements I return; } - const data = this._data?.getProduct(); + const data = this._data?.getProduct() ?? [{}]; + + const transformData = this.runBeforeTransform(data); + let markData: Datum[]; - const transformData = array( + if ((transformData as any)?.progressive) { + this.renderContext = { + beforeTransformProgressive: (transformData as any).progressive as IProgressiveTransformResult + }; + const p = (transformData as any).progressive; + if (p.canAnimate && p.canAnimate() && p.unfinished()) { + this._updateAttrsOfGroup(); + return; + } + } else { + markData = array(transformData); + this._runGroupData(markData); + } + + this._runJoin(markData); + this._runState(); + this._runEncoder(); + + if (!this.renderContext?.progressive) { this.runTransforms( this._transform?.filter(transformSpec => { if (transformSpec.type) { const transform = Factory.getGrammarTransform(transformSpec.type); - return !transform?.isGraphic; + return transform?.isGraphic; } return false; }), - data - ) - ); - - this.runGroupData(transformData); - this.runJoin(transformData); - this.runState(); - this.runEncoder(); - - this.runTransforms( - this._transform?.filter(transformSpec => { - if (transformSpec.type) { - const transform = Factory.getGrammarTransform(transformSpec.type); - return transform?.isGraphic; - } - - return false; - }), - this._graphics - ); - - const attrs = this.getAttrsFromConfig(); - - this._product?.setAttributes(attrs); + this._graphics + ); + } + this._updateAttrsOfGroup(); } render() { if (this._isCommited) { - this.runMainTasks(); + this._runMainTasks(); // 接入动画后,需要等动画结束在清除exit节点 - this.cleanExitGraphics(); + this._cleanExitGraphics(); } this.uncommit(); @@ -907,16 +989,16 @@ export class BaseMark extends CompilableMark implements I this._graphics.forEach(g => { if (this.state.checkOneState(g, g.context.data, stateInfo) === 'in') { - g.addState(key); + g.addState(key, true); } else { g.removeState(key); } }); } - cleanExitGraphics() { + protected _cleanExitGraphics() { this._graphicMap.forEach((g, key) => { - if (g.context.diffState === DiffState.exit && !g.context.isReserved) { + if (g.context.diffState === DiffState.exit) { this._graphicMap.delete(key); if (g.parent) { g.parent.removeChild(g); @@ -925,4 +1007,72 @@ export class BaseMark extends CompilableMark implements I } }); } + + isProgressive() { + return this.renderContext && (!!this.renderContext.progressive || !!this.renderContext.beforeTransformProgressive); + } + + canAnimateAfterProgressive() { + return ( + this.renderContext && + this.renderContext.beforeTransformProgressive && + this.renderContext.beforeTransformProgressive.canAnimate?.() + ); + } + + isDoingProgressive() { + return ( + this.renderContext && + ((this.renderContext.progressive && + this.renderContext.progressive.currentIndex < this.renderContext.progressive.totalStep) || + (this.renderContext.beforeTransformProgressive && this.renderContext.beforeTransformProgressive.unfinished())) + ); + } + + clearProgressive() { + if (this.renderContext && this.renderContext.progressive) { + this._graphics = []; + + (this._product as any).children.forEach((group: IGroup) => { + group.incrementalClearChild(); + }); + (this._product as any).removeAllChild(); + } + + if (this.renderContext && this.renderContext.beforeTransformProgressive) { + this.renderContext.beforeTransformProgressive.release(); + } + + this.renderContext = null; + } + + restartProgressive() { + if (this.renderContext && this.renderContext.progressive) { + this.renderContext.progressive.currentIndex = 0; + } + } + protected renderBeforeProgressive() { + const transform = this.renderContext.beforeTransformProgressive; + + transform.progressiveRun(); + const output = transform.output(); + + if (transform.canAnimate?.()) { + if (transform.unfinished()) { + return; + } + + this._runGroupData(output); + this._runJoin(output); + this._runState(); + this._runEncoder(); + } + } + + renderProgressive() { + if (this.renderContext?.beforeTransformProgressive) { + this.renderBeforeProgressive(); + return; + } + } } diff --git a/packages/vchart/src/mark/box-plot.ts b/packages/vchart/src/mark/box-plot.ts index 95cf5f71a6..5571188e87 100644 --- a/packages/vchart/src/mark/box-plot.ts +++ b/packages/vchart/src/mark/box-plot.ts @@ -9,6 +9,36 @@ import type { Datum } from '../typings/common'; import { isValidNumber } from '@visactor/vutils'; import { registerLine, registerRect } from '@visactor/vrender-kits'; +const BAR_BOX_PLOT_CHANNELS = [ + 'x', + 'y', + 'minMaxWidth', + 'minMaxHeight', + 'q1q3Width', + 'q1q3Height', + 'q1', + 'q3', + 'min', + 'max', + 'median', + 'angle' +]; + +const BOX_PLOT_CHANNELS = [ + 'x', + 'y', + 'boxWidth', + 'boxHeight', + 'ruleWidth', + 'ruleHeight', + 'q1', + 'q3', + 'min', + 'max', + 'median', + 'angle' +]; + export class BoxPlotMark extends GlyphMark implements IBoxPlotMark @@ -42,8 +72,14 @@ export class BoxPlotMark defaultAttributes: { x: 0, y: 0 } } }; + this._positionChannels = BAR_BOX_PLOT_CHANNELS; + this._channelEncoder = { + minMaxFillOpacity: (val: number) => ({ minMaxBox: { fillOpacity: val } }), + lineWidth: (val: number) => ({ minMaxBox: { lineWidth: 0 }, q1q3Box: { lineWidth: 0 } }), + stroke: (val: number) => ({ minMaxBox: { stroke: false }, q1q3Box: { stroke: false } }) + }; - this._functionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + this._positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { const { x = g.attribute.x, y = g.attribute.y, @@ -134,8 +170,9 @@ export class BoxPlotMark min: { type: 'line', defaultAttributes: { x: 0, y: 0 } }, median: { type: 'line', defaultAttributes: { x: 0, y: 0 } } }; - - this._functionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + this._positionChannels = BOX_PLOT_CHANNELS; + this._channelEncoder = null; + this._positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { const { x = g.attribute.x, y = g.attribute.y, diff --git a/packages/vchart/src/mark/cell.ts b/packages/vchart/src/mark/cell.ts index cbc6d186bc..7350ccf5de 100644 --- a/packages/vchart/src/mark/cell.ts +++ b/packages/vchart/src/mark/cell.ts @@ -24,14 +24,14 @@ export class CellMark extends BaseMark implements ICellMark { return defaultStyle; } - createGraphic(attrs: any = {}): IGraphic { + protected _createGraphic(attrs: any = {}): IGraphic { return createSymbol(attrs); } protected _transformGraphicAttributes(g: IGraphic, attrs: any, groupAttrs?: any) { const symbolAttrs = super._transformGraphicAttributes(g, attrs, groupAttrs); const symbolType = - symbolAttrs.shape ?? symbolAttrs.symbolType ?? (g.attribute as ISymbolGraphicAttribute).symbolType; + symbolAttrs.shape ?? symbolAttrs.symbolType ?? (g.attribute as ISymbolGraphicAttribute)?.symbolType; if (isNil(symbolType)) { symbolAttrs.symbolType = 'rect'; diff --git a/packages/vchart/src/mark/component.ts b/packages/vchart/src/mark/component.ts index 55afebaf52..c91930e529 100644 --- a/packages/vchart/src/mark/component.ts +++ b/packages/vchart/src/mark/component.ts @@ -32,8 +32,8 @@ export class ComponentMark extends BaseMark implements IComponentMa return this._component; } - getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}) { - const configAttrs = super.getAttrsFromConfig(attrs); + protected _getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}) { + const configAttrs = super._getAttrsFromConfig(attrs); if (!isNil(this._markConfig.interactive)) { configAttrs.pickable = this._markConfig.interactive; @@ -46,13 +46,9 @@ export class ComponentMark extends BaseMark implements IComponentMa this._attributesTransform = t; } - render(): void { - if (!this._isCommited) { - return; - } - + renderInner() { const { [STATE_VALUE_ENUM.STATE_NORMAL]: normalStyle } = this.stateStyle; - let attrs = this.getAttrsFromConfig({ ...normalStyle } as unknown as IGroupGraphicAttribute); + let attrs = this._getAttrsFromConfig({ ...normalStyle } as unknown as IGroupGraphicAttribute); if (this._attributesTransform) { attrs = this._attributesTransform(attrs); @@ -63,25 +59,33 @@ export class ComponentMark extends BaseMark implements IComponentMa mode: this._mode, skipDefault: this._markConfig.skipTheme }); - this._product.appendChild(this._component); + this._component && this._product.appendChild(this._component); } else { this._component.setAttributes(attrs as any); } + if (this._component) { + this._component.context = this._getCommonContext(); + } + + if (this._markConfig.support3d && this._product) { + this._product.setMode('3d'); + } + } + + render(): void { + if (!this._isCommited) { + return; + } + this.renderInner(); + this.uncommit(); } - /** 创建语法元素对象 */ - // protected _initProduct(group?: string | IGroupMark) { - // const view = this.getVGrammarView(); - - // // 声明语法元素 - // const id = this.getProductId(); - // this._product = view - // .mark(GrammarMarkType.component, group ?? view.rootMark, { componentType: this._componentType, mode: this._mode }) - // .id(id); - // this._compiledProductId = id; - // } + release() { + super.release(); + this.removeProduct(); + } } export const registerComponentMark = () => { diff --git a/packages/vchart/src/mark/glyph.ts b/packages/vchart/src/mark/glyph.ts index 71561e62a1..77530da1b1 100644 --- a/packages/vchart/src/mark/glyph.ts +++ b/packages/vchart/src/mark/glyph.ts @@ -7,6 +7,9 @@ import type { MarkType } from './interface/type'; import { Factory } from '../core/factory'; import type { Datum } from '../typings/common'; import { registerGlyph, registerShadowRoot } from '@visactor/vrender-kits'; +import type { IMarkGraphic } from './interface/common'; +import { DiffState } from './interface/enum'; +import { merge } from '@visactor/vutils'; export abstract class GlyphMark extends BaseMark implements IGlyphMark @@ -35,30 +38,76 @@ export abstract class GlyphMark return this._glyphConfig; } - protected _progressiveChannels: string[]; + protected _positionChannels: string[]; - getProgressiveChannels() { - return this._progressiveChannels; + getPositionChannels() { + return this._positionChannels; } - protected _functionEncoder: (glyphAttrs: any, datum: Datum, g: IGlyph) => Record; + protected _positionEncoder: (glyphAttrs: any, datum: Datum, g: IGlyph) => Record; + + protected _channelEncoder: Record Record>; private _onGlyphAttributeUpdate(glyph: IGlyph) { return (newAttributes: any) => { - const subAttrsMap = this._functionEncoder?.(newAttributes, glyph?.context?.data?.[0], glyph); + const positionChannels = this.getPositionChannels(); + let subAttrsMap = + positionChannels && this._positionEncoder && Object.keys(newAttributes).some(k => positionChannels.includes(k)) + ? this._positionEncoder(newAttributes, glyph?.context?.data?.[0], glyph) + : null; + + if (this._channelEncoder) { + Object.keys(this._channelEncoder).forEach(channel => { + if (channel in newAttributes) { + const channelAttrsMap = this._channelEncoder[channel](newAttributes[channel]); + + subAttrsMap = subAttrsMap ? merge(subAttrsMap, channelAttrsMap) : channelAttrsMap; + } + }); + } - glyph.getSubGraphic().forEach(subGraphic => { - if (subGraphic) { - subGraphic.setAttributes(subAttrsMap?.[subGraphic.name]); - } - }); + if (subAttrsMap) { + glyph.getSubGraphic().forEach(subGraphic => { + if (subGraphic && subAttrsMap[subGraphic.name]) { + subGraphic.setAttributes(subAttrsMap[subGraphic.name]); + } + }); + } return newAttributes; }; } - createGraphic(attrs: IGlyphGraphicAttribute = {}): IGraphic { + protected _setStateOfGraphic = (g: IMarkGraphic) => { + g.clearStates(); + g.stateProxy = null; + + if (g.context.diffState === DiffState.enter || g.context.diffState === DiffState.update) { + g.glyphStateProxy = (stateName: string, nexStates: string[]) => { + const glyphAttrs = { + attributes: { + ...this._runEncoderOfGraphic(this._encoderOfState?.[stateName], g), + ...(g.runtimeStateCache ? g.runtimeStateCache[stateName] : null) + } + }; + + // 更新缓存 + if (!g.glyphStates) { + g.glyphStates = { [stateName]: glyphAttrs }; + } else if (!g.glyphStates[stateName]) { + g.glyphStates[stateName] = glyphAttrs; + } + + return glyphAttrs; + }; + + g.useStates(g.context.states); + } + }; + + protected _createGraphic(attrs: IGlyphGraphicAttribute = {}): IGraphic { const glyph = createGlyph(attrs); + glyph.onBeforeAttributeUpdate = this._onGlyphAttributeUpdate(glyph); const subMarks = this._subMarks; if (subMarks) { @@ -83,26 +132,10 @@ export abstract class GlyphMark glyph.setSubGraphic(subGraphics); } - glyph.onBeforeAttributeUpdate = this._onGlyphAttributeUpdate(glyph); + (glyph as any).onBeforeAttributeUpdate(attrs); return glyph; } - - /** 创建语法元素对象 */ - // protected _initProduct(group?: string | IGroupMark) { - // const shaftShape = this.getStyle('shaftShape'); - // const view = this.getVGrammarView(); - - // // 声明语法元素 - // const id = this.getProductId(); - // const glyphType = shaftShape === 'bar' ? BAR_BOX_PLOT_GLYPH_TYPE : BOX_PLOT_GLYPH_TYPE; - // const direction = this.getStyle('direction'); - // this._product = view - // .glyph(glyphType, group ?? view.rootMark) - // .id(id) - // .configureGlyph({ direction }); - // this._compiledProductId = id; - // } } export const registerGlyphMark = () => { diff --git a/packages/vchart/src/mark/group.ts b/packages/vchart/src/mark/group.ts index 79aa32d755..5ae5e3aaac 100644 --- a/packages/vchart/src/mark/group.ts +++ b/packages/vchart/src/mark/group.ts @@ -4,7 +4,7 @@ import type { Maybe } from '../typings'; import { warn } from '../util/debug'; import type { IGroupMarkSpec } from '../typings/visual'; import { BaseMark } from './base/base-mark'; -import type { IGroupMark, IMark, IMarkStyle, MarkType } from './interface'; +import type { IGroupMark, IMark, IMarkGraphic, MarkType } from './interface'; // eslint-disable-next-line no-duplicate-imports import { MarkTypeEnum } from './interface/type'; import { STATE_VALUE_ENUM, type IMarkCompileOption } from '../compile/mark'; @@ -93,16 +93,12 @@ export class GroupMark extends BaseMark implements IGroupMark { // 编译子元素 this.getMarks().forEach(mark => { - // TODO: 如果语法元素已创建,先删除再重新指定父结点生成。vgrammar 是否可以动态指定 mark 父结点? - if (mark.getProduct()) { - mark.removeProduct(); - } mark.compile({ group: this._product }); }); } - getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}) { - const configAttrs = super.getAttrsFromConfig(attrs); + protected _getAttrsFromConfig(attrs: IGroupGraphicAttribute = {}) { + const configAttrs = super._getAttrsFromConfig(attrs); if (!isNil(this._markConfig.interactive)) { configAttrs.pickable = this._markConfig.interactive; @@ -110,7 +106,15 @@ export class GroupMark extends BaseMark implements IGroupMark { return attrs; } + getGraphics(): IMarkGraphic[] { + return [this._product]; + } + protected _renderSelf() { + if (!this._product) { + return; + } + const { [STATE_VALUE_ENUM.STATE_NORMAL]: normalStyle } = this.stateStyle; const attrs: any = {}; @@ -121,8 +125,8 @@ export class GroupMark extends BaseMark implements IGroupMark { attrs[key] = this._computeAttribute(key, 'normal')({}); }); - - this._product?.setAttributes(this.getAttrsFromConfig(attrs)); + this._product.context = this._getCommonContext(); + this._product.setAttributes(this._getAttrsFromConfig(attrs)); } render(): void { @@ -135,6 +139,11 @@ export class GroupMark extends BaseMark implements IGroupMark { mark.render(); }); } + + release() { + super.release(); + this.removeProduct(); + } } export const registerGroupMark = () => { diff --git a/packages/vchart/src/mark/index.ts b/packages/vchart/src/mark/index.ts index 642523d22c..04f36f6477 100644 --- a/packages/vchart/src/mark/index.ts +++ b/packages/vchart/src/mark/index.ts @@ -22,6 +22,8 @@ import { Pyramid3dMark, registerPyramid3dMark } from './polygon/pyramid-3d'; import { ImageMark, registerImageMark } from './image'; import { LiquidMark, registerLiquidMark } from './liquid'; import { BoxPlotMark, registerBoxPlotMark } from './box-plot'; +import { registerMarkFilterTransform } from './transform/filter'; +import { registerMarkMapTransform } from './transform/map'; export type { IBoxPlotMarkSpec, @@ -82,7 +84,9 @@ export { registerPyramid3dMark, registerRippleMark, registerImageMark, - registerComponentMark + registerComponentMark, + registerMarkMapTransform, + registerMarkFilterTransform }; export const registerAllMarks = () => { diff --git a/packages/vchart/src/mark/interface/common.ts b/packages/vchart/src/mark/interface/common.ts index 32692543b1..35365a7e0c 100644 --- a/packages/vchart/src/mark/interface/common.ts +++ b/packages/vchart/src/mark/interface/common.ts @@ -2,7 +2,7 @@ import type { IGlobalScale } from '../../scale/interface'; import type { ICommonSpec, VisualType, ValueType, FunctionType } from '../../typings/visual'; import type { IModel } from '../../model/interface'; import type { IBaseScale } from '@visactor/vscale'; -import type { MarkType } from './type'; +import type { MarkType, MarkTypeEnum } from './type'; import type { ICompilableMark, ICompilableMarkOption, @@ -10,8 +10,8 @@ import type { IModelMarkAttributeContext, StateValueType } from '../../compile/mark/interface'; -import type { StringOrNumber } from '../../typings'; -import type { ICustomPath2D, IGraphic } from '@visactor/vrender-core'; +import type { Datum, StringOrNumber } from '../../typings'; +import type { ICustomPath2D, IGlyph, IGraphic } from '@visactor/vrender-core'; import type { IGroupMark } from './mark'; export interface VisualScaleType { @@ -55,6 +55,61 @@ export type IMarkStateStyle = Record = { [key in keyof T]: MarkInputStyle; }; + +export type DiffStateValues = 'update' | 'enter' | 'exit'; + +export interface IGraphicContext { + markType: MarkTypeEnum; + /** + * 图形所属mark对应的id,自增id + */ + markId: number; + /** + * 图形所属model对应的id,自增id + */ + modelId: number; + /** + * 图形所属mark对应的用户设置id + */ + markUserId?: number | string; + /** + * 图形所属model对应的用户设置id + */ + modelUserId?: number | string; + /** + * 数据对比状态 + */ + diffState?: DiffStateValues; + /** + * 数据 + */ + data?: Datum[]; + /** + * 唯一key + */ + key?: string; + /** + * 分组key + */ + groupKey?: string; + /** + * 状态 + */ + states?: string[]; +} + +export interface IMarkGraphic extends IGraphic { + /** + * 缓存运行时的状态编码数据 + */ + runtimeStateCache?: Record; + + /** + * 上下文数据 + */ + context?: IGraphicContext; +} + /********** mark ***************/ export interface IMarkRaw extends ICompilableMark { readonly stateStyle: IMarkStateStyle; @@ -66,10 +121,10 @@ export interface IMarkRaw extends ICompilableMark { /** @deprecated VChart 层尽量使用 IModel.setMarkStyle() */ setStyle: (style: Partial>, state?: StateValueType, level?: number) => void; - setReferer: (mark: IMarkRaw, styleKey?: string, state?: StateValueType, stateStyle?: IMarkStateStyle) => void; + setReferer: (mark: IMarkRaw, styleKey?: string, state?: StateValueType) => void; /** @deprecated VChart 层尽量使用 IModel.initMarkStyleWithSpec() */ - initStyleWithSpec: (spec: any, key?: string) => void; + initStyleWithSpec: (spec: any) => void; created: () => void; @@ -84,7 +139,22 @@ export interface IMarkRaw extends ICompilableMark { render: () => void; - getGraphics: () => IGraphic[]; + getGraphics: () => IMarkGraphic[]; + + reuse: (mark: IMark) => void; + + /** 是否启动了增量渲染模式 */ + isProgressive: () => boolean; + /** 是否正在执行增量渲染 */ + isDoingProgressive: () => boolean; + /** 清除增量渲染相关状态 */ + clearProgressive: () => void; + /** 从第一帧开始增量计算 */ + restartProgressive: () => void; + /** 分片执行 */ + renderProgressive: () => void; + /** 增量流程后,是否执行动画 */ + canAnimateAfterProgressive: () => boolean; } export type IMark = IMarkRaw; @@ -212,3 +282,12 @@ export type IMarkDataTransform = ( options: Options, data: Input ) => Output | IProgressiveTransformResult; + +export interface ProgressiveContext { + currentIndex: number; + totalStep: number; + step: number; + data: any[]; + groupKeys?: string[]; + groupedData?: Map; +} diff --git a/packages/vchart/src/mark/interface/mark.ts b/packages/vchart/src/mark/interface/mark.ts index a1fe211e42..c3e49d0230 100644 --- a/packages/vchart/src/mark/interface/mark.ts +++ b/packages/vchart/src/mark/interface/mark.ts @@ -1,4 +1,4 @@ -import type { IGroup } from '@visactor/vrender-core'; +import type { IGraphic } from '@visactor/vrender-core'; import type { IMarkSpec } from '../../typings'; import type { IArc3dMarkSpec, @@ -25,7 +25,8 @@ import type { IMark, IMarkRaw } from './common'; import type { MarkType } from './type'; export interface IComponentMark extends IMarkRaw { - getComponent: () => IGroup; + renderInner: () => void; + getComponent: () => IGraphic; setAttributeTransform: (t: (attrs: any) => any) => any; } @@ -41,7 +42,7 @@ export interface IGlyphMark extend } >; - getProgressiveChannels: () => string[]; + getPositionChannels: () => string[]; } export interface ILabelMark extends ITextMark { diff --git a/packages/vchart/src/mark/label.ts b/packages/vchart/src/mark/label.ts index a246501500..ceddcffc13 100644 --- a/packages/vchart/src/mark/label.ts +++ b/packages/vchart/src/mark/label.ts @@ -2,7 +2,7 @@ import { Factory } from './../core/factory'; import type { IMark } from './interface/common'; import { MarkTypeEnum } from './interface/type'; import { registerTextMark, TextMark } from './text'; -import type { ILabelMark } from './interface/mark'; +import type { IComponentMark, ILabelMark } from './interface/mark'; export class LabelMark extends TextMark implements ILabelMark { static readonly type = MarkTypeEnum.text; @@ -29,11 +29,11 @@ export class LabelMark extends TextMark implements ILabelMark { } } - private _component: IMark; + private _component: IComponentMark; getComponent() { return this._component; } - setComponent(component: IMark) { + setComponent(component: IComponentMark) { this._component = component; } diff --git a/packages/vchart/src/mark/link-path.ts b/packages/vchart/src/mark/link-path.ts index 14225e3c0c..37e62eb6f4 100644 --- a/packages/vchart/src/mark/link-path.ts +++ b/packages/vchart/src/mark/link-path.ts @@ -187,7 +187,31 @@ export class LinkPathMark extends GlyphMark } }; - protected _functionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + protected _positionChannels: string[] = [ + 'x0', + 'y0', + 'x1', + 'y1', + 'thickness', + 'round', + 'curvature', + 'ratio', + 'pathType', + 'align', + 'endArrow', + 'startArrow', + 'ratio' + ]; + + protected _channelEncoder = { + backgroundStyle: (val: any) => { + return { + back: val + }; + } + }; + + protected _positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { const newAttrs = { ...g.attribute, ...glyphAttrs diff --git a/packages/vchart/src/mark/liquid.ts b/packages/vchart/src/mark/liquid.ts index 46876a5214..888eb35566 100644 --- a/packages/vchart/src/mark/liquid.ts +++ b/packages/vchart/src/mark/liquid.ts @@ -46,7 +46,9 @@ export class LiquidMark extends GlyphMark implements ILiquidMar } }; - protected _functionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + protected _positionChannels: string[] = ['wave', 'y', 'height']; + + protected _positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { const { wave = (g.attribute as any).wave, y = (g.attribute as any).y, diff --git a/packages/vchart/src/mark/ripple.ts b/packages/vchart/src/mark/ripple.ts index 8931114307..72aa265868 100644 --- a/packages/vchart/src/mark/ripple.ts +++ b/packages/vchart/src/mark/ripple.ts @@ -44,7 +44,9 @@ export class RippleMark extends GlyphMark implements IRippleMar } }; - protected _functionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + protected _positionChannels: string[] = ['ripple']; + + protected _positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { const { ripple = (g.attribute as any).ripple, size = (g.attribute as any).size } = glyphAttrs; const r = clamp(ripple, 0, 1); const rippleSize = size * 0.5; diff --git a/packages/vchart/src/mark/rule.ts b/packages/vchart/src/mark/rule.ts index 3991665627..b85febc58d 100644 --- a/packages/vchart/src/mark/rule.ts +++ b/packages/vchart/src/mark/rule.ts @@ -33,7 +33,7 @@ export class RuleMark extends BaseMark implements IRuleMark { }; } - createGraphic(attrs: ILineGraphicAttribute = {}): IGraphic { + protected _createGraphic(attrs: ILineGraphicAttribute = {}): IGraphic { return createLine(attrs); } } diff --git a/packages/vchart/src/mark/symbol.ts b/packages/vchart/src/mark/symbol.ts index 11c05f3df9..91552c7795 100644 --- a/packages/vchart/src/mark/symbol.ts +++ b/packages/vchart/src/mark/symbol.ts @@ -4,9 +4,11 @@ import { BaseMark } from './base/base-mark'; import type { IMarkStyle, ISymbolMark } from './interface'; // eslint-disable-next-line no-duplicate-imports import { MarkTypeEnum } from './interface/type'; +import type { IGraphic, ISymbolGraphicAttribute } from '@visactor/vrender-core'; import { createSymbol } from '@visactor/vrender-core'; import { registerShadowRoot, registerSymbol } from '@visactor/vrender-kits'; import { registerSymbolDataLabel } from '@visactor/vrender-components'; +import { isNil } from '@visactor/vutils'; export class SymbolMark extends BaseMark implements ISymbolMark { static readonly type = MarkTypeEnum.symbol; @@ -21,6 +23,19 @@ export class SymbolMark extends BaseMark implements ISymbolMark }; return defaultStyle as IMarkStyle; } + + protected _transformGraphicAttributes(g: IGraphic, attrs: any, groupAttrs?: any) { + const symbolAttrs = super._transformGraphicAttributes(g, attrs, groupAttrs); + const symbolType = + symbolAttrs.shape ?? symbolAttrs.symbolType ?? (g.attribute as ISymbolGraphicAttribute)?.symbolType; + + if (isNil(symbolType)) { + symbolAttrs.symbolType = 'circle'; + } else { + symbolAttrs.symbolType = symbolType; + } + return symbolAttrs; + } } export const registerSymbolMark = () => { diff --git a/packages/vchart/src/mark/text.ts b/packages/vchart/src/mark/text.ts index 3f472010ac..9c3e9c8202 100644 --- a/packages/vchart/src/mark/text.ts +++ b/packages/vchart/src/mark/text.ts @@ -35,8 +35,8 @@ export class TextMark extends BaseMark implements ITextMa return defaultStyle; } - initStyleWithSpec(spec: ITextSpec, key?: string) { - super.initStyleWithSpec(spec, key); + initStyleWithSpec(spec: ITextSpec) { + super.initStyleWithSpec(spec); if (spec.textType) { this._textType = spec.textType; } @@ -47,15 +47,17 @@ export class TextMark extends BaseMark implements ITextMa const { text } = textAttrs; - if ((isObject(text) && this._textType === 'rich', isValid((text as any).text))) { + if (isObject(text) && isValid((text as any).text) && (this._textType === 'rich' || (text as any).type === 'rich')) { textAttrs.textConfig = (text as any).text; } return textAttrs; } - createGraphic(attrs: ITextGraphicAttribute | IRichTextGraphicAttribute = {}): IGraphic { - return this._textType === 'rich' + protected _createGraphic(attrs: ITextGraphicAttribute | IRichTextGraphicAttribute = {}): IGraphic { + const { text } = attrs; + + return this._textType === 'rich' || (text as any)?.type === 'rich' ? createRichText(attrs as IRichTextGraphicAttribute) : createText(attrs as ITextGraphicAttribute); } diff --git a/packages/vchart/src/mark/transform/symbol-overlap.ts b/packages/vchart/src/mark/transform/symbol-overlap.ts index c2488d811a..a87158b65c 100644 --- a/packages/vchart/src/mark/transform/symbol-overlap.ts +++ b/packages/vchart/src/mark/transform/symbol-overlap.ts @@ -1,12 +1,13 @@ -import type { IGraphic, ISymbolGraphicAttribute } from '@visactor/vrender-core'; +import type { ISymbolGraphicAttribute } from '@visactor/vrender-core'; import { PREFIX } from '../../constant/base'; import { isNil } from '@visactor/vutils'; import { Factory } from '../../core/factory'; +import type { IMarkGraphic } from '../interface'; import { MarkTypeEnum } from '../interface'; export const OVERLAP_HIDE_KEY = `${PREFIX}_hide_`; -function reset(graphics: IGraphic[]) { +function reset(graphics: IMarkGraphic[]) { graphics.forEach(g => { const hide = g[OVERLAP_HIDE_KEY]; @@ -19,7 +20,7 @@ function reset(graphics: IGraphic[]) { return graphics; } -function overlapX(graphics: IGraphic[], delta: number, deltaMul: number, useRadius: boolean) { +function overlapX(graphics: IMarkGraphic[], delta: number, deltaMul: number, useRadius: boolean) { if (useRadius) { let lastX = -Infinity; let lastR = 0; @@ -51,7 +52,7 @@ function overlapX(graphics: IGraphic[], delta: number, deltaMul: number, useRadi } } -function overlapY(graphics: IGraphic[], delta: number, deltaMul: number, useRadius: boolean) { +function overlapY(graphics: IMarkGraphic[], delta: number, deltaMul: number, useRadius: boolean) { if (useRadius) { let lastY = -Infinity; let lastR = 0; @@ -83,7 +84,7 @@ function overlapY(graphics: IGraphic[], delta: number, deltaMul: number, useRadi } } -function overlapXY(graphics: IGraphic[], delta: number, deltaMul: number, useRadius: boolean) { +function overlapXY(graphics: IMarkGraphic[], delta: number, deltaMul: number, useRadius: boolean) { if (useRadius) { const lastX = -Infinity; let lastY = -Infinity; @@ -137,7 +138,7 @@ export const transform = ( groupBy?: string; sort?: boolean; }, - upstreamData: IGraphic[] + upstreamData: IMarkGraphic[] ) => { if (!upstreamData || upstreamData.length === 0) { return; @@ -151,7 +152,7 @@ export const transform = ( const { direction, delta, deltaMul = 1, groupBy } = options; - const handleOverlap = (graphics: IGraphic[]) => { + const handleOverlap = (graphics: IMarkGraphic[]) => { reset(graphics); const sortedgraphics = options.sort @@ -173,7 +174,7 @@ export const transform = ( handleOverlap(upstreamData); } else { // 分组 - const map = upstreamData.reduce((res: { [key: string]: IGraphic[] }, g: IGraphic) => { + const map = upstreamData.reduce((res: { [key: string]: IMarkGraphic[] }, g: IMarkGraphic) => { const groupName = g.context.data?.[0]?.[groupBy]; if (res[groupName]) { diff --git a/packages/vchart/src/mark/utils/common.ts b/packages/vchart/src/mark/utils/common.ts index d96c12d674..f6a30d0c5d 100644 --- a/packages/vchart/src/mark/utils/common.ts +++ b/packages/vchart/src/mark/utils/common.ts @@ -3,9 +3,6 @@ import type { Datum } from '../../typings'; import { Direction } from '../../typings'; import type { GroupedData } from '../interface/common'; import { DEFAULT_KEY } from '../../constant/data'; -import { isNil, isString } from '@visactor/vutils'; -import type { IColor, IColorStop } from '@visactor/vrender-core'; -import { Factory } from '../../core/factory'; export const MultiDatumMark = [MarkTypeEnum.line, MarkTypeEnum.area, 'trail']; @@ -26,7 +23,7 @@ export function is3DMark(type: MarkTypeEnum) { export function groupData( data: T[], - groupBy: (datum: Datum) => string, + groupBy: (datum: Datum, index: number) => string, sort?: (a: T, b: T) => number ): GroupedData { const groupedData = new Map(); @@ -40,7 +37,7 @@ export function groupData( // const keyGetter = parseField(key); if (data.length === 1) { - const key = groupBy(data[0]); + const key = groupBy(data[0], 0); groupedData.set(key, [data[0]]); return { @@ -50,8 +47,8 @@ export function groupData( } const keys = new Set(); - data.forEach(entry => { - const key = groupBy(entry); + data.forEach((entry, index) => { + const key = groupBy(entry, index); const lastData = groupedData.get(key) ?? []; lastData.push(entry); groupedData.set(key, lastData); @@ -66,100 +63,10 @@ export function groupData( } export const runEncoder = (styles: Record any>, datum: Datum, attrs: any = {}) => { - Object.keys(styles).forEach(attrName => { - attrs[attrName] = styles[attrName](datum); - }); + styles && + Object.keys(styles).forEach(attrName => { + attrs[attrName] = styles[attrName](datum); + }); return attrs; }; - -export const isStopsEqual = (prev: IColorStop[], next: IColorStop[]) => { - if (prev === next) { - return true; - } - const prevLength = (prev && prev.length) ?? 0; - const nextLength = (next && next.length) ?? 0; - - if (prevLength !== nextLength || prevLength === 0) { - return false; - } - - return prev.every((prevEntry, prevIndex) => { - return ( - (!prevEntry && !next[prevIndex]) || - (prevEntry && - next[prevIndex] && - prevEntry.color === next[prevIndex].color && - prevEntry.offset === next[prevIndex].offset) - ); - }); -}; - -const isColorAttrEqual = (prev: IColor, next: IColor) => { - if (prev === next) { - return true; - } - - if (typeof prev !== typeof next) { - return false; - } - - if (isString(prev)) { - return false; - } - - if (prev.gradient !== (next as any).gradient) { - return false; - } - - const prevKeys = Object.keys(prev); - const nextKeys = Object.keys(next); - - if (prevKeys.length !== nextKeys.length) { - return false; - } - - return prevKeys.every(key => { - if (key === 'stops') { - return isStopsEqual(prev[key], (next as any)[key]); - } - - return (prev as any)[key] === (next as any)[key]; - }); -}; - -const isLineDashEqual = (prev: number[], next: number[]) => { - if (prev.length !== next.length) { - return false; - } - - if (prev.join('-') === next.join('-')) { - return true; - } - - return false; -}; - -export const isSegmentAttrEqual = (prev: any, next: any, key: string) => { - if (isNil(prev) && isNil(next)) { - return true; - } - - if (isNil(prev)) { - return false; - } - - if (isNil(next)) { - return false; - } - - if (key === 'lineDash') { - return isLineDashEqual(prev, next); - } - - if (key === 'stroke' || key === 'fill') { - return isColorAttrEqual(prev, next); - } - - return prev === next; -}; diff --git a/packages/vchart/src/mark/utils/glyph.ts b/packages/vchart/src/mark/utils/glyph.ts new file mode 100644 index 0000000000..d500f223a7 --- /dev/null +++ b/packages/vchart/src/mark/utils/glyph.ts @@ -0,0 +1,17 @@ +import type { IMarkGraphic } from '../interface/common'; + +export const addRuntimeState = ( + g: IMarkGraphic, + stateName: string, + attrs: any, + keepCurrentStates: boolean = true, + hasAnimation?: boolean +) => { + if (!g.runtimeStateCache) { + g.runtimeStateCache = {}; + } + + g.runtimeStateCache[stateName] = attrs; + + g.addState(stateName, keepCurrentStates, hasAnimation); +}; diff --git a/packages/vchart/src/mark/utils/index.ts b/packages/vchart/src/mark/utils/index.ts index d0b9323665..1e5c158970 100644 --- a/packages/vchart/src/mark/utils/index.ts +++ b/packages/vchart/src/mark/utils/index.ts @@ -1 +1,3 @@ export * from './common'; +export * from './line'; +export * from './glyph'; diff --git a/packages/vchart/src/mark/utils/line.ts b/packages/vchart/src/mark/utils/line.ts new file mode 100644 index 0000000000..eaadf790d1 --- /dev/null +++ b/packages/vchart/src/mark/utils/line.ts @@ -0,0 +1,93 @@ +import { isNil, isString } from '@visactor/vutils'; +import type { IColor, IColorStop } from '@visactor/vrender-core'; + +export const isStopsEqual = (prev: IColorStop[], next: IColorStop[]) => { + if (prev === next) { + return true; + } + const prevLength = (prev && prev.length) ?? 0; + const nextLength = (next && next.length) ?? 0; + + if (prevLength !== nextLength || prevLength === 0) { + return false; + } + + return prev.every((prevEntry, prevIndex) => { + return ( + (!prevEntry && !next[prevIndex]) || + (prevEntry && + next[prevIndex] && + prevEntry.color === next[prevIndex].color && + prevEntry.offset === next[prevIndex].offset) + ); + }); +}; + +const isColorAttrEqual = (prev: IColor, next: IColor) => { + if (prev === next) { + return true; + } + + if (typeof prev !== typeof next) { + return false; + } + + if (isString(prev)) { + return false; + } + + if (prev.gradient !== (next as any).gradient) { + return false; + } + + const prevKeys = Object.keys(prev); + const nextKeys = Object.keys(next); + + if (prevKeys.length !== nextKeys.length) { + return false; + } + + return prevKeys.every(key => { + if (key === 'stops') { + return isStopsEqual(prev[key], (next as any)[key]); + } + + return (prev as any)[key] === (next as any)[key]; + }); +}; + +const isLineDashEqual = (prev: number[], next: number[]) => { + if (prev.length !== next.length) { + return false; + } + + if (prev.join('-') === next.join('-')) { + return true; + } + + return false; +}; + +export const isSegmentAttrEqual = (prev: any, next: any, key: string) => { + if (isNil(prev) && isNil(next)) { + return true; + } + + if (isNil(prev)) { + return false; + } + + if (isNil(next)) { + return false; + } + + if (key === 'lineDash') { + return isLineDashEqual(prev, next); + } + + if (key === 'stroke' || key === 'fill') { + return isColorAttrEqual(prev, next); + } + + return prev === next; +}; diff --git a/packages/vchart/src/model/base-model.ts b/packages/vchart/src/model/base-model.ts index a2d7ddbbe3..ae614c8549 100644 --- a/packages/vchart/src/model/base-model.ts +++ b/packages/vchart/src/model/base-model.ts @@ -221,7 +221,7 @@ export abstract class BaseModel extends CompilableBase imp mark.setStyle(this._convertMarkStyle(style), state, level); } - initMarkStyleWithSpec(mark?: IMark, spec?: any, key?: string) { + initMarkStyleWithSpec(mark?: IMark, spec?: any) { if (!isValid(mark) || !isValid(spec)) { return; } @@ -237,7 +237,7 @@ export abstract class BaseModel extends CompilableBase imp newSpec.state[key] = this._convertMarkStyle(state[key]); }); } - mark.initStyleWithSpec(newSpec, key); + mark.initStyleWithSpec(newSpec); } protected stateKeyToSignalName(key: string, opt?: string) { diff --git a/packages/vchart/src/model/interface.ts b/packages/vchart/src/model/interface.ts index 6c831be70f..2bfdbf554c 100644 --- a/packages/vchart/src/model/interface.ts +++ b/packages/vchart/src/model/interface.ts @@ -132,7 +132,7 @@ export interface IModel extends ICompilable { level?: number ) => void; - initMarkStyleWithSpec: (mark?: IMark, spec?: any, key?: string) => void; + initMarkStyleWithSpec: (mark?: IMark, spec?: any) => void; getSpecInfo: () => IModelSpecInfo; } diff --git a/packages/vchart/src/plugin/components/tooltip-handler/base.ts b/packages/vchart/src/plugin/components/tooltip-handler/base.ts index 572d37136a..91a76f492c 100644 --- a/packages/vchart/src/plugin/components/tooltip-handler/base.ts +++ b/packages/vchart/src/plugin/components/tooltip-handler/base.ts @@ -28,7 +28,6 @@ import type { IGroup } from '@visactor/vrender-core'; import type { AABBBounds } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports import { isNumber, isObject, isValidNumber, isValid, isFunction } from '@visactor/vutils'; -import type { IElement } from '@visactor/vgrammar-core'; import type { ILayoutModel } from '../../../model/interface'; import type { IContainerSize } from '@visactor/vrender-components'; import type { IChartOption } from '../../../chart/interface'; @@ -293,8 +292,8 @@ export abstract class BaseTooltipHandler extends BasePlugin implements ITooltipH if (mode === 'mark') { isFixedPosition = true; - const element = params.item as IElement; - const bounds = element?.getBounds() as AABBBounds; + const markGraphic = params.item; + const bounds = markGraphic?.AABBBounds as AABBBounds; if (bounds && startPoint) { dim1 = (dim === 'x' ? bounds.x1 : bounds.y1) + startPoint[dim]; dim2 = (dim === 'x' ? bounds.x2 : bounds.y2) + startPoint[dim]; diff --git a/packages/vchart/src/plugin/other.ts b/packages/vchart/src/plugin/other.ts index 5d27c6eb8d..03064b6e84 100644 --- a/packages/vchart/src/plugin/other.ts +++ b/packages/vchart/src/plugin/other.ts @@ -8,8 +8,8 @@ import { // import { registerViewMorphAPI, registerAnimate as registerAnimateAPI } from '@visactor/vgrammar-core'; import { registerVGrammarCommonAnimation } from '../animation/config'; -import { Factory } from '../core'; import { DragNDrop, Gesture } from '@visactor/vrender-kits'; +import { Factory } from '../core/factory'; export const register3DPlugin = () => { registerDirectionalLight(); diff --git a/packages/vchart/src/region/interface.ts b/packages/vchart/src/region/interface.ts index 02b046778d..94375532c0 100644 --- a/packages/vchart/src/region/interface.ts +++ b/packages/vchart/src/region/interface.ts @@ -2,7 +2,6 @@ import type { IMark, IGroupMark } from '../mark/interface'; import type { ILayoutModel, IModelConstructor, IModelOption, IModelSpecInfo } from '../model/interface'; import type { ISeries, SeriesType } from '../series/interface'; import type { CoordinateType } from '../typings/coordinate'; -import type { IInteraction } from '../interaction/interface'; import type { IProjectionSpec } from '../component/geo/interface'; import type { ConvertToMarkStyleSpec, IRectMarkSpec } from '../typings/visual'; import type { IAnimate } from '../animation/interface'; @@ -11,7 +10,6 @@ import type { ILayoutItemSpec } from '../layout/interface'; export interface IRegion extends ILayoutModel { animate?: IAnimate; - interaction: IInteraction; //stack getStackInverse: () => boolean; diff --git a/packages/vchart/src/region/region.ts b/packages/vchart/src/region/region.ts index fbc083e48c..3488a25965 100644 --- a/packages/vchart/src/region/region.ts +++ b/packages/vchart/src/region/region.ts @@ -1,14 +1,11 @@ // eslint-disable-next-line no-duplicate-imports import { isEmpty, isEqual, array, isValid } from '@visactor/vutils'; -import { STATE_VALUE_ENUM_REVERSE } from '../compile/mark/interface'; -import { DimensionTrigger } from '../interaction/dimension-trigger'; import { MarkTypeEnum } from '../mark/interface/type'; import type { ISeries } from '../series/interface'; import type { IModelOption } from '../model/interface'; import type { CoordinateType } from '../typings/coordinate'; import type { IGeoRegionSpec, IRegion, IRegionSpec, IRegionSpecInfo } from './interface'; -import type { IInteraction, ITrigger } from '../interaction/interface'; -import { Interaction } from '../interaction/interaction'; +import type { IInteraction } from '../interaction/interface/common'; import { ChartEvent } from '../constant/event'; import { LayoutZIndex } from '../constant/layout'; import { AttributeLevel } from '../constant/attribute'; @@ -35,8 +32,6 @@ export class Region extends LayoutModel animate?: IAnimate; - interaction: IInteraction = new Interaction(); - declare getSpecInfo: () => IRegionSpecInfo; getMaxWidth() { @@ -74,8 +69,6 @@ export class Region extends LayoutModel protected _backgroundMark?: IRectMark; protected _foregroundMark?: IRectMark; - protected _trigger: ITrigger; - constructor(spec: T, ctx: IModelOption) { super(spec, ctx); this.userId = spec.id; @@ -85,7 +78,6 @@ export class Region extends LayoutModel getCompiler: ctx.getCompiler }); } - this.interaction.setDisableActiveEffect(this._option.disableTriggerEvent); } protected _getClipDefaultValue() { @@ -120,9 +112,19 @@ export class Region extends LayoutModel // hack: region 的样式不能设置在groupMark上,因为groupMark目前没有计算dirtyBound,会导致拖影问题 if (!isEmpty(this._spec.style)) { - this._backgroundMark = this._createMark({ type: MarkTypeEnum.rect, name: 'regionBackground' }) as IRectMark; + this._backgroundMark = this._createMark( + { type: MarkTypeEnum.rect, name: 'regionBackground' }, + { + parent: this._groupMark + } + ) as IRectMark; if (clip) { - this._foregroundMark = this._createMark({ type: MarkTypeEnum.rect, name: 'regionForeground' }) as IRectMark; + this._foregroundMark = this._createMark( + { type: MarkTypeEnum.rect, name: 'regionForeground' }, + { + parent: this._groupMark + } + ) as IRectMark; } [this._backgroundMark, this._foregroundMark].forEach(mark => { if (mark) { @@ -142,7 +144,6 @@ export class Region extends LayoutModel this._backgroundMark && this._backgroundMark.setMarkConfig({ zIndex: LayoutZIndex.SeriesGroup - 1 }); this._foregroundMark && this._foregroundMark.setMarkConfig({ zIndex: LayoutZIndex.Mark + 1 }); } - this.createTrigger(); } private _createGroupMark(name: string, userId: StringOrNumber, zIndex: number) { @@ -179,8 +180,6 @@ export class Region extends LayoutModel super.init(option); this.initMark(); this.initSeriesDataflow(); - this.initInteraction(); - this.initTrigger(); } initMark() { this._initBackgroundMarkStyle(); @@ -323,43 +322,6 @@ export class Region extends LayoutModel super.release(); this._series = []; } - /** dimension */ - createTrigger() { - const triggerOptions = { - ...this._option, - model: this, - interaction: this.interaction - }; - this._trigger = new DimensionTrigger(triggerOptions); - } - - initTrigger() { - // register all mark - // trigger check mark enable - this._series.forEach(s => { - s.getMarksWithoutRoot().forEach(m => { - this._trigger.registerMark(m); - }); - }); - this._trigger.init(); - } - - initInteraction() { - if (this._option.disableTriggerEvent) { - return; - } - - // 注册所有支持反选状态mark - this._series.forEach(s => { - s.getMarksWithoutRoot().forEach(m => { - for (const key in STATE_VALUE_ENUM_REVERSE) { - if (!isEmpty(m.stateStyle[STATE_VALUE_ENUM_REVERSE[key]])) { - this.interaction.registerMark(STATE_VALUE_ENUM_REVERSE[key], m); - } - } - }); - }); - } compileMarks(group?: IGroup) { this.getMarks().forEach(m => { diff --git a/packages/vchart/src/series/base/base-series.ts b/packages/vchart/src/series/base/base-series.ts index 964b2a0ad8..f3047b35c7 100644 --- a/packages/vchart/src/series/base/base-series.ts +++ b/packages/vchart/src/series/base/base-series.ts @@ -57,7 +57,7 @@ import type { IModelEvaluateOption, IModelRenderOption, IUpdateSpecResult } from import type { AddVChartPropertyContext } from '../../data/transforms/add-property'; // eslint-disable-next-line no-duplicate-imports import { addVChartProperty } from '../../data/transforms/add-property'; -import type { IBaseInteractionSpec, IHoverSpec, ISelectSpec } from '../../interaction/interface'; +import type { IBaseInteractionSpec, IHoverSpec, ISelectSpec } from '../../interaction/interface/spec'; import { registerDataSetInstanceTransform } from '../../data/register'; import { BaseSeriesTooltipHelper } from './tooltip-helper'; // eslint-disable-next-line no-duplicate-imports @@ -91,10 +91,13 @@ import { getDefaultInteractionConfigByMode } from '../../interaction/config'; import { LayoutZIndex } from '../../constant/layout'; import type { ILabelSpec } from '../../component/label/interface'; import type { StatisticOperations } from '../../data/transforms/interface'; -import { is3DMark } from '../../mark/utils'; +import { is3DMark } from '../../mark/utils/common'; import type { GraphicEventType } from '@visactor/vrender-core'; import type { ICompilableData } from '../../compile/data'; import { CompilableData } from '../../compile/data'; +import { filterMarksOfInteraction } from '../../interaction/triggers/util'; +import type { IBaseTriggerOptions } from '../../interaction/interface/trigger'; +import { TRIGGER_TYPE_ENUM } from '../../interaction/triggers/enum'; export abstract class BaseSeries extends BaseModel implements ISeries { readonly specKey: string = 'series'; @@ -292,9 +295,6 @@ export abstract class BaseSeries extends BaseModel imp this.initAnimation(); } - if (!this._option.disableTriggerEvent) { - this.initInteraction(); - } this.afterInitMark(); // event @@ -778,33 +778,6 @@ export abstract class BaseSeries extends BaseModel imp /** mark */ - protected _parseSelectorOfInteraction(interactionSpec: IBaseInteractionSpec, marks: IMark[]) { - if (!marks || !marks.length) { - return []; - } - const selector: string[] = []; - - if (interactionSpec.markIds) { - marks.filter(mark => { - if (interactionSpec.markIds.includes(mark.getProductId())) { - selector.push(`#${mark.getProductId()}`); - } - }); - } else if (interactionSpec.markNames) { - marks.forEach(mark => { - if (interactionSpec.markNames.includes(mark.name)) { - selector.push(`#${mark.getProductId()}`); - } - }); - } else { - marks.forEach(mark => { - selector.push(`#${mark.getProductId()}`); - }); - } - - return selector; - } - protected _parseDefaultInteractionConfig(mainMarks?: IMark[]) { if (!mainMarks?.length) { return []; @@ -829,27 +802,40 @@ export abstract class BaseSeries extends BaseModel imp finalSelectSpec.enable = true; finalSelectSpec = mergeSpec(finalSelectSpec, selectSpec); } - const res = []; + const res: { trigger: Partial; marks: IMark[] }[] = [ + { + trigger: { + type: TRIGGER_TYPE_ENUM.DIMENSION_HOVER as string + }, + marks: mainMarks + } + ]; if (finalHoverSpec.enable) { - const selector: string[] = this._parseSelectorOfInteraction(finalHoverSpec as IBaseInteractionSpec, mainMarks); + const marks: IMark[] = filterMarksOfInteraction(finalHoverSpec as IBaseInteractionSpec, mainMarks); - selector.length && res.push(this._defaultHoverConfig(selector, finalHoverSpec)); + marks.length && + res.push({ + trigger: this._defaultHoverConfig(finalHoverSpec), + marks + }); } if (finalSelectSpec.enable) { - const selector: string[] = this._parseSelectorOfInteraction(finalSelectSpec as IBaseInteractionSpec, mainMarks); - selector.length && res.push(this._defaultSelectConfig(selector, finalSelectSpec)); + const marks: IMark[] = filterMarksOfInteraction(finalSelectSpec as IBaseInteractionSpec, mainMarks); + + marks.length && + res.push({ + trigger: this._defaultSelectConfig(finalSelectSpec) as Partial, + marks + }); } return res; } - protected _defaultHoverConfig(selector: string[], finalHoverSpec: IHoverSpec) { + protected _defaultHoverConfig(finalHoverSpec: IHoverSpec) { return { - seriesId: this.id, - regionId: this._region.id, - selector, - type: 'element-highlight', + type: TRIGGER_TYPE_ENUM.ELEMENT_HIGHLIGHT, trigger: finalHoverSpec.trigger as GraphicEventType, triggerOff: finalHoverSpec.triggerOff as GraphicEventType, blurState: STATE_VALUE_ENUM.STATE_HOVER_REVERSE, @@ -857,7 +843,7 @@ export abstract class BaseSeries extends BaseModel imp }; } - protected _defaultSelectConfig(selector: string[], finalSelectSpec: ISelectSpec) { + protected _defaultSelectConfig(finalSelectSpec: ISelectSpec) { const isMultiple = finalSelectSpec.mode === 'multiple'; const triggerOff = isValid(finalSelectSpec.triggerOff) ? finalSelectSpec.triggerOff @@ -865,10 +851,7 @@ export abstract class BaseSeries extends BaseModel imp ? ['empty'] : ['empty', finalSelectSpec.trigger]; return { - type: 'element-select', - seriesId: this.id, - regionId: this._region.id, - selector, + type: TRIGGER_TYPE_ENUM.ELEMENT_SELECT, trigger: finalSelectSpec.trigger as GraphicEventType, triggerOff: triggerOff as GraphicEventType, reverseState: STATE_VALUE_ENUM.STATE_SELECTED_REVERSE, @@ -878,39 +861,29 @@ export abstract class BaseSeries extends BaseModel imp } protected _parseInteractionConfig(mainMarks?: IMark[]) { - const compiler = this.getCompiler(); - if (!compiler) { - return; - } - const { interactions } = this._spec; const res = this._parseDefaultInteractionConfig(mainMarks); - if (res && res.length) { - res.forEach(interaction => { - compiler.addInteraction(interaction); - }); - } - if (interactions && interactions.length) { interactions.forEach(interaction => { - const selectors: string[] = this._parseSelectorOfInteraction(interaction, this.getMarks()); - - if (selectors.length) { - compiler.addInteraction({ - ...interaction, - selector: selectors, - seriesId: this.id, - regionId: this._region.id - }); + const marks: IMark[] = filterMarksOfInteraction(interaction, this.getMarks()); + + if (marks.length) { + res.push({ trigger: interaction, marks }); } }); } + + return res; } - initInteraction() { - const marks = this.getMarksWithoutRoot(); - this._parseInteractionConfig(marks); + getInteractionTriggers() { + if (this._option.disableTriggerEvent !== true) { + const marks = this.getMarksWithoutRoot(); + return this._parseInteractionConfig(marks); + } + + return []; } initAnimation() { @@ -1015,7 +988,7 @@ export abstract class BaseSeries extends BaseModel imp protected _releaseEvent(): void { super._releaseEvent(); - this.getCompiler().removeInteraction(this.id); + // todo release interactions } /** event end */ @@ -1308,7 +1281,6 @@ export abstract class BaseSeries extends BaseModel imp dataView, parent, isSeriesMark, - depend, stateSort, noSeparateStyle = false } = option; @@ -1340,10 +1312,6 @@ export abstract class BaseSeries extends BaseModel imp m.setSkipBeforeLayouted(skipBeforeLayouted); } - if (isValid(depend)) { - m.setDepend(...array(depend)); - } - if (!isNil(groupKey)) { m.setGroupKey(groupKey); } @@ -1363,7 +1331,7 @@ export abstract class BaseSeries extends BaseModel imp m.setMarkConfig(markConfig); - this.initMarkStyleWithSpec(m, mergeSpec({}, themeSpec, markSpec || spec[m.name])); + this.initMarkStyleWithSpec(m, mergeSpec({}, themeSpec, markSpec || (spec as any)[m.name])); } return m; } diff --git a/packages/vchart/src/series/funnel/3d/funnel-3d.ts b/packages/vchart/src/series/funnel/3d/funnel-3d.ts index dc9f5d9551..d0fbf42cb7 100644 --- a/packages/vchart/src/series/funnel/3d/funnel-3d.ts +++ b/packages/vchart/src/series/funnel/3d/funnel-3d.ts @@ -125,11 +125,11 @@ export class Funnel3dSeries this._labelMark = labelMark; if (this._funnelOuterLabelMark?.label) { - this._funnelOuterLabelMark.label.setDepend(labelMark.getComponent()); + // this._funnelOuterLabelMark.label.setDepend(labelMark.getComponent()); } if (this._funnelOuterLabelMark?.line) { - this._funnelOuterLabelMark.line.setDepend(...this._funnelOuterLabelMark.line.getDepend()); + // this._funnelOuterLabelMark.line.setDepend(...this._funnelOuterLabelMark.line.getDepend()); } } } diff --git a/packages/vchart/src/series/funnel/funnel.ts b/packages/vchart/src/series/funnel/funnel.ts index d84345a1a6..2638ac7730 100644 --- a/packages/vchart/src/series/funnel/funnel.ts +++ b/packages/vchart/src/series/funnel/funnel.ts @@ -9,7 +9,7 @@ import { DEFAULT_DATA_KEY } from '../../constant/data'; import { PREFIX } from '../../constant/base'; import { registerDataSetInstanceTransform } from '../../data/register'; import { DataView } from '@visactor/vdataset'; -import type { ILabelMark, IMark, IPolygonMark, IRuleMark, ITextMark } from '../../mark/interface'; +import type { ILabelMark, IMark, IMarkGraphic, IPolygonMark, IRuleMark, ITextMark } from '../../mark/interface'; import { MarkTypeEnum } from '../../mark/interface/type'; import type { IFunnelOpt } from '../../data/transforms/funnel'; import { funnel, funnelTransform } from '../../data/transforms/funnel'; @@ -50,7 +50,7 @@ import { Factory } from '../../core/factory'; import { FunnelSeriesSpecTransformer } from './funnel-transformer'; import type { ICompilableData } from '../../compile/data'; import { CompilableData } from '../../compile/data'; -import type { INode } from '@visactor/vrender-core'; +import type { IGraphic, INode } from '@visactor/vrender-core'; export class FunnelSeries extends BaseSeries @@ -210,8 +210,6 @@ export class FunnelSeries { themeSpec: this._theme?.transform, skipBeforeLayouted: true, - dataView: this._viewDataTransform.getDataView(), - dataProductId: this._viewDataTransform.getProductId(), stateSort: this._spec.transform?.stateSort, noSeparateStyle: true }, @@ -219,6 +217,9 @@ export class FunnelSeries setCustomizedShape: this._spec.transform?.customShape } ); + if (this._funnelTransformMark) { + this._funnelTransformMark.setData(this._viewDataTransform); + } } if (this._spec?.outerLabel?.visible) { @@ -235,7 +236,6 @@ export class FunnelSeries this._funnelOuterLabelMark.line = this._createMark(FunnelSeries.mark.outerLabelLine, { themeSpec: lineTheme, markSpec: line, - depend: [this._funnelOuterLabelMark.label], noSeparateStyle: true }) as IRuleMark; } @@ -354,11 +354,11 @@ export class FunnelSeries AttributeLevel.Series ); if (this._funnelOuterLabelMark?.label) { - this._funnelOuterLabelMark.label.setDepend(component); + // this._funnelOuterLabelMark.label.setDepend(component); } if (this._funnelOuterLabelMark?.line) { - this._funnelOuterLabelMark.line.setDepend(...this._funnelOuterLabelMark.line.getDepend()); + // this._funnelOuterLabelMark.line.setDepend(...this._funnelOuterLabelMark.line.getDepend()); } } else if (this._funnelTransformMark && target === this._funnelTransformMark) { this._transformLabelMark = labelMark; @@ -391,7 +391,7 @@ export class FunnelSeries width: () => { const rootMark = this.getRootMark().getProduct(); if (rootMark) { - const { x1, x2 } = rootMark.getBounds(); + const { x1, x2 } = rootMark.AABBBounds; return Math.max(x1, x2); // rootMark.x === 0, so need to find largest bound x instead of bounds width } return this.getLayoutRect().width; @@ -399,7 +399,7 @@ export class FunnelSeries height: () => { const rootMark = this.getRootMark().getProduct(); if (rootMark) { - const { y1, y2 } = rootMark.getBounds(); + const { y1, y2 } = rootMark.AABBBounds; return Math.max(y1, y2); } return this.getLayoutRect().height; @@ -808,17 +808,16 @@ export class FunnelSeries private _computeOuterLabelLinePosition(datum: Datum) { const categoryField = this.getCategoryField(); const outerLabelMarkBounds = this._funnelOuterLabelMark?.label - ?.getProduct() - ?.elements?.find((el: any) => el.data[0]?.[categoryField] === datum[categoryField]) - ?.getBounds(); - - const labelMarkBounds = this._labelMark - ?.getComponent() - ?.getProduct() - ?.getGroupGraphicItem() - ?.find(({ attribute, type }: { attribute: LabelItem; type: string }) => { + ?.getGraphics() + ?.find((g: IMarkGraphic) => g.context.data[0]?.[categoryField] === datum[categoryField])?.AABBBounds; + const labelComponent = this._labelMark?.getComponent()?.getComponent(); + + const labelMarkBounds = (labelComponent as any)?.find( + ({ attribute, type }: { attribute: LabelItem; type: string }) => { return type === 'text' && attribute.data?.[categoryField] === datum[categoryField]; - }, true)?.AABBBounds; + }, + true + )?.AABBBounds; const outerLabelSpec = this._spec.outerLabel ?? {}; let x1; let x2; diff --git a/packages/vchart/src/series/gauge/gauge-pointer.ts b/packages/vchart/src/series/gauge/gauge-pointer.ts index a7cdbe78cb..205eb5ace0 100644 --- a/packages/vchart/src/series/gauge/gauge-pointer.ts +++ b/packages/vchart/src/series/gauge/gauge-pointer.ts @@ -191,8 +191,8 @@ export class GaugePointerSeries< } } - initInteraction(): void { - this._parseInteractionConfig(this._pointerMark ? [this._pointerMark] : []); + getInteractionTriggers() { + return this._parseInteractionConfig(this._pointerMark ? [this._pointerMark] : []); } initAnimation() { diff --git a/packages/vchart/src/series/gauge/gauge.ts b/packages/vchart/src/series/gauge/gauge.ts index b9eeeec07f..68ce3caa38 100644 --- a/packages/vchart/src/series/gauge/gauge.ts +++ b/packages/vchart/src/series/gauge/gauge.ts @@ -117,8 +117,8 @@ export class GaugeSeries extends return style; } - initMarkStyleWithSpec(mark?: IMark, spec?: any, key?: string): void { - super.initMarkStyleWithSpec(mark, spec, key); + initMarkStyleWithSpec(mark?: IMark, spec?: any): void { + super.initMarkStyleWithSpec(mark, spec); if (mark && mark.name === SeriesMarkNameEnum.segment) { // radius 配置需要额外处理比例值 const segmentSpec = this.getSpec()[SeriesMarkNameEnum.segment]; diff --git a/packages/vchart/src/series/heatmap/heatmap.ts b/packages/vchart/src/series/heatmap/heatmap.ts index b2aacfd330..3907c61ec0 100644 --- a/packages/vchart/src/series/heatmap/heatmap.ts +++ b/packages/vchart/src/series/heatmap/heatmap.ts @@ -155,8 +155,8 @@ export class HeatmapSeries ex }; } - initInteraction(): void { - this._parseInteractionConfig(this._cellMark ? [this._cellMark] : []); + getInteractionTriggers() { + return this._parseInteractionConfig(this._cellMark ? [this._cellMark] : []); } initAnimation() { diff --git a/packages/vchart/src/series/interface/series.ts b/packages/vchart/src/series/interface/series.ts index e0c27185f5..919a748c29 100644 --- a/packages/vchart/src/series/interface/series.ts +++ b/packages/vchart/src/series/interface/series.ts @@ -15,6 +15,7 @@ import type { IGeoCoordinateHelper } from '../../component/geo/interface'; import type { ILabelSpec, ILabelInfo } from '../../component/label/interface'; import type { StatisticOperations } from '../../data/transforms/interface'; import type { ILegend } from '../../component/legend/interface'; +import type { IBaseTriggerOptions } from '../../interaction/interface/trigger'; // 使用类型约束系列支持的样式,但是感觉这样不合理 不使用这样的方式去做 // export interface ISeries extends IModel @@ -33,7 +34,7 @@ export interface ISeries extends IModel { readonly coordinate: CoordinateType; // layout - onLayoutEnd: (ctx: any) => void; + onLayoutEnd: () => void; // 数据 getRawData: () => DataView | undefined; @@ -199,6 +200,8 @@ export interface ISeries extends IModel { legendSelectedFilter?: (component: ILegend, selectedKeys: StringOrNumber[]) => StringOrNumber[]; parseLabelStyle?: (labelStyle: any, labelSpec: any, labelMark?: ILabelMark) => any; + + getInteractionTriggers?: () => { trigger: Partial; marks: IMark[] }[]; } export interface ICartesianSeries extends ISeries { diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index 3011a9ad7f..1444c59ec7 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -326,7 +326,7 @@ export class LinkSeries extends Car }; } - initInteraction(): void { + getInteractionTriggers() { const marks: IMark[] = []; if (this._linkMark) { @@ -336,7 +336,7 @@ export class LinkSeries extends Car if (this._arrowMark) { marks.push(this._arrowMark); } - this._parseInteractionConfig(marks); + return this._parseInteractionConfig(marks); } protected initTooltip() { diff --git a/packages/vchart/src/series/liquid/liquid.ts b/packages/vchart/src/series/liquid/liquid.ts index 35d65409c5..6d180bad04 100644 --- a/packages/vchart/src/series/liquid/liquid.ts +++ b/packages/vchart/src/series/liquid/liquid.ts @@ -20,6 +20,7 @@ import { BaseSeries } from '../base'; import { registerGroupMark } from '../../mark/group'; import { getShapes } from './util'; import { createRect, createSymbol } from '@visactor/vrender-core'; +import type { IColor, IGlyph } from '@visactor/vrender-core'; import { labelSmartInvert } from '@visactor/vrender-components'; import { normalizeLayoutPaddingSpec } from '../../util'; import type { DataView } from '@visactor/vdataset'; @@ -292,8 +293,8 @@ export class LiquidSeries exten this._liquidMark && this._tooltipHelper.activeTriggerSet.mark.add(this._liquidMark); } - initInteraction(): void { - this._parseInteractionConfig(this._liquidMark ? [this._liquidMark] : []); + getInteractionTriggers() { + return this._parseInteractionConfig(this._liquidMark ? [this._liquidMark] : []); } initAnimation() { @@ -345,7 +346,7 @@ export class LiquidSeries exten // wave item比较特殊, 由wave1、wave2、wave3在x方向上偏移叠加而成 // 由于在水波图中只需要判断y方向上是否重叠, 所以此处取wave1做y方向上对比 const grammarMark = this._liquidMark.getProduct(); - const waveItem = (grammarMark.elements[0] as any).glyphGraphicItems.wave1; + const waveItem = (grammarMark.getGraphics()[0] as IGlyph).getSubGraphic()?.[0]; let { y1: waveY1, y2: waveY2 } = waveItem.globalAABBBounds; waveY1 += this._region.getLayoutStartPoint().y; waveY2 += this._region.getLayoutStartPoint().y; @@ -358,7 +359,7 @@ export class LiquidSeries exten if (waveY1 < textY1 && waveY2 > textY2) { const foregroundColor = text.attribute.fill; const backgroundColor = waveItem.attribute.fill; - const invertColor = labelSmartInvert(foregroundColor, backgroundColor); + const invertColor = labelSmartInvert(foregroundColor, backgroundColor as IColor); text.setAttribute('fill', invertColor); } }); diff --git a/packages/vchart/src/series/map/map.ts b/packages/vchart/src/series/map/map.ts index 640452c6fc..a316b044f7 100644 --- a/packages/vchart/src/series/map/map.ts +++ b/packages/vchart/src/series/map/map.ts @@ -243,7 +243,7 @@ export class MapSeries extends GeoSer return; } - const pathGroup = this.getRootMark().getProduct()?.getGroupGraphicItem(); + const pathGroup = this.getRootMark().getProduct(); if (pathGroup) { if (!pathGroup.attribute.postMatrix) { pathGroup.setAttributes({ @@ -252,10 +252,10 @@ export class MapSeries extends GeoSer } pathGroup.scale(scale, scale, scaleCenter); } - const vgrammarLabel = this._labelMark?.getComponent()?.getProduct(); + const vgrammarLabel = this._labelMark?.getComponent(); if (vgrammarLabel) { - (vgrammarLabel as any).evaluate(null, null); + vgrammarLabel.renderInner(); } } @@ -264,7 +264,7 @@ export class MapSeries extends GeoSer if (delta[0] === 0 && delta[1] === 0) { return; } - const pathGroup = this.getRootMark().getProduct()?.getGroupGraphicItem(); + const pathGroup = this.getRootMark().getProduct(); if (pathGroup) { if (!pathGroup.attribute.postMatrix) { pathGroup.setAttributes({ @@ -273,10 +273,10 @@ export class MapSeries extends GeoSer } pathGroup.translate(delta[0], delta[1]); } - const vgrammarLabel = this._labelMark?.getComponent()?.getProduct(); + const vgrammarLabel = this._labelMark?.getComponent(); if (vgrammarLabel) { - (vgrammarLabel as any).evaluate(null, null); + vgrammarLabel.renderInner(); } } diff --git a/packages/vchart/src/series/pictogram/pictogram.ts b/packages/vchart/src/series/pictogram/pictogram.ts index 21bab65c2a..a63e60b143 100644 --- a/packages/vchart/src/series/pictogram/pictogram.ts +++ b/packages/vchart/src/series/pictogram/pictogram.ts @@ -18,16 +18,18 @@ import type { IMatrix } from '@visactor/vutils'; import { Bounds, Matrix, isValid, merge } from '@visactor/vutils'; import type { Datum } from '../../typings'; import { createRect } from '@visactor/vrender-core'; -import type { GraphicEventType, Group } from '@visactor/vrender-core'; -import { VGRAMMAR_HOOK_EVENT } from '../../constant/event'; -import type { IHoverSpec, ISelectSpec } from '../../interaction/interface'; +import type { GraphicEventType, Group, IGroup } from '@visactor/vrender-core'; +import { ChartEvent, HOOK_EVENT } from '../../constant/event'; +import type { IHoverSpec, ISelectSpec } from '../../interaction/interface/spec'; import { STATE_VALUE_ENUM } from '../../compile/mark'; -import { registerElementHighlightByGraphicName, registerElementSelectByGraphicName } from '@visactor/vgrammar-core'; import type { IGroupMark, IMark, ITextMark } from '../../mark/interface'; import { PictogramSeriesTooltipHelper } from './tooltip-helper'; import { graphicAttributeTransform, pictogram } from '../../data/transforms/pictogram'; import type { IPoint } from '../../typings/coordinate'; import { CompilableData } from '../../compile/data'; +import { registerElementHighlightByGraphicName } from '../../interaction/triggers/element-highlight-by-graphic-name'; +import { registerElementSelectByGraphicName } from '../../interaction/triggers/element-select-by-graphic-name'; +import { TRIGGER_TYPE_ENUM } from '../../interaction/triggers/enum'; export interface SVGParsedElementExtend extends SVGParsedElement { _finalAttributes: Record; @@ -85,12 +87,9 @@ export class PictogramSeries { return isValid(result) ? result : this._spec.defaultFillColor; @@ -296,11 +290,11 @@ export class PictogramSeries { - bounds = bounds.union(m.getProduct().getGroupGraphicItem().globalAABBBounds); + bounds = bounds.union(m.getGraphics()[0].globalAABBBounds); }); } else { mark.forEach(m => { - bounds = bounds.union(m.getProduct().getBounds()); + bounds = bounds.union(m.getProduct().AABBBounds); }); } @@ -332,8 +326,8 @@ export class PictogramSeries number; + growFrom: (datum: Datum, g: IMarkGraphic, state: AnimationStateEnum) => number; } export type PieAppearPreset = 'growAngle' | 'growRadius' | 'fadeIn'; diff --git a/packages/vchart/src/series/pie/pie.ts b/packages/vchart/src/series/pie/pie.ts index c027d04bda..920cc8974a 100644 --- a/packages/vchart/src/series/pie/pie.ts +++ b/packages/vchart/src/series/pie/pie.ts @@ -30,7 +30,7 @@ import { isDataEmpty, pie } from '../../data/transforms/pie'; import { registerDataSetInstanceTransform } from '../../data/register'; import { registerEmptyCircleAnimation, registerPieAnimation } from './animation/animation'; import { animationConfig, shouldMarkDoMorph, userAnimationConfig } from '../../animation/utils'; -import type { IAnimationTypeConfig } from '../../animation/interface'; +import type { IAnimationTypeConfig, TypeAnimationConfig } from '../../animation/interface'; import { AnimationStateEnum } from '../../animation/interface'; import type { IBasePieSeriesSpec, IPieAnimationParams, IPieSeriesSpec, PieAppearPreset } from './interface'; import type { IStateAnimateSpec } from '../../animation/spec'; @@ -268,8 +268,8 @@ export class BasePieSeries extends PolarSeries } } - initInteraction(): void { - this._parseInteractionConfig(this._pieMark ? [this._pieMark] : []); + getInteractionTriggers() { + return this._parseInteractionConfig(this._pieMark ? [this._pieMark] : []); } protected initTooltip() { @@ -278,8 +278,8 @@ export class BasePieSeries extends PolarSeries this._pieMark && this._tooltipHelper.activeTriggerSet.mark.add(this._pieMark); } - initMarkStyleWithSpec(mark?: IMark, spec?: any, key?: string): void { - super.initMarkStyleWithSpec(mark, spec, key); + initMarkStyleWithSpec(mark?: IMark, spec?: any): void { + super.initMarkStyleWithSpec(mark, spec); if (mark.name === this._pieMarkName) { // radius 配置需要额外处理比例值 const pieSpec = this.getSpec()[mark.name as 'pie']; @@ -366,16 +366,16 @@ export class BasePieSeries extends PolarSeries getRadius(state: StateValueType = 'normal'): number { const styleRadius = state === 'normal' - ? this.getSpec()[this._pieMark?.name || 'pie']?.style?.outerRadius - : this.getSpec()[this._pieMark?.name || 'pie']?.state?.[state]?.outerRadius; + ? (this.getSpec() as any)[this._pieMark?.name || 'pie']?.style?.outerRadius + : (this.getSpec() as any)[this._pieMark?.name || 'pie']?.state?.[state]?.outerRadius; return styleRadius ?? this._outerRadius; } getInnerRadius(state: StateValueType = 'normal'): number { const styleRadius = state === 'normal' - ? this.getSpec()[this._pieMark?.name || 'pie']?.style?.innerRadius - : this.getSpec()[this._pieMark?.name || 'pie']?.state?.[state]?.innerRadius; + ? (this.getSpec() as any)[this._pieMark?.name || 'pie']?.style?.innerRadius + : (this.getSpec() as any)[this._pieMark?.name || 'pie']?.state?.[state]?.innerRadius; return styleRadius ?? this._innerRadius; } @@ -446,32 +446,29 @@ export class BasePieSeries extends PolarSeries initAnimation() { const animationParams: IPieAnimationParams = { - growFrom: (datum, element, state) => { + growFrom: (datum, graphic, state) => { if (state === AnimationStateEnum.appear) { return this._startAngle; } if (state === AnimationStateEnum.disappear) { return this._endAngle; } - const outState = [AnimationStateEnum.disappear, AnimationStateEnum.exit]; - const markElements = element.mark.elements; - + const markElements = this._pieMark.getGraphics(); const data = datum; const dataIndex = data?.[DEFAULT_DATA_INDEX]; - // 当前 mark 在上个状态是否处于第一个 - if (markElements.find(e => e.data[0]?.[DEFAULT_DATA_INDEX] < dataIndex) === undefined) { + if (markElements.find(e => e.context.data[0]?.[DEFAULT_DATA_INDEX] < dataIndex) === undefined) { return this._startAngle; } // 当前 mark 在上个状态是否处于最后一个 - if (markElements.find(e => e.data[0]?.[DEFAULT_DATA_INDEX] > dataIndex) === undefined) { + if (markElements.find(e => e.context.data[0]?.[DEFAULT_DATA_INDEX] > dataIndex) === undefined) { return this._endAngle; } - // 扇形不在边缘时,获取扇形生长点:获取相邻状态下相邻扇形的边缘 - const prevMarkElement = [...markElements].reverse().find(e => e.data[0]?.[DEFAULT_DATA_INDEX] < dataIndex); - + const prevMarkElement = [...markElements] + .reverse() + .find(e => e.context.data[0]?.[DEFAULT_DATA_INDEX] < dataIndex); if (outState.includes(state)) { return prevMarkElement?.getNextGraphicAttributes()?.endAngle; } @@ -479,23 +476,19 @@ export class BasePieSeries extends PolarSeries } }; const appearPreset = (this._spec.animationAppear as IStateAnimateSpec)?.preset; - if (this._pieMark) { const pieAnimationConfig = animationConfig( Factory.getAnimationInKey('pie')?.(animationParams, appearPreset), userAnimationConfig(SeriesMarkNameEnum.pie, this._spec, this._markAttributeContext) ); - - if (pieAnimationConfig.normal && (pieAnimationConfig.normal as IAnimationTypeConfig).type) { + if (pieAnimationConfig.normal && (pieAnimationConfig.normal as TypeAnimationConfig).type) { pieAnimationConfig.normal = centerOffsetConfig( this._pieMark, pieAnimationConfig.normal as IAnimationTypeConfig ); } - this._pieMark.setAnimationConfig(pieAnimationConfig); } - if (this._emptyArcMark) { const pieAnimationConfig = animationConfig( Factory.getAnimationInKey('emptyCircle')?.(animationParams, appearPreset ?? 'fadeIn') diff --git a/packages/vchart/src/series/progress/circular/circular.ts b/packages/vchart/src/series/progress/circular/circular.ts index ae4b7860df..f3aa6d4a23 100644 --- a/packages/vchart/src/series/progress/circular/circular.ts +++ b/packages/vchart/src/series/progress/circular/circular.ts @@ -96,7 +96,7 @@ export class CircularProgressSeries< } } - initInteraction(): void { + getInteractionTriggers() { const marks: IMark[] = []; if (this._trackMark) { @@ -106,7 +106,7 @@ export class CircularProgressSeries< if (this._progressMark) { marks.push(this._progressMark); } - this._parseInteractionConfig(marks); + return this._parseInteractionConfig(marks); } protected initTooltip() { diff --git a/packages/vchart/src/series/progress/linear/linear.ts b/packages/vchart/src/series/progress/linear/linear.ts index ef25c38c29..b0d85bdc5c 100644 --- a/packages/vchart/src/series/progress/linear/linear.ts +++ b/packages/vchart/src/series/progress/linear/linear.ts @@ -264,7 +264,7 @@ export class LinearProgressSeries< } } - initInteraction(): void { + getInteractionTriggers() { const marks: IMark[] = []; if (this._trackMark) { @@ -274,7 +274,7 @@ export class LinearProgressSeries< if (this._progressMark) { marks.push(this._progressMark); } - this._parseInteractionConfig(marks); + return this._parseInteractionConfig(marks); } initAnimation() { diff --git a/packages/vchart/src/series/sankey/sankey.ts b/packages/vchart/src/series/sankey/sankey.ts index 1dd238070c..306684ed89 100644 --- a/packages/vchart/src/series/sankey/sankey.ts +++ b/packages/vchart/src/series/sankey/sankey.ts @@ -21,7 +21,6 @@ import { Bounds, array, isNil, isValid, isNumber, isArray } from '@visactor/vuti import { registerSankeyAnimation } from './animation'; import type { ISankeySeriesSpec, SankeyLinkElement, ISankeyLabelSpec, ISankeyAnimationParams } from './interface'; import type { ExtendEventParam } from '../../event/interface'; -import type { IElement, IGlyphElement, IMark as IVgrammarMark } from '@visactor/vgrammar-core'; import type { IMarkAnimateSpec } from '../../animation/spec'; import { ColorOrdinalScale } from '../../scale/color-ordinal-scale'; import { registerRectMark } from '../../mark/rect'; @@ -31,13 +30,15 @@ import { sankeySeriesMark } from './constant'; import { flatten } from '../../data/transforms/flatten'; import type { SankeyNodeElement } from '@visactor/vgrammar-sankey'; import { Factory } from '../../core/factory'; -import type { IGlyphMark, ILinkPathMark, IMark, IRectMark, ITextMark } from '../../mark/interface'; +import type { IGlyphMark, ILinkPathMark, IMark, IMarkGraphic, IRectMark, ITextMark } from '../../mark/interface'; import { TransformLevel } from '../../data/initialize'; import type { IBaseScale } from '@visactor/vscale'; import { addDataKey, initKeyMap } from '../../data/transforms/data-key'; import { SankeySeriesSpecTransformer } from './sankey-transformer'; import { getFormatFunction } from '../../component/util'; import type { ILabelSpec } from '../../component'; +import { getDatumOfGraphic } from '../../util'; +import { addRuntimeState } from '../../mark/utils/glyph'; export class SankeySeries extends CartesianSeries { static readonly type: string = SeriesTypeEnum.sankey; @@ -490,21 +491,21 @@ export class SankeySeries exten protected _handleEmphasisElement = (params: ExtendEventParam) => { const emphasisSpec = this._spec.emphasis ?? ({} as T['emphasis']); - const element = params.item; + const graphic = params.item; if (emphasisSpec.effect === 'adjacency') { - if (element && element.mark === this._nodeMark?.getProduct()) { - this._handleNodeAdjacencyClick(element); - } else if (element && element.mark === this._linkMark?.getProduct()) { - this._handleLinkAdjacencyClick(element); + if (graphic && params.mark === this._nodeMark) { + this._handleNodeAdjacencyClick(graphic); + } else if (graphic && params.mark === this._linkMark) { + this._handleLinkAdjacencyClick(graphic); } else { this._handleClearEmpty(); } } else if (emphasisSpec.effect === 'related') { - if (element && element.mark === this._nodeMark?.getProduct()) { - this._handleNodeRelatedClick(element); - } else if (element && element.mark === this._linkMark?.getProduct()) { - this._handleLinkRelatedClick(element); + if (graphic && params.mark === this._nodeMark) { + this._handleNodeRelatedClick(graphic); + } else if (graphic && params.mark === this._linkMark) { + this._handleLinkRelatedClick(graphic); } else { this._handleClearEmpty(); } @@ -528,31 +529,33 @@ export class SankeySeries exten return; } - const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; + // const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; allNodeElements.forEach(el => { - el.removeState(states); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); }); allLinkElements.forEach(el => { - el.removeState(states); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); }); this._needClear = false; }; - protected _handleNodeAdjacencyClick = (element: IElement) => { - const nodeDatum = element.getDatum(); + protected _handleNodeAdjacencyClick = (graphic: IMarkGraphic) => { + const nodeDatum = getDatumOfGraphic(graphic) as Datum; const highlightNodes: string[] = [nodeDatum.key]; if (this._linkMark) { - const allLinkElements = this._linkMark.getProductElements(); + const allLinkElements = this._linkMark.getGraphics(); if (!allLinkElements || !allLinkElements.length) { return; } - allLinkElements.forEach((linkEl: IElement, i: number) => { - const linkDatum = linkEl.getDatum(); + allLinkElements.forEach((linkEl: IMarkGraphic, i: number) => { + const linkDatum = getDatumOfGraphic(linkEl) as Datum; if (linkDatum.source === nodeDatum.key) { // 下游link @@ -561,7 +564,7 @@ export class SankeySeries exten } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); // 设置上用户配置选中状态 + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 } else if (linkDatum.target === nodeDatum.key) { // 上游link if (!highlightNodes.includes(linkDatum.source)) { @@ -569,71 +572,72 @@ export class SankeySeries exten } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); // 设置上用户配置选中状态 + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 } else { linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } if (this._nodeMark) { - this._highLightElements(this._nodeMark.getProductElements(), highlightNodes); + this._highLightElements(this._nodeMark.getGraphics(), highlightNodes); } this._needClear = true; }; - protected _handleLinkAdjacencyClick = (element: IGlyphElement) => { - const curLinkDatum = element.getDatum(); + protected _handleLinkAdjacencyClick = (graphic: IMarkGraphic) => { + const curLinkDatum = getDatumOfGraphic(graphic) as Datum; const highlightNodes: string[] = [curLinkDatum.source, curLinkDatum.target]; if (this._linkMark) { - const allLinkElements = this._linkMark.getProductElements(); + const allLinkElements = this._linkMark.getGraphics(); if (!allLinkElements || !allLinkElements.length) { return; } allLinkElements.forEach(linkEl => { - if (linkEl === element) { + if (linkEl === graphic) { linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); + + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); } else { linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } if (this._nodeMark) { - this._highLightElements(this._nodeMark.getProductElements(), highlightNodes); + this._highLightElements(this._nodeMark.getGraphics(), highlightNodes); } this._needClear = true; }; - protected _handleNodeRelatedClick = (element: IElement) => { - const nodeDatum = element.getDatum(); - const allNodeElements = this._nodeMark.getProductElements(); + protected _handleNodeRelatedClick = (graphic: IMarkGraphic) => { + const nodeDatum = getDatumOfGraphic(graphic) as Datum; + const allNodeElements = this._nodeMark.getGraphics(); if (!allNodeElements || !allNodeElements.length) { return; } - const allLinkElements = this._linkMark.getProductElements(); + const allLinkElements = this._linkMark.getGraphics(); if (!allLinkElements || !allLinkElements.length) { return; } - const father = allLinkElements[0].getDatum()?.parents ? 'parents' : 'source'; + const father = (getDatumOfGraphic(allLinkElements[0]) as Datum)?.parents ? 'parents' : 'source'; if (father === 'source') { // node-link 型数据 const highlightNodes: string[] = [nodeDatum.key]; const highlightLinks: string[] = []; - allLinkElements.forEach((linkEl: IElement, i: number) => { - const linkDatum = linkEl.getDatum(); + allLinkElements.forEach((linkEl: IMarkGraphic, i: number) => { + const linkDatum = getDatumOfGraphic(linkEl) as Datum; const father = linkDatum?.parents ? 'parents' : 'source'; if (array(linkDatum[father]).includes(nodeDatum.key)) { @@ -715,25 +719,27 @@ export class SankeySeries exten }); if (this._linkMark) { - const allLinkElements = this._linkMark.getProductElements(); + const allLinkElements = this._linkMark.getGraphics(); if (!allLinkElements || !allLinkElements.length) { return; } - allLinkElements.forEach((linkEl: IElement, i: number) => { - if (highlightLinks.includes(linkEl.getDatum().key ?? linkEl.getDatum().index)) { + allLinkElements.forEach((linkEl: IMarkGraphic, i: number) => { + const linkDatum = getDatumOfGraphic(linkEl) as Datum; + + if (highlightLinks.includes(linkDatum.key ?? linkDatum.index)) { linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); } else { linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } if (this._nodeMark) { - this._highLightElements(this._nodeMark.getProductElements(), highlightNodes); + this._highLightElements(this._nodeMark.getGraphics(), highlightNodes); } } else { // 层级型数据 @@ -766,8 +772,8 @@ export class SankeySeries exten return res; }, []); - allLinkElements.forEach((linkEl: IElement, i: number) => { - const linkDatum = linkEl.getDatum(); + allLinkElements.forEach((linkEl: IMarkGraphic, i: number) => { + const linkDatum = getDatumOfGraphic(linkEl) as Datum; const originalDatum = linkDatum.datum; const selectedDatum = originalDatum ? originalDatum.filter((entry: any) => entry[father].some((par: any) => par.key === nodeDatum.key)) @@ -793,7 +799,8 @@ export class SankeySeries exten const ratio = val / linkDatum.value; linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); // 设置默认的部分高亮 + + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); return; } @@ -809,53 +816,60 @@ export class SankeySeries exten } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: upSelectedLink.value / linkDatum.value }); // 设置默认的部分高亮 + + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { + ratio: upSelectedLink.value / linkDatum.value + }); // 设置默认的部分高亮 return; } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); return; }); if (this._nodeMark) { - this._highLightElements(this._nodeMark.getProductElements(), highlightNodes); + this._highLightElements(this._nodeMark.getGraphics(), highlightNodes); } } this._needClear = true; }; - protected _handleLinkRelatedClick = (element: IGlyphElement) => { - const allNodeElements = this._nodeMark.getProductElements(); + protected _handleLinkRelatedClick = (graphic: IMarkGraphic) => { + const allNodeElements = this._nodeMark.getGraphics(); if (!allNodeElements || !allNodeElements.length) { return; } - const allLinkElements = this._linkMark.getProductElements(); + const allLinkElements = this._linkMark.getGraphics(); if (!allLinkElements || !allLinkElements.length) { return; } - const father = element.getDatum()?.parents ? 'parents' : 'source'; + const father = (getDatumOfGraphic(graphic) as Datum) ? 'parents' : 'source'; if (father === 'source') { - const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; + // const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; if (this._linkMark) { allLinkElements.forEach(linkEl => { - linkEl.removeState(states); + // linkEl.removeState(states); + linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); }); } if (this._nodeMark) { allNodeElements.forEach(el => { - el.removeState(states); + // el.removeState(states); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); }); } } else { - const curLinkDatum = element.getDatum(); + const curLinkDatum = getDatumOfGraphic(graphic) as Datum; const highlightNodes: string[] = [curLinkDatum.source, curLinkDatum.target]; const upstreamLinks: Array<{ source: string; target: string; value: number }> = []; @@ -888,13 +902,14 @@ export class SankeySeries exten }); allLinkElements.forEach(linkEl => { - const linkDatum = linkEl.getDatum(); + const linkDatum = getDatumOfGraphic(linkEl) as Datum; const originalDatum = linkDatum.datum; if (linkDatum.source === curLinkDatum.source && linkDatum.target === curLinkDatum.target) { // 自身 linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); + return; } @@ -927,7 +942,8 @@ export class SankeySeries exten const ratio = val / linkDatum.value; linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); // 设置默认的部分高亮 + + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); // 设置默认的部分高亮 return; } @@ -945,12 +961,15 @@ export class SankeySeries exten highlightNodes.push(linkDatum.target); } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: upSelectedLink.value / linkDatum.value }); // 设置默认的部分高亮 + + addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { + ratio: upSelectedLink.value / (linkDatum as Datum).value + }); // 设置默认的部分高亮 return; } linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); return; }); @@ -961,18 +980,20 @@ export class SankeySeries exten this._needClear = true; }; - protected _highLightElements(vGrammarElements: IVgrammarMark['elements'], highlightNodes: string[]) { - if (!vGrammarElements || !vGrammarElements.length) { + protected _highLightElements(graphics: IMarkGraphic[], highlightNodes: string[]) { + if (!graphics || !graphics.length) { return; } - vGrammarElements.forEach(el => { - el.removeState([STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS]); + graphics.forEach(g => { + // todo 升级 vrender版本,切换成数组 + g.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + g.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - if (highlightNodes.includes(el.getDatum().key)) { - el.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + if (highlightNodes.includes((getDatumOfGraphic(g) as Datum).key)) { + g.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); } else { - el.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + g.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } diff --git a/packages/vchart/src/series/scatter/scatter.ts b/packages/vchart/src/series/scatter/scatter.ts index fe643f87cc..8ea0e86835 100644 --- a/packages/vchart/src/series/scatter/scatter.ts +++ b/packages/vchart/src/series/scatter/scatter.ts @@ -23,11 +23,10 @@ import { registerScatterAnimation } from './animation'; import { registerSymbolMark } from '../../mark/symbol'; import { scatterSeriesMark } from './constant'; import { Factory } from '../../core/factory'; -import type { ILabelMark, IMark, ISymbolMark } from '../../mark/interface'; +import type { ILabelMark, IMark, IMarkGraphic, ISymbolMark } from '../../mark/interface'; import { ScatterSeriesSpecTransformer } from './scatter-transformer'; import { getGroupAnimationParams } from '../util/utils'; import { registerCartesianLinearAxis, registerCartesianBandAxis } from '../../component/axis/cartesian'; -import type { IGraphic } from '@visactor/vrender-core'; export class ScatterSeries extends CartesianSeries { static readonly type: string = SeriesTypeEnum.scatter; @@ -366,7 +365,7 @@ export class ScatterSeries ex return; } - graphics.forEach((graphicItem: IGraphic, i: number) => { + graphics.forEach((graphicItem: IMarkGraphic, i: number) => { const datum = graphicItem?.context?.data?.[0]; const newPosition = this.dataToPosition(datum); if (newPosition && graphicItem) { diff --git a/packages/vchart/src/series/sunburst/animation/utils.ts b/packages/vchart/src/series/sunburst/animation/utils.ts index 31a66b56b4..3eeb51b128 100644 --- a/packages/vchart/src/series/sunburst/animation/utils.ts +++ b/packages/vchart/src/series/sunburst/animation/utils.ts @@ -1,6 +1,6 @@ import { minInArray } from '@visactor/vutils'; import { DiffState } from '../../../mark/interface/enum'; -import type { IGraphic } from '@visactor/vrender-core'; +import type { IMarkGraphic } from '../../../mark/interface/common'; /** * 计算角度对于起点的比例 @@ -24,7 +24,7 @@ export const computeRatio = (angle: number, range: [number, number]) => { /** * 得到最内层的Elements */ -export const getInnerMostElements = (g: IGraphic) => { +export const getInnerMostElements = (g: IMarkGraphic) => { // 所有待更新的marks const updateElements = g.mark.graphics.filter(g => g.context.diffState === DiffState.update); // 得到最内层级 diff --git a/packages/vchart/src/series/treemap/treemap.ts b/packages/vchart/src/series/treemap/treemap.ts index 257d96861d..7370897975 100644 --- a/packages/vchart/src/series/treemap/treemap.ts +++ b/packages/vchart/src/series/treemap/treemap.ts @@ -1,6 +1,6 @@ /* eslint-disable no-duplicate-imports */ import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; -import { VGRAMMAR_HOOK_EVENT } from '../../constant/event'; +import { HOOK_EVENT } from '../../constant/event'; import { AttributeLevel } from '../../constant/attribute'; import { DEFAULT_DATA_KEY } from '../../constant/data'; import type { IMark, IRectMark, ILabelMark } from '../../mark/interface'; @@ -466,7 +466,7 @@ export class TreemapSeries extends CartesianSeries { // 缩放过程中会有新增/减少的element,对应执行enter/exit动画,会使得缩放交互效果体验很差 // 这里在缩放过程中先关闭所有动画 this.disableMarkAnimation(); - this.event.on(VGRAMMAR_HOOK_EVENT.AFTER_DO_RENDER, this._enableAnimationHook); + this.event.on(HOOK_EVENT.AFTER_DO_RENDER, this._enableAnimationHook); this._viewBox.transformWithMatrix(this._matrix); this._runTreemapTransform(true); } @@ -487,7 +487,7 @@ export class TreemapSeries extends CartesianSeries { protected enableMarkAnimation() { this.getMarks().forEach(mark => { - mark.getProduct().animate?.enable(); + // mark.getProduct().animate?.enable(); }); [this._labelMark, this._nonLeafLabelMark].forEach(m => { if (m && m.getComponent()) { @@ -495,12 +495,12 @@ export class TreemapSeries extends CartesianSeries { } }); // 在所有动画执行之后关闭动画 - this.event.off(VGRAMMAR_HOOK_EVENT.AFTER_DO_RENDER, this._enableAnimationHook); + this.event.off(HOOK_EVENT.AFTER_DO_RENDER, this._enableAnimationHook); } protected disableMarkAnimation() { this.getMarks().forEach(mark => { - mark.getProduct().animate?.disable(); + // mark.getProduct().animate?.disable(); }); [this._labelMark, this._nonLeafLabelMark].forEach(m => { if (m && m.getComponent()) { diff --git a/packages/vchart/src/series/word-cloud/word-cloud-3d.ts b/packages/vchart/src/series/word-cloud/word-cloud-3d.ts index 85bcbc80b2..9a4adbcd7f 100644 --- a/packages/vchart/src/series/word-cloud/word-cloud-3d.ts +++ b/packages/vchart/src/series/word-cloud/word-cloud-3d.ts @@ -68,9 +68,10 @@ export class WordCloud3dSeries< this._wordMark.setAnimationConfig( animationConfig( Factory.getAnimationInKey('wordCloud3d')?.(() => { - const srView = this.getCompiler().getVGrammarView(); - const width = srView.width() - padding.left || 0 - padding.right || 0; - const height = srView.height() - padding.top || 0 - padding.bottom || 0; + const stage = this.getCompiler().getStage(); + + const width = stage.width - padding.left || 0 - padding.right || 0; + const height = stage.height - padding.top || 0 - padding.bottom || 0; const r = Math.max(width, height) / 2; return { center: { x: r, y: r, z: this._spec.depth_3d ?? r }, diff --git a/packages/vchart/src/typings/common.ts b/packages/vchart/src/typings/common.ts index 5a476cd8e0..76ead9efac 100644 --- a/packages/vchart/src/typings/common.ts +++ b/packages/vchart/src/typings/common.ts @@ -16,3 +16,9 @@ export type Maybe = T | undefined | null; export type Quadrant = 1 | 2 | 3 | 4; export type ValueOf = T[K]; + +export type DiffResult = { + enter: { next: Next }[]; + update: { prev: Prev; next: Next }[]; + exit: { prev: Prev }[]; +}; diff --git a/packages/vchart/src/typings/spec/common.ts b/packages/vchart/src/typings/spec/common.ts index e9c78f3d6b..0cce8b9c0b 100644 --- a/packages/vchart/src/typings/spec/common.ts +++ b/packages/vchart/src/typings/spec/common.ts @@ -11,7 +11,7 @@ import type { IDsvParserOptions } from '@visactor/vdataset'; import type { RegionSpec } from '../../region/interface'; -import type { IHoverSpec, ISelectSpec, IInteractionSpec } from '../../interaction/interface'; +import type { IHoverSpec, ISelectSpec, IInteractionSpec } from '../../interaction/interface/spec'; import type { IRenderOption } from '../../compile/interface'; import type { ISeriesTooltipSpec, ITooltipSpec } from '../../component/tooltip/interface'; // eslint-disable-next-line no-duplicate-imports @@ -77,10 +77,6 @@ export interface IInitOption extends Omit { dataSet?: DataSet; /** 是否自适应容器大小 */ autoFit?: boolean; - /** - * 性能测试钩子 - */ - performanceHook?: IPerformanceHook; /** * 是否开启动画 */ @@ -565,9 +561,9 @@ export type IMarkStateFilter = | (( datum: Datum, options: { - mark: IMark; - type: string; - renderNode: IGraphic; + mark?: IMark; + type?: string; + renderNode?: IGraphic; } ) => boolean); @@ -644,58 +640,58 @@ export interface IPerformanceHook { afterInitializeChart?: (vchart?: IVChart) => void; // 编译 - beforeCompileToVGrammar?: () => void; - afterCompileToVGrammar?: () => void; + beforeCompileToVGrammar?: (vchart?: IVChart) => void; + afterCompileToVGrammar?: (vchart?: IVChart) => void; // 各个图表模块编译 - beforeRegionCompile?: () => void; - afterRegionCompile?: () => void; - beforeSeriesCompile?: () => void; - afterSeriesCompile?: () => void; - beforeComponentCompile?: () => void; - afterComponentCompile?: () => void; + beforeRegionCompile?: (vchart?: IVChart) => void; + afterRegionCompile?: (vchart?: IVChart) => void; + beforeSeriesCompile?: (vchart?: IVChart) => void; + afterSeriesCompile?: (vchart?: IVChart) => void; + beforeComponentCompile?: (vchart?: IVChart) => void; + afterComponentCompile?: (vchart?: IVChart) => void; // resize的时候的钩子 - beforeResizeWithUpdate?: () => void; - afterResizeWithUpdate?: () => void; + beforeResizeWithUpdate?: (vchart?: IVChart) => void; + afterResizeWithUpdate?: (vchart?: IVChart) => void; // LayoutWithSceneGraph 二次布局 - beforeLayoutWithSceneGraph?: () => void; - afterLayoutWithSceneGraph?: () => void; + beforeLayoutWithSceneGraph?: (vchart?: IVChart) => void; + afterLayoutWithSceneGraph?: (vchart?: IVChart) => void; // VGrammar 解析spec - beforeParseView?: () => void; - afterParseView?: () => void; + beforeParseView?: (vchart?: IVChart) => void; + afterParseView?: (vchart?: IVChart) => void; // 初始化runtime - beforeCreateRuntime?: () => void; - afterCreateRuntime?: () => void; + beforeCreateRuntime?: (vchart?: IVChart) => void; + afterCreateRuntime?: (vchart?: IVChart) => void; // VGrammar EvaluateAsync 时间 - beforeSrViewEvaluateAsync?: () => void; - afterSrViewEvaluateAsync?: () => void; + beforeSrViewEvaluateAsync?: (vchart?: IVChart) => void; + afterSrViewEvaluateAsync?: (vchart?: IVChart) => void; // VGrammar RunAsync 时间 - beforeSrViewRunAsync?: () => void; - afterSrViewRunAsync?: () => void; + beforeSrViewRunAsync?: (vchart?: IVChart) => void; + afterSrViewRunAsync?: (vchart?: IVChart) => void; // transform测量 - beforeTransform?: (name: string) => void; - afterTransform?: (name: string) => void; + beforeTransform?: (name: string, vchart?: IVChart) => void; + afterTransform?: (name: string, vchart?: IVChart) => void; // Create VRender Stage 时间 - beforeCreateVRenderStage?: () => void; - afterCreateVRenderStage?: () => void; + beforeCreateVRenderStage?: (vchart?: IVChart) => void; + afterCreateVRenderStage?: (vchart?: IVChart) => void; // Create VRender Mark 时间 - beforeCreateVRenderMark?: () => void; - afterCreateVRenderMark?: () => void; + beforeCreateVRenderMark?: (vchart?: IVChart) => void; + afterCreateVRenderMark?: (vchart?: IVChart) => void; // VGrammar 创建元素完成,vrender 绘图之前 beforeDoRender?: (vchart?: IVChart) => void; // VRender Draw 时间 - beforeVRenderDraw?: () => void; - afterVRenderDraw?: () => void; + beforeVRenderDraw?: (vchart?: IVChart) => void; + afterVRenderDraw?: (vchart?: IVChart) => void; } export type IBuildinMarkSpec = { diff --git a/packages/vchart/src/util/index.ts b/packages/vchart/src/util/index.ts index 4c691c6806..1c345aa7b2 100644 --- a/packages/vchart/src/util/index.ts +++ b/packages/vchart/src/util/index.ts @@ -19,5 +19,6 @@ export * from './text'; export * from './data'; export * from './hierarchy'; export * from './style'; +export * from './mark'; export const Utils = { TimeUtil }; diff --git a/packages/vchart/src/util/mark.ts b/packages/vchart/src/util/mark.ts new file mode 100644 index 0000000000..ca5b75ae43 --- /dev/null +++ b/packages/vchart/src/util/mark.ts @@ -0,0 +1,34 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import { MarkTypeEnum } from '../mark/interface'; +import type { IMarkGraphic } from '../mark/interface/common'; + +export const isCollectionMark = (type: string) => { + return type === MarkTypeEnum.line || type === MarkTypeEnum.area; +}; + +export const getDatumOfGraphic = (g: IMarkGraphic) => { + if (!g || !g.context) { + return null; + } + + const { data, markType } = g.context; + if (isCollectionMark(markType)) { + return data; + } + + return data?.[0]; +}; + +export const findMarkGraphic = (rootGroup: IGraphic, target: IGraphic) => { + let g = target; + + while (g?.parent && g.parent !== rootGroup) { + g = g.parent; + + if ((g as IMarkGraphic).context) { + return g; + } + } + + return null; +};