diff --git a/common/changes/@visactor/vchart/feat-mark-point-dimension-hover-state_2026-07-16-08-43.json b/common/changes/@visactor/vchart/feat-mark-point-dimension-hover-state_2026-07-16-08-43.json new file mode 100644 index 0000000000..e27d691147 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-mark-point-dimension-hover-state_2026-07-16-08-43.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: support dimension hover state for mark point target symbol", + "type": "minor", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "lixuef1313@163.com" +} diff --git a/packages/vchart-types/types/component/marker/interface.d.ts b/packages/vchart-types/types/component/marker/interface.d.ts index 389467b509..0fcb57718f 100644 --- a/packages/vchart-types/types/component/marker/interface.d.ts +++ b/packages/vchart-types/types/component/marker/interface.d.ts @@ -101,7 +101,7 @@ export type IMarkerSymbol = IMarkerRef & { } & Partial>>; export type MarkerStyleCallback = (markerData: DataView, context: IMarkerAttributeContext) => T; export type MarkerStateCallback = (markerData: DataView, context: IMarkerAttributeContext) => T; -export type MarkerStateValue = 'hover' | 'hover_reverse' | 'selected' | 'selected_reverse'; +export type MarkerStateValue = 'hover' | 'hover_reverse' | 'dimension_hover' | 'dimension_hover_reverse' | 'selected' | 'selected_reverse'; export type IMarkerState = { style?: T | MarkerStyleCallback; state?: Record>; diff --git a/packages/vchart-types/types/component/marker/mark-point/base-mark-point.d.ts b/packages/vchart-types/types/component/marker/mark-point/base-mark-point.d.ts index 89396dc72a..d6c91385f6 100644 --- a/packages/vchart-types/types/component/marker/mark-point/base-mark-point.d.ts +++ b/packages/vchart-types/types/component/marker/mark-point/base-mark-point.d.ts @@ -1,6 +1,7 @@ import type { IMarkPoint, IMarkPointSpec } from './interface'; import { MarkPoint as MarkPointComponent } from '@visactor/vrender-components'; import { BaseMarker } from '../base-marker'; +import type { DimensionEventParams } from '../../../event/events/dimension/interface'; import type { IGroup } from '@visactor/vrender-core'; export declare abstract class BaseMarkPoint extends BaseMarker implements IMarkPoint { static specKey: string; @@ -8,6 +9,10 @@ export declare abstract class BaseMarkPoint extends BaseMarker i layoutZIndex: number; protected _markerComponent: MarkPointComponent; protected abstract _computePointsAttr(): any; + protected initEvent(): void; + protected _handleDimensionHover: (params: DimensionEventParams) => void; + private _getTargetSymbolGraphic; + private _isDimensionHoverTarget; static _getMarkerCoordinateType(markerSpec: any): string; protected _createMarkerComponent(): IGroup; protected _markerLayout(): void; diff --git a/packages/vchart/__tests__/runtime/browser/test-page/mark-point-target-symbol-hover.ts b/packages/vchart/__tests__/runtime/browser/test-page/mark-point-target-symbol-hover.ts new file mode 100644 index 0000000000..f9f3ded498 --- /dev/null +++ b/packages/vchart/__tests__/runtime/browser/test-page/mark-point-target-symbol-hover.ts @@ -0,0 +1,110 @@ +import type { ISpec } from '../../../../src'; + +export const targetSymbolHoverSpec: ISpec = { + type: 'line', + title: { + text: 'MarkPoint targetSymbol 的 dimension_hover 状态' + }, + data: [ + { + id: 'target-symbol-hover-data', + values: [ + { month: '1月', value: 28 }, + { month: '2月', value: 46 }, + { month: '3月', value: 66 }, + { month: '4月', value: 51 }, + { month: '5月', value: 74 }, + { month: '6月', value: 62 } + ] + } + ], + xField: 'month', + yField: 'value', + axes: [{ orient: 'bottom' }, { orient: 'left', grid: { visible: true } }], + tooltip: { + visible: false + }, + point: { + style: { + size: 8 + } + }, + markPoint: [ + { + coordinate: { month: '3月', value: 66 }, + itemContent: { + type: 'text', + offsetX: 42, + offsetY: -72, + autoRotate: false, + style: { + text: '悬停到对应 x 轴区域:targetSymbol 变透明;移出后恢复', + fill: '#d94801', + fontSize: 14 + } + }, + itemLine: { + startSymbol: { visible: false }, + line: { + style: { + stroke: '#d94801', + lineWidth: 2 + } + } + }, + targetSymbol: { + visible: true, + size: 32, + style: { + symbolType: 'circle', + fill: '#fff7ed', + stroke: '#ea580c', + lineWidth: 4 + }, + state: { + dimension_hover: { + opacity: 0.001 + } + } + } + }, + { + coordinate: { month: '5月', value: 74 }, + itemContent: { + type: 'text', + offsetX: -42, + offsetY: -72, + autoRotate: false, + style: { + text: '仅命中当前 x 值的 targetSymbol', + fill: '#0f766e', + fontSize: 14 + } + }, + itemLine: { + startSymbol: { visible: false }, + line: { + style: { + stroke: '#0f766e', + lineWidth: 2 + } + } + }, + targetSymbol: { + visible: true, + size: 32, + style: { + symbolType: 'circle', + fill: '#f0fdfa', + stroke: '#0f766e', + lineWidth: 4 + }, + state: { + dimension_hover: { + opacity: 0.001 + } + } + } + } + ] +}; diff --git a/packages/vchart/__tests__/unit/component/marker/mark-point/mark-point-style.test.ts b/packages/vchart/__tests__/unit/component/marker/mark-point/mark-point-style.test.ts index 269321dfee..871cbb094a 100644 --- a/packages/vchart/__tests__/unit/component/marker/mark-point/mark-point-style.test.ts +++ b/packages/vchart/__tests__/unit/component/marker/mark-point/mark-point-style.test.ts @@ -100,4 +100,64 @@ describe('MarkPoint Style', () => { expect(itemContent.style.textConfig).toEqual(richText); expect(itemContent.style.text).toBeUndefined(); }); + + it('should apply targetSymbol dimension_hover state for the matching x value', () => { + const spec = { + coordinate: { + month: '3月', + value: 66 + }, + itemContent: { + type: 'text', + text: { + text: 'Hello' + } + }, + itemLine: {}, + targetSymbol: { + visible: true, + state: { + dimension_hover: { + opacity: 0.001 + } + } + } + }; + const ctx: any = { + getChart: () => ({ + getTheme: () => ({}) + }) + }; + + const markPoint = new TestMarkPoint(spec as any, ctx); + const targetSymbol = { + addState: jest.fn(), + removeState: jest.fn() + }; + (markPoint as any)._markerComponent = { + find: () => targetSymbol + }; + (markPoint as any)._markerData = { + latestData: [ + { + x: ['3月'], + y: [66] + } + ] + }; + + (markPoint as any)._handleDimensionHover({ + action: 'enter', + dimensionInfo: [{ value: '3月', data: [] }] + }); + + expect(targetSymbol.addState).toHaveBeenCalledWith('dimension_hover', true); + + (markPoint as any)._handleDimensionHover({ + action: 'leave', + dimensionInfo: [{ value: '3月', data: [] }] + }); + + expect(targetSymbol.removeState).toHaveBeenCalledWith('dimension_hover'); + }); }); diff --git a/packages/vchart/src/component/marker/interface.ts b/packages/vchart/src/component/marker/interface.ts index b2d67958b6..39ba1bae8b 100644 --- a/packages/vchart/src/component/marker/interface.ts +++ b/packages/vchart/src/component/marker/interface.ts @@ -384,7 +384,13 @@ export type MarkerStateCallback = ( */ context: IMarkerAttributeContext ) => T; -export type MarkerStateValue = 'hover' | 'hover_reverse' | 'selected' | 'selected_reverse'; +export type MarkerStateValue = + | 'hover' + | 'hover_reverse' + | 'dimension_hover' + | 'dimension_hover_reverse' + | 'selected' + | 'selected_reverse'; export type IMarkerState = { /** 默认样式设置 */ style?: T | MarkerStyleCallback; diff --git a/packages/vchart/src/component/marker/mark-point/base-mark-point.ts b/packages/vchart/src/component/marker/mark-point/base-mark-point.ts index 48e21b585d..b8d0755ac7 100644 --- a/packages/vchart/src/component/marker/mark-point/base-mark-point.ts +++ b/packages/vchart/src/component/marker/mark-point/base-mark-point.ts @@ -11,11 +11,13 @@ import { import type { MarkPointAttrs } from '@visactor/vrender-components'; // eslint-disable-next-line no-duplicate-imports import { MarkPoint as MarkPointComponent } from '@visactor/vrender-components'; -import { isValid, merge } from '@visactor/vutils'; +import { array, isValid, merge } from '@visactor/vutils'; import { transformToGraphic } from '../../../util/style'; import { BaseMarker } from '../base-marker'; import { LayoutZIndex } from '../../../constant/layout'; -import type { IGroup } from '@visactor/vrender-core'; +import { STATE_VALUE_ENUM } from '../../../compile/mark/interface'; +import { DimensionEventEnum, type DimensionEventParams } from '../../../event/events/dimension/interface'; +import type { IGraphic, IGroup } from '@visactor/vrender-core'; import type { IMarkerLabelSpec, IMarkerLabelWithoutRefSpec } from '../interface'; const isRichTextStyle = (style: any) => style?.type === 'rich'; @@ -44,6 +46,60 @@ export abstract class BaseMarkPoint extends BaseMarker implement protected abstract _computePointsAttr(): any; + protected initEvent() { + super.initEvent(); + + const targetSymbolState = this._spec.targetSymbol?.state; + if ( + this.coordinateType === 'cartesian' && + (targetSymbolState?.dimension_hover || targetSymbolState?.dimension_hover_reverse) + ) { + this.event.on(DimensionEventEnum.dimensionHover, this._handleDimensionHover); + } + } + + protected _handleDimensionHover = (params: DimensionEventParams) => { + const targetSymbol = this._getTargetSymbolGraphic(); + if (!targetSymbol) { + return; + } + + switch (params.action) { + case 'enter': { + targetSymbol.removeState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); + targetSymbol.removeState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE); + + if (this._isDimensionHoverTarget(params)) { + targetSymbol.addState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, true); + } else if (this._spec.targetSymbol?.state?.dimension_hover_reverse) { + targetSymbol.addState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE, true); + } + break; + } + case 'leave': + targetSymbol.removeState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); + targetSymbol.removeState(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE); + break; + default: + break; + } + }; + + private _getTargetSymbolGraphic() { + return this._markerComponent?.find( + node => node.name === 'mark-point-targetItem', + true + ) as unknown as IGraphic | null; + } + + private _isDimensionHoverTarget(params: DimensionEventParams) { + const latestData = this._markerData?.latestData; + const markerData = latestData?.[0]?.latestData ?? latestData; + const markerXValues = array(markerData?.[0]?.x).filter(isValid); + + return markerXValues.some(value => params.dimensionInfo.some(dimension => dimension.value === value)); + } + static _getMarkerCoordinateType(markerSpec: any): string { const { doPolarProcess, doGeoProcess } = getMarkPointProcessInfo(markerSpec); if (markerSpec.coordinateType === 'polar' || doPolarProcess) {