diff --git a/common/changes/@visactor/vchart/feat-pie-empty-placeholder_2024-07-15-07-28.json b/common/changes/@visactor/vchart/feat-pie-empty-placeholder_2024-07-15-07-28.json new file mode 100644 index 0000000000..3e750bbcad --- /dev/null +++ b/common/changes/@visactor/vchart/feat-pie-empty-placeholder_2024-07-15-07-28.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "add emptyPlaceholder and emptyCircle for pie chart", + "type": "none" + } + ], + "packageName": "@visactor/vchart" +} \ No newline at end of file diff --git a/docs/assets/option/en/series/pie.md b/docs/assets/option/en/series/pie.md index 70dbfaf709..0490738578 100644 --- a/docs/assets/option/en/series/pie.md +++ b/docs/assets/option/en/series/pie.md @@ -225,3 +225,29 @@ Optional values: Enable tangent constraint. The default value is `true`. + +#${prefix} emptyPlaceholder(Object) + +Set the placeholder to be displayed when data is empty. + +##${prefix} showEmptyCircle(Boolean) + +Supported since version `1.12.0`. +Determines whether to show a placeholder circle when data is empty. +The default value is `false`. + +##${prefix} emptyCircle(Object) + +Empty circle style configuration. + +```ts +emptyPlaceholder: { + showEmptyCircle: true, + emptyCircle: { + style: { + innerRadius: 0.5, + fill: '#66ccff' + } + } +} +``` diff --git a/docs/assets/option/zh/series/pie.md b/docs/assets/option/zh/series/pie.md index a9bb47fefb..2bc32cf062 100644 --- a/docs/assets/option/zh/series/pie.md +++ b/docs/assets/option/zh/series/pie.md @@ -226,3 +226,28 @@ pie: { 是否启用切线约束。 默认值为`true`。 + +#${prefix} emptyPlaceholder(Object) + +设置当数据为空时呈现的占位符。 + +##${prefix} showEmptyCircle(Boolean) + +从 1.12.0 版本开始支持,是否在数据为空时显示占位圆。 +默认值为`false`。 + +##${prefix} emptyCircle(Object) + +占位圆图元样式配置。 + +```ts +emptyPlaceholder: { + showEmptyCircle: true, + emptyCircle: { + style: { + innerRadius: 0.5, + fill: '#66ccff' + } + } +} +``` diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index b3408365a0..51d486ea4a 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -57,7 +57,7 @@ import type { IBoundsLike } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports import { isFunction, isEmpty, isNil, isString, isEqual, pickWithout } from '@visactor/vutils'; import { getDataScheme } from '../../theme/color-scheme/util'; -import type { IRunningConfig as IMorphConfig, IView } from '@visactor/vgrammar-core'; +import type { IElement, IRunningConfig as IMorphConfig, IView } from '@visactor/vgrammar-core'; import { CompilableBase } from '../../compile/compilable-base'; import type { IStateInfo } from '../../compile/mark/interface'; // eslint-disable-next-line no-duplicate-imports @@ -1218,7 +1218,7 @@ export class BaseChart extends CompilableBase implements I if (!filter || (isFunction(filter) && filter(s, m))) { const isCollect = m.getProduct().isCollectionMark(); const elements = m.getProduct().elements; - let pickElements = elements; + let pickElements = [] as IElement[]; if (isCollect) { pickElements = elements.filter(e => { const elDatum = e.getDatum(); diff --git a/packages/vchart/src/chart/pie/base/pie-transformer.ts b/packages/vchart/src/chart/pie/base/pie-transformer.ts index c28da7e54b..c779389f3d 100644 --- a/packages/vchart/src/chart/pie/base/pie-transformer.ts +++ b/packages/vchart/src/chart/pie/base/pie-transformer.ts @@ -23,7 +23,10 @@ export class BasePieChartSpecTransformer extends PolarC cornerRadius: spec.cornerRadius, padAngle: spec.padAngle, - minAngle: spec.minAngle + minAngle: spec.minAngle, + + emptyPlaceholder: spec.emptyPlaceholder, + emptyCircle: spec.emptyPlaceholder?.emptyCircle }; } } diff --git a/packages/vchart/src/series/pie/animation/animation.ts b/packages/vchart/src/series/pie/animation/animation.ts index bad6305478..8d4ee77819 100644 --- a/packages/vchart/src/series/pie/animation/animation.ts +++ b/packages/vchart/src/series/pie/animation/animation.ts @@ -101,6 +101,14 @@ export const registerPieAnimation = () => { }); }; +export const registerEmptyCircleAnimation = () => { + Factory.registerAnimation('emptyCircle', (params: IPieAnimationParams, preset: PieAppearPreset) => { + return { + appear: piePresetAnimation(params, preset) + }; + }); +}; + export const registerPie3dAnimation = () => { Factory.registerAnimation('pie3d', (params: IPieAnimationParams, preset: PieAppearPreset) => { return { diff --git a/packages/vchart/src/series/pie/interface.ts b/packages/vchart/src/series/pie/interface.ts index 61b2229bce..403719fc85 100644 --- a/packages/vchart/src/series/pie/interface.ts +++ b/packages/vchart/src/series/pie/interface.ts @@ -76,6 +76,17 @@ export interface IPieSeriesSpec extends IPolarSeriesSpec, IAnimationSpec; /** 标签配置 */ [SeriesMarkNameEnum.label]?: IMultiLabelSpec; + + /** 数据为空时显示的占位图形 */ + emptyPlaceholder?: { + /** 是否显示占位圆 + * @default false + */ + showEmptyCircle?: boolean; + + /** 占位圆样式 */ + emptyCircle?: IMarkSpec; + }; } export interface IPieSeriesTheme extends IPolarSeriesTheme { @@ -92,6 +103,10 @@ export interface IPieSeriesTheme extends IPolarSeriesTheme { * @since 1.5.1 */ outerLabel?: IArcLabelSpec; + /** 数据为空时显示的占位圆样式 + * @since 1.12.0 + */ + emptyCircle?: Partial>; } export type IPie3dSeriesSpec = { diff --git a/packages/vchart/src/series/pie/pie.ts b/packages/vchart/src/series/pie/pie.ts index 7aa5754b3a..5b29e8e89f 100644 --- a/packages/vchart/src/series/pie/pie.ts +++ b/packages/vchart/src/series/pie/pie.ts @@ -18,13 +18,13 @@ import { ChartEvent, DEFAULT_DATA_KEY } from '../../constant'; -import type { IPoint, Datum, StateValueType } from '../../typings'; +import type { IPoint, Datum, StateValueType, IArcMarkSpec } from '../../typings'; import { normalizeStartEndAngle } from '../../util/math'; import { isSpecValueWithScale } from '../../util/scale'; import { field } from '../../util/object'; import type { IModelLayoutOption } from '../../model/interface'; import { PolarSeries } from '../polar/polar'; -import type { IMark } from '../../mark/interface'; +import type { IMark, IMarkStyle } from '../../mark/interface'; import { MarkTypeEnum } from '../../mark/interface/type'; import type { IArcMark } from '../../mark/arc'; import type { ITextMark } from '../../mark/text'; @@ -36,7 +36,7 @@ import type { IPieOpt } from '../../data/transforms/pie'; import { pie } from '../../data/transforms/pie'; import { registerDataSetInstanceTransform } from '../../data/register'; import type { IPieAnimationParams, PieAppearPreset } from './animation/animation'; -import { registerPieAnimation } from './animation/animation'; +import { registerEmptyCircleAnimation, registerPieAnimation } from './animation/animation'; import { animationConfig, shouldMarkDoMorph, userAnimationConfig } from '../../animation/utils'; import { AnimationStateEnum } from '../../animation/interface'; import type { IBasePieSeriesSpec, IPieSeriesSpec } from './interface'; @@ -81,6 +81,9 @@ export class BasePieSeries extends PolarSeries protected _labelMark: ITextMark | null = null; protected _labelLineMark: IPathMark | null = null; + protected _showEmptyCircle: boolean; + protected _emptyArcMark: IArcMark | null = null; + protected _buildMarkAttributeContext() { super._buildMarkAttributeContext(); // center @@ -116,6 +119,8 @@ export class BasePieSeries extends PolarSeries this._specAngleField = this._angleField.slice(); this._specRadiusField = []; + + this._showEmptyCircle = this._spec.emptyPlaceholder?.showEmptyCircle ?? false; } initData() { @@ -174,6 +179,16 @@ export class BasePieSeries extends PolarSeries stateSort: this._spec.pie?.stateSort } ) as IArcMark; + + this._emptyArcMark = this._createMark( + { + name: 'emptyCircle', + type: 'arc' + }, + { + dataView: false + } + ) as IArcMark; } private startAngleScale(datum: Datum) { @@ -185,25 +200,35 @@ export class BasePieSeries extends PolarSeries } initMarkStyle(): void { + const initialStyle: Partial> = { + x: () => this.getCenter().x, + y: () => this.getCenter().y, + fill: this.getColorAttribute(), + outerRadius: isSpecValueWithScale(this._outerRadius) + ? this._outerRadius + : () => this._computeLayoutRadius() * this._outerRadius, + innerRadius: isSpecValueWithScale(this._innerRadius) + ? this._innerRadius + : () => this._computeLayoutRadius() * this._innerRadius, + cornerRadius: () => this._computeLayoutRadius() * this._cornerRadius, + startAngle: datum => this.startAngleScale(datum), + endAngle: datum => this.endAngleScale(datum), + padAngle: this._padAngle, + centerOffset: this._centerOffset + }; + const pieMark = this._pieMark; if (pieMark) { + this.setMarkStyle(pieMark, initialStyle, 'normal', AttributeLevel.Series); + } + + const emptyPieMark = this._emptyArcMark; + if (emptyPieMark) { this.setMarkStyle( - pieMark, + emptyPieMark, { - x: () => this.getCenter().x, - y: () => this.getCenter().y, - fill: this.getColorAttribute(), - outerRadius: isSpecValueWithScale(this._outerRadius) - ? this._outerRadius - : () => this._computeLayoutRadius() * this._outerRadius, - innerRadius: isSpecValueWithScale(this._innerRadius) - ? this._innerRadius - : () => this._computeLayoutRadius() * this._innerRadius, - cornerRadius: () => this._computeLayoutRadius() * this._cornerRadius, - startAngle: datum => this.startAngleScale(datum), - endAngle: datum => this.endAngleScale(datum), - padAngle: this._padAngle, - centerOffset: this._centerOffset + ...initialStyle, + visible: () => this._showEmptyCircle && this.getViewData().latestData.length === 0 }, 'normal', AttributeLevel.Series @@ -232,6 +257,10 @@ export class BasePieSeries extends PolarSeries } } } + if (mark.name === 'emptyCircle') { + // 使用emptyCircle的radius比例值进行覆盖 + this.setMarkStyle(mark, this.generateRadiusStyle(spec.style), 'normal', AttributeLevel.User_Mark); + } } initLabelMarkStyle(textMark: ITextMark) { @@ -441,6 +470,13 @@ export class BasePieSeries extends PolarSeries this._pieMark.setAnimationConfig(pieAnimationConfig); } + + if (this._emptyArcMark) { + const pieAnimationConfig = animationConfig( + Factory.getAnimationInKey('emptyCircle')?.(animationParams, appearPreset ?? 'fadeIn') + ); + this._emptyArcMark.setAnimationConfig(pieAnimationConfig); + } } getDefaultShapeType() { @@ -480,5 +516,6 @@ export class PieSeries extends BasePi export const registerPieSeries = () => { registerArcMark(); registerPieAnimation(); + registerEmptyCircleAnimation(); Factory.registerSeries(PieSeries.type, PieSeries); }; diff --git a/packages/vchart/src/theme/builtin/common/series/pie.ts b/packages/vchart/src/theme/builtin/common/series/pie.ts index 4706d36b52..b9c98f10de 100644 --- a/packages/vchart/src/theme/builtin/common/series/pie.ts +++ b/packages/vchart/src/theme/builtin/common/series/pie.ts @@ -21,5 +21,11 @@ export const pie: IPieSeriesTheme = { style: { lineWidth: 2 } + }, + emptyCircle: { + style: { + fill: { type: 'palette', key: 'emptyCircleColor' }, + fillOpacity: 1 + } } }; diff --git a/packages/vchart/src/theme/builtin/dark/color-scheme.ts b/packages/vchart/src/theme/builtin/dark/color-scheme.ts index 2158347cd0..575bdd3f9a 100644 --- a/packages/vchart/src/theme/builtin/dark/color-scheme.ts +++ b/packages/vchart/src/theme/builtin/dark/color-scheme.ts @@ -73,7 +73,10 @@ export const colorScheme: IThemeColorScheme = { /** 图例翻页器按钮颜色 */ discreteLegendPagerHandlerColor: '#BBBDC3', /** 图例翻页器按钮颜色(disable 态) */ - discreteLegendPagerHandlerDisableColor: '#55595F' + discreteLegendPagerHandlerDisableColor: '#55595F', + + /** 占位圆颜色 */ + emptyCircleColor: '#bbbdc3' } as BuiltinColorPalette } }; diff --git a/packages/vchart/src/theme/builtin/light/color-scheme.ts b/packages/vchart/src/theme/builtin/light/color-scheme.ts index 402fcb3006..bdd75ae52a 100644 --- a/packages/vchart/src/theme/builtin/light/color-scheme.ts +++ b/packages/vchart/src/theme/builtin/light/color-scheme.ts @@ -73,7 +73,10 @@ export const colorScheme: IThemeColorScheme = { /** 图例翻页器按钮颜色 */ discreteLegendPagerHandlerColor: 'rgb(47, 69, 84)', /** 图例翻页器按钮颜色(disable 态) */ - discreteLegendPagerHandlerDisableColor: 'rgb(170, 170, 170)' + discreteLegendPagerHandlerDisableColor: 'rgb(170, 170, 170)', + + /** 占位圆颜色 */ + emptyCircleColor: '#e3e5e8' } as BuiltinColorPalette } };