From 5cf6eb61184f1a1f141c1ab59ea734ef0706b887 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 30 Sep 2024 16:45:16 +0800 Subject: [PATCH 1/4] feat: add series mark selection --- .../vstory/demo/src/demos/VChartGraphic.tsx | 2 + packages/vstory/src/edit/edit-action.ts | 13 +- .../src/edit/edit-component/base-selection.ts | 28 +++- .../edit-control/series-mark-control/bar.ts | 9 ++ .../edit-control/series-mark-control/base.ts | 28 ++++ .../edit-control/series-mark-control/index.ts | 8 + .../edit-control/series-mark-control/line.ts | 9 ++ .../series-mark/series-mark-selection.ts | 137 ++++++++++++++++++ packages/vstory/src/edit/edit.ts | 11 ++ packages/vstory/src/edit/interface.ts | 8 +- .../vstory/src/story/utils/vchart-pick.ts | 2 +- 11 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts create mode 100644 packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index 847232e..e889a86 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -5,6 +5,7 @@ import '../../../src/story/index'; import { Edit } from '../../../src/edit/edit'; import { BoxSelection } from '../../../src/edit/edit-component/box-selection'; import { TextSelection } from '../../../src/edit/edit-component/text-selection'; +import { SeriesMarkSelection } from '../../../src/edit/edit-component/series-mark/series-mark-selection'; // import { RichTextSelection } from '../../../src/edit/edit-component/richtext-selection'; import { loadAllSelection } from '../../../src/edit/edit-component'; @@ -12,6 +13,7 @@ loadAllSelection(); Edit.registerEditComponent('text', TextSelection); // Edit.registerEditComponent('richtext', RichTextSelection); Edit.registerEditComponent('box-selection', BoxSelection); +Edit.registerEditComponent('series-mark-selection', SeriesMarkSelection); const spec = { direction: 'vertical', diff --git a/packages/vstory/src/edit/edit-action.ts b/packages/vstory/src/edit/edit-action.ts index b13e903..c0e9dec 100644 --- a/packages/vstory/src/edit/edit-action.ts +++ b/packages/vstory/src/edit/edit-action.ts @@ -5,7 +5,7 @@ import type { BoxSelection } from './edit-component/box-selection'; import { EventEmitter, isArray } from '@visactor/vutils'; import { PickEventType } from './const'; import type { Story } from '../story/story'; -import type { ContinuousActionType, IEditActionInfo } from './interface'; +import type { ContinuousActionType, IEditActionInfo, IEditOverActionInfo } from './interface'; import { EditActionEnum } from './interface'; import type { StoryEvent } from '../story/interface/runtime-interface'; @@ -147,8 +147,15 @@ export class EditAction { characterId: this.lastOverGraphic.character?.id, character: this.lastOverGraphic.character, event, - detail: this.lastOverGraphic.characterInfo - }); + detail: this.lastOverGraphic.characterInfo, + nextAction: { + actionType: EditActionEnum.pointerOverCharacter, + characterId: character?.id, + character: character, + event, + detail: characterInfo + } + } as IEditOverActionInfo); actionType = EditActionEnum.pointerOverCharacter; } else { // 是同一个元素,并且是同一个target diff --git a/packages/vstory/src/edit/edit-component/base-selection.ts b/packages/vstory/src/edit/edit-component/base-selection.ts index 91a6334..70ccb8e 100644 --- a/packages/vstory/src/edit/edit-component/base-selection.ts +++ b/packages/vstory/src/edit/edit-component/base-selection.ts @@ -6,6 +6,7 @@ import { TransformControl, type TransformAttributes } from './edit-control/trans import { throwError } from '../../util/common'; import type { VRenderPointerEvent } from '../../interface/type'; import type { ICharacter } from '../../story/character/runtime-interface'; +import { createGroup, type IGroup } from '@visactor/vrender-core'; export abstract class BaseSelection implements IEditComponent { declare readonly level: number; @@ -16,10 +17,18 @@ export abstract class BaseSelection implements IEditComponent { isEditing: boolean = false; protected _layoutComponent?: ITransformControl; protected _activeCharacter?: ICharacter | null; + protected _overGraphic: IGroup; - constructor(public readonly edit: Edit) {} + constructor(public readonly edit: Edit) { + this._initOverGraphic(); + } declare type: string; + protected _initOverGraphic() { + this._overGraphic = createGroup({ pickable: false }); + this.edit.getEditGroup().add(this._overGraphic); + } + endEdit(emitEvent: boolean = true): void { this.isEditing = false; const actionInfo = this._actionInfo; @@ -45,13 +54,17 @@ export abstract class BaseSelection implements IEditComponent { return character.type === this.editCharacterType; } + enableEditActionInfo(actionInfo: IEditActionInfo | IEditSelectionInfo) { + return this.enableEditCharacter((actionInfo as IEditSelectionInfo).character); + } + checkActionWhileEditing(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { if (actionInfo.type === EditActionEnum.unSelection) { return false; } if (actionInfo.type === EditActionEnum.singleSelection) { // 选中其他内容了,return false - if (!this.enableEditCharacter((actionInfo as IEditSelectionInfo).character)) { + if (!this.enableEditActionInfo(actionInfo)) { return false; } else if ((actionInfo as IEditSelectionInfo).character !== this._activeCharacter) { // 选中同类型其他元素 @@ -70,10 +83,7 @@ export abstract class BaseSelection implements IEditComponent { } checkActionWhileNoEditing(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { - if ( - actionInfo.type === EditActionEnum.singleSelection && - this.enableEditCharacter((actionInfo as IEditSelectionInfo).character) - ) { + if (actionInfo.type === EditActionEnum.singleSelection && this.enableEditActionInfo(actionInfo)) { // this.startEdit(actionInfo); // graphic return true; @@ -184,4 +194,10 @@ export abstract class BaseSelection implements IEditComponent { this._activeCharacter.setConfig({ position: data }); } } + + abstract checkOver?(actionInfo: IEditActionInfo): void; + + release() { + this.inActiveLayoutComponent(); + } } diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts new file mode 100644 index 0000000..1b7c04e --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts @@ -0,0 +1,9 @@ +import type { IEditActionInfo } from '../../../interface'; +import { BaseMarkControl } from './base'; + +// Todo 修改柱宽度,柱高度 +export class BarMarkControl extends BaseMarkControl { + startWithActionInfo(actionInfo: IEditActionInfo) { + super.startWithActionInfo(actionInfo); + } +} diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts new file mode 100644 index 0000000..e510f48 --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts @@ -0,0 +1,28 @@ +import { createGroup, type IGroup } from '@visactor/vrender-core'; +import type { IEditActionInfo } from '../../../interface'; +import type { Edit } from '../../../edit'; + +export interface IMarkControlConstructor { + new (edit: Edit): BaseMarkControl; +} + +// Todo 修改柱宽度,柱高度 +export class BaseMarkControl { + protected _actionInfo: IEditActionInfo; + protected _graphicGroup: IGroup; + + constructor(public readonly edit: Edit) { + this._graphicGroup = createGroup({ visible: false }); + this.edit.getEditGroup().add(this._graphicGroup); + } + + startWithActionInfo(actionInfo: IEditActionInfo) { + this._actionInfo = actionInfo; + } + + release() { + this._actionInfo = null; + this.edit.getEditGroup().removeChild(this._graphicGroup); + this._graphicGroup = null; + } +} diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts new file mode 100644 index 0000000..fef052d --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts @@ -0,0 +1,8 @@ +import { BarMarkControl } from './bar'; +import type { IMarkControlConstructor } from './base'; +import { LineMarkControl } from './line'; + +export const SeriesMarkControl: { [key: string]: IMarkControlConstructor } = { + bar: BarMarkControl, + line: LineMarkControl +}; diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts new file mode 100644 index 0000000..de6e1a5 --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts @@ -0,0 +1,9 @@ +import type { IEditActionInfo } from '../../../interface'; +import { BaseMarkControl } from './base'; + +// Todo 修改柱宽度,柱高度 +export class LineMarkControl extends BaseMarkControl { + startWithActionInfo(actionInfo: IEditActionInfo) { + super.startWithActionInfo(actionInfo); + } +} diff --git a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts new file mode 100644 index 0000000..603ad2b --- /dev/null +++ b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts @@ -0,0 +1,137 @@ +import { SeriesMarkControl } from './../edit-control/series-mark-control/index'; +import type { BaseMarkControl } from './../edit-control/series-mark-control/base'; +import type { IEditOverActionInfo } from './../../interface'; +import { createRect, IGroup } from '@visactor/vrender-core'; +import type { IEditSelectionInfo } from '../../interface'; +import { EditActionEnum, type IEditActionInfo, type IEditComponent } from '../../interface'; + +import { BaseSelection } from './../base-selection'; + +export class SeriesMarkSelection extends BaseSelection implements IEditComponent { + readonly level = 4; + editCharacterType = 'VChart'; + type = 'chart'; + + protected declare _actionInfo: IEditSelectionInfo; + + //TODO series mark control + // 根据不同的当前选中系列,加载对应的control + protected _seriesMarkControl: BaseMarkControl; + + enableEditActionInfo(actionInfo: IEditActionInfo | IEditSelectionInfo) { + const result = super.enableEditActionInfo(actionInfo); + if (!result) { + return result; + } + // 如果不是系列mark + if ((actionInfo as IEditSelectionInfo).detail?.part !== 'seriesMark') { + return false; + } + return true; + } + + checkAction(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { + // if (this.isEditing && actionInfo.type === EditActionEnum.singleSelection) { + // debugger; + // } + return super.checkAction(actionInfo); + } + + activeLayoutComponent(): void { + // 不创建 + } + + startEdit(actionInfo: IEditActionInfo) { + super.startEdit(actionInfo, false); + this.edit.emitter.emit('startEdit', { + type: 'chartSelection', + actionInfo, + selection: this + }); + // @ts-ignore; + const character = this._actionInfo.character; + character.graphic.graphic.addEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); + character.graphic.graphic.addEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + + // 添加编辑时框选效果 + this.addSelectedBorder(); + } + + endEdit() { + if (!this._actionInfo) { + return; + } + // @ts-ignore; + const character = this._actionInfo.character; + character.graphic.graphic.removeEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); + character.graphic.graphic.removeEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + super.endEdit(); + + // 删除 + this.removeSelectedBorder(); + } + + addSelectedBorder() { + // + } + removeSelectedBorder() { + // + } + + unLoadSeriesMarkControl() { + if (this._seriesMarkControl) { + this._seriesMarkControl.release(); + this._seriesMarkControl = null; + } + } + loadSeriesMarkControl() { + this.unLoadSeriesMarkControl(); + const markControlC = SeriesMarkControl[this._actionInfo.detail.modelInfo.model.type]; + if (!markControlC) { + return; + } + this._seriesMarkControl = new markControlC(this.edit); + this._seriesMarkControl.startWithActionInfo(this._actionInfo); + } + + handlerChartClick = (e: any) => { + // + }; + + handlerDoubleClick = (e: any) => { + // TODO:双击进入下一层 + }; + + checkOver?(action: IEditActionInfo): void { + // action + if (action.type === EditActionEnum.pointerOverCharacter && action.detail?.part === 'seriesMark') { + // show over graphic + this._showOverGraphic(action as IEditOverActionInfo); + } + if (action.type === EditActionEnum.pointerOutCharacter) { + // 前提:如果当前over在展示 + // TODO:系列mark补充over展示逻辑 + // 删除的几种情况: + // 1 全选状态下,seriesType是否一致 + // 2 分组选中下,是否是同一组 + // 3 单个选中下,是否是同一个 + if (this._overGraphic.attribute.visible) { + this._overGraphic.removeAllChild(); + this._overGraphic.setAttribute('visible', false); + } + } + } + + protected _showOverGraphic(action: IEditOverActionInfo) { + console.warn('series _showOverGraphic', action); + this._overGraphic.setAttribute('visible', true); + const test = createRect({ x: 100, y: 100, width: 100, height: 100, fill: 'red' }); + this._overGraphic.add(test); + } + + release(): void { + this.unLoadSeriesMarkControl(); + this.endEdit(); + super.release(); + } +} diff --git a/packages/vstory/src/edit/edit.ts b/packages/vstory/src/edit/edit.ts index cb6a8da..9c69d1d 100644 --- a/packages/vstory/src/edit/edit.ts +++ b/packages/vstory/src/edit/edit.ts @@ -87,7 +87,18 @@ export class Edit { this.editAction.onStoryEvent(event, type); } + // TODO: over不能正确的分发到全部selection onAction(actionInfo: IEditActionInfo) { + // over + if ( + actionInfo.type === EditActionEnum.pointerOverCharacter || + actionInfo.type === EditActionEnum.pointerOutCharacter + ) { + this._componentList.forEach(c => c.checkOver?.(actionInfo)); + return; + } + + // 选中 if (this._currentComponent) { // 优先上一次的编辑组件 if (this._currentComponent.checkAction(actionInfo)) { diff --git a/packages/vstory/src/edit/interface.ts b/packages/vstory/src/edit/interface.ts index e2995cb..8b59e72 100644 --- a/packages/vstory/src/edit/interface.ts +++ b/packages/vstory/src/edit/interface.ts @@ -28,6 +28,10 @@ export interface IEditSelectionInfo extends IEditActionInfoBase { detail: IEditSelectionDetailChart | IEditSelectionDetailComponent; } +export interface IEditOverActionInfo extends IEditActionInfoBase, IEditSelectionInfo { + nextActionInfo?: IEditActionInfo; +} + export interface VREvent extends Event { pickParams?: { shadowTarget?: IGraphic; @@ -39,7 +43,7 @@ export interface IEditActionInfoBase { event: VREvent; } -export type IEditActionInfo = IEditActionInfoBase | IEditSelectionInfo; +export type IEditActionInfo = IEditActionInfoBase | IEditSelectionInfo | IEditOverActionInfo; export type ContinuousActionType = 'boxSelection' | 'layerZoom' | 'layerMove'; @@ -51,6 +55,8 @@ export interface IEditComponent { // 是否 开始/继续 编辑 返回false的话,会导致当前编辑结束 checkAction: (actionInfo: IEditActionInfo | IEditSelectionInfo) => boolean; + checkOver?: (actionInfo: IEditActionInfo | IEditSelectionInfo) => void; + // 编辑开始 startEdit: (actionInfo: IEditActionInfo | IEditSelectionInfo, emitEvent?: boolean) => void; diff --git a/packages/vstory/src/story/utils/vchart-pick.ts b/packages/vstory/src/story/utils/vchart-pick.ts index 23868a7..fbe0506 100644 --- a/packages/vstory/src/story/utils/vchart-pick.ts +++ b/packages/vstory/src/story/utils/vchart-pick.ts @@ -199,7 +199,7 @@ export function getGraphicModelMark( if (!graphic) { return null; } - if (graphic.layer !== chart.getStage().layer) { + if (graphic.layer !== chart.getStage().defaultLayer) { return null; } const modelPick = modelCheck.find(mc => mc.check(graphic, graphicPath)); From d16731011325abb3c51a53a3dba6fe3e8f2bf863 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 30 Sep 2024 16:45:16 +0800 Subject: [PATCH 2/4] feat: add series mark selection --- .../vstory/demo/src/demos/VChartGraphic.tsx | 2 + packages/vstory/src/edit/edit-action.ts | 13 +- .../src/edit/edit-component/base-selection.ts | 28 +++- .../edit-control/series-mark-control/bar.ts | 9 ++ .../edit-control/series-mark-control/base.ts | 28 ++++ .../edit-control/series-mark-control/index.ts | 8 + .../edit-control/series-mark-control/line.ts | 9 ++ .../series-mark/series-mark-selection.ts | 137 ++++++++++++++++++ packages/vstory/src/edit/edit.ts | 11 ++ packages/vstory/src/edit/interface.ts | 8 +- .../vstory/src/story/utils/vchart-pick.ts | 2 +- 11 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts create mode 100644 packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts create mode 100644 packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index 847232e..e889a86 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -5,6 +5,7 @@ import '../../../src/story/index'; import { Edit } from '../../../src/edit/edit'; import { BoxSelection } from '../../../src/edit/edit-component/box-selection'; import { TextSelection } from '../../../src/edit/edit-component/text-selection'; +import { SeriesMarkSelection } from '../../../src/edit/edit-component/series-mark/series-mark-selection'; // import { RichTextSelection } from '../../../src/edit/edit-component/richtext-selection'; import { loadAllSelection } from '../../../src/edit/edit-component'; @@ -12,6 +13,7 @@ loadAllSelection(); Edit.registerEditComponent('text', TextSelection); // Edit.registerEditComponent('richtext', RichTextSelection); Edit.registerEditComponent('box-selection', BoxSelection); +Edit.registerEditComponent('series-mark-selection', SeriesMarkSelection); const spec = { direction: 'vertical', diff --git a/packages/vstory/src/edit/edit-action.ts b/packages/vstory/src/edit/edit-action.ts index b13e903..c0e9dec 100644 --- a/packages/vstory/src/edit/edit-action.ts +++ b/packages/vstory/src/edit/edit-action.ts @@ -5,7 +5,7 @@ import type { BoxSelection } from './edit-component/box-selection'; import { EventEmitter, isArray } from '@visactor/vutils'; import { PickEventType } from './const'; import type { Story } from '../story/story'; -import type { ContinuousActionType, IEditActionInfo } from './interface'; +import type { ContinuousActionType, IEditActionInfo, IEditOverActionInfo } from './interface'; import { EditActionEnum } from './interface'; import type { StoryEvent } from '../story/interface/runtime-interface'; @@ -147,8 +147,15 @@ export class EditAction { characterId: this.lastOverGraphic.character?.id, character: this.lastOverGraphic.character, event, - detail: this.lastOverGraphic.characterInfo - }); + detail: this.lastOverGraphic.characterInfo, + nextAction: { + actionType: EditActionEnum.pointerOverCharacter, + characterId: character?.id, + character: character, + event, + detail: characterInfo + } + } as IEditOverActionInfo); actionType = EditActionEnum.pointerOverCharacter; } else { // 是同一个元素,并且是同一个target diff --git a/packages/vstory/src/edit/edit-component/base-selection.ts b/packages/vstory/src/edit/edit-component/base-selection.ts index 91a6334..70ccb8e 100644 --- a/packages/vstory/src/edit/edit-component/base-selection.ts +++ b/packages/vstory/src/edit/edit-component/base-selection.ts @@ -6,6 +6,7 @@ import { TransformControl, type TransformAttributes } from './edit-control/trans import { throwError } from '../../util/common'; import type { VRenderPointerEvent } from '../../interface/type'; import type { ICharacter } from '../../story/character/runtime-interface'; +import { createGroup, type IGroup } from '@visactor/vrender-core'; export abstract class BaseSelection implements IEditComponent { declare readonly level: number; @@ -16,10 +17,18 @@ export abstract class BaseSelection implements IEditComponent { isEditing: boolean = false; protected _layoutComponent?: ITransformControl; protected _activeCharacter?: ICharacter | null; + protected _overGraphic: IGroup; - constructor(public readonly edit: Edit) {} + constructor(public readonly edit: Edit) { + this._initOverGraphic(); + } declare type: string; + protected _initOverGraphic() { + this._overGraphic = createGroup({ pickable: false }); + this.edit.getEditGroup().add(this._overGraphic); + } + endEdit(emitEvent: boolean = true): void { this.isEditing = false; const actionInfo = this._actionInfo; @@ -45,13 +54,17 @@ export abstract class BaseSelection implements IEditComponent { return character.type === this.editCharacterType; } + enableEditActionInfo(actionInfo: IEditActionInfo | IEditSelectionInfo) { + return this.enableEditCharacter((actionInfo as IEditSelectionInfo).character); + } + checkActionWhileEditing(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { if (actionInfo.type === EditActionEnum.unSelection) { return false; } if (actionInfo.type === EditActionEnum.singleSelection) { // 选中其他内容了,return false - if (!this.enableEditCharacter((actionInfo as IEditSelectionInfo).character)) { + if (!this.enableEditActionInfo(actionInfo)) { return false; } else if ((actionInfo as IEditSelectionInfo).character !== this._activeCharacter) { // 选中同类型其他元素 @@ -70,10 +83,7 @@ export abstract class BaseSelection implements IEditComponent { } checkActionWhileNoEditing(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { - if ( - actionInfo.type === EditActionEnum.singleSelection && - this.enableEditCharacter((actionInfo as IEditSelectionInfo).character) - ) { + if (actionInfo.type === EditActionEnum.singleSelection && this.enableEditActionInfo(actionInfo)) { // this.startEdit(actionInfo); // graphic return true; @@ -184,4 +194,10 @@ export abstract class BaseSelection implements IEditComponent { this._activeCharacter.setConfig({ position: data }); } } + + abstract checkOver?(actionInfo: IEditActionInfo): void; + + release() { + this.inActiveLayoutComponent(); + } } diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts new file mode 100644 index 0000000..1b7c04e --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts @@ -0,0 +1,9 @@ +import type { IEditActionInfo } from '../../../interface'; +import { BaseMarkControl } from './base'; + +// Todo 修改柱宽度,柱高度 +export class BarMarkControl extends BaseMarkControl { + startWithActionInfo(actionInfo: IEditActionInfo) { + super.startWithActionInfo(actionInfo); + } +} diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts new file mode 100644 index 0000000..e510f48 --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts @@ -0,0 +1,28 @@ +import { createGroup, type IGroup } from '@visactor/vrender-core'; +import type { IEditActionInfo } from '../../../interface'; +import type { Edit } from '../../../edit'; + +export interface IMarkControlConstructor { + new (edit: Edit): BaseMarkControl; +} + +// Todo 修改柱宽度,柱高度 +export class BaseMarkControl { + protected _actionInfo: IEditActionInfo; + protected _graphicGroup: IGroup; + + constructor(public readonly edit: Edit) { + this._graphicGroup = createGroup({ visible: false }); + this.edit.getEditGroup().add(this._graphicGroup); + } + + startWithActionInfo(actionInfo: IEditActionInfo) { + this._actionInfo = actionInfo; + } + + release() { + this._actionInfo = null; + this.edit.getEditGroup().removeChild(this._graphicGroup); + this._graphicGroup = null; + } +} diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts new file mode 100644 index 0000000..fef052d --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/index.ts @@ -0,0 +1,8 @@ +import { BarMarkControl } from './bar'; +import type { IMarkControlConstructor } from './base'; +import { LineMarkControl } from './line'; + +export const SeriesMarkControl: { [key: string]: IMarkControlConstructor } = { + bar: BarMarkControl, + line: LineMarkControl +}; diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts new file mode 100644 index 0000000..de6e1a5 --- /dev/null +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/line.ts @@ -0,0 +1,9 @@ +import type { IEditActionInfo } from '../../../interface'; +import { BaseMarkControl } from './base'; + +// Todo 修改柱宽度,柱高度 +export class LineMarkControl extends BaseMarkControl { + startWithActionInfo(actionInfo: IEditActionInfo) { + super.startWithActionInfo(actionInfo); + } +} diff --git a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts new file mode 100644 index 0000000..603ad2b --- /dev/null +++ b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts @@ -0,0 +1,137 @@ +import { SeriesMarkControl } from './../edit-control/series-mark-control/index'; +import type { BaseMarkControl } from './../edit-control/series-mark-control/base'; +import type { IEditOverActionInfo } from './../../interface'; +import { createRect, IGroup } from '@visactor/vrender-core'; +import type { IEditSelectionInfo } from '../../interface'; +import { EditActionEnum, type IEditActionInfo, type IEditComponent } from '../../interface'; + +import { BaseSelection } from './../base-selection'; + +export class SeriesMarkSelection extends BaseSelection implements IEditComponent { + readonly level = 4; + editCharacterType = 'VChart'; + type = 'chart'; + + protected declare _actionInfo: IEditSelectionInfo; + + //TODO series mark control + // 根据不同的当前选中系列,加载对应的control + protected _seriesMarkControl: BaseMarkControl; + + enableEditActionInfo(actionInfo: IEditActionInfo | IEditSelectionInfo) { + const result = super.enableEditActionInfo(actionInfo); + if (!result) { + return result; + } + // 如果不是系列mark + if ((actionInfo as IEditSelectionInfo).detail?.part !== 'seriesMark') { + return false; + } + return true; + } + + checkAction(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { + // if (this.isEditing && actionInfo.type === EditActionEnum.singleSelection) { + // debugger; + // } + return super.checkAction(actionInfo); + } + + activeLayoutComponent(): void { + // 不创建 + } + + startEdit(actionInfo: IEditActionInfo) { + super.startEdit(actionInfo, false); + this.edit.emitter.emit('startEdit', { + type: 'chartSelection', + actionInfo, + selection: this + }); + // @ts-ignore; + const character = this._actionInfo.character; + character.graphic.graphic.addEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); + character.graphic.graphic.addEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + + // 添加编辑时框选效果 + this.addSelectedBorder(); + } + + endEdit() { + if (!this._actionInfo) { + return; + } + // @ts-ignore; + const character = this._actionInfo.character; + character.graphic.graphic.removeEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); + character.graphic.graphic.removeEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + super.endEdit(); + + // 删除 + this.removeSelectedBorder(); + } + + addSelectedBorder() { + // + } + removeSelectedBorder() { + // + } + + unLoadSeriesMarkControl() { + if (this._seriesMarkControl) { + this._seriesMarkControl.release(); + this._seriesMarkControl = null; + } + } + loadSeriesMarkControl() { + this.unLoadSeriesMarkControl(); + const markControlC = SeriesMarkControl[this._actionInfo.detail.modelInfo.model.type]; + if (!markControlC) { + return; + } + this._seriesMarkControl = new markControlC(this.edit); + this._seriesMarkControl.startWithActionInfo(this._actionInfo); + } + + handlerChartClick = (e: any) => { + // + }; + + handlerDoubleClick = (e: any) => { + // TODO:双击进入下一层 + }; + + checkOver?(action: IEditActionInfo): void { + // action + if (action.type === EditActionEnum.pointerOverCharacter && action.detail?.part === 'seriesMark') { + // show over graphic + this._showOverGraphic(action as IEditOverActionInfo); + } + if (action.type === EditActionEnum.pointerOutCharacter) { + // 前提:如果当前over在展示 + // TODO:系列mark补充over展示逻辑 + // 删除的几种情况: + // 1 全选状态下,seriesType是否一致 + // 2 分组选中下,是否是同一组 + // 3 单个选中下,是否是同一个 + if (this._overGraphic.attribute.visible) { + this._overGraphic.removeAllChild(); + this._overGraphic.setAttribute('visible', false); + } + } + } + + protected _showOverGraphic(action: IEditOverActionInfo) { + console.warn('series _showOverGraphic', action); + this._overGraphic.setAttribute('visible', true); + const test = createRect({ x: 100, y: 100, width: 100, height: 100, fill: 'red' }); + this._overGraphic.add(test); + } + + release(): void { + this.unLoadSeriesMarkControl(); + this.endEdit(); + super.release(); + } +} diff --git a/packages/vstory/src/edit/edit.ts b/packages/vstory/src/edit/edit.ts index cb6a8da..9c69d1d 100644 --- a/packages/vstory/src/edit/edit.ts +++ b/packages/vstory/src/edit/edit.ts @@ -87,7 +87,18 @@ export class Edit { this.editAction.onStoryEvent(event, type); } + // TODO: over不能正确的分发到全部selection onAction(actionInfo: IEditActionInfo) { + // over + if ( + actionInfo.type === EditActionEnum.pointerOverCharacter || + actionInfo.type === EditActionEnum.pointerOutCharacter + ) { + this._componentList.forEach(c => c.checkOver?.(actionInfo)); + return; + } + + // 选中 if (this._currentComponent) { // 优先上一次的编辑组件 if (this._currentComponent.checkAction(actionInfo)) { diff --git a/packages/vstory/src/edit/interface.ts b/packages/vstory/src/edit/interface.ts index e2995cb..8b59e72 100644 --- a/packages/vstory/src/edit/interface.ts +++ b/packages/vstory/src/edit/interface.ts @@ -28,6 +28,10 @@ export interface IEditSelectionInfo extends IEditActionInfoBase { detail: IEditSelectionDetailChart | IEditSelectionDetailComponent; } +export interface IEditOverActionInfo extends IEditActionInfoBase, IEditSelectionInfo { + nextActionInfo?: IEditActionInfo; +} + export interface VREvent extends Event { pickParams?: { shadowTarget?: IGraphic; @@ -39,7 +43,7 @@ export interface IEditActionInfoBase { event: VREvent; } -export type IEditActionInfo = IEditActionInfoBase | IEditSelectionInfo; +export type IEditActionInfo = IEditActionInfoBase | IEditSelectionInfo | IEditOverActionInfo; export type ContinuousActionType = 'boxSelection' | 'layerZoom' | 'layerMove'; @@ -51,6 +55,8 @@ export interface IEditComponent { // 是否 开始/继续 编辑 返回false的话,会导致当前编辑结束 checkAction: (actionInfo: IEditActionInfo | IEditSelectionInfo) => boolean; + checkOver?: (actionInfo: IEditActionInfo | IEditSelectionInfo) => void; + // 编辑开始 startEdit: (actionInfo: IEditActionInfo | IEditSelectionInfo, emitEvent?: boolean) => void; diff --git a/packages/vstory/src/story/utils/vchart-pick.ts b/packages/vstory/src/story/utils/vchart-pick.ts index 23868a7..fbe0506 100644 --- a/packages/vstory/src/story/utils/vchart-pick.ts +++ b/packages/vstory/src/story/utils/vchart-pick.ts @@ -199,7 +199,7 @@ export function getGraphicModelMark( if (!graphic) { return null; } - if (graphic.layer !== chart.getStage().layer) { + if (graphic.layer !== chart.getStage().defaultLayer) { return null; } const modelPick = modelCheck.find(mc => mc.check(graphic, graphicPath)); From e49576986ecf00d072b28b66dd75a54582bfc927 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 9 Oct 2024 14:17:17 +0800 Subject: [PATCH 3/4] feat: add bar width edit control --- common/config/rush/pnpm-config.json | 3 +- common/config/rush/pnpm-lock.yaml | 33 +- .../vstory/demo/src/demos/VChartGraphic.tsx | 3 + packages/vstory/package.json | 3 +- packages/vstory/src/edit/const.ts | 26 + packages/vstory/src/edit/edit-action.ts | 9 +- .../src/edit/edit-component/base-selection.ts | 13 +- .../edit/edit-component/chart-selection.ts | 1 - .../edit-control/series-mark-control/bar.ts | 502 +++++++++++++++++- .../edit-control/series-mark-control/base.ts | 14 +- .../edit-control/transform-control.ts | 1 + .../series-mark/series-mark-selection.ts | 215 +++++++- packages/vstory/src/edit/edit.ts | 26 +- packages/vstory/src/edit/interface.ts | 2 +- packages/vstory/src/edit/utils/chart.ts | 79 +++ packages/vstory/src/edit/utils/graphic.ts | 96 ++++ packages/vstory/src/edit/utils/scale.ts | 33 ++ packages/vstory/src/edit/utils/space.ts | 43 ++ .../character/chart/runtime/component-spec.ts | 3 +- .../src/story/character/dsl-interface.ts | 24 +- .../src/story/interface/runtime-interface.ts | 2 + packages/vstory/src/util/logger.ts | 7 + 22 files changed, 1076 insertions(+), 62 deletions(-) create mode 100644 packages/vstory/src/edit/utils/chart.ts create mode 100644 packages/vstory/src/edit/utils/graphic.ts create mode 100644 packages/vstory/src/edit/utils/scale.ts create mode 100644 packages/vstory/src/edit/utils/space.ts create mode 100644 packages/vstory/src/util/logger.ts diff --git a/common/config/rush/pnpm-config.json b/common/config/rush/pnpm-config.json index 2c6df0f..1653758 100644 --- a/common/config/rush/pnpm-config.json +++ b/common/config/rush/pnpm-config.json @@ -92,7 +92,8 @@ "@visactor/vrender": "0.20.1", "@visactor/vrender-core": "0.20.1", "@visactor/vrender-kits": "0.20.1", - "@visactor/vrender-components": "0.20.1" + "@visactor/vrender-components": "0.20.1", + "@visactor/vscale": "0.18.5" // "example2": "npm:@company/example2@^1.0.0" }, diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index eb3a06b..4f5e0b3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -5,6 +5,7 @@ overrides: '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 '@visactor/vrender-components': 0.20.1 + '@visactor/vscale': 0.18.5 importers: @@ -82,6 +83,7 @@ importers: '@visactor/vrender-components': 0.20.1 '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 + '@visactor/vscale': 0.18.5 '@visactor/vutils': ~0.18.4 '@vitejs/plugin-react': 3.1.0 canvas: 2.11.2 @@ -104,6 +106,7 @@ importers: '@visactor/vrender-components': 0.20.1 '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 + '@visactor/vscale': 0.18.5 '@visactor/vutils': 0.18.8 devDependencies: '@douyinfe/semi-ui': 2.62.1_nnrd3gsncyragczmpvfhocinkq @@ -3238,7 +3241,7 @@ packages: '@visactor/vrender-components': 0.20.1 '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 - '@visactor/vscale': 0.18.15 + '@visactor/vscale': 0.18.5 '@visactor/vutils': 0.18.15 '@visactor/vutils-extension': 1.12.4-alpha.1 dev: false @@ -3303,7 +3306,7 @@ packages: '@visactor/vrender-components': 0.20.1 '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 - '@visactor/vscale': 0.18.15 + '@visactor/vscale': 0.18.5 '@visactor/vutils': 0.18.15 dev: false @@ -3360,7 +3363,7 @@ packages: '@visactor/vgrammar-util': 0.14.5 '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 - '@visactor/vscale': 0.18.15 + '@visactor/vscale': 0.18.5 '@visactor/vutils': 0.18.15 dev: false @@ -3379,7 +3382,7 @@ packages: dependencies: '@visactor/vrender-core': 0.20.1 '@visactor/vrender-kits': 0.20.1 - '@visactor/vscale': 0.18.14 + '@visactor/vscale': 0.18.5 '@visactor/vutils': 0.18.14 dev: false @@ -3406,16 +3409,10 @@ packages: '@visactor/vrender-kits': 0.20.1 dev: false - /@visactor/vscale/0.18.14: - resolution: {integrity: sha512-uhyI8yqOPyMurdwqz1oqcqfTXZebpY5kYdnOM5xG79Lz54L95YNZ1J5BNu+SOxOBWbul5hnyjVpdTbZIoujveQ==} + /@visactor/vscale/0.18.5: + resolution: {integrity: sha512-j4xRIQ0lKcJADqp4jCoZn5129aONrFlENb1BGULXzhdlI0dtj6ANFMzne19o54OMPO0TGAM+FubcWeYibfiHIA==} dependencies: - '@visactor/vutils': 0.18.14 - dev: false - - /@visactor/vscale/0.18.15: - resolution: {integrity: sha512-09dDWc6muJbOMxzp4odCsyLjqAF6u3BOx9kAJJ0tEpKE1AuHL4BTejNe697mJAnXqAo2ynAA+dn+cgWYiW1WQg==} - dependencies: - '@visactor/vutils': 0.18.15 + '@visactor/vutils': 0.18.5 dev: false /@visactor/vutils-extension/1.12.4-alpha.1: @@ -3441,6 +3438,14 @@ packages: eventemitter3: 4.0.7 dev: false + /@visactor/vutils/0.18.5: + resolution: {integrity: sha512-FtJFWaoZlnVipdUvq9U8X1KCPuSd5G2pxfeybJeekLibcDd1nyHoAVV4gH2oZeN1hul4p4Rx9ZZMlHfbIW0Faw==} + dependencies: + '@turf/helpers': 6.5.0 + '@turf/invariant': 6.5.0 + eventemitter3: 4.0.7 + dev: false + /@visactor/vutils/0.18.8: resolution: {integrity: sha512-9+YODg9msVyObDbamt94lsEF/idV8gyW3lf31DhuKsLKbuB/ajvSg6jNKD/FTMoXpmCNwfZgZ0F6wXLwI5aIpw==} dependencies: @@ -6317,7 +6322,7 @@ packages: resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} engines: {node: '>= 4.0'} os: [darwin] - deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 + deprecated: Upgrade to fsevents v2 to mitigate potential security issues requiresBuild: true dependencies: bindings: 1.5.0 diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index e889a86..eb33f86 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -730,6 +730,9 @@ export const VChartGraphic = () => { story.canvas.getCanvas().style.background = 'blue'; const edit = new Edit(story); window.edit = edit; + edit.emitter.on('startEdit', (...args) => { + console.log(args); + }); // const vchart = story.getCharactersById('vchart')?.graphic.vchart; // window.vchart = vchart; diff --git a/packages/vstory/package.json b/packages/vstory/package.json index 7c6a8cc..757c9a7 100644 --- a/packages/vstory/package.json +++ b/packages/vstory/package.json @@ -31,7 +31,8 @@ "@visactor/vrender-components": "0.20.1", "@visactor/vutils": "~0.18.4", "@visactor/vchart-extension": "0.0.2", - "@visactor/vdataset": "~0.18.4" + "@visactor/vdataset": "~0.18.4", + "@visactor/vscale": "~0.18.4" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vstory/src/edit/const.ts b/packages/vstory/src/edit/const.ts index 89b2e45..d92cf23 100644 --- a/packages/vstory/src/edit/const.ts +++ b/packages/vstory/src/edit/const.ts @@ -1,6 +1,32 @@ +export const SHAPE_SELECT_COLOR = '#3073F2'; // 图元选中的颜色 +export const SHAPE_OVER_COLOR = '#60A3F2'; // 图元over的颜色 + export const PickEventType: { [key: string]: boolean } = { pointerup: true, click: true, pointerdown: true, dblclick: true }; + +export const PickGraphicAttribute = { + fill: false, + stroke: SHAPE_SELECT_COLOR, + lineWidth: 2, + lineDash: [1, 0], + pickable: false +}; + +export enum SeriesMarkMode { + all = 'all', + seriesGroup = 'seriesGroup', + single = 'single' +} + +export enum EditEditingState { + continuingEditing = 'continuingEditing', + normal = 'normal' +} + +export const VCHART_DATA_INDEX = '__VCHART_DEFAULT_DATA_INDEX'; + +export const MaxAxisPaddingOuter = 0.9; diff --git a/packages/vstory/src/edit/edit-action.ts b/packages/vstory/src/edit/edit-action.ts index c0e9dec..50d36b4 100644 --- a/packages/vstory/src/edit/edit-action.ts +++ b/packages/vstory/src/edit/edit-action.ts @@ -147,14 +147,7 @@ export class EditAction { characterId: this.lastOverGraphic.character?.id, character: this.lastOverGraphic.character, event, - detail: this.lastOverGraphic.characterInfo, - nextAction: { - actionType: EditActionEnum.pointerOverCharacter, - characterId: character?.id, - character: character, - event, - detail: characterInfo - } + detail: this.lastOverGraphic.characterInfo } as IEditOverActionInfo); actionType = EditActionEnum.pointerOverCharacter; } else { diff --git a/packages/vstory/src/edit/edit-component/base-selection.ts b/packages/vstory/src/edit/edit-component/base-selection.ts index 70ccb8e..294c2ee 100644 --- a/packages/vstory/src/edit/edit-component/base-selection.ts +++ b/packages/vstory/src/edit/edit-component/base-selection.ts @@ -7,6 +7,7 @@ import { throwError } from '../../util/common'; import type { VRenderPointerEvent } from '../../interface/type'; import type { ICharacter } from '../../story/character/runtime-interface'; import { createGroup, type IGroup } from '@visactor/vrender-core'; +import { EditEditingState } from '../const'; export abstract class BaseSelection implements IEditComponent { declare readonly level: number; @@ -25,8 +26,7 @@ export abstract class BaseSelection implements IEditComponent { declare type: string; protected _initOverGraphic() { - this._overGraphic = createGroup({ pickable: false }); - this.edit.getEditGroup().add(this._overGraphic); + this._overGraphic = createGroup({ pickable: false, visible: false }); } endEdit(emitEvent: boolean = true): void { @@ -129,6 +129,13 @@ export abstract class BaseSelection implements IEditComponent { } }); + component.onEditorStart(() => { + this.edit.clearOverGraphic(); + this.edit.setEditGlobalState(EditEditingState.continuingEditing, true); + }); + component.onEditorEnd(() => { + this.edit.setEditGlobalState(EditEditingState.continuingEditing, false); + }); component.onUpdate(this.handlerTransformChange.bind(this)); return component; } @@ -159,6 +166,8 @@ export abstract class BaseSelection implements IEditComponent { // TODO 直接release this._layoutComponent.release(); this._layoutComponent = null; + + this.edit.setEditGlobalState(EditEditingState.continuingEditing, false); } updateComponent() { diff --git a/packages/vstory/src/edit/edit-component/chart-selection.ts b/packages/vstory/src/edit/edit-component/chart-selection.ts index 7dfce40..77df296 100644 --- a/packages/vstory/src/edit/edit-component/chart-selection.ts +++ b/packages/vstory/src/edit/edit-component/chart-selection.ts @@ -22,7 +22,6 @@ export class ChartSelection extends BaseSelection implements IEditComponent { const group = actionInfo.character.getGraphicParent(); const { angle } = group.attribute; this._layoutComponent.updateBoundsAndAngle(actionInfo.character.getLayoutBounds(), angle); - // this._layoutComponent.updateBoundsAndAngle(actionInfo.character.getGraphicParent().AABBBounds, 0); } enableEditCharacter(character: ICharacter) { diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts index 1b7c04e..90de07d 100644 --- a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts @@ -1,9 +1,507 @@ -import type { IEditActionInfo } from '../../../interface'; +import type { CharacterChart } from './../../../../story/character/chart/character'; +import { Bounds, type IPointLike } from '@visactor/vutils'; +import type { IGroup, IGroupGraphicAttribute, IRect, IRectGraphicAttribute } from '@visactor/vrender-core'; +import { createGroup, createRect } from '@visactor/vrender-core'; +import { MaxAxisPaddingOuter, PickGraphicAttribute } from '../../../const'; +import type { Edit } from '../../../edit'; +import type { IEditSelectionInfo } from '../../../interface'; import { BaseMarkControl } from './base'; +import { SHAPE_HOVER_COLOR } from '../constants'; +import type { ICartesianSeries } from '@visactor/vchart'; +import { getVChartFromCharacter } from '../../../utils/chart'; +import type { BandScale } from '@visactor/vscale'; +import { + getChartToGlobalMatrix, + transformBoundsWithMatrix, + transformPointToEditGroup, + transformPointWithMatrix +} from '../../../utils/space'; +import type { StoryEvent } from '../../../../story/interface'; +import { computeScalePadding } from '../../../utils/scale'; + +const handlerSize = 9; +const handlerLength = 40; +const handlerLengthMaxPercent = 0.6; + +const defaultHandleAttribute: IRectGraphicAttribute = { + visible: true, + pickable: false, + cornerRadius: 10, + fill: SHAPE_HOVER_COLOR, + x: 0, + y: 50 +}; + +const defaultEditGroupAttribute: IGroupGraphicAttribute = { + pickable: true, + visible: true, + zIndex: 10, + x: 0, + y: 0, + height: 100 +}; // Todo 修改柱宽度,柱高度 export class BarMarkControl extends BaseMarkControl { - startWithActionInfo(actionInfo: IEditActionInfo) { + private _startGroup: IGroup; + private _endGroup: IGroup; + private _startHandle: IRect; + private _endHandle: IRect; + private _barBorder: IRect; + + private _dragInfo: { + startPos: IPointLike; + series: any; + axis: any; + fields: string[]; + rawScale: BandScale[]; + tempScale: BandScale[]; + handle: 'start' | 'end'; + currentPadding: number; + hasChange: boolean; + } = { + startPos: { x: 0, y: 0 }, + series: null, + axis: null, + fields: [], + rawScale: [], + tempScale: [], + handle: 'start', + currentPadding: 0, + hasChange: false + }; + + private _editState: 'over' | 'start' | 'dragging' | 'none' = 'none'; + + private _currentEditorData: any; + private _currentEditorDimensionField: string; + private _currentEditorDimensionValue: string; + + constructor(edit: Edit) { + super(edit); + this._initEditGraphic(); + } + + private _initEditGraphic() { + // border + this._barBorder = createRect({ + ...PickGraphicAttribute, + visible: false + }); + this._graphicGroup.add(this._barBorder); + + this._startGroup = createGroup({ ...defaultEditGroupAttribute }); + this._startHandle = createRect({ + ...defaultHandleAttribute + }); + this._graphicGroup.add(this._startGroup); + this._startGroup.add(this._startHandle); + this._endGroup = createGroup({ ...defaultEditGroupAttribute }); + this._endHandle = createRect({ + ...defaultHandleAttribute + }); + this._graphicGroup.add(this._endGroup); + this._endGroup.add(this._endHandle); + + [this._startGroup, this._endGroup].forEach(h => { + h.addEventListener('pointerdown', (e: StoryEvent) => { + this._onResizeStart(h, e); + }); + + h.addEventListener('pointerover', (e: StoryEvent) => { + const borderIsVisible = this._barBorder.attribute.visible; + this._showHandleGraphic(); + this._barBorder.setAttributes({ visible: borderIsVisible }); + }); + h.addEventListener('pointerout', (e: StoryEvent) => { + if (this._editState === 'dragging') { + return; + } + this._hideHandleGraphic(); + }); + }); + } + + startWithActionInfo(actionInfo: IEditSelectionInfo) { super.startWithActionInfo(actionInfo); + this._setCurrentEditData(actionInfo); + this._setEditGraphic(); + } + + private _setEditGraphic() { + this._setBorderAttribute(); + this._setHandleAttribute(); + + this._graphicGroup.setAttribute('visible', true); + this._graphicGroup.showAll(); + } + + private _setBorderAttribute() { + const currentEditorElements = this._getAllElementInDimension(); + if (currentEditorElements.length) { + const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); + // 重新设置border属性 + let bounds = new Bounds(); + currentEditorElements.forEach(e => { + bounds.union(e.graphicItem.globalAABBBounds); + }); + bounds = transformBoundsWithMatrix(matrix, bounds) as unknown as Bounds; + this._barBorder.setAttributes({ + x: bounds.x1, + x1: bounds.x2, + y: bounds.y1, + y1: bounds.y2 + }); + } + } + + private _setHandleAttribute() { + const pickGraphic = this._barBorder; + const currentDirection = this._actionInfo.detail.modelInfo.model.direction; + const sizeH = pickGraphic.attribute.height ?? pickGraphic.attribute.y1 - pickGraphic.attribute.y; + const sizeW = pickGraphic.attribute.width ?? pickGraphic.attribute.x1 - pickGraphic.attribute.x; + if (currentDirection === 'vertical') { + const handlerLengthTemp = Math.min(handlerLength, Math.abs(sizeH * handlerLengthMaxPercent)); + this._startGroup.setAttributes({ + x: pickGraphic.attribute.x - handlerSize * 0.5, + y: pickGraphic.attribute.y, + height: sizeH, + width: handlerSize, + cursor: 'col-resize' + }); + this._startHandle.setAttributes({ + y: sizeH / 2 - handlerLengthTemp / 2, + x: 0, + width: handlerSize, + height: handlerLengthTemp + }); + this._endGroup.setAttributes({ + x: pickGraphic.attribute.x + sizeW - handlerSize * 0.5, + y: pickGraphic.attribute.y, + height: sizeH, + width: handlerSize, + cursor: 'col-resize' + }); + this._endHandle.setAttributes({ + y: sizeH / 2 - handlerLengthTemp / 2, + x: 0, + width: handlerSize, + height: handlerLengthTemp + }); + } else { + const handlerLengthTemp = Math.min(handlerLength, Math.abs(sizeW * handlerLengthMaxPercent)); + this._startGroup.setAttributes({ + x: pickGraphic.attribute.x, + y: pickGraphic.attribute.y - handlerSize * 0.5, + width: sizeW, + height: handlerSize, + cursor: 'row-resize' + }); + this._startHandle.setAttributes({ + x: sizeW / 2 - handlerLengthTemp / 2, + y: 0, + width: handlerLengthTemp, + height: handlerSize + }); + this._endGroup.setAttributes({ + x: pickGraphic.attribute.x, + y: pickGraphic.attribute.y + sizeH - handlerSize * 0.5, + width: sizeW, + height: handlerSize, + cursor: 'row-resize' + }); + this._endHandle.setAttributes({ + x: sizeW / 2 - handlerLengthTemp / 2, + y: 0, + width: handlerLengthTemp, + height: handlerSize + }); + } + } + + onMarkPointOver(actionInfo: IEditSelectionInfo) { + super.onMarkPointOver(actionInfo); + this._setCurrentEditData(actionInfo); + } + + onMarkPointOut(actionInfo: IEditSelectionInfo) { + super.onMarkPointOut(actionInfo); + } + + private _setCurrentEditData(actionInfo: IEditSelectionInfo) { + this._currentEditorData = actionInfo.detail.modelInfo.datum[0]; + this._currentEditorDimensionField = this._actionInfo.detail.modelInfo.model.getDimensionField()[0]; + this._currentEditorDimensionValue = this._currentEditorData[this._currentEditorDimensionField]; + } + + private _showHandleGraphic() { + // TODO: 显示手柄 + } + + private _hideHandleGraphic() { + // TODO: 隐藏手柄 + } + + private _showBorderGraphic() { + // TODO: 显示边框 + } + + private _hideBorderGraphic() { + // TODO: 隐藏边框 + } + + private _onResizeStart(h: IGroup, e: StoryEvent) { + // 当前选择数据已不存在 + if (!this._actionInfo) { + return; + } + const series = this._actionInfo.detail.modelInfo.model as ICartesianSeries; + const vchart = getVChartFromCharacter(this._actionInfo.character); + const axis = vchart + .getChart() + .getAllComponents() + .find(c => { + if (c.specKey !== 'axes') { + return false; + } + if (c.type !== 'cartesianAxis-band') { + return false; + } + if ( + series.direction === 'vertical' && + // @ts-ignore + c.layoutOrient !== 'top' && + // @ts-ignore + c.layoutOrient !== 'bottom' + ) { + return false; + } + if ( + series.direction === 'horizontal' && + // @ts-ignore + c.layoutOrient !== 'left' && + // @ts-ignore + c.layoutOrient !== 'right' + ) { + return false; + } + return true; + }); + if (!axis) { + return; + } + this._barBorder.setAttributes({ visible: true }); + this._editState = 'dragging'; + // drag 使用的临时数据 + // @ts-ignore + this._dragInfo.handle = h === this._startGroup ? 'start' : 'end'; + const layerPos = transformPointToEditGroup(this.edit, e.canvas); + this._dragInfo.startPos.x = layerPos.x; + this._dragInfo.startPos.y = layerPos.y; + this._dragInfo.series = series; + this._dragInfo.axis = axis; + this._dragInfo.rawScale = []; + this._dragInfo.tempScale = []; + this._dragInfo.hasChange = false; + const fields = series.direction === 'vertical' ? series.fieldX : series.fieldY; + this._dragInfo.fields = fields; + const axisHelper = series.direction === 'vertical' ? series.getXAxisHelper() : series.getYAxisHelper(); + fields.forEach((f, i) => { + this._dragInfo.rawScale.push(axisHelper.getScale(i) as BandScale); + this._dragInfo.tempScale.push(this._dragInfo.rawScale[i].clone() as BandScale); + }); + this._dragInfo.currentPadding = this._dragInfo.rawScale[0].paddingInner(); + this.edit.getStage().addEventListener('pointermove', this._handleMove as any); + window.addEventListener('pointerup', this._handleUp, true); + } + + private _handleMove = (e: StoryEvent) => { + const layerPos = transformPointToEditGroup(this.edit, e.canvas); + const dx = layerPos.x - this._dragInfo.startPos.x; + const dy = layerPos.y - this._dragInfo.startPos.y; + + // 计算 + // 第一层 + const scale = this._dragInfo.rawScale[0]; + // const scale2 = this._dragInfo.rawScale[1]; + // @ts-ignore + const index = scale._index.get(`${this._currentEditorData[this._dragInfo.fields[0]]}`) - 1; + const temp = this._getDragTempValue(scale, null, index); + const bandWidthCount = temp.bandWidthCount; + let currentValue = temp.currentValue; + currentValue += this._dragInfo.series.direction === 'vertical' ? dx : dy; + const padding = computeScalePadding( + { + total: Math.abs(scale.range()[0] - scale.range()[1]), + bandWidthCount: scale.domain().length, + paddingCount: scale.domain().length + 1 + }, + { + total: currentValue, + bandWidthCount, + paddingCount: index + 1 + }, + 0, + MaxAxisPaddingOuter + ); + if (padding === false) { + return; + } + this._dragInfo.hasChange = true; + // set temp scale + this._dragInfo.tempScale[0].paddingInner(padding); + this._dragInfo.tempScale[0].paddingOuter(padding); + // scale2 && this._dragInfo.tempScale[1].range([0, this._dragInfo.tempScale[0].bandwidth()]); + // 得到当前值 + const { currentValue: tempCurrentValue } = this._getDragTempValue(this._dragInfo.tempScale[0], null, index); + currentValue = tempCurrentValue; + const region = this._dragInfo.series.getRegion(); + const regionGraphic = region.getGroupMark().getProduct().elements[0]; + const regionLayoutMeta = { + x: regionGraphic.graphicItem.globalAABBBounds.x1, + y: regionGraphic.graphicItem.globalAABBBounds.y1 + }; + currentValue += this._dragInfo.series.direction === 'vertical' ? regionLayoutMeta.x : regionLayoutMeta.y; + // 计算位置 + let start = currentValue; + let end = currentValue; + if (this._dragInfo.handle === 'start') { + end = currentValue + this._dragInfo.tempScale[0].bandwidth(); + } else { + start = currentValue - this._dragInfo.tempScale[0].bandwidth(); + } + const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); + if (this._dragInfo.series.direction === 'vertical') { + start = transformPointWithMatrix(matrix, { x: start, y: 0 }).x; + end = transformPointWithMatrix(matrix, { x: end, y: 0 }).x; + // console.log(`start = `, start - handlerSize * 0.5, 'end = ', end - handlerSize * 0.5); + this._startGroup.setAttributes({ + x: start - handlerSize * 0.5 + }); + this._endGroup.setAttributes({ + x: end - handlerSize * 0.5 + }); + this._barBorder.setAttributes({ + x: start, + x1: end + }); + } else { + start = transformPointWithMatrix(matrix, { x: 0, y: start }).y; + end = transformPointWithMatrix(matrix, { x: 0, y: end }).y; + this._startGroup.setAttributes({ + y: start - handlerSize * 0.5 + }); + this._endGroup.setAttributes({ + y: end - handlerSize * 0.5 + }); + this._barBorder.setAttributes({ + y: start, + y1: end + }); + } + + this._dragInfo.currentPadding = padding; + }; + + private _getDragTempValue(scale: BandScale, scale2: BandScale, index: number) { + let bandWidthCount = index; + let currentValue = scale.scale(this._currentEditorData[this._dragInfo.fields[0]]); + if (this._dragInfo.handle === 'start') { + if (scale2) { + bandWidthCount += + // @ts-ignore + (scale2._index.get(`${this._currentEditorData[this._dragInfo.fields[1]]}`) - 1) / scale2.domain().length; + currentValue += scale2.scale(this._currentEditorData[this._dragInfo.fields[1]]); + } + } else { + if (scale2) { + bandWidthCount += + // @ts-ignore + scale2._index.get(`${this._currentEditorData[this._dragInfo.fields[1]]}`) / scale2.domain().length; + currentValue += scale2.scale(this._currentEditorData[this._dragInfo.fields[1]]) + scale2.bandwidth(); + } else { + bandWidthCount += 1; + currentValue += scale.bandwidth(); + } + } + + return { + bandWidthCount, + currentValue + }; + } + + private _handleUp = (e: StoryEvent) => { + this._barBorder.setAttributes({ visible: false }); + this._editState = 'none'; + this.edit.getStage().removeEventListener('pointermove', this._handleMove as any); + window.removeEventListener('pointerup', this._handleUp, true); + if (this._dragInfo.hasChange) { + this._actionInfo.character.setConfig({ + option: { + axes: [ + { + userId: this._dragInfo.axis.userId, + specIndex: this._dragInfo.axis.getSpecIndex(), + spec: { + paddingInner: [this._dragInfo.currentPadding, 0], + paddingOuter: [this._dragInfo.currentPadding, 0] + } + } + ] + } + }); + // 更新属性 + // this._dragInfo.series = null; + // this._dragInfo.axis = null; + // this._dragInfo.rawScale = []; + // this.chart.reRenderWithUpdateSpec(); + // // upgrade 更新当前的一些图表数据 + // this._upgradeVChartLink(); + // // 柱宽编辑事件 + // CONTEXT.TeaEvent('edit_element', { + // element_type: 'chart', + // part: 'series', + // chart_type: this._chart.vchartType, + // data_source_type: this._chart.dataSourceType, + // action_type: 'update', + // edit_property: 'barWidth', + // trigger_by: 'chart_interaction' + // }); + } + }; + + private _getAllElementInDimension() { + const marks: any[] = []; + this._actionInfo.character.graphic.graphic.vchart + .getChart() + .getAllSeries() + .forEach((s: ICartesianSeries) => { + const dimensionField = (s.direction === 'vertical' ? s.fieldX : s.fieldY)[0]; + if (this._currentEditorDimensionField !== dimensionField) { + return; + } + const bar = s.getMarkInName('bar'); + if (!bar) { + return; + } + bar.getProduct().elements.forEach(e => { + if (e.data[0][dimensionField] === this._currentEditorDimensionValue) { + marks.push(e); + } + }); + }); + return marks; + } + + release(): void { + [this._startGroup, this._endGroup].forEach(g => { + g.removeAllListeners('pointerdown'); + g.removeAllListeners('pointerover'); + g.removeAllListeners('pointerout'); + }); + super.release(); + + this._startGroup = this._endGroup = this._startHandle = this._endHandle = this._barBorder = null; } } diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts index e510f48..cefde61 100644 --- a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts @@ -1,5 +1,5 @@ import { createGroup, type IGroup } from '@visactor/vrender-core'; -import type { IEditActionInfo } from '../../../interface'; +import type { IEditSelectionInfo } from '../../../interface'; import type { Edit } from '../../../edit'; export interface IMarkControlConstructor { @@ -8,7 +8,7 @@ export interface IMarkControlConstructor { // Todo 修改柱宽度,柱高度 export class BaseMarkControl { - protected _actionInfo: IEditActionInfo; + protected _actionInfo: IEditSelectionInfo; protected _graphicGroup: IGroup; constructor(public readonly edit: Edit) { @@ -16,7 +16,7 @@ export class BaseMarkControl { this.edit.getEditGroup().add(this._graphicGroup); } - startWithActionInfo(actionInfo: IEditActionInfo) { + startWithActionInfo(actionInfo: IEditSelectionInfo) { this._actionInfo = actionInfo; } @@ -25,4 +25,12 @@ export class BaseMarkControl { this.edit.getEditGroup().removeChild(this._graphicGroup); this._graphicGroup = null; } + + onMarkPointOver(actionInfo: IEditSelectionInfo) { + // nothing + } + + onMarkPointOut(actionInfo: IEditSelectionInfo) { + // nothing + } } diff --git a/packages/vstory/src/edit/edit-component/edit-control/transform-control.ts b/packages/vstory/src/edit/edit-component/edit-control/transform-control.ts index 0b1fc01..9cb6e32 100644 --- a/packages/vstory/src/edit/edit-component/edit-control/transform-control.ts +++ b/packages/vstory/src/edit/edit-component/edit-control/transform-control.ts @@ -276,6 +276,7 @@ export class TransformControl extends AbstractComponent { + this.endEditCbs?.forEach(cb => cb()); // this._endHandler(this._editorBox.getTransformAttribute()); // this._editorBox.isEditor = false; // this._snapLineX.setAttributes({ visible: false }); diff --git a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts index 603ad2b..8137084 100644 --- a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts +++ b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts @@ -1,11 +1,21 @@ +import { isNumber } from '@visactor/vutils'; +import { EditEditingState, SeriesMarkMode } from './../../const'; +import { Logger } from './../../../util/logger'; +import type { CharacterChart } from './../../../story/character/chart/character'; import { SeriesMarkControl } from './../edit-control/series-mark-control/index'; import type { BaseMarkControl } from './../edit-control/series-mark-control/base'; import type { IEditOverActionInfo } from './../../interface'; -import { createRect, IGroup } from '@visactor/vrender-core'; +import type { IGroup, IGraphic } from '@visactor/vrender-core'; +import { createGroup } from '@visactor/vrender-core'; import type { IEditSelectionInfo } from '../../interface'; import { EditActionEnum, type IEditActionInfo, type IEditComponent } from '../../interface'; import { BaseSelection } from './../base-selection'; +import { getChartToGlobalMatrix } from '../../utils/space'; +import { cloneEditGraphic } from '../../utils/graphic'; +import { SHAPE_OVER_COLOR, SHAPE_SELECT_COLOR } from '../../const'; +import type { Edit } from '../../edit'; +import { getKeyValueMapWithScaleMap, getSeriesKeyField, getSeriesKeyScalesMap } from '../../utils/chart'; export class SeriesMarkSelection extends BaseSelection implements IEditComponent { readonly level = 4; @@ -18,6 +28,18 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent // 根据不同的当前选中系列,加载对应的control protected _seriesMarkControl: BaseMarkControl; + protected _selectGraphic: IGroup; + + constructor(public readonly edit: Edit) { + super(edit); + this._initSelectGraphic(); + } + + protected _initSelectGraphic() { + this._selectGraphic = createGroup({ pickable: false, visible: false }); + this.edit.getEditGroup().add(this._selectGraphic); + } + enableEditActionInfo(actionInfo: IEditActionInfo | IEditSelectionInfo) { const result = super.enableEditActionInfo(actionInfo); if (!result) { @@ -30,52 +52,134 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent return true; } - checkAction(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { - // if (this.isEditing && actionInfo.type === EditActionEnum.singleSelection) { - // debugger; - // } - return super.checkAction(actionInfo); + checkActionWhileEditing(actionInfo: IEditActionInfo | IEditSelectionInfo): boolean { + const result = super.checkActionWhileEditing(actionInfo); + if (actionInfo.type === EditActionEnum.singleSelection && result === true) { + this._checkChangeSeriesMark(actionInfo as IEditSelectionInfo); + } + if (result === false) { + if ( + actionInfo.type === EditActionEnum.unSelection || + (actionInfo.type === EditActionEnum.singleSelection && actionInfo.detail?.part !== 'seriesMark') + // TODO: 支持标签编辑后 打开注释 + // && actionInfo.detail?.part !== 'label' + ) { + this.edit.setEditGlobalState('seriesMarkMode', SeriesMarkMode.all); + } + } + return result; + } + + protected _checkChangeSeriesMark(actionInfo: IEditSelectionInfo) { + // 检测是否选中另一组 mark + Logger.debug('series mark checkChangeSeriesMark'); + if (actionInfo.detail.graphic === this._actionInfo.detail.graphic) { + // 同一个图形,必然是不用切换的 + return; + } + // 全选下 同一个系列类型 + mark名称 不用切换 + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.all) { + if ( + actionInfo.detail.modelInfo.model.type === this._actionInfo.detail.modelInfo.model.type && + actionInfo.detail.modelInfo.mark.name === this._actionInfo.detail.modelInfo.mark.name + ) { + return; + } + } + // 数据组选中下 同series + 数据key 不用切换 + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.all) { + const seriesField = this._actionInfo.detail.modelInfo.model.getSeriesField(); + if ( + actionInfo.detail.modelInfo.model === this._actionInfo.detail.modelInfo.model && + actionInfo.detail.modelInfo.datum[0][seriesField] === this._actionInfo.detail.modelInfo.datum[0][seriesField] + ) { + return; + } + } + // 单选下 只有同一个图形 才不用切换,顶部已经判定,到这里就是必须切换了 + + // 开始切换 + // 先发送一次编辑结束 + this.endEdit(); + // 再开启一次编辑 + this.startEdit(actionInfo); } activeLayoutComponent(): void { // 不创建 } - startEdit(actionInfo: IEditActionInfo) { + startEdit(actionInfo: IEditSelectionInfo) { + Logger.debug('series mark startEdit'); super.startEdit(actionInfo, false); + const seriesMarkInfo: { + selectMode: string; + markName?: string; + datumMatch?: { [key: string]: number }; + seriesMatch?: { specIndex?: number; userId?: string; type?: string }; + } = { selectMode: this.edit.editGlobalState.seriesMarkMode }; + seriesMarkInfo.markName = actionInfo.detail.modelInfo.mark.name; + seriesMarkInfo.seriesMatch = { + type: actionInfo.detail.modelInfo.model.type + }; + // single + group + if (this.edit.editGlobalState.seriesMarkMode !== SeriesMarkMode.all) { + seriesMarkInfo.markName = actionInfo.detail.modelInfo.mark.name; + seriesMarkInfo.seriesMatch = { + specIndex: actionInfo.detail.modelInfo.model.getSpecIndex(), + userId: actionInfo.detail.modelInfo.model.userId + }; + seriesMarkInfo.datumMatch = this._getSeriesGroupMatchMatch(actionInfo as IEditSelectionInfo); + } + + // single + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.single) { + seriesMarkInfo.datumMatch = this._getSingleDatumMatch(actionInfo as IEditSelectionInfo); + } + actionInfo = { ...actionInfo, seriesMarkInfo } as any; this.edit.emitter.emit('startEdit', { - type: 'chartSelection', + type: 'seriesMarkSelection', actionInfo, selection: this }); + this._actionInfo = actionInfo; // @ts-ignore; const character = this._actionInfo.character; - character.graphic.graphic.addEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); - character.graphic.graphic.addEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + character.graphic.graphic.vchart.on('pointerdown', { level: 'mark' }, this.handlerChartClick); + character.graphic.graphic.vchart.on('dblclick', { level: 'mark' }, this.handlerDoubleClick); // 添加编辑时框选效果 this.addSelectedBorder(); + // 添加系列编辑模块 + this.loadSeriesMarkControl(); } endEdit() { + Logger.debug('series mark endEdit'); if (!this._actionInfo) { return; } // @ts-ignore; const character = this._actionInfo.character; - character.graphic.graphic.removeEventListener('pointerdown', { level: 'mark' }, this.handlerChartClick); - character.graphic.graphic.removeEventListener('dblclick', { level: 'mark' }, this.handlerDoubleClick); + character.graphic.graphic.vchart.off('pointerdown', this.handlerChartClick); + character.graphic.graphic.vchart.off('dblclick', this.handlerDoubleClick); super.endEdit(); // 删除 this.removeSelectedBorder(); + this.unLoadSeriesMarkControl(); } addSelectedBorder() { - // + this.removeSelectedBorder(); + this._selectGraphic.setAttribute('visible', true); + this._addPickGraphic(this._actionInfo, this._selectGraphic, { stroke: SHAPE_SELECT_COLOR }); } removeSelectedBorder() { - // + if (this._selectGraphic.attribute.visible) { + this._selectGraphic.removeAllChild(); + this._selectGraphic.setAttribute('visible', false); + } } unLoadSeriesMarkControl() { @@ -99,7 +203,19 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent }; handlerDoubleClick = (e: any) => { - // TODO:双击进入下一层 + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.single) { + return; + } + + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.all) { + this.edit.setEditGlobalState('seriesMarkMode', SeriesMarkMode.seriesGroup); + } else if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.seriesGroup) { + this.edit.setEditGlobalState('seriesMarkMode', SeriesMarkMode.single); + } + const actionInfo = this._actionInfo; + this.endEdit(); + this.startEdit(actionInfo); + this._showOverGraphic(actionInfo); }; checkOver?(action: IEditActionInfo): void { @@ -116,17 +232,80 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent // 2 分组选中下,是否是同一组 // 3 单个选中下,是否是同一个 if (this._overGraphic.attribute.visible) { + this.edit.clearOverGraphic(); this._overGraphic.removeAllChild(); this._overGraphic.setAttribute('visible', false); } + this._seriesMarkControl?.onMarkPointOut(action); } } protected _showOverGraphic(action: IEditOverActionInfo) { - console.warn('series _showOverGraphic', action); + if (this.edit.editGlobalState[EditEditingState.continuingEditing] === true) { + return; + } + this._seriesMarkControl?.onMarkPointOver(action); + this._overGraphic.removeAllChild(); this._overGraphic.setAttribute('visible', true); - const test = createRect({ x: 100, y: 100, width: 100, height: 100, fill: 'red' }); - this._overGraphic.add(test); + this._addPickGraphic(action, this._overGraphic, { stroke: SHAPE_OVER_COLOR }); + this.edit.showOverGraphic(this._overGraphic); + } + + protected _getChartItemInSeriesMark(action: IEditOverActionInfo): IGraphic[] { + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.all) { + const seriesList = action.character.graphic.graphic.vchart + .getChart() + .getAllSeries() + .filter(s => s.type === action.detail.modelInfo.model.type); + const markList = seriesList.reduce((pre: any, cur: any) => { + return pre.concat(cur.getMarkInName(action.detail.modelInfo.mark.name)); + }, []); + return markList.reduce((pre: any, cur: any) => { + if (cur?.getProduct().graphicItem.children) { + return pre.concat([...cur.getProduct().graphicItem.children]); + } + return pre; + }, []); + } + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.seriesGroup) { + const result: IGraphic[] = []; + const seriesField = action.detail.modelInfo.model.getSeriesField(); + const seriesValue = action.detail.modelInfo.datum[0][seriesField]; + action.detail.modelInfo.mark.getProduct().elements.forEach(el => { + if (el.data[0]?.[seriesField] === seriesValue) { + result.push(el.graphicItem); + } + }); + return result; + } + if (this.edit.editGlobalState.seriesMarkMode === SeriesMarkMode.single) { + return [action.detail.graphic]; + } + return []; + } + + protected _addPickGraphic(action: IEditOverActionInfo, parent: IGroup, attr?: any) { + const itemList = this._getChartItemInSeriesMark(action); + const matrix = getChartToGlobalMatrix(action.character as CharacterChart, this.edit); + itemList.forEach((item: IGraphic) => { + const graphic = cloneEditGraphic(item, matrix, { ...(attr ?? {}) }); + parent.add(graphic); + }); + } + + private _getSingleDatumMatch(actionInfo: IEditSelectionInfo) { + const itemKeys = getSeriesKeyField(actionInfo.detail.modelInfo.model); + const keyScaleMap = getSeriesKeyScalesMap(actionInfo.detail.modelInfo.model); + return getKeyValueMapWithScaleMap(itemKeys, keyScaleMap, actionInfo.detail.modelInfo.datum); + } + private _getSeriesGroupMatchMatch(actionInfo: IEditSelectionInfo) { + const series = actionInfo.detail.modelInfo.model; + const seriesField = series.getSeriesField(); + const seriesValue = actionInfo.detail.modelInfo.datum[0][seriesField]; + + return { + [seriesField]: seriesValue + }; } release(): void { diff --git a/packages/vstory/src/edit/edit.ts b/packages/vstory/src/edit/edit.ts index 9c69d1d..ca8f0f2 100644 --- a/packages/vstory/src/edit/edit.ts +++ b/packages/vstory/src/edit/edit.ts @@ -11,6 +11,7 @@ import { } from './interface'; import type { IGroup, IGraphic } from '@visactor/vrender'; import { createGroup } from '@visactor/vrender'; +import { SeriesMarkMode } from './const'; export class Edit { readonly editAction: EditAction; @@ -30,6 +31,19 @@ export class Edit { protected _editGroup: IGroup; + // 提供给组件一个编辑的全局状态,组件可以读取/设置。 + // 比如当前的chart元素选中层级,全部/数据组/单个 这个状态在标签,系列mark 中共同使用 + private _editGlobalState: { [key: string]: any } = { + seriesMarkMode: SeriesMarkMode.all + }; + get editGlobalState(): { [key: string]: any } { + return this._editGlobalState; + } + + setEditGlobalState(key: string, value: any) { + this._editGlobalState[key] = value; + } + constructor(public readonly story: Story) { this.emitter = new EventEmitter(); this.editAction = new EditAction(story); @@ -65,6 +79,10 @@ export class Edit { return this._editGroup; } + getStage() { + return this._editGroup.stage; + } + protected _initComponent() { this._componentMap = {}; Object.keys(Edit.componentConstructorMap).forEach(key => { @@ -80,7 +98,7 @@ export class Edit { if (event.path.find(g => g === this._editGroup || g === this._overGraphicGroup)) { // 具体判断是否编辑到交互元素,如果pick到group,就不算 const pathTarget = event.path[event.path.length - 1]; - if (!pathTarget.isContainer) { + if (!pathTarget.isContainer || pathTarget.attribute?.pickable === true) { return; } } @@ -148,8 +166,12 @@ export class Edit { showOverGraphic(graphic: IGraphic, clearOther: boolean = true) { if (clearOther) { - this._overGraphicGroup.removeAllChild(true); + this._overGraphicGroup.removeAllChild(); } this._overGraphicGroup.add(graphic); } + + clearOverGraphic() { + this._overGraphicGroup.removeAllChild(); + } } diff --git a/packages/vstory/src/edit/interface.ts b/packages/vstory/src/edit/interface.ts index 8b59e72..ed27bd9 100644 --- a/packages/vstory/src/edit/interface.ts +++ b/packages/vstory/src/edit/interface.ts @@ -29,7 +29,7 @@ export interface IEditSelectionInfo extends IEditActionInfoBase { } export interface IEditOverActionInfo extends IEditActionInfoBase, IEditSelectionInfo { - nextActionInfo?: IEditActionInfo; + nextAction?: IEditActionInfo; } export interface VREvent extends Event { diff --git a/packages/vstory/src/edit/utils/chart.ts b/packages/vstory/src/edit/utils/chart.ts new file mode 100644 index 0000000..f13b350 --- /dev/null +++ b/packages/vstory/src/edit/utils/chart.ts @@ -0,0 +1,79 @@ +import type { ICartesianSeries, ISeries, IVChart } from '@visactor/vchart'; +import { isContinuous } from '@visactor/vscale'; +import { isArray } from '@visactor/vutils'; +import { VCHART_DATA_INDEX } from '../const'; +import type { ICharacter } from '../../story/character'; + +// 特殊系列,会在获取系列数据的唯一key时,增加index +const SpecialSeriesMap: { [key: string]: boolean } = { + sankey: true, + map: true, + wordCloud: true, + scatter: true +}; + +export function getSeriesKeyField(series: ISeries) { + const dimensionFields = [...series.getDimensionField()]; + + const seriesField = series.getSeriesField(); + if (!dimensionFields.includes(seriesField)) { + dimensionFields.push(seriesField); + } + if (SpecialSeriesMap[series.type] === true) { + dimensionFields.push(VCHART_DATA_INDEX); + } + return dimensionFields; +} + +export function getSeriesKeyScalesMap(series: ISeries) { + let axisHelper: any; + let fields: string[]; + const map: { [key: string]: any } = {}; + if ((series).direction) { + if ((series).direction === 'vertical') { + axisHelper = (series).getXAxisHelper(); + fields = (series).fieldX; + } else { + axisHelper = (series).getYAxisHelper(); + fields = (series).fieldY; + } + if (axisHelper?.getScale) { + fields.forEach((f, i) => { + map[f] = axisHelper.getScale(i); + }); + } + } + const seriesField = series.getSeriesField(); + if (!map[seriesField]) { + if (seriesField) { + if (series.getOption().globalScale.getScale('color')) { + map[seriesField] = series.getOption().globalScale.getScale('color'); + } + } + } + return map; +} + +export function getKeyValueMapWithScaleMap(keys: string[], scaleMap: { [key: string]: any } = {}, datum: any) { + if (isArray(datum)) { + datum = datum[0]; + } + const result: { [key: string]: any } = {}; + keys.forEach(key => { + const scale = scaleMap[key]; + if (!scale) { + result[key] = datum[key]; + } else if (isContinuous(scale.type)) { + result[VCHART_DATA_INDEX] = datum[VCHART_DATA_INDEX]; + } else if (scale._index) { + result[key] = scale._index.get(`${datum[key]}`); + } else { + result[key] = datum[key]; + } + }); + return result; +} + +export function getVChartFromCharacter(character: ICharacter): IVChart { + return character.graphic.graphic.vchart; +} diff --git a/packages/vstory/src/edit/utils/graphic.ts b/packages/vstory/src/edit/utils/graphic.ts new file mode 100644 index 0000000..90a4266 --- /dev/null +++ b/packages/vstory/src/edit/utils/graphic.ts @@ -0,0 +1,96 @@ +import type { ILine, IRect } from '@visactor/vrender-core'; +import { createLine, createRect, type IGraphic, type IGraphicAttribute } from '@visactor/vrender-core'; +import { isValidNumber, type Matrix } from '@visactor/vutils'; +import { transformBoundsWithMatrix, transformPointWithMatrix } from './space'; +import { PickGraphicAttribute } from '../const'; + +export function cloneEditGraphic(graphic: IGraphic, matrix: Matrix, attribute?: Partial) { + const cloneItem = copyGraphicToGlobal(graphic, { + ...PickGraphicAttribute, + // 矩形通用 + // @ts-ignore + cornerRadius: 0, + ...attribute + }); + transformGraphicWithMatrix(cloneItem, matrix); + if (cloneItem.type === 'text') { + return createRect({ + ...PickGraphicAttribute, + ...attribute, + x: cloneItem.AABBBounds.x1, + y: cloneItem.AABBBounds.y1, + width: cloneItem.AABBBounds.width(), + height: cloneItem.AABBBounds.height() + }); + } + return cloneItem; +} + +export function copyGraphicToGlobal(graphic: IGraphic, attribute?: Partial) { + let item = graphic.clone(); + if (item.type === 'area') { + item = createLine(item.attribute); + } + attribute && item.setAttributes(attribute); + transformGraphicWithMatrix(item, graphic.parent.globalTransMatrix.getInverse()); + return item; +} + +export function transformGraphicWithMatrix(graphic: IGraphic, m: Matrix) { + if (graphic.type === 'rect') { + transformRectMarkAttributeWithMatrix(graphic, m); + } + // 如果转换了自身坐标,就不应该转换 points + if (isValidNumber(graphic.attribute.x)) { + graphic.setAttributes(transformPointWithMatrix(m, { x: graphic.attribute.x, y: graphic.attribute.y })); + } else { + transformPointAttributeWithMatrix(graphic, m); + } +} + +export function transformRectMarkAttributeWithMatrix(item: IRect, m: Matrix) { + const x = item.attribute.x; + const y = item.attribute.y; + const width = item.attribute.width; + const height = item.attribute.height; + let x1 = item.attribute.x1; + let y1 = item.attribute.y1; + if (!isValidNumber(x1)) { + x1 = item.attribute.x + item.attribute.width; + } + if (!isValidNumber(y1)) { + y1 = item.attribute.y + item.attribute.height; + } + const tempBounds = transformBoundsWithMatrix(m, { x1: x, y1: y, x2: x1, y2: y1 }); + // set size + if (isValidNumber(width)) { + item.setAttribute('width', tempBounds.x2 - tempBounds.x1); + } + if (isValidNumber(height)) { + item.setAttribute('height', tempBounds.y2 - tempBounds.y1); + } + // set pos + if (isValidNumber(item.attribute.x1)) { + item.setAttribute('x1', tempBounds.x2); + } + if (isValidNumber(item.attribute.y1)) { + item.setAttribute('y1', tempBounds.y2); + } +} + +export function transformPointAttributeWithMatrix(item: ILine, m: Matrix) { + if (!item.attribute.points) { + return; + } + const newPoints = item.attribute.points.map(p => { + const tempBounds = transformBoundsWithMatrix(m, { x1: p.x, y1: p.y, x2: p.x1 ?? p.x, y2: p.y1 ?? p.y }); + return { + context: p.context, + x: tempBounds.x1, + y: tempBounds.y1, + x1: p.x1 ? tempBounds.x2 : undefined, + y1: p.y1 ? tempBounds.y2 : undefined + }; + }); + item.setAttribute('points', newPoints); +} diff --git a/packages/vstory/src/edit/utils/scale.ts b/packages/vstory/src/edit/utils/scale.ts new file mode 100644 index 0000000..4480d23 --- /dev/null +++ b/packages/vstory/src/edit/utils/scale.ts @@ -0,0 +1,33 @@ +type ScaleComputeInfo = { + total: number; + bandWidthCount: number; + paddingCount: number; +}; + +export function computeScalePadding( + infoA: ScaleComputeInfo, + infoB: ScaleComputeInfo, + min?: number, + max?: number +): number | false { + // 首先,如果成等比,那么就无法计算, + if (infoA.bandWidthCount / infoA.paddingCount === infoB.bandWidthCount / infoB.paddingCount) { + return false; + } + // 先消元 padding + const BAScale = infoA.paddingCount / infoB.paddingCount; + const tempB = { ...infoB }; + tempB.total *= BAScale; + tempB.bandWidthCount *= BAScale; + const tempA = { ...infoA }; + tempA.total -= tempB.total; + tempA.bandWidthCount -= tempB.bandWidthCount; + const bandWidth = tempA.total / tempA.bandWidthCount; + if (bandWidth < 0) { + // 错误的编辑信息可以算出负的柱宽度,认为是不合法的操作,直接返回 + return false; + } + const paddingSize = (infoA.total - bandWidth * infoA.bandWidthCount) / infoA.paddingCount; + + return Math.min(Math.max(paddingSize / (bandWidth + paddingSize), min ?? 0.01), max ?? 1); +} diff --git a/packages/vstory/src/edit/utils/space.ts b/packages/vstory/src/edit/utils/space.ts new file mode 100644 index 0000000..2fe2042 --- /dev/null +++ b/packages/vstory/src/edit/utils/space.ts @@ -0,0 +1,43 @@ +import type { Edit } from './../edit'; +import type { IBoundsLike, IPointLike } from '@visactor/vutils'; +import { Matrix } from '@visactor/vutils'; +import type { CharacterChart } from './../../story/character/chart/character'; + +// character.graphic.graphic.vchart + +export function getChartToGlobalMatrix(character: CharacterChart, edit: Edit): Matrix { + if (!character.graphic?.graphic?.vchart) { + return new Matrix(); + } + const vchartStage = character.graphic.graphic.vchart.getStage(); + const chartToView = vchartStage.window.getViewBoxTransform().getInverse(); + const viewToLayer = edit.getEditGroup().globalTransMatrix.getInverse(); + viewToLayer.multiply(chartToView.a, chartToView.b, chartToView.c, chartToView.d, chartToView.e, chartToView.f); + return viewToLayer; +} + +export function transformBoundsWithMatrix(m: Matrix, b: IBoundsLike) { + const next1 = { x: 0, y: 0 }; + m.transformPoint({ x: b.x1, y: b.y1 }, next1); + const next2 = { x: 0, y: 0 }; + m.transformPoint({ x: b.x2, y: b.y2 }, next2); + return { + x1: next1.x, + y1: next1.y, + x2: next2.x, + y2: next2.y, + width: Math.abs(next2.x - next1.x), + height: Math.abs(next2.y - next1.y) + }; +} + +export function transformPointWithMatrix(m: Matrix, point: IPointLike): IPointLike { + const nextP = { x: 0, y: 0 }; + m.transformPoint(point, nextP); + return nextP; +} + +export function transformPointToEditGroup(edit: Edit, point: IPointLike): IPointLike { + const viewToLayer = edit.getEditGroup().globalTransMatrix.getInverse(); + return transformPointWithMatrix(viewToLayer, point); +} diff --git a/packages/vstory/src/story/character/chart/runtime/component-spec.ts b/packages/vstory/src/story/character/chart/runtime/component-spec.ts index 8ddf79b..ce49eba 100644 --- a/packages/vstory/src/story/character/chart/runtime/component-spec.ts +++ b/packages/vstory/src/story/character/chart/runtime/component-spec.ts @@ -1,4 +1,4 @@ -import { merge, isValid } from '@visactor/vutils'; +import { merge, isValid, array } from '@visactor/vutils'; import type { IComponentConfig } from '../../dsl-interface'; import type { CharacterChart } from '../character'; import type { IChartCharacterRuntime } from './interface'; @@ -49,6 +49,7 @@ export class ComponentSpecRuntime implements IChartCharacterRuntime { if (!rawSpec[key]) { rawSpec[key] = []; } + rawSpec[key] = array(rawSpec[key]); const s = rawSpec[key].find((a: any, index: number) => { if (ChartSpecMatch(a, index, componentSpec.matchInfo)) { return true; diff --git a/packages/vstory/src/story/character/dsl-interface.ts b/packages/vstory/src/story/character/dsl-interface.ts index e974db8..6b9885a 100644 --- a/packages/vstory/src/story/character/dsl-interface.ts +++ b/packages/vstory/src/story/character/dsl-interface.ts @@ -1,4 +1,4 @@ -import type { IInitOption } from '@visactor/vchart'; +import type { IInitOption, ISpec } from '@visactor/vchart'; import type { IRichTextGraphicAttribute, ITextGraphicAttribute } from '@visactor/vrender'; import type { DirectionType } from './chart/const'; @@ -52,11 +52,17 @@ export interface IComponentMatch { [key: string]: any; } -export interface IComponentConfig { - specKey: string; - matchInfo: IComponentMatch; - spec: any; -} +export type ChartModelMatch = + | { + usrId: string; + } + | { + specIndex: number | 'all'; // all 表示所有 + }; + +export type IComponentConfig = ChartModelMatch & { + spec: T; +}; export interface IChartCharacterConfig extends ICharacterConfigBase { options: { @@ -68,9 +74,11 @@ export interface IChartCharacterConfig extends ICharacterConfigBase { // 数据源 data?: any; // 标题 - title?: any; + title?: IComponentConfig[]; // 图例 - legends?: any; + legends?: IComponentConfig[]; + // axes + axes?: IComponentConfig[]; }; } diff --git a/packages/vstory/src/story/interface/runtime-interface.ts b/packages/vstory/src/story/interface/runtime-interface.ts index a1b9140..208e0f3 100644 --- a/packages/vstory/src/story/interface/runtime-interface.ts +++ b/packages/vstory/src/story/interface/runtime-interface.ts @@ -1,3 +1,4 @@ +import type { IPointLike } from '@visactor/vutils'; import type { IGraphic, IStage } from '@visactor/vrender'; import type { ICharacter, ICharacterConfig } from '../character'; import type { IPlayer } from '../../player/interface/player'; @@ -62,4 +63,5 @@ export type StoryEvent = Event & { path: IGraphic[]; canvasX?: number; canvasY?: number; + canvas?: IPointLike; }; diff --git a/packages/vstory/src/util/logger.ts b/packages/vstory/src/util/logger.ts new file mode 100644 index 0000000..c5e5c50 --- /dev/null +++ b/packages/vstory/src/util/logger.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-console */ + +export class Logger { + static debug(...message: any) { + console.log(...message); + } +} From 7c7ad25571554097420bd350f608e5b0ddb81a1d Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 9 Oct 2024 14:22:29 +0800 Subject: [PATCH 4/4] feat: add bar width edit control with transMatrix feat: add bar width edit control with transMatrix --- .../vstory/demo/src/demos/VChartGraphic.tsx | 299 ++++++++++++++++-- .../src/edit/edit-component/base-selection.ts | 2 +- .../edit-control/series-mark-control/bar.ts | 89 +++--- .../edit-control/series-mark-control/base.ts | 2 +- .../series-mark/series-mark-selection.ts | 21 +- packages/vstory/src/edit/edit.ts | 2 +- packages/vstory/src/edit/utils/chart.ts | 9 + packages/vstory/src/edit/utils/graphic.ts | 2 +- 8 files changed, 339 insertions(+), 87 deletions(-) diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index eb33f86..423e186 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -526,46 +526,279 @@ const spec = { }; const spec1 = { - type: 'bar', - background: 'transparent', - data: { - values: [ - { type: 'Nail polish', country: 'Africa', value: 4229 }, - { type: 'Nail polish', country: 'EU', value: 4376 }, - { type: 'Nail polish', country: 'China', value: 3054 }, - { type: 'Nail polish', country: 'USA', value: 12814 }, - { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, - { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, - { type: 'Eyebrow pencil', country: 'China', value: 5067 }, - { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, - { type: 'Rouge', country: 'Africa', value: 5221 }, - { type: 'Rouge', country: 'EU', value: 3574 }, - { type: 'Rouge', country: 'China', value: 7004 }, - { type: 'Rouge', country: 'USA', value: 11624 }, - { type: 'Lipstick', country: 'Africa', value: 9256 }, - { type: 'Lipstick', country: 'EU', value: 4376 }, - { type: 'Lipstick', country: 'China', value: 9054 }, - { type: 'Lipstick', country: 'USA', value: 8814 }, - { type: 'Eyeshadows', country: 'Africa', value: 3308 }, - { type: 'Eyeshadows', country: 'EU', value: 4572 }, - { type: 'Eyeshadows', country: 'China', value: 12043 }, - { type: 'Eyeshadows', country: 'USA', value: 12998 } - ] + direction: 'vertical', + type: 'common', + color: ['#00295C', '#2568BD', '#9F9F9F', '#C5C5C5', '#00B0F0', '#4BCFFF', '#C2C2C2', '#D7D7D7'], + series: [ + { + type: 'bar', + stack: true, + direction: 'vertical', + bar: { + style: { + stroke: '', + lineWidth: 1 + }, + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + }, + barBackground: { + style: { + stroke: '', + lineWidth: 1 + } + }, + label: { + visible: true, + position: 'inside', + style: { + lineHeight: '100%', + fontSize: 16, + fontWeight: 'bold' + }, + overlap: { + strategy: [] + }, + smartInvert: true, + formatConfig: {}, + interactive: true + }, + xField: ['_editor_dimension_field', '_editor_type_field'], + yField: '_editor_value_field', + dataId: '0', + id: 'series-0', + EDITOR_SERIES_DATA_KEY: 'a', + seriesField: '_editor_type_field' + }, + { + type: 'bar', + stack: true, + direction: 'vertical', + bar: { + style: { + stroke: '', + lineWidth: 1 + }, + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + }, + barBackground: { + style: { + stroke: '', + lineWidth: 1 + } + }, + label: { + visible: true, + position: 'inside', + style: { + lineHeight: '100%', + fontSize: 16, + fontWeight: 'bold' + }, + overlap: { + strategy: [] + }, + smartInvert: true, + formatConfig: {}, + interactive: true + }, + xField: ['_editor_dimension_field', '_editor_type_field'], + yField: '_editor_value_field', + dataId: '1', + id: 'series-1', + EDITOR_SERIES_DATA_KEY: 'b', + seriesField: '_editor_type_field' + } + ], + legends: { + id: 'legend-discrete', + visible: false, + autoPage: false, + position: 'start', + interactive: false, + item: { + label: { + style: { + fill: '#1F2329', + fontSize: 16 + } + } + }, + _originalVisible: false + }, + region: [ + { + id: 'region-0' + } + ], + tooltip: { + visible: true, + mark: { + content: [{}], + title: {} + }, + dimension: { + content: [{}], + title: {} + } }, - xField: ['type', 'country'], - yField: 'value', - seriesField: 'country', - legends: [{ visible: true, position: 'middle', orient: 'bottom' }], axes: [ { orient: 'left', + id: 'axis-left', + type: 'linear', label: { - formatMethod(val) { - return `${(val * 100).toFixed(2)}%`; + autoLimit: false, + style: { + fill: '#1F2329', + fontSize: 16 + }, + formatConfig: {} + }, + domainLine: { + visible: true, + style: { + stroke: '#000000' + } + }, + tick: { + visible: true, + style: { + stroke: '#000000' + } + }, + grid: { + visible: false, + style: { + stroke: '#bbbfc4' + } + }, + autoIndent: false, + maxWidth: null, + maxHeight: null + }, + { + orient: 'bottom', + id: 'axis-bottom', + type: 'band', + label: { + autoLimit: false, + style: { + fill: '#1F2329', + fontSize: 16 + }, + formatConfig: {} + }, + domainLine: { + visible: true, + style: { + stroke: '#000000' + }, + onZero: true + }, + tick: { + visible: true, + style: { + stroke: '#000000' + } + }, + grid: { + visible: false, + style: { + stroke: '#bbbfc4' + } + }, + autoIndent: false, + maxWidth: null, + maxHeight: null, + trimPadding: false, + paddingInner: [0.18791312559017942, 0], + paddingOuter: [0.18791312559017942, 0] + } + ], + data: [ + { + id: '0', + sourceKey: 'a', + values: [ + { + _editor_dimension_field: 'x1', + _editor_value_field: 20, + _editor_type_field: 'a' + }, + { + _editor_dimension_field: 'x2', + _editor_value_field: 23, + _editor_type_field: 'a' + }, + { + _editor_dimension_field: 'x3', + _editor_value_field: 26, + _editor_type_field: 'a' + } + ], + specField: { + _editor_dimension_field: { + type: 'dimension', + order: 0 + }, + _editor_type_field: { + type: 'series', + order: 0 + }, + _editor_value_field: { + type: 'value', + order: 0 + } + } + }, + { + id: '1', + sourceKey: 'b', + values: [ + { + _editor_dimension_field: 'x1', + _editor_value_field: 20, + _editor_type_field: 'b' + }, + { + _editor_dimension_field: 'x2', + _editor_value_field: 24, + _editor_type_field: 'b' + }, + { + _editor_dimension_field: 'x3', + _editor_value_field: 29, + _editor_type_field: 'b' + } + ], + specField: { + _editor_dimension_field: { + type: 'dimension', + order: 0 + }, + _editor_type_field: { + type: 'series', + order: 0 + }, + _editor_value_field: { + type: 'value', + order: 0 } } } - ] + ], + labelLayout: 'region', + background: 'transparent' }; const storySpec: IStorySpec = { @@ -673,7 +906,7 @@ const storySpec: IStorySpec = { height: 400 }, options: { - spec: spec, + spec: spec1, initOption: { animation: false, interactive: true, diff --git a/packages/vstory/src/edit/edit-component/base-selection.ts b/packages/vstory/src/edit/edit-component/base-selection.ts index f72b472..294c2ee 100644 --- a/packages/vstory/src/edit/edit-component/base-selection.ts +++ b/packages/vstory/src/edit/edit-component/base-selection.ts @@ -26,7 +26,7 @@ export abstract class BaseSelection implements IEditComponent { declare type: string; protected _initOverGraphic() { - this._overGraphic = createGroup({ pickable: false }); + this._overGraphic = createGroup({ pickable: false, visible: false }); } endEdit(emitEvent: boolean = true): void { diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts index 90de07d..fcf8e81 100644 --- a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/bar.ts @@ -1,21 +1,15 @@ -import type { CharacterChart } from './../../../../story/character/chart/character'; import { Bounds, type IPointLike } from '@visactor/vutils'; import type { IGroup, IGroupGraphicAttribute, IRect, IRectGraphicAttribute } from '@visactor/vrender-core'; import { createGroup, createRect } from '@visactor/vrender-core'; -import { MaxAxisPaddingOuter, PickGraphicAttribute } from '../../../const'; +import { EditEditingState, MaxAxisPaddingOuter, PickGraphicAttribute } from '../../../const'; import type { Edit } from '../../../edit'; import type { IEditSelectionInfo } from '../../../interface'; import { BaseMarkControl } from './base'; import { SHAPE_HOVER_COLOR } from '../constants'; import type { ICartesianSeries } from '@visactor/vchart'; -import { getVChartFromCharacter } from '../../../utils/chart'; +import { getChartRenderMatrix, getVChartFromCharacter } from '../../../utils/chart'; import type { BandScale } from '@visactor/vscale'; -import { - getChartToGlobalMatrix, - transformBoundsWithMatrix, - transformPointToEditGroup, - transformPointWithMatrix -} from '../../../utils/space'; +import { transformPointWithMatrix } from '../../../utils/space'; import type { StoryEvent } from '../../../../story/interface'; import { computeScalePadding } from '../../../utils/scale'; @@ -36,12 +30,12 @@ const defaultEditGroupAttribute: IGroupGraphicAttribute = { pickable: true, visible: true, zIndex: 10, + boundsPadding: 4, x: 0, y: 0, height: 100 }; -// Todo 修改柱宽度,柱高度 export class BarMarkControl extends BaseMarkControl { private _startGroup: IGroup; private _endGroup: IGroup; @@ -109,21 +103,24 @@ export class BarMarkControl extends BaseMarkControl { }); h.addEventListener('pointerover', (e: StoryEvent) => { - const borderIsVisible = this._barBorder.attribute.visible; this._showHandleGraphic(); - this._barBorder.setAttributes({ visible: borderIsVisible }); }); h.addEventListener('pointerout', (e: StoryEvent) => { if (this._editState === 'dragging') { return; } this._hideHandleGraphic(); + this._hideBorderGraphic(); }); }); } startWithActionInfo(actionInfo: IEditSelectionInfo) { super.startWithActionInfo(actionInfo); + // 设置绘图变换矩阵 + const matrix = getChartRenderMatrix(actionInfo.character.graphic.graphic); + this._graphicGroup.setAttributes({ postMatrix: matrix }); + this._setCurrentEditData(actionInfo); this._setEditGraphic(); } @@ -134,18 +131,19 @@ export class BarMarkControl extends BaseMarkControl { this._graphicGroup.setAttribute('visible', true); this._graphicGroup.showAll(); + this._hideBorderGraphic(); } private _setBorderAttribute() { const currentEditorElements = this._getAllElementInDimension(); if (currentEditorElements.length) { - const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); + // const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); // 重新设置border属性 - let bounds = new Bounds(); + const bounds = new Bounds(); currentEditorElements.forEach(e => { bounds.union(e.graphicItem.globalAABBBounds); }); - bounds = transformBoundsWithMatrix(matrix, bounds) as unknown as Bounds; + // bounds = transformBoundsWithMatrix(matrix, bounds) as unknown as Bounds; this._barBorder.setAttributes({ x: bounds.x1, x1: bounds.x2, @@ -222,10 +220,13 @@ export class BarMarkControl extends BaseMarkControl { onMarkPointOver(actionInfo: IEditSelectionInfo) { super.onMarkPointOver(actionInfo); this._setCurrentEditData(actionInfo); + this._setEditGraphic(); } onMarkPointOut(actionInfo: IEditSelectionInfo) { super.onMarkPointOut(actionInfo); + this._hideBorderGraphic(); + this._hideHandleGraphic(); } private _setCurrentEditData(actionInfo: IEditSelectionInfo) { @@ -235,19 +236,23 @@ export class BarMarkControl extends BaseMarkControl { } private _showHandleGraphic() { - // TODO: 显示手柄 + this._startHandle.setAttributes({ visible: true }); + this._endHandle.setAttributes({ visible: true }); } private _hideHandleGraphic() { - // TODO: 隐藏手柄 - } - - private _showBorderGraphic() { - // TODO: 显示边框 + if (this._editState === 'dragging') { + return; + } + this._startHandle.setAttributes({ visible: false }); + this._endHandle.setAttributes({ visible: false }); } private _hideBorderGraphic() { - // TODO: 隐藏边框 + if (this._editState === 'dragging') { + return; + } + this._barBorder.setAttributes({ visible: false }); } private _onResizeStart(h: IGroup, e: StoryEvent) { @@ -292,10 +297,11 @@ export class BarMarkControl extends BaseMarkControl { } this._barBorder.setAttributes({ visible: true }); this._editState = 'dragging'; + this.edit.setEditGlobalState(EditEditingState.continuingEditing, true); // drag 使用的临时数据 // @ts-ignore this._dragInfo.handle = h === this._startGroup ? 'start' : 'end'; - const layerPos = transformPointToEditGroup(this.edit, e.canvas); + const layerPos = transformPointWithMatrix(this._graphicGroup.globalTransMatrix, e.canvas); this._dragInfo.startPos.x = layerPos.x; this._dragInfo.startPos.y = layerPos.y; this._dragInfo.series = series; @@ -316,7 +322,7 @@ export class BarMarkControl extends BaseMarkControl { } private _handleMove = (e: StoryEvent) => { - const layerPos = transformPointToEditGroup(this.edit, e.canvas); + const layerPos = transformPointWithMatrix(this._graphicGroup.globalTransMatrix, e.canvas); const dx = layerPos.x - this._dragInfo.startPos.x; const dy = layerPos.y - this._dragInfo.startPos.y; @@ -370,10 +376,10 @@ export class BarMarkControl extends BaseMarkControl { } else { start = currentValue - this._dragInfo.tempScale[0].bandwidth(); } - const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); + // const matrix = getChartToGlobalMatrix(this._actionInfo.character as CharacterChart, this.edit); if (this._dragInfo.series.direction === 'vertical') { - start = transformPointWithMatrix(matrix, { x: start, y: 0 }).x; - end = transformPointWithMatrix(matrix, { x: end, y: 0 }).x; + // start = transformPointWithMatrix(matrix, { x: start, y: 0 }).x; + // end = transformPointWithMatrix(matrix, { x: end, y: 0 }).x; // console.log(`start = `, start - handlerSize * 0.5, 'end = ', end - handlerSize * 0.5); this._startGroup.setAttributes({ x: start - handlerSize * 0.5 @@ -386,8 +392,8 @@ export class BarMarkControl extends BaseMarkControl { x1: end }); } else { - start = transformPointWithMatrix(matrix, { x: 0, y: start }).y; - end = transformPointWithMatrix(matrix, { x: 0, y: end }).y; + // start = transformPointWithMatrix(matrix, { x: 0, y: start }).y; + // end = transformPointWithMatrix(matrix, { x: 0, y: end }).y; this._startGroup.setAttributes({ y: start - handlerSize * 0.5 }); @@ -434,9 +440,12 @@ export class BarMarkControl extends BaseMarkControl { private _handleUp = (e: StoryEvent) => { this._barBorder.setAttributes({ visible: false }); this._editState = 'none'; + this.edit.setEditGlobalState(EditEditingState.continuingEditing, false); this.edit.getStage().removeEventListener('pointermove', this._handleMove as any); window.removeEventListener('pointerup', this._handleUp, true); if (this._dragInfo.hasChange) { + // TODO 设置对应的轴 padding + // 这里的 setConfig 方法需要是增量设置 this._actionInfo.character.setConfig({ option: { axes: [ @@ -451,23 +460,13 @@ export class BarMarkControl extends BaseMarkControl { ] } }); + // 更新属性 - // this._dragInfo.series = null; - // this._dragInfo.axis = null; - // this._dragInfo.rawScale = []; - // this.chart.reRenderWithUpdateSpec(); - // // upgrade 更新当前的一些图表数据 - // this._upgradeVChartLink(); - // // 柱宽编辑事件 - // CONTEXT.TeaEvent('edit_element', { - // element_type: 'chart', - // part: 'series', - // chart_type: this._chart.vchartType, - // data_source_type: this._chart.dataSourceType, - // action_type: 'update', - // edit_property: 'barWidth', - // trigger_by: 'chart_interaction' - // }); + this._dragInfo.series = null; + this._dragInfo.axis = null; + this._dragInfo.rawScale = []; + this._dragInfo.tempScale = []; + this._dragInfo.hasChange = false; } }; diff --git a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts index cefde61..ae5cdea 100644 --- a/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts +++ b/packages/vstory/src/edit/edit-component/edit-control/series-mark-control/base.ts @@ -12,7 +12,7 @@ export class BaseMarkControl { protected _graphicGroup: IGroup; constructor(public readonly edit: Edit) { - this._graphicGroup = createGroup({ visible: false }); + this._graphicGroup = createGroup({ visible: false, zIndex: 10 }); this.edit.getEditGroup().add(this._graphicGroup); } diff --git a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts index d56db7b..dd5a8cf 100644 --- a/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts +++ b/packages/vstory/src/edit/edit-component/series-mark/series-mark-selection.ts @@ -1,6 +1,5 @@ import { EditEditingState, SeriesMarkMode } from './../../const'; import { Logger } from './../../../util/logger'; -import type { CharacterChart } from './../../../story/character/chart/character'; import { SeriesMarkControl } from './../edit-control/series-mark-control/index'; import type { BaseMarkControl } from './../edit-control/series-mark-control/base'; import type { IEditOverActionInfo } from './../../interface'; @@ -10,11 +9,15 @@ import type { IEditSelectionInfo } from '../../interface'; import { EditActionEnum, type IEditActionInfo, type IEditComponent } from '../../interface'; import { BaseSelection } from './../base-selection'; -import { getChartToGlobalMatrix } from '../../utils/space'; import { cloneEditGraphic } from '../../utils/graphic'; import { SHAPE_OVER_COLOR, SHAPE_SELECT_COLOR } from '../../const'; import type { Edit } from '../../edit'; -import { getKeyValueMapWithScaleMap, getSeriesKeyField, getSeriesKeyScalesMap } from '../../utils/chart'; +import { + getChartRenderMatrix, + getKeyValueMapWithScaleMap, + getSeriesKeyField, + getSeriesKeyScalesMap +} from '../../utils/chart'; export class SeriesMarkSelection extends BaseSelection implements IEditComponent { readonly level = 4; @@ -111,6 +114,11 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent startEdit(actionInfo: IEditSelectionInfo) { Logger.debug('series mark startEdit'); super.startEdit(actionInfo, false); + // 设置绘图变换矩阵 + const matrix = getChartRenderMatrix(this._actionInfo.character.graphic.graphic); + this._selectGraphic.setAttributes({ postMatrix: matrix }); + this._overGraphic.setAttributes({ postMatrix: matrix }); + const seriesMarkInfo: { selectMode: string; markName?: string; @@ -220,6 +228,9 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent checkOver?(action: IEditActionInfo): void { // action if (action.type === EditActionEnum.pointerOverCharacter && action.detail?.part === 'seriesMark') { + // 设置绘图变换矩阵 + const matrix = getChartRenderMatrix(action.character.graphic.graphic); + this._overGraphic.setAttributes({ postMatrix: matrix }); // show over graphic this._showOverGraphic(action as IEditOverActionInfo); } @@ -285,9 +296,9 @@ export class SeriesMarkSelection extends BaseSelection implements IEditComponent protected _addPickGraphic(action: IEditOverActionInfo, parent: IGroup, attr?: any) { const itemList = this._getChartItemInSeriesMark(action); - const matrix = getChartToGlobalMatrix(action.character as CharacterChart, this.edit); + // const matrix = getChartToGlobalMatrix(action.character as CharacterChart, this.edit); itemList.forEach((item: IGraphic) => { - const graphic = cloneEditGraphic(item, matrix, { ...(attr ?? {}) }); + const graphic = cloneEditGraphic(item, null, { ...(attr ?? {}) }); parent.add(graphic); }); } diff --git a/packages/vstory/src/edit/edit.ts b/packages/vstory/src/edit/edit.ts index ca8f0f2..68de417 100644 --- a/packages/vstory/src/edit/edit.ts +++ b/packages/vstory/src/edit/edit.ts @@ -70,7 +70,7 @@ export class Edit { editLayer.clipInViewBox = false; editLayer.add(this._editGroup); - this._overGraphicGroup = createGroup({}); + this._overGraphicGroup = createGroup({ pickable: false }); this._overGraphicGroup.name = 'over_group'; editLayer.add(this._overGraphicGroup); } diff --git a/packages/vstory/src/edit/utils/chart.ts b/packages/vstory/src/edit/utils/chart.ts index f13b350..4715660 100644 --- a/packages/vstory/src/edit/utils/chart.ts +++ b/packages/vstory/src/edit/utils/chart.ts @@ -1,8 +1,10 @@ import type { ICartesianSeries, ISeries, IVChart } from '@visactor/vchart'; import { isContinuous } from '@visactor/vscale'; +import type { Matrix } from '@visactor/vutils'; import { isArray } from '@visactor/vutils'; import { VCHART_DATA_INDEX } from '../const'; import type { ICharacter } from '../../story/character'; +import type { VChartGraphic } from '../../story/character/chart/graphic/vrender/vchart-graphic'; // 特殊系列,会在获取系列数据的唯一key时,增加index const SpecialSeriesMap: { [key: string]: boolean } = { @@ -77,3 +79,10 @@ export function getKeyValueMapWithScaleMap(keys: string[], scaleMap: { [key: str export function getVChartFromCharacter(character: ICharacter): IVChart { return character.graphic.graphic.vchart; } + +export function getChartRenderMatrix(chart: VChartGraphic): Matrix { + const matrix = chart.globalTransMatrix.clone(); + const stageMatrix = chart.stage.window.getViewBoxTransform().clone(); + stageMatrix.multiply(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f); + return stageMatrix; +} diff --git a/packages/vstory/src/edit/utils/graphic.ts b/packages/vstory/src/edit/utils/graphic.ts index 90a4266..20c92e9 100644 --- a/packages/vstory/src/edit/utils/graphic.ts +++ b/packages/vstory/src/edit/utils/graphic.ts @@ -12,7 +12,7 @@ export function cloneEditGraphic(graphic: IGraphic, matrix: Matrix, attribute?: cornerRadius: 0, ...attribute }); - transformGraphicWithMatrix(cloneItem, matrix); + matrix && transformGraphicWithMatrix(cloneItem, matrix); if (cloneItem.type === 'text') { return createRect({ ...PickGraphicAttribute,