diff --git a/common/changes/@visactor/vstory-core/feat-runtime-series-mark_2024-12-23-08-38.json b/common/changes/@visactor/vstory-core/feat-runtime-series-mark_2024-12-23-08-38.json new file mode 100644 index 00000000..219479b7 --- /dev/null +++ b/common/changes/@visactor/vstory-core/feat-runtime-series-mark_2024-12-23-08-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: change runtime logic about get the character config\n\n", + "type": "none", + "packageName": "@visactor/vstory-core" + } + ], + "packageName": "@visactor/vstory-core", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vstory/feat-runtime-series-mark_2024-12-23-08-38.json b/common/changes/@visactor/vstory/feat-runtime-series-mark_2024-12-23-08-38.json new file mode 100644 index 00000000..d8999846 --- /dev/null +++ b/common/changes/@visactor/vstory/feat-runtime-series-mark_2024-12-23-08-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: change runtime logic about get the character config\n\n", + "type": "none", + "packageName": "@visactor/vstory" + } + ], + "packageName": "@visactor/vstory", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/packages/vstory-core/src/character/chart/character-chart.ts b/packages/vstory-core/src/character/chart/character-chart.ts index 2ae474c7..97234311 100644 --- a/packages/vstory-core/src/character/chart/character-chart.ts +++ b/packages/vstory-core/src/character/chart/character-chart.ts @@ -15,6 +15,8 @@ import { ChartConfigProcess } from './chart-config-process'; import type { ICharacterChart } from './interface/character-chart'; import { mergeChartOption } from '../../utils/chart'; import type { IComponent, ISeries, IVChart } from '@visactor/vchart'; +import { MarkStyleRuntimeInstance } from './runtime/mark-style'; +import { LabelStyleRuntimeInstance } from './runtime/label-style'; export class CharacterChart extends CharacterBase @@ -24,6 +26,10 @@ export class CharacterChart protected declare _graphic: VChartGraphic; protected declare _config: IChartCharacterConfig; + // 临时记录 vchart 对象。在第一次执行 afterInitializeChart 后赋值, 在 afterVRenderDraw 中使用 + // 不临时记录的话,第一次 afterVRenderDraw 时,graphic 对象还未执行完初始化,当前对象的 _graphic 为 null + protected _vchart: IVChart; + protected _ticker: ITicker; protected _timeline: ITimeline; protected _runtime: IChartCharacterRuntime[] = []; @@ -146,7 +152,12 @@ export class CharacterChart } protected _initRuntime(): void { - this._runtime.push(CommonSpecRuntimeInstance, CommonLayoutRuntimeInstance); + this._runtime.push( + CommonSpecRuntimeInstance, + CommonLayoutRuntimeInstance, + MarkStyleRuntimeInstance, + LabelStyleRuntimeInstance + ); } protected _clearRuntime(): void { this._runtime.length = 0; @@ -188,12 +199,12 @@ export class CharacterChart { performanceHook: { afterInitializeChart: (vchart: IVChart) => { - // (this.configProcess.dataTempTransform?.specTemp)?.afterInitialize({ character: this }); + this._vchart = vchart; this._runtime.forEach(r => r.afterInitialize?.(this, vchart)); }, afterVRenderDraw: () => { - this._runtime.forEach(r => r.afterVRenderDraw?.(this)); + this._runtime.forEach(r => r.afterVRenderDraw?.(this, this._graphic?.vchart ?? this._vchart)); } } }, @@ -201,4 +212,13 @@ export class CharacterChart ) }; } + + protected _clearGraphic(): void { + super._clearGraphic(); + this._vchart = null; + } + + getRuntimeConfig() { + return this; + } } diff --git a/packages/vstory-core/src/character/chart/interface/character-chart.ts b/packages/vstory-core/src/character/chart/interface/character-chart.ts index 06a727c0..239b82a8 100644 --- a/packages/vstory-core/src/character/chart/interface/character-chart.ts +++ b/packages/vstory-core/src/character/chart/interface/character-chart.ts @@ -1,6 +1,11 @@ -import type { ICharacter } from '../../../interface/character'; +import type { ICharacter, ICharacterRuntimeConfig } from '../../../interface/character'; import type { IChartCharacterConfig } from '../../../interface/dsl/chart'; export interface ICharacterChart extends ICharacter { config: IChartCharacterConfig; + getRuntimeConfig: () => ICharacterChartRuntimeConfig; +} + +export interface ICharacterChartRuntimeConfig extends ICharacterRuntimeConfig { + config: IChartCharacterConfig; } diff --git a/packages/vstory-core/src/character/chart/interface/runtime.ts b/packages/vstory-core/src/character/chart/interface/runtime.ts index 14778a7f..8de1e493 100644 --- a/packages/vstory-core/src/character/chart/interface/runtime.ts +++ b/packages/vstory-core/src/character/chart/interface/runtime.ts @@ -1,16 +1,16 @@ import type { IVChart } from '@visactor/vchart'; -import type { ICharacter } from '../../../interface/character'; import type { ICharacterConfig } from '../../../interface/dsl/dsl'; +import type { ICharacterChartRuntimeConfig } from './character-chart'; export interface IChartCharacterRuntime { readonly type: string; // 应用config到attribute - applyConfigToAttribute?: (character: ICharacter) => void; + applyConfigToAttribute?: (character: ICharacterChartRuntimeConfig) => void; // 图表初始化完成 - afterInitialize?: (character: ICharacter, vchart: IVChart) => void; + afterInitialize?: (character: ICharacterChartRuntimeConfig, vchart: IVChart) => void; // 图表绘制完成 - afterVRenderDraw?: (character: ICharacter) => void; + afterVRenderDraw?: (character: ICharacterChartRuntimeConfig, vchart: IVChart) => void; } export interface IChartCharacterRuntimeConstructor { diff --git a/packages/vstory-core/src/character/chart/runtime/common-layout.ts b/packages/vstory-core/src/character/chart/runtime/common-layout.ts index 8acec090..8e4bb468 100644 --- a/packages/vstory-core/src/character/chart/runtime/common-layout.ts +++ b/packages/vstory-core/src/character/chart/runtime/common-layout.ts @@ -6,8 +6,8 @@ export class CommonLayoutRuntime implements IChartCharacterRuntime { type = 'CommonLayout'; applyConfigToAttribute(character: ICharacterChart): void { - const rawAttribute = character.getAttribute(); - const config = character.config; + const rawAttribute = character.getRuntimeConfig().getAttribute(); + const config = character.getRuntimeConfig().config; const layoutData = getLayoutFromWidget(config.position); const layout = getLayoutFromWidget(config.position); const viewBox = { diff --git a/packages/vstory-core/src/character/chart/runtime/common-spec.ts b/packages/vstory-core/src/character/chart/runtime/common-spec.ts index 94608b44..19f8bf87 100644 --- a/packages/vstory-core/src/character/chart/runtime/common-spec.ts +++ b/packages/vstory-core/src/character/chart/runtime/common-spec.ts @@ -6,9 +6,10 @@ export class CommonSpecRuntime implements IChartCharacterRuntime { type = 'CommonSpec'; applyConfigToAttribute(character: ICharacterChart): void { - const rawAttribute = character.getAttribute(); + const rawAttribute = character.getRuntimeConfig().getAttribute(); + const config = character.getRuntimeConfig().config; const { spec } = rawAttribute; - const options = character.config.options; + const options = config.options; const { title, legends, data, color, axes, rootConfig = {}, padding } = options; if (title) { diff --git a/packages/vstory-core/src/character/chart/runtime/const.ts b/packages/vstory-core/src/character/chart/runtime/const.ts new file mode 100644 index 00000000..d7ecd580 --- /dev/null +++ b/packages/vstory-core/src/character/chart/runtime/const.ts @@ -0,0 +1,92 @@ +// vchart 内置的数据序号 +export const VCHART_DATA_INDEX = '__VCHART_DEFAULT_DATA_INDEX'; + +export const EDITOR_SERIES_MARK_STYLE_LEVEL = 90; +export const EDITOR_SERIES_MARK_SINGLE_LEVEL = 100; + +const CommonMarkAttribute = ['visible', 'stroke', 'strokeOpacity', 'lineWidth', 'lineDash', 'curveType', 'zIndex']; +export const fillMarkAttribute = [...CommonMarkAttribute, 'fill', 'fillOpacity']; +export const rectMarkAttribute = [...fillMarkAttribute, 'cornerRadius']; +export const arcMarkAttribute = [...fillMarkAttribute, 'cornerRadius', 'centerOffset', 'innerRadius', 'outerRadius']; +export const pointMarkAttribute = [...CommonMarkAttribute, 'fill', 'fillOpacity', 'size', 'shape', 'symbolType']; +export const UseDefaultSeriesStyle = '_story_series_style_default'; +export const CommonLabelStyleMap = { + style: [...fillMarkAttribute, 'font', 'fontSize', 'fontStyle', 'fontWeight', 'underline', 'background'], + attribute: ['position', 'offset', 'overlap', 'smartInvert'] +}; + +export const CommonMarkAttributeMap: { [key: string]: string[] } = { + arc: arcMarkAttribute, + rect: rectMarkAttribute, + symbol: pointMarkAttribute, + text: CommonLabelStyleMap.style +}; + +export const SeriesMarkStyleMap: { + // 系列 类型 + [key: string]: { + // 系列内的 mark name | 或者某种系列层属性 + [key: string]: { + style: string[]; // mark 的可编辑样式 key 的数组 + attribute: string[]; // mark 的可编辑属性 key 的数组 + }; + }; +} = { + bar: { + bar: { + style: [...rectMarkAttribute], + attribute: [] + }, + label: CommonLabelStyleMap + }, + line: { + line: { + style: [...CommonMarkAttribute], + attribute: [] + }, + point: { + style: pointMarkAttribute, + attribute: [] + }, + label: CommonLabelStyleMap + }, + area: { + line: { + style: [...CommonMarkAttribute], + attribute: [] + }, + area: { + style: [...fillMarkAttribute], + attribute: [] + }, + point: { + style: pointMarkAttribute, + attribute: [] + }, + label: CommonLabelStyleMap + }, + waterfall: { + bar: { + style: [...rectMarkAttribute], + attribute: [] + }, + label: CommonLabelStyleMap + }, + pie: { + pie: { + style: [...arcMarkAttribute], + attribute: [] + }, + label: CommonLabelStyleMap + }, + funnel: { + funnel: { + style: [...rectMarkAttribute], + attribute: [] + }, + label: CommonLabelStyleMap + } +}; + +export const FieldLink = '_filedLink_'; +export const ValueLink = '_valueLink_'; diff --git a/packages/vstory-core/src/character/chart/runtime/label-style.ts b/packages/vstory-core/src/character/chart/runtime/label-style.ts new file mode 100644 index 00000000..c9f5ba8c --- /dev/null +++ b/packages/vstory-core/src/character/chart/runtime/label-style.ts @@ -0,0 +1,265 @@ +import { array, isValid, merge } from '@visactor/vutils'; +import type { IChartCharacterRuntime } from '../interface/runtime'; +import type { ICharacterChart } from '../interface/character-chart'; +import type { ISeries, IVChart } from '@visactor/vchart'; +import type { ILabelInfo, Label as VChartLabelComponent } from '@visactor/vchart/esm/component/label/label'; +import { MarkStyleRuntime } from './mark-style'; +import { getSeriesKeyScalesMap, isSeriesMatch, matchDatumWithScaleMap } from './utils'; +import type { IGraphic } from '@visactor/vrender-core'; +import { StroyAllDataGroup } from '../../../interface/dsl/chart'; +import type { IMark } from '@visactor/vchart/esm/mark/interface'; +import { CommonMarkAttributeMap, fillMarkAttribute, SeriesMarkStyleMap } from './const'; + +export class LabelStyleRuntime implements IChartCharacterRuntime { + type = 'LabelStyle'; + + applyConfigToAttribute(character: ICharacterChart) { + // 设置 visible 为 true 关闭标签能力放到分组上 + // 当前 dataGroupStyle 中有 label.visible 配置,在这里添加上 visible = true + const config = character.getRuntimeConfig().config; + const dataGroupStyle = config.options?.dataGroupStyle; + if (!dataGroupStyle) { + return; + } + let hasLabelVisible = false; + Object.keys(dataGroupStyle).forEach(key => { + if (hasLabelVisible) { + return; + } + if (isValid(dataGroupStyle[key]?.label?.visible)) { + hasLabelVisible = true; + } + }); + // 如果没有设置 visible,不处理 + if (!hasLabelVisible) { + return; + } + // 否则全部设置为 true + const rawAttribute = character.getRuntimeConfig().getAttribute(); + const { spec } = rawAttribute; + if (!spec.label) { + spec.label = { visible: true }; + } else { + spec.label.visible = true; + } + spec.series?.forEach((s: any) => { + if (!s.label) { + s.label = { visible: true }; + } else { + s.label.visible = true; + } + }); + } + + /** + * 处理 fill stroke 之外的样式 + * format 在这里处理,否则防重叠会无法正确使用format之后的值进行计算 + * @param character + * @param vchart + * @returns + */ + afterInitialize(character: ICharacterChart, vchart: IVChart) { + const labelComponent = vchart.getChart().getComponentsByKey('label')[0] as VChartLabelComponent; + if (!labelComponent) { + return; + } + this._setDataGroupStyle(character, labelComponent); + } + + private _setDataGroupStyle(character: ICharacterChart, labelComponent: VChartLabelComponent) { + const config = character.getRuntimeConfig().config; + const dataGroupStyle = config.options?.dataGroupStyle; + if (!dataGroupStyle) { + return; + } + + const singleLabelStyleKeys: { [key: string]: boolean } = {}; + const hasLabelStyle = !!config.options?.labelStyle; + if (hasLabelStyle) { + Object.values(config.options?.labelStyle).forEach(ls => { + Object.keys(ls.style).forEach(k => (singleLabelStyleKeys[k] = true)); + }); + } + + labelComponent.getMarks().forEach(componentMark => { + // @ts-ignore + const infos = labelComponent._labelComponentMap.get(componentMark)(); + if (!infos) { + return; + } + array(infos).forEach(info => { + const { series, labelMark } = info as { series: ISeries; labelMark: IMark }; + const keyScaleMap = getSeriesKeyScalesMap(series); + // 先看单标签样式 + const findKey = hasLabelStyle + ? Object.keys(config.options.labelStyle).find(k => + isSeriesMatch(config.options.labelStyle[k].seriesMatch, series) + ) + : null; + const singleConfig = findKey ? config.options.labelStyle[findKey] : null; + // 系列分组key + const seriesField = series.getSeriesField(); + // style Map 是 能设置的样式 + const styleKeys = + SeriesMarkStyleMap[series.type]?.label?.style ?? CommonMarkAttributeMap.label ?? fillMarkAttribute; + + // TODO: 在这里完成组样式下的标签 format + // 多组数据在同一个系列,使用vchart mark后处理 + styleKeys.forEach((key: string) => { + // fill 和 stroke 使用vrender后处理 + if (key === 'fill' || key === 'stroke') { + return; + } + if (!labelMark.stateStyle.normal?.[key]) { + // TODO VChart bug。如果直接设置属性为 undefined 会报错 + // 默认值 还必须这样写 + labelMark.setAttribute(key, (): any => undefined); + } + // 如果是有单标签样式的 + if (singleLabelStyleKeys[key] && singleConfig && isValid(singleConfig.style[key])) { + labelMark.setPostProcess(key, (result, datum) => { + // 如果匹配到单标签样式 + if (matchDatumWithScaleMap(singleConfig.itemKeys, singleConfig.itemKeyMap, keyScaleMap, datum)) { + // TODO: 单标签format处理 + return singleConfig.style[key]; + } + // 否则匹配组样式 + return ( + MarkStyleRuntime.getMarkStyle(labelMark, dataGroupStyle, key, datum, seriesField, 'label') ?? result + ); + }); + } else { + // 没有单标签样式的 + // 直接匹配组样式 + labelMark.setPostProcess(key, (result, datum) => { + return ( + MarkStyleRuntime.getMarkStyle(labelMark, dataGroupStyle, key, datum, seriesField, 'label') ?? result + ); + }); + } + }); + // visible 单独设置 + if (!labelMark.stateStyle.normal?.visible) { + // TODO VChart bug。如果直接设置属性为 undefined 会报错 + // 默认值 还必须这样写 + labelMark.setAttribute('visible', (): any => undefined); + } + const spec = config.options.spec; + labelMark.setPostProcess('visible', (result, datum) => { + return ( + dataGroupStyle[datum[seriesField]]?.label?.visible ?? // 单组 visible + dataGroupStyle[StroyAllDataGroup]?.label?.visible ?? // 全部组visible + spec?.series?.[series.getSpecIndex()]?.label?.visible ?? // 单系列 visible + spec?.label?.visible ?? // 全局 visible + result + ); + }); + }); + }); + } + + /** + * 只处理 fill stroke 值, + * 因为智能反色逻辑会修改它们,在 afterInitialize 中设置无效。 + * @param character + * @param vchart + * @returns + */ + afterVRenderDraw(character: ICharacterChart, vchart: IVChart) { + const config = character.getRuntimeConfig().config; + const dataGroupStyle = config.options?.dataGroupStyle; + const labelStyle = config.options?.labelStyle; + if (!labelStyle && !dataGroupStyle) { + return; + } + + const labelComponent = vchart.getChart().getComponentsByKey('label')[0] as VChartLabelComponent; + if (!labelComponent) { + return; + } + // 遍历mark + labelComponent.getMarks().forEach(componentMark => { + // @ts-ignore + const infos = labelComponent._labelComponentMap.get(componentMark)(); + array(infos).forEach(info => { + const { series: series } = info as { series: ISeries; labelMark: IMark }; + const keyScaleMap = getSeriesKeyScalesMap(series); + const labelGraphics: IGraphic[] = []; + findLabelGraphicWithInfo(componentMark.getProduct().graphicItem, info, labelGraphics); + + // 先设置分组样式 + if (dataGroupStyle) { + const seriesField = series.getSeriesField(); + const groupValueList = series.getRawDataStatisticsByField(seriesField)?.values as string[]; + groupValueList.forEach(groupValue => { + // 是否存在分组样式 + if (!dataGroupStyle[groupValue]?.label?.style && !dataGroupStyle[StroyAllDataGroup]?.label?.style) { + return; + } + const style = merge( + {}, + dataGroupStyle[StroyAllDataGroup].label.style ?? {}, + dataGroupStyle[groupValue].label.style ?? {} + ); + // 只设置 fill 和 stroke 颜色 + if (!isValid(style.fill) && !isValid(style.stroke)) { + return; + } + const labels = labelGraphics.filter(l => (l.attribute as any).data[seriesField] === groupValue); + labels.forEach(l => { + isValid(style.fill) && l.setAttribute('fill', style.fill); + isValid(style.stroke) && l.setAttribute('stroke', style.stroke); + }); + }); + } + + // 再设置单标签样式 + if (labelStyle) { + const findKeys = !!labelStyle + ? Object.keys(labelStyle).filter(k => isSeriesMatch(labelStyle[k].seriesMatch, series)) + : null; + findKeys.forEach(findKey => { + const item = labelStyle[findKey]; + // 只设置 fill 和 stroke 颜色 + if (!isValid(item.style.fill) && !isValid(item.style.stroke)) { + return; + } + // 找到对应的标签 + const label = labelGraphics.find(l => + matchDatumWithScaleMap(item.itemKeys, item.itemKeyMap, keyScaleMap, (l.attribute as any).data as any) + ); + if (!label) { + return; + } + isValid(item.style.fill) && label.setAttribute('fill', item.style.fill); + isValid(item.style.stroke) && label.setAttribute('stroke', item.style.stroke); + }); + } + }); + }); + return; + } +} + +function _collectAllLabelGraphic(g: IGraphic, list: IGraphic[]) { + if (g.type === 'text' || g.type === 'richtext') { + list.push(g); + return; + } + if (g.children) { + g.children.forEach((child: IGraphic) => _collectAllLabelGraphic(child, list)); + } +} + +function findLabelGraphicWithInfo(g: IGraphic, info: ILabelInfo, list: IGraphic[]) { + const matchLabel = g.children[0].children.find( + // @ts-ignore + (c: IGraphic) => c.attribute.baseMarkGroupName === info.baseMark.getProduct().graphicItem.name + ); + if (!matchLabel) { + return; + } + _collectAllLabelGraphic(matchLabel, list); +} + +export const LabelStyleRuntimeInstance = new LabelStyleRuntime(); diff --git a/packages/vstory-core/src/character/chart/runtime/mark-style.ts b/packages/vstory-core/src/character/chart/runtime/mark-style.ts new file mode 100644 index 00000000..4bffaca2 --- /dev/null +++ b/packages/vstory-core/src/character/chart/runtime/mark-style.ts @@ -0,0 +1,198 @@ +import type { IChartCharacterRuntime } from '../interface/runtime'; +import type { ICharacterChart } from '../interface/character-chart'; +import type { ISeries, IVChart } from '@visactor/vchart'; +import { getSeriesKeyScalesMap, GetVChartSeriesWithMatch, matchDatumWithScaleMap } from './utils'; +import type { IChartCharacterConfig } from '../../../interface/dsl/chart'; +import { StroyAllDataGroup } from '../../../interface/dsl/chart'; +import type { IMark } from '@visactor/vchart/esm/mark/interface'; +import { + CommonMarkAttributeMap, + EDITOR_SERIES_MARK_SINGLE_LEVEL, + EDITOR_SERIES_MARK_STYLE_LEVEL, + fillMarkAttribute, + SeriesMarkStyleMap, + UseDefaultSeriesStyle +} from './const'; +import { isArray, merge, isValid } from '@visactor/vutils'; + +export class MarkStyleRuntime implements IChartCharacterRuntime { + type = 'MarkStyle'; + + static getMarkStyle( + mark: IMark, + dataGroupStyle: IChartCharacterConfig['options']['dataGroupStyle'], + key: string, + datum: any, + seriesField: string, + markName?: string + ) { + if (!dataGroupStyle) { + return null; + } + const value = + dataGroupStyle[datum[seriesField]]?.[markName ?? mark.name]?.style?.[key] ?? + dataGroupStyle[StroyAllDataGroup]?.[markName ?? mark.name]?.style?.[key]; + + if (value === UseDefaultSeriesStyle) { + return null; + } + return value; + } + + applyConfigToAttribute(character: ICharacterChart) { + // visible + // 如果 dataGroupStyle 中有 visible 配置,在这里添加上 visible = true + // 具体 visible 的逻辑在下方 afterInitialize 中设置到 mark 上 + const config = character.getRuntimeConfig().config; + const dataGroupStyle = config.options?.dataGroupStyle; + // 没有的话,忽略 + if (!dataGroupStyle) { + return; + } + const rawAttribute = character.getRuntimeConfig().getAttribute(); + const { spec } = rawAttribute; + const visibleMarkNames: string[] = []; + // 得到全部被设置过 visible 的 markName + Object.values(dataGroupStyle).forEach(groupConfig => { + Object.keys(groupConfig).forEach(markName => { + if (isValid(groupConfig[markName]?.visible)) { + visibleMarkNames.push(markName); + } + }); + }); + // 设置到 spec 上 + if (spec.series) { + spec.series.forEach((s: any) => { + visibleMarkNames.forEach(name => { + s[name] = s[name] || { visible: true }; + s[name].visible = true; + }); + }); + } else { + visibleMarkNames.forEach(name => { + spec[name] = spec[name] || { visible: true }; + spec[name].visible = true; + }); + } + + return; + } + + afterInitialize(character: ICharacterChart, vchart: IVChart) { + this._setDataGroupStyle(character, vchart); + this._setMarkStyle(character, vchart); + return; + } + + private _setDataGroupStyle(character: ICharacterChart, vchart: IVChart) { + const config = character.getRuntimeConfig().config; + const dataGroupStyle = config.options?.dataGroupStyle; + if (!dataGroupStyle) { + return; + } + + // seriesStyle + const seriesList = vchart.getChart().getAllSeries(); + if (!seriesList?.length) { + return; + } + seriesList.forEach(s => { + // 一个 series 对应一组数据 + // 系列分组key + const seriesField = s.getSeriesField(); + const groupValueList = s.getRawDataStatisticsByField(seriesField)?.values; + const groupValue = groupValueList?.[0]; + s.getMarks().forEach(m => { + // set visible first + const visible = + dataGroupStyle[groupValue]?.[m.name]?.visible ?? dataGroupStyle[StroyAllDataGroup]?.[m.name]?.visible; + if (isValid(visible)) { + m.setVisible(visible); + } + // 系列分组key + if (groupValueList && groupValueList.length === 1) { + // 一个 series 对应一组数据 简化处理,优化性能 + if (!dataGroupStyle[groupValue]?.[m.name]?.style && !dataGroupStyle[StroyAllDataGroup]?.[m.name]?.style) { + return; + } + const markStyle = merge( + {}, + dataGroupStyle[StroyAllDataGroup][m.name].style ?? {}, + dataGroupStyle[groupValue][m.name].style ?? {} + ); + if (Object.keys(markStyle).length === 0) { + return; + } + m.setStyle( + { + ...markStyle + }, + 'normal', + EDITOR_SERIES_MARK_STYLE_LEVEL + ); + } else { + // 如果有 style map 的话, 只有这些属性可以被设置 + const styleKeys = + SeriesMarkStyleMap[s.type]?.[m.name]?.style ?? CommonMarkAttributeMap[m.type] ?? fillMarkAttribute; + + // 多组数据在同一个系列,使用后处理 + styleKeys.forEach(key => { + if (!m.stateStyle.normal?.[key]) { + // TODO VChart bug。如果直接设置属性为 undefined 会报错 + // 默认值 还必须这样写 + m.setAttribute(key, (): any => undefined); + } + + m.setPostProcess(key, (result, datum) => { + const temp = MarkStyleRuntime.getMarkStyle(m, dataGroupStyle, key, datum, seriesField) ?? result; + if (s.type === 'area' && key === 'stroke' && m.name === 'area') { + if (!isArray(temp)) { + return [temp, false, false, false]; + } + } + return temp; + }); + }); + } + }); + }); + } + + private _setMarkStyle(character: ICharacterChart, vchart: IVChart) { + const config = character.getRuntimeConfig().config; + const markStyle = config.options?.markStyle; + if (!markStyle) { + return; + } + const chart = vchart.getChart(); + Object.values(markStyle).forEach(i => { + const series = GetVChartSeriesWithMatch(chart, i.seriesMatch) as ISeries; + if (!series) { + return; + } + const mark = series.getMarkInName(i.markName); + if (!mark) { + return; + } + const keyScaleMap = getSeriesKeyScalesMap(series); + const stateKey = i.id; + mark.setStyle( + { + ...i.style + }, + stateKey, + EDITOR_SERIES_MARK_SINGLE_LEVEL + ); + chart.updateState({ + [stateKey]: { + filter: (datum: any) => { + return matchDatumWithScaleMap(i.itemKeys, i.itemKeyMap, keyScaleMap, datum); + }, + level: 10 + } + }); + }); + } +} + +export const MarkStyleRuntimeInstance = new MarkStyleRuntime(); diff --git a/packages/vstory-core/src/character/chart/runtime/ranking-bar.ts b/packages/vstory-core/src/character/chart/runtime/ranking-bar.ts index 001149f4..7f280526 100644 --- a/packages/vstory-core/src/character/chart/runtime/ranking-bar.ts +++ b/packages/vstory-core/src/character/chart/runtime/ranking-bar.ts @@ -5,7 +5,7 @@ export class RankingBarRuntime implements IChartCharacterRuntime { type = 'RankingBar'; applyConfigToAttribute(character: ICharacterChart): void { - const rawAttribute = character.getAttribute(); + const rawAttribute = character.getRuntimeConfig().getAttribute(); const { spec } = rawAttribute; // 关掉player显示 spec.player = { diff --git a/packages/vstory-core/src/character/chart/runtime/series-spec.ts b/packages/vstory-core/src/character/chart/runtime/series-spec.ts new file mode 100644 index 00000000..1307830a --- /dev/null +++ b/packages/vstory-core/src/character/chart/runtime/series-spec.ts @@ -0,0 +1,7 @@ +import type { IChartCharacterRuntime } from '../interface/runtime'; + +export class SeriesSpecRuntime implements IChartCharacterRuntime { + type = 'SeriesSpec'; +} + +export const SeriesSpecRuntimeInstance = new SeriesSpecRuntime(); diff --git a/packages/vstory-core/src/character/chart/runtime/utils.ts b/packages/vstory-core/src/character/chart/runtime/utils.ts new file mode 100644 index 00000000..b28ffd7d --- /dev/null +++ b/packages/vstory-core/src/character/chart/runtime/utils.ts @@ -0,0 +1,104 @@ +import { isArray, isValid } from '@visactor/vutils'; +import type { IChart } from '@visactor/vchart/esm/chart/interface'; +import type { ICartesianSeries, ISeries } from '@visactor/vchart'; +import { isContinuous } from '@visactor/vscale'; +import { VCHART_DATA_INDEX, ValueLink, FieldLink } from './const'; +import type { IComponentMatch } from '../../../interface/dsl/chart'; + +export function ChartSpecMatch(rawSpec: any, index: number, matchInfo: IComponentMatch) { + if (!matchInfo) { + return false; + } + if (isValid(matchInfo.usrId)) { + return rawSpec.id === matchInfo.usrId; + } else if (isValid(matchInfo.specIndex)) { + return matchInfo.specIndex === 'all' || index === matchInfo.specIndex; + } + + return false; +} + +export function GetVChartSeriesWithMatch(vchart: IChart, seriesMatch: IComponentMatch & { type: string }) { + if (!isValid(seriesMatch.specIndex) && seriesMatch.type) { + return vchart.getAllSeries().filter(s => s.type === seriesMatch.type); + } + if (!isValid(seriesMatch.specIndex)) { + return null; + } + return vchart + .getAllSeries() + .find(s => + isValid(seriesMatch.usrId) ? s.userId === seriesMatch.usrId : s.getSpecIndex() === seriesMatch.specIndex + ); +} + +export function isSeriesMatch(seriesMatch: IComponentMatch & { type: string }, series: ISeries) { + if (isValid(seriesMatch.type) && series.type !== seriesMatch.type) { + return false; + } + if (isValid(seriesMatch.usrId) && series.userId !== seriesMatch.usrId) { + return false; + } + if (isValid(seriesMatch.specIndex) && series.getSpecIndex() !== seriesMatch.specIndex) { + return false; + } + return true; +} + +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 matchDatumWithScaleMap( + keys: string[], + keyValueMap: { [key: string]: number }, + scaleMap: { [key: string]: any } = {}, + datum: any +) { + if (isArray(datum)) { + datum = datum[0]; + } + return keys.every(key => { + const scale = scaleMap[key]; + if (!scale) { + return keyValueMap[key] === datum[key]; + } + if (isContinuous(scale.type)) { + return keyValueMap[VCHART_DATA_INDEX] === datum[VCHART_DATA_INDEX]; + } + return keyValueMap[key] === scaleMap[key]._index.get(`${datum[key]}`); + }); +} + +export function getMarkStyleId(markName: string, itemKeys: string[], itemKeyMap: { [key: string]: any }) { + return itemKeys.reduce((pre, cur) => { + return pre + `${FieldLink}${cur}${ValueLink}${itemKeyMap[cur]}`; + }, markName); +} diff --git a/packages/vstory-core/src/character/chart/runtime/wave-scatter.ts b/packages/vstory-core/src/character/chart/runtime/wave-scatter.ts index acdf02ef..d77951c8 100644 --- a/packages/vstory-core/src/character/chart/runtime/wave-scatter.ts +++ b/packages/vstory-core/src/character/chart/runtime/wave-scatter.ts @@ -29,9 +29,9 @@ export class WaveScatterRuntime implements IChartCharacterRuntime { type = 'WaveScatter'; applyConfigToAttribute(character: ICharacterChart): void { - const rawAttribute = character.getAttribute(); + const rawAttribute = character.getRuntimeConfig().getAttribute(); const { spec } = rawAttribute; - const config = character.config as any; + const config = character.getRuntimeConfig().config as any; const { waveDuration = 1000, categoryField, diff --git a/packages/vstory-core/src/interface/character.ts b/packages/vstory-core/src/interface/character.ts index a13e39b4..58998bf6 100644 --- a/packages/vstory-core/src/interface/character.ts +++ b/packages/vstory-core/src/interface/character.ts @@ -38,4 +38,12 @@ export interface ICharacter extends IReleaseable { setConfig: (config: ICharacterConfig) => void; getAttribute: () => any; + + getRuntimeConfig: () => ICharacterRuntimeConfig; +} + +export interface ICharacterRuntimeConfig { + config: ICharacterConfig; + canvas: IStoryCanvas; + getAttribute: () => any; } diff --git a/packages/vstory-core/src/interface/dsl/chart.ts b/packages/vstory-core/src/interface/dsl/chart.ts index 9d8f3801..ba6c9a4c 100644 --- a/packages/vstory-core/src/interface/dsl/chart.ts +++ b/packages/vstory-core/src/interface/dsl/chart.ts @@ -33,8 +33,8 @@ export interface IMarkStyle { export interface IDataGroupStyle { // markName , label 也在这里,需要 label runtime 处理 [key: string]: { - style: IMarkStyle['style']; // markStyle - visible: boolean; // 是否可见 + style?: IMarkStyle['style']; // markStyle + visible?: boolean; // 是否可见 [key: string]: any; // 其他可能存在的逻辑配置 }; } diff --git a/packages/vstory/demo/src/App.tsx b/packages/vstory/demo/src/App.tsx index 8371ca03..2676a073 100644 --- a/packages/vstory/demo/src/App.tsx +++ b/packages/vstory/demo/src/App.tsx @@ -45,6 +45,10 @@ import { BigDataWordCloud } from './demos/infographic/big-data-wordcloud'; import { AreaChart } from './demos/infographic/source-of-new-contacts-area-chart'; import { MarketingWordcloud } from './demos/infographic/marking-wordcloud'; +// VchartEditor Runtime +import { RuntimeSeriesMark } from './demos/runtime/series-mark'; +import { RuntimeLabelStyle } from './demos/runtime/label-style'; + type MenusType = ( | { name: string; @@ -270,6 +274,19 @@ const App = () => { component: BaseChart } ] + }, + { + name: 'Runtime', + subMenus: [ + { + name: 'Series Mark', + component: RuntimeSeriesMark + }, + { + name: 'Label Style', + component: RuntimeLabelStyle + } + ] } ]; const getSelectedMenu = useCallback<(menus: MenusType) => any>( diff --git a/packages/vstory/demo/src/demos/runtime/label-style.tsx b/packages/vstory/demo/src/demos/runtime/label-style.tsx new file mode 100644 index 00000000..9430ff4b --- /dev/null +++ b/packages/vstory/demo/src/demos/runtime/label-style.tsx @@ -0,0 +1,544 @@ +import React, { useEffect } from 'react'; +import { Player, Story, initVR, registerGraphics, registerCharacters, IStoryDSL } from '../../../../../vstory-core/src'; +import { registerVComponentAction, registerVChartAction } from '../../../../../vstory-player/src'; +import { StroyAllDataGroup } from '../../../../../vstory-core/src/interface/dsl/chart'; + +registerGraphics(); +registerCharacters(); +registerVChartAction(); +registerVComponentAction(); +initVR(); + +function loadDSL() { + const barSpec0 = { + 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 + }, + totalLabel: { + visible: true, + position: 'top', + overlap: false, + clampForce: false, + formatConfig: { + fixed: 0, + content: 'value' + }, + style: { + lineHeight: '100%', + lineWidth: 1, + fill: '#1F2329', + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + interactive: true + }, + seriesLabel: { + visible: true, + position: 'end', + label: { + style: { + lineHeight: '100%', + lineWidth: 1, + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + space: 10 + } + }, + xField: '_editor_dimension_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: false, + position: 'inside', + style: { + lineHeight: '100%', + fontSize: 16, + fontWeight: 'bold' + }, + overlap: { + strategy: [] + }, + smartInvert: true, + formatConfig: {}, + interactive: true + }, + totalLabel: { + visible: true, + position: 'top', + overlap: false, + clampForce: false, + formatConfig: { + fixed: 0, + content: 'value' + }, + style: { + lineHeight: '100%', + lineWidth: 1, + fill: '#1F2329', + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + interactive: true + }, + seriesLabel: { + visible: true, + position: 'end', + label: { + style: { + lineHeight: '100%', + lineWidth: 1, + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + space: 10 + } + }, + xField: '_editor_dimension_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: {} + } + }, + axes: [ + { + orient: 'left', + id: 'axis-left', + type: 'linear', + label: { + 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.2, 0], + paddingOuter: [0.2, 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' + }; + const barSpec1 = { + type: 'bar', + data: [ + { + values: [ + { type: 'Category One', min: 76, max: 100, range: 'A', type2: 'p', color: 'A_p' }, + { type: 'Category Two', min: 56, max: 108, range: 'A', type2: 'p', color: 'A_p' }, + { type: 'Category One', min: 56, max: 100, range: 'B', type2: 'p', color: 'B_p' }, + { type: 'Category Two', min: 36, max: 108, range: 'B', type2: 'p', color: 'B_p' }, + + { type: 'Category One', min: 76, max: 100, range: 'A', type2: 'k', color: 'A_k' }, + { type: 'Category Two', min: 56, max: 108, range: 'A', type2: 'k', color: 'A_k' }, + { type: 'Category One', min: 56, max: 100, range: 'B', type2: 'k', color: 'B_k' }, + { type: 'Category Two', min: 36, max: 108, range: 'B', type2: 'k', color: 'B_k' } + ] + } + ], + xField: ['type', 'range', 'type2'], + yField: 'min', + seriesField: 'color', + paddingInner: [0.6, 0.6, 0.6], + bandPadding: [0.6, 0.6, 0.6], + label: { + position: 'bothEnd' + }, + axes: [ + { + orient: 'bottom', + showAllGroupLayers: true, + sampling: false, + tick: { + tickCount: 2 + } + } + ], + legends: { + visible: true + } + }; + const dsl: IStoryDSL = { + characters: [ + { + type: 'VChart', + id: 'chart0', + zIndex: 10, + position: { + top: 50, + left: 50, + width: 500, + height: 300 + }, + options: { + spec: barSpec0, + initOption: { + interactive: true, + animation: false, + disableTriggerEvent: true, + disableDirtyBounds: true + }, + dataGroupStyle: { + [StroyAllDataGroup]: { + label: { + // visible: true, + style: { + fill: 'green', + stroke: 'yellow', + lineWidth: 2 + } + } + }, + a: { + label: { + // visible: true, + style: { + fill: 'red' + } + } + }, + b: { + label: { + // visible: true, + style: { + stroke: 'blue', + lineWidth: 5 + } + } + } + } + } + } + // { + // type: 'VChart', + // id: 'chart1', + // zIndex: 10, + // position: { + // top: 380, + // left: 50, + // width: 500, + // height: 300 + // }, + // options: { + // spec: barSpec1, + // initOption: { + // interactive: true, + // animation: false, + // disableTriggerEvent: true, + // disableDirtyBounds: true + // }, + // dataGroupStyle: { + // [StroyAllDataGroup]: { + // bar: { + // style: { + // fill: 'green', + // stroke: 'yellow', + // lineWidth: 2 + // } + // } + // }, + // A_p: { + // bar: { + // style: { + // fill: 'red' + // } + // } + // }, + // B_k: { + // bar: { + // style: { + // stroke: 'blue', + // lineWidth: 5 + // } + // } + // } + // } + // } + // } + ], + acts: [] + }; + dsl.acts = [ + { + id: 'defaultAct', + scenes: [ + { + id: 'defaultScene', + actions: [ + { + characterId: dsl.characters.map(i => i.id), + characterActions: [ + { + action: 'appear' + } + ] + } + ] + } + ] + } + ]; + return dsl; +} + +export const RuntimeLabelStyle = () => { + const id = 'RuntimeLabelStyle'; + + useEffect(() => { + const container = document.getElementById(id); + const canvas = document.createElement('canvas'); + container?.appendChild(canvas); + + const story = new Story(null, { canvas, width: 800, height: 800, background: 'pink' }); + const player = new Player(story); + story.init(player); + // @ts-ignore + window.story = story; + // @ts-ignore + window.player = player; + const dsl = loadDSL(); + story.load(dsl); + player.play(-1); + + const chart0 = story.getCharacterById('chart0'); + // @ts-ignore + window.chart0 = chart0; + + return () => { + story.release(); + }; + }, []); + + return
; +}; diff --git a/packages/vstory/demo/src/demos/runtime/series-mark.tsx b/packages/vstory/demo/src/demos/runtime/series-mark.tsx new file mode 100644 index 00000000..71c10941 --- /dev/null +++ b/packages/vstory/demo/src/demos/runtime/series-mark.tsx @@ -0,0 +1,539 @@ +import React, { useEffect } from 'react'; +import { Player, Story, initVR, registerGraphics, registerCharacters, IStoryDSL } from '../../../../../vstory-core/src'; +import { registerVComponentAction, registerVChartAction } from '../../../../../vstory-player/src'; +import { StroyAllDataGroup } from '../../../../../vstory-core/src/interface/dsl/chart'; + +registerGraphics(); +registerCharacters(); +registerVChartAction(); +registerVComponentAction(); +initVR(); + +function loadDSL() { + const barSpec0 = { + 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: { + position: 'inside', + style: { + lineHeight: '100%', + fontSize: 16, + fontWeight: 'bold' + }, + overlap: { + strategy: [] + }, + smartInvert: true, + formatConfig: {}, + interactive: true + }, + totalLabel: { + visible: true, + position: 'top', + overlap: false, + clampForce: false, + formatConfig: { + fixed: 0, + content: 'value' + }, + style: { + lineHeight: '100%', + lineWidth: 1, + fill: '#1F2329', + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + interactive: true + }, + seriesLabel: { + visible: true, + position: 'end', + label: { + style: { + lineHeight: '100%', + lineWidth: 1, + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + space: 10 + } + }, + xField: '_editor_dimension_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: { + position: 'inside', + style: { + lineHeight: '100%', + fontSize: 16, + fontWeight: 'bold' + }, + overlap: { + strategy: [] + }, + smartInvert: true, + formatConfig: {}, + interactive: true + }, + totalLabel: { + visible: true, + position: 'top', + overlap: false, + clampForce: false, + formatConfig: { + fixed: 0, + content: 'value' + }, + style: { + lineHeight: '100%', + lineWidth: 1, + fill: '#1F2329', + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + interactive: true + }, + seriesLabel: { + visible: true, + position: 'end', + label: { + style: { + lineHeight: '100%', + lineWidth: 1, + stroke: '#ffffff', + fontSize: 16, + fontWeight: 'bold' + }, + space: 10 + } + }, + xField: '_editor_dimension_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: {} + } + }, + axes: [ + { + orient: 'left', + id: 'axis-left', + type: 'linear', + label: { + 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.2, 0], + paddingOuter: [0.2, 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' + }; + const barSpec1 = { + type: 'bar', + data: [ + { + values: [ + { type: 'Category One', min: 76, max: 100, range: 'A', type2: 'p', color: 'A_p' }, + { type: 'Category Two', min: 56, max: 108, range: 'A', type2: 'p', color: 'A_p' }, + { type: 'Category One', min: 56, max: 100, range: 'B', type2: 'p', color: 'B_p' }, + { type: 'Category Two', min: 36, max: 108, range: 'B', type2: 'p', color: 'B_p' }, + + { type: 'Category One', min: 76, max: 100, range: 'A', type2: 'k', color: 'A_k' }, + { type: 'Category Two', min: 56, max: 108, range: 'A', type2: 'k', color: 'A_k' }, + { type: 'Category One', min: 56, max: 100, range: 'B', type2: 'k', color: 'B_k' }, + { type: 'Category Two', min: 36, max: 108, range: 'B', type2: 'k', color: 'B_k' } + ] + } + ], + xField: ['type', 'range', 'type2'], + yField: 'min', + seriesField: 'color', + paddingInner: [0.6, 0.6, 0.6], + bandPadding: [0.6, 0.6, 0.6], + label: { + position: 'bothEnd' + }, + axes: [ + { + orient: 'bottom', + showAllGroupLayers: true, + sampling: false, + tick: { + tickCount: 2 + } + } + ], + legends: { + visible: true + } + }; + const dsl: IStoryDSL = { + characters: [ + { + type: 'VChart', + id: 'chart0', + zIndex: 10, + position: { + top: 50, + left: 50, + width: 500, + height: 300 + }, + options: { + spec: barSpec0, + initOption: { + interactive: true, + animation: false, + disableTriggerEvent: true, + disableDirtyBounds: true + }, + dataGroupStyle: { + [StroyAllDataGroup]: { + bar: { + style: { + fill: 'green', + stroke: 'yellow', + lineWidth: 2 + } + } + }, + a: { + bar: { + style: { + fill: 'red' + } + } + }, + b: { + bar: { + style: { + stroke: 'blue', + lineWidth: 5 + } + } + } + } + } + }, + { + type: 'VChart', + id: 'chart1', + zIndex: 10, + position: { + top: 380, + left: 50, + width: 500, + height: 300 + }, + options: { + spec: barSpec1, + initOption: { + interactive: true, + animation: false, + disableTriggerEvent: true, + disableDirtyBounds: true + }, + dataGroupStyle: { + [StroyAllDataGroup]: { + bar: { + style: { + fill: 'green', + stroke: 'yellow', + lineWidth: 2 + } + } + }, + A_p: { + bar: { + style: { + fill: 'red' + } + } + }, + B_k: { + bar: { + style: { + stroke: 'blue', + lineWidth: 5 + } + } + } + } + } + } + ], + acts: [] + }; + dsl.acts = [ + { + id: 'defaultAct', + scenes: [ + { + id: 'defaultScene', + actions: [ + { + characterId: dsl.characters.map(i => i.id), + characterActions: [ + { + action: 'appear' + } + ] + } + ] + } + ] + } + ]; + return dsl; +} + +export const RuntimeSeriesMark = () => { + const id = 'RuntimeSeriesMark'; + + useEffect(() => { + const container = document.getElementById(id); + const canvas = document.createElement('canvas'); + container?.appendChild(canvas); + + const story = new Story(null, { canvas, width: 800, height: 800, background: 'pink' }); + const player = new Player(story); + story.init(player); + // @ts-ignore + window.story = story; + // @ts-ignore + window.player = player; + const dsl = loadDSL(); + story.load(dsl); + player.play(-1); + + const chart0 = story.getCharacterById('chart0'); + // @ts-ignore + window.chart0 = chart0; + + return () => { + story.release(); + }; + }, []); + + return
; +};