From c6a3852ba84b977718b5c587018af10a6b735e0a Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Thu, 22 Aug 2024 17:48:27 +0800 Subject: [PATCH] feat: support vchart model pick --- packages/vstory/demo/src/demos/Playground.tsx | 1 - .../vstory/demo/src/demos/VChartGraphic.tsx | 334 +++++------------- packages/vstory/demo/src/demos/lv/bar1.tsx | 1 - .../edit/edit-component/chart-selection.ts | 13 +- .../edit-control/transform-control.ts | 9 + .../edit-control/transform-drag.ts | 9 +- .../src/story/character/chart/character.ts | 25 +- .../character/chart/graphic/vchart-graphic.ts | 56 +-- packages/vstory/src/story/utils/layout.ts | 4 +- 9 files changed, 159 insertions(+), 293 deletions(-) diff --git a/packages/vstory/demo/src/demos/Playground.tsx b/packages/vstory/demo/src/demos/Playground.tsx index e3e1817..07900d9 100644 --- a/packages/vstory/demo/src/demos/Playground.tsx +++ b/packages/vstory/demo/src/demos/Playground.tsx @@ -4,7 +4,6 @@ import { Story } from '../../../src/story/story'; import { Edit } from '../../../src/edit/edit'; import '../../../src/story/index'; import { cloneDeep } from '@visactor/vutils'; -import { CommonEditComponent } from '../../../src/edit/edit-component/common'; import { BoxSelection } from '../../../src/edit/edit-component/box-selection'; import { TextSelection } from '../../../src/edit/edit-component/text-selection'; import { RichTextSelection } from '../../../src/edit/edit-component/richtext-selection'; diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index d5f08a7..b1b0a30 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -3,263 +3,115 @@ import { IStorySpec } from '../../../src/story/interface'; import { Story } from '../../../src/story/story'; import '../../../src/story/index'; import { Edit } from '../../../src/edit/edit'; -import { CommonEditComponent } from '../../../src/edit/edit-component/common'; import { BoxSelection } from '../../../src/edit/edit-component/box-selection'; import { TextSelection } from '../../../src/edit/edit-component/text-selection'; import { RichTextSelection } from '../../../src/edit/edit-component/richtext-selection'; -Edit.registerEditComponent('common', CommonEditComponent); Edit.registerEditComponent('text', TextSelection); Edit.registerEditComponent('richtext', RichTextSelection); Edit.registerEditComponent('box-selection', BoxSelection); -const goldenMedals = { - 2000: [ - { country: 'USA', value: 37 }, - { country: 'Russia', value: 32 }, - { country: 'China', value: 28 }, - { country: 'Australia', value: 16 }, - { country: 'Germany', value: 13 }, - { country: 'France', value: 13 }, - { country: 'Italy', value: 13 }, - { country: 'Netherlands', value: 12 }, - { country: 'Cuba', value: 11 }, - { country: 'U.K.', value: 11 } - ], - 2004: [ - { country: 'USA', value: 36 }, - { country: 'China', value: 32 }, - { country: 'Russia', value: 28 }, - { country: 'Australia', value: 17 }, - { country: 'Japan', value: 16 }, - { country: 'Germany', value: 13 }, - { country: 'France', value: 11 }, - { country: 'Italy', value: 10 }, - { country: 'South Korea', value: 9 }, - { country: 'U.K.', value: 9 } - ], - 2008: [ - { country: 'China', value: 48 }, - { country: 'USA', value: 36 }, - { country: 'Russia', value: 24 }, - { country: 'U.K.', value: 19 }, - { country: 'Germany', value: 16 }, - { country: 'Australia', value: 14 }, - { country: 'South Korea', value: 13 }, - { country: 'Japan', value: 9 }, - { country: 'Italy', value: 8 }, - { country: 'France', value: 7 } - ], - 2012: [ - { country: 'USA', value: 46 }, - { country: 'China', value: 39 }, - { country: 'U.K.', value: 29 }, - { country: 'Russia', value: 19 }, - { country: 'South Korea', value: 13 }, - { country: 'Germany', value: 11 }, - { country: 'France', value: 11 }, - { country: 'Australia', value: 8 }, - { country: 'Italy', value: 8 }, - { country: 'Hungary', value: 8 } - ], - 2016: [ - { country: 'USA', value: 46 }, - { country: 'U.K.', value: 27 }, - { country: 'China', value: 26 }, - { country: 'Russia', value: 19 }, - { country: 'Germany', value: 17 }, - { country: 'Japan', value: 12 }, - { country: 'France', value: 10 }, - { country: 'South Korea', value: 9 }, - { country: 'Italy', value: 8 }, - { country: 'Australia', value: 8 } - ], - 2020: [ - { country: 'USA', value: 39 }, - { country: 'China', value: 38 }, - { country: 'Japan', value: 27 }, - { country: 'U.K.', value: 22 }, - { country: 'Russian Olympic Committee', value: 20 }, - { country: 'Australia', value: 17 }, - { country: 'Netherlands', value: 10 }, - { country: 'France', value: 10 }, - { country: 'Germany', value: 10 }, - { country: 'Italy', value: 10 } - ] -}; - -const colors = { - China: '#d62728', - USA: '#1664FF', - Russia: '#B2CFFF', - 'U.K.': '#1AC6FF', - Australia: '#94EFFF', - Japan: '#FF8A00', - Cuba: '#FFCE7A', - Germany: '#3CC780', - France: '#B9EDCD', - Italy: '#7442D4', - 'South Korea': '#DDC5FA', - 'Russian Olympic Committee': '#B2CFFF', - Netherlands: '#FFC400', - Hungary: '#FAE878' -}; - -const dataSpecs = Object.keys(goldenMedals).map(year => { - return { - data: [ - { - id: 'id', - values: goldenMedals[year] - .sort((a, b) => b.value - a.value) - .map(v => { - return { ...v, fill: colors[v.country] }; - }) - }, - { - id: 'year', - values: [{ year }] - } - ] - }; -}); -const duration = 1000; -const exchangeDuration = 600; - const spec = { type: 'bar', - padding: { - top: 12, - right: 100, - bottom: 12 - }, - data: dataSpecs[0].data, + data: [ + { + id: 'barData', + values: [ + { + name: 'Apple', + value: 214480 + }, + { + name: 'Google', + value: 155506 + }, + { + name: 'Amazon', + value: 100764 + }, + { + name: 'Microsoft', + value: 92715 + }, + { + name: 'Coca-Cola', + value: 66341 + }, + { + name: 'Samsung', + value: 59890 + }, + { + name: 'Toyota', + value: 53404 + }, + { + name: 'Mercedes-Benz', + value: 48601 + }, + { + name: 'Facebook', + value: 45168 + }, + { + name: "McDonald's", + value: 43417 + }, + { + name: 'Intel', + value: 43293 + }, + { + name: 'IBM', + value: 42972 + }, + { + name: 'BMW', + value: 41006 + }, + { + name: 'Disney', + value: 39874 + }, + { + name: 'Cisco', + value: 34575 + }, + { + name: 'GE', + value: 32757 + }, + { + name: 'Nike', + value: 30120 + }, + { + name: 'Louis Vuitton', + value: 28152 + }, + { + name: 'Oracle', + value: 26133 + }, + { + name: 'Honda', + value: 23682 + } + ] + } + ], direction: 'horizontal', - yField: 'country', xField: 'value', - seriesField: 'country', - bar: { - style: { - fill: datum => datum.fill - } - }, + yField: 'name', axes: [ { - animation: true, orient: 'bottom', - type: 'linear', - visible: true, - max: 50, - grid: { - visible: true - } - }, - { - animation: true, - id: 'axis-left', - orient: 'left', - width: 130, - tick: { visible: false }, - label: { visible: true }, - type: 'band' + visible: false } ], - title: { + label: { visible: true, - text: 'Top 10 Olympic Gold Medals by Country Since 2000' - }, - animationUpdate: { - bar: [ - { - type: 'update', - options: { excludeChannels: ['y'] }, - easing: 'linear', - duration - }, - { - channel: ['y'], - easing: 'circInOut', - duration: exchangeDuration - } - ], - axis: { - duration: exchangeDuration, - easing: 'circInOut' - } - }, - animationEnter: { - bar: [ - { - type: 'moveIn', - duration: exchangeDuration, - easing: 'circInOut', - options: { - direction: 'y', - orient: 'negative' - } - } - ] - }, - animationExit: { - bar: [ - { - type: 'fadeOut', - duration: exchangeDuration - } - ] - }, - customMark: [ - { - type: 'text', - dataId: 'year', - style: { - textBaseline: 'bottom', - fontSize: 200, - textAlign: 'right', - fontFamily: 'PingFang SC', - fontWeight: 600, - text: datum => datum.year, - x: (datum, ctx) => { - return ctx.vchart.getChart().getCanvasRect()?.width - 50; - }, - y: (datum, ctx) => { - return ctx.vchart.getChart().getCanvasRect()?.height - 50; - }, - fill: 'grey', - fillOpacity: 0.5 - } - } - ], - player: { - type: 'continuous', - orient: 'bottom', - auto: true, - loop: true, - dx: 80, - position: 'middle', - interval: duration, - specs: dataSpecs, - slider: { - railStyle: { - height: 6 - } - }, - controller: { - backward: { - style: { - size: 12 - } - }, - forward: { - style: { - size: 12 - } - }, - start: { - order: 1, - position: 'end' - } - } + animation: false } }; @@ -334,6 +186,8 @@ export const VChartGraphic = () => { payload: { style: {}, animation: { + effect: 'move', + move: { pos: 'top' }, duration: 1000, easing: 'linear' } as any @@ -351,6 +205,8 @@ export const VChartGraphic = () => { payload: { style: {}, animation: { + effect: 'fade', + move: { pos: 'top' }, duration: 1000, easing: 'linear' } as any diff --git a/packages/vstory/demo/src/demos/lv/bar1.tsx b/packages/vstory/demo/src/demos/lv/bar1.tsx index f18f28c..3655d44 100644 --- a/packages/vstory/demo/src/demos/lv/bar1.tsx +++ b/packages/vstory/demo/src/demos/lv/bar1.tsx @@ -4,7 +4,6 @@ import { Story } from '../../../../src/story/story'; import { Edit } from '../../../../src/edit/edit'; import '../../../../src/story/index'; import { cloneDeep } from '@visactor/vutils'; -import { CommonEditComponent } from '../../../../src/edit/edit-component/common'; import { BoxSelection } from '../../../../src/edit/edit-component/box-selection'; import { TextSelection } from '../../../../src/edit/edit-component/text-selection'; import { RichTextSelection } from '../../../../src/edit/edit-component/richtext-selection'; diff --git a/packages/vstory/src/edit/edit-component/chart-selection.ts b/packages/vstory/src/edit/edit-component/chart-selection.ts index 28e33ec..507742b 100644 --- a/packages/vstory/src/edit/edit-component/chart-selection.ts +++ b/packages/vstory/src/edit/edit-component/chart-selection.ts @@ -20,18 +20,9 @@ export class ChartSelection extends BaseSelection implements IEditComponent { return; } - const viewBox = (actionInfo.character.graphic as Chart).vchart.getStage().viewBox; const group = actionInfo.character.getGraphicParent(); - const { angle, x, y } = group.attribute; - this._layoutComponent.updateBoundsAndAngle( - { - x1: viewBox.x1 + x, - y1: viewBox.y1 + y, - x2: viewBox.x2 + x, - y2: viewBox.y2 + y - }, - angle - ); + const { angle } = group.attribute; + this._layoutComponent.updateBoundsAndAngle(actionInfo.character.getLayoutBounds(), angle); // this._layoutComponent.updateBoundsAndAngle(actionInfo.character.getGraphicParent().AABBBounds, 0); } 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 5947932..39c32c1 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 @@ -891,7 +891,16 @@ export class TransformControl extends AbstractComponent): void { - // this.group.setAttributes(attr); - this._graphic.setAttributes(attr); - // this._text.updateAttribute({}); + // character 的属性 + if (attr.position) { + this._spec.position = attr.position; + // 位置属性 + this._graphic.updateViewBox(this.getViewBoxFromSpec().viewBox); + } } getViewBoxFromSpec() { const layout = getLayoutFromWidget(this._spec.position); @@ -116,7 +119,6 @@ export class CharacterChart extends CharacterVisactor { return; } protected _updateVisactorSpec(): void { - // console.log('_updateVisactorSpec', this._specProcess.getVisSpec()); this._graphic?.updateSpec(this._specProcess.getVisSpec()); } @@ -133,13 +135,18 @@ export class CharacterChart extends CharacterVisactor { if (!(event.detailPath ?? event.path).some(g => g === this._graphic)) { return false; } - // 超出vchart viewBox 外的图表元素,依然会绘制并且能被选中 - // if (!this._graphic.pointInVChart((event as any).canvasX, (event as any).canvasY)) { - // return false; - // } const chartPath = event.detailPath[event.detailPath.length - 1]; const result = getChartModelWithEvent(this._graphic.vProduct, event); if (!result) { + // 点击到图表的空白区域 + if (this._graphic.pointInViewBox((event as any).canvasX, (event as any).canvasY)) { + return { + part: 'null', + graphic: null, + modelInfo: null, + graphicType: 'null' + }; + } return false; } const graphic = chartPath?.[chartPath.length - 1]; diff --git a/packages/vstory/src/story/character/chart/graphic/vchart-graphic.ts b/packages/vstory/src/story/character/chart/graphic/vchart-graphic.ts index f06a2a5..8d613af 100644 --- a/packages/vstory/src/story/character/chart/graphic/vchart-graphic.ts +++ b/packages/vstory/src/story/character/chart/graphic/vchart-graphic.ts @@ -41,7 +41,10 @@ export class Chart extends Group implements IVisactorGraphic { return this._vchart; } - private _chartViewBox: IBoundsLike = { x1: 0, y1: 0, x2: 100, y2: 100 }; + // 设置的 viewBox 是全局值 + private _globalViewBox: IBoundsLike = { x1: 0, y1: 0, x2: 100, y2: 100 }; + // 设置的 viewBox 相对于 vchart-graphic 的位置 + private _localViewBox: IBoundsLike = { x1: 0, y1: 0, x2: 100, y2: 100 }; private _BoundsViewBox: IBoundsLike = { x1: 0, y1: 0, x2: 100, y2: 100 }; drawTag = false; @@ -121,17 +124,13 @@ export class Chart extends Group implements IVisactorGraphic { stage.pauseTriggerEvent(); } if (params.viewBox) { - const x1 = params.viewBox.x1 ?? 0; - const y1 = params.viewBox.y1 ?? 0; - const x2 = params.viewBox.x2 ?? 0; - const y2 = params.viewBox.y2 ?? 0; - this.setAttributes({ - x: x1, - y: y1, - width: x2 - x1, - height: y2 - y1 - }); + this.updateViewBox(params.viewBox); } + + this.setAttributes({ + fill: 'red', + fillOpacity: 0.5 + }); } /** @@ -150,21 +149,15 @@ export class Chart extends Group implements IVisactorGraphic { return isPointInBounds(target, vchart.getStage().viewBox); } - setAttributes(attrs: Partial) { - super.setAttributes(attrs); - const vchart = this._vchart; - const viewBox = vchart.getStage().viewBox; - - const x1 = attrs.x ?? viewBox.x1; - const y1 = attrs.y ?? viewBox.y1; - const width = attrs.width ?? viewBox.width(); - const height = attrs.height ?? viewBox.height(); - this.updateViewBox({ - x1, - y1, - x2: x1 + width, - y2: y1 + height - }); + /** + * 判定点是否在设置 viewBox 内。设置 viewBox 会小于展示 bounds + * @param canvasX + * @param canvasY + */ + pointInViewBox(canvasX: number, canvasY: number): boolean { + const target = { x: 0, y: 0 }; + this.globalTransMatrix.transformPoint({ x: canvasX, y: canvasY }, target); + return isPointInBounds(target, this._localViewBox); } updateSpec(spec: ISpec, viewBox?: IBoundsLike, forceMerge = false, morphConfig = false) { @@ -176,7 +169,9 @@ export class Chart extends Group implements IVisactorGraphic { updateViewBox(viewBox: IBoundsLike) { // 图表的设置大小 - this._chartViewBox = { ...viewBox }; + this._globalViewBox = { ...viewBox }; + this._localViewBox = { x1: 0, y1: 0, x2: viewBox.x2 - viewBox.x1, y2: viewBox.y2 - viewBox.y1 }; + this._updateViewBox(); } @@ -189,13 +184,18 @@ export class Chart extends Group implements IVisactorGraphic { this._vchart.getStage().defaultLayer.translateTo(-rootBounds.x1, -rootBounds.y1); this._BoundsViewBox = rootBounds; - const viewBox = { ...this._chartViewBox }; + const viewBox = { ...this._globalViewBox }; this.setAttributes({ x: viewBox.x1 + rootBounds.x1, y: viewBox.y1 + rootBounds.y1, width: rootBounds.x2 - rootBounds.x1, height: rootBounds.y2 - rootBounds.y1 }); + // viewBox 在展示 bounds 下的位置 + this._localViewBox.x1 = -rootBounds.x1; + this._localViewBox.y1 = -rootBounds.y1; + this._localViewBox.x2 += -rootBounds.x1; + this._localViewBox.y2 += -rootBounds.y1; // viewBox.x2 -= viewBox.x1; viewBox.y2 -= viewBox.y1; diff --git a/packages/vstory/src/story/utils/layout.ts b/packages/vstory/src/story/utils/layout.ts index 265c675..210a998 100644 --- a/packages/vstory/src/story/utils/layout.ts +++ b/packages/vstory/src/story/utils/layout.ts @@ -2,8 +2,8 @@ import type { ILayoutAttribute, IWidgetData } from '../character'; export function getLayoutFromWidget(w: Partial): Partial { return { - x: w.left, - y: w.top, + x: w.x ?? w.left, + y: w.y ?? w.top, width: 'width' in w ? w.width : (w as any).right - w.left, height: 'height' in w ? w.height : (w as any).bottom - w.top };