diff --git a/packages/vstory/demo/src/demos/VChartGraphic.tsx b/packages/vstory/demo/src/demos/VChartGraphic.tsx index 8d699d4..d5f08a7 100644 --- a/packages/vstory/demo/src/demos/VChartGraphic.tsx +++ b/packages/vstory/demo/src/demos/VChartGraphic.tsx @@ -13,6 +13,256 @@ 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, + direction: 'horizontal', + yField: 'country', + xField: 'value', + seriesField: 'country', + bar: { + style: { + fill: datum => datum.fill + } + }, + 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' + } + ], + title: { + 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' + } + } + } +}; + export const VChartGraphic = () => { const id = 'storyBar'; @@ -31,7 +281,7 @@ export const VChartGraphic = () => { const tempSpec: IStorySpec = { characters: [ { - type: 'RectComponent', + type: 'Rect', id: 'test-graphics-0', zIndex: 10, position: { @@ -53,99 +303,17 @@ export const VChartGraphic = () => { } }, { - type: 'BarChart', + type: 'VChart', id: 'test-chart-0', - zIndex: 9, + zIndex: 10, position: { - top: 200, - left: 100, - width: 400, - height: 400 + top: 40, + left: 50, + width: 500, + height: 500 }, options: { - title: { - text: 'Timeline Chart', - orient: 'bottom', - align: 'center', - textStyle: { - fontSize: 10, - lineHeight: 10 - } - }, - padding: 12, - data: [ - { - id: 'id0', - values: chartData - } - ], - direction: 'vertical', - seriesSpec: [ - { - matchInfo: { specIndex: 'all' }, - spec: { - type: 'bar', - xField: 'year', - yField: 'value' - } - } - ], - componentSpec: [ - { - specKey: 'axes', - matchInfo: { orient: 'left' }, - spec: { - label: { - style: { - fontSize: 20 - } - } - } - }, - { - specKey: 'axes', - matchInfo: { orient: 'bottom' }, - spec: { - type: 'band' - } - }, - { - specKey: 'markLine', - matchInfo: { orient: 'left' }, - spec: { - coordinates: [chartData[0], chartData[5]], - line: { - style: { - lineDash: [0], - lineWidth: 2, - stroke: '#000' - } - }, - label: { - position: 'middle', - text: `asdadsasd% CARG`, - labelBackground: { - padding: 8, - style: { - fill: '#fff', - fillOpacity: 1, - stroke: '#3CC780', - lineWidth: 2, - cornerRadius: 8 - } - }, - style: { - fill: '#3CC780' - } - }, - endSymbol: { - size: 12, - refX: -4 - }, - offsetY: -100 - } - } - ] + spec } } ], @@ -161,14 +329,13 @@ export const VChartGraphic = () => { characterActions: [ { startTime: 0, - duration: 0, action: 'appear', + selector: '*', payload: { style: {}, animation: { - duration: 0, - easing: 'linear', - effect: 'fadeIn' + duration: 1000, + easing: 'linear' } as any } } @@ -179,8 +346,15 @@ export const VChartGraphic = () => { characterActions: [ { startTime: 0, - duration: 0, - action: 'appear' + action: 'appear', + selector: '*', + payload: { + style: {}, + animation: { + duration: 1000, + easing: 'linear' + } as any + } } ] } @@ -191,7 +365,7 @@ export const VChartGraphic = () => { ] }; const story = new Story(tempSpec, { dom: id }); - story.play(); + story.play(false); const edit = new Edit(story); edit.emitter.on('startEdit', msg => { if (msg.type === 'commonEdit' && msg.actionInfo.character) { diff --git a/packages/vstory/src/edit/edit-component/image-selection.ts b/packages/vstory/src/edit/edit-component/image-selection.ts index 5e4880f..a501b2c 100644 --- a/packages/vstory/src/edit/edit-component/image-selection.ts +++ b/packages/vstory/src/edit/edit-component/image-selection.ts @@ -1,4 +1,3 @@ -// import { StoryGraphicType } from '../../dsl/constant'; import { StoryComponentType } from '../../constants/character'; import { type IEditComponent } from '../interface'; import { BaseSelection } from './base-selection'; diff --git a/packages/vstory/src/story/character/chart/character.ts b/packages/vstory/src/story/character/chart/character.ts index 0d849fd..77443d7 100644 --- a/packages/vstory/src/story/character/chart/character.ts +++ b/packages/vstory/src/story/character/chart/character.ts @@ -14,6 +14,7 @@ import { SeriesSpecRuntime } from './runtime/series-spec'; import type { StoryEvent } from '../../interface/runtime-interface'; import type { ICharacterPickInfo } from '../runtime-interface'; import { getLayoutFromWidget } from '../../utils/layout'; +import { getChartModelWithEvent } from '../../utils/vchart-pick'; export class CharacterChart extends CharacterVisactor { static type = 'CharacterChart'; @@ -137,9 +138,16 @@ export class CharacterChart extends CharacterVisactor { // return false; // } const chartPath = event.detailPath[event.detailPath.length - 1]; + const result = getChartModelWithEvent(this._graphic.vProduct, event); + if (!result) { + return false; + } + const graphic = chartPath?.[chartPath.length - 1]; return { - part: chartPath?.[chartPath.length - 1]?.type, - graphicType: chartPath?.[chartPath.length - 1]?.type + part: result.type, + modelInfo: result, + graphic, + graphicType: graphic.type }; } diff --git a/packages/vstory/src/story/character/runtime-interface.ts b/packages/vstory/src/story/character/runtime-interface.ts index 477363f..25eb7e9 100644 --- a/packages/vstory/src/story/character/runtime-interface.ts +++ b/packages/vstory/src/story/character/runtime-interface.ts @@ -1,13 +1,14 @@ -import type { IGroup } from '@visactor/vrender'; +import type { IGroup, IGraphic } from '@visactor/vrender'; import type { IBoundsLike, IPointLike } from '@visactor/vutils'; -import type { StoryCanvas } from '../canvas/canvas'; import type { IStory, IStoryCanvas, StoryEvent } from '../interface/runtime-interface'; import type { ICharacterSpec } from './dsl-interface'; import type { Graphic } from './component/graphic/graphic'; export interface ICharacterPickInfo { part: string; + graphic: IGraphic; graphicType: string; + modelInfo: any; } export interface ICharacter { id: string; diff --git a/packages/vstory/src/story/utils/vchart-pick.ts b/packages/vstory/src/story/utils/vchart-pick.ts new file mode 100644 index 0000000..0adaa6e --- /dev/null +++ b/packages/vstory/src/story/utils/vchart-pick.ts @@ -0,0 +1,166 @@ +import type { IGraphic, IGraphicAttribute } from '@visactor/vrender'; +import type { VChart } from '@visactor/vchart'; +import type { StoryEvent } from '../interface/runtime-interface'; + +function commonModelInfo(model: any) { + return { + type: model.type, + model, + specKey: model.specKey, + specIndex: model.getSpecIndex() + }; +} + +export const seriesMarkPick = { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => { + return graphic.name?.startsWith('seriesGroup_'); + }, + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) => { + const nameInfo = graphic.name.split('_'); + const seriesId = +nameInfo[2]; + const markGraphic = graphicPath[index + 1]; + const markId = +markGraphic.name.split('_')[1]; + const series = chart.getChart().getSeriesInIds([seriesId])[0]; + const datum = graphicPath[graphicPath.length - 1].__vgrammar_scene_item__.data; + return { + type: 'seriesMark', + model: series, + mark: series.getMarkInId(markId), + specKey: series.specKey, + specIndex: series.getSpecIndex(), + datum + }; + } +}; + +export const axisMarkPick = { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => { + return graphic.name === 'axis' || graphic.name === 'axis-grid'; + }, + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) => { + const axisGroup = graphicPath[index - 1]; + const axisId = +axisGroup.name.split('_')[1]; + const axis = chart + .getChart() + .getAllComponents() + // @ts-ignore + .find(c => c._axisMark?.id === axisId || c._gridMark?.id === axisId); + return commonModelInfo(axis); + } +}; + +const MarkerClassName: { [key: string]: boolean } = { + MarkLine: true, + MarkArea: true, + MarkPoint: true, + MarkArcLine: true, + MarkArcArea: true +}; +export const markerMarkPick = { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => { + return !!MarkerClassName[graphic.constructor.name]; + }, + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[]) => { + const markerId = +graphic.id.split('-')[1]; + const model = chart + .getChart() + .getAllComponents() + // @ts-ignore + .find(c => c.id === markerId); + return commonModelInfo(model); + } +}; + +export const labelMarkPick = { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => { + return graphic.name === 'data-label'; + }, + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) => { + const id = +graphicPath[index - 1].name.split('_')[1]; + const model = chart + .getChart() + .getAllComponents() + // @ts-ignore + .find(c => { + if (c.type !== 'label' && c.type !== 'totalLabel') { + return false; + } + return c.getMarks().some(m => m.id === id); + }); + // @ts-ignore + return { ...commonModelInfo(model), datum: graphicPath[graphicPath.length - 1].attribute.data }; + } +}; + +function commonModePick(vrenderGraphicClassName: string, modelName: string) { + return { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => { + return graphic.constructor.name === vrenderGraphicClassName; + }, + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) => { + return commonModelInfo( + chart + .getChart() + .getAllComponents() + // @ts-ignore + .find(c => c.type === modelName) + ); + } + }; +} + +export const discreteLegendMarkPick = commonModePick('DiscreteLegend', 'discreteLegend'); +export const colorLegendMarkPick = commonModePick('ColorContinuousLegend', 'colorLegend'); +export const sizeLegendMarkPick = commonModePick('SizeContinuousLegend', 'sizeLegend'); +export const scrollBarMarkPick = commonModePick('ScrollBar', 'scrollBar'); +export const titleBarMarkPick = commonModePick('Title', 'title'); +export const continuousPlayerMarkPick = commonModePick('ContinuousPlayer', 'player'); +export const discretePlayerMarkPick = commonModePick('DiscretePlayer', 'player'); + +const modelCheck: { + check: (graphic: IGraphic, graphicPath: IGraphic[]) => boolean; + modelInfo: (chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) => any; +}[] = [ + seriesMarkPick, + axisMarkPick, + discreteLegendMarkPick, + colorLegendMarkPick, + sizeLegendMarkPick, + markerMarkPick, + scrollBarMarkPick, + labelMarkPick, + titleBarMarkPick, + continuousPlayerMarkPick, + discretePlayerMarkPick +]; + +/** + * 从event属性上,读取当前pick到的图表模块内容 + * @param event + */ +export function getChartModelWithEvent(chart: VChart, event: StoryEvent) { + const graphicPath = event.detailPath[event.detailPath.length - 1] as unknown as IGraphic< + Partial + >[]; + if (!graphicPath) { + return null; + } + const pickGraphic = graphicPath?.[graphicPath.length - 1]; + if (!pickGraphic) { + return null; + } + return getGraphicModelMark(chart, pickGraphic, graphicPath, 0); +} + +export function getGraphicModelMark(chart: VChart, graphic: IGraphic, graphicPath: IGraphic[], index: number) { + const modelPick = modelCheck.find(mc => mc.check(graphic, graphicPath)); + if (modelPick) { + return modelPick.modelInfo(chart, graphic, graphicPath, index); + } + // @ts-ignore + if (index >= graphicPath.length - 1) { + return null; + } + + return getGraphicModelMark(chart, graphicPath[index + 1], graphicPath, index + 1); +}