diff --git a/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-14-57.json b/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-14-57.json new file mode 100644 index 0000000000..e5f762c181 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-14-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: optimize the type definition related to padding on the bandAxis, and it only takes effect on the first layer of scale", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} \ No newline at end of file diff --git a/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-15-00.json b/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-15-00.json new file mode 100644 index 0000000000..ad7e894c0c --- /dev/null +++ b/common/changes/@visactor/vchart/feat-issue-328_2023-08-02-15-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "feat: support `barGapInGroup` for bar series and rangeColumn series, used to set the spacing between bars within a group, relate #328", + "type": "minor" + } + ], + "packageName": "@visactor/vchart" +} \ No newline at end of file diff --git a/packages/vchart/src/chart/bar/bar-3d.ts b/packages/vchart/src/chart/bar/bar-3d.ts index 1e0ce9b4c7..c7f3b6a529 100644 --- a/packages/vchart/src/chart/bar/bar-3d.ts +++ b/packages/vchart/src/chart/bar/bar-3d.ts @@ -3,6 +3,7 @@ import { CartesianChart } from '../cartesian/cartesian'; import { ChartTypeEnum } from '../interface'; import { VChart } from '../../core/vchart'; import { Bar3dSeries } from '../../series'; +import type { IBar3dChartSpec } from './interface'; VChart.useSeries([Bar3dSeries]); export class Bar3dChart extends CartesianChart { @@ -10,4 +11,14 @@ export class Bar3dChart extends CartesianChart { static readonly view: string = 'singleDefault'; readonly type: string = ChartTypeEnum.bar3d; readonly seriesType: string = SeriesTypeEnum.bar3d; + + protected _getDefaultSeriesSpec(spec: any): any { + return { + ...super._getDefaultSeriesSpec(spec), + barWidth: (spec).barWidth, + barMaxWidth: (spec).barMaxWidth, + barMinWidth: (spec).barMinWidth, + barGapInGroup: (spec).barGapInGroup + }; + } } diff --git a/packages/vchart/src/chart/bar/bar.ts b/packages/vchart/src/chart/bar/bar.ts index 8bf9cc3c40..c527079f13 100644 --- a/packages/vchart/src/chart/bar/bar.ts +++ b/packages/vchart/src/chart/bar/bar.ts @@ -18,7 +18,8 @@ export class BarChart extends CartesianChart { ...super._getDefaultSeriesSpec(spec), barWidth: (spec).barWidth, barMaxWidth: (spec).barMaxWidth, - barMinWidth: (spec).barMinWidth + barMinWidth: (spec).barMinWidth, + barGapInGroup: (spec).barGapInGroup }; } diff --git a/packages/vchart/src/chart/range-column/range-column-3d.ts b/packages/vchart/src/chart/range-column/range-column-3d.ts index 570b5c7163..f733982e2c 100644 --- a/packages/vchart/src/chart/range-column/range-column-3d.ts +++ b/packages/vchart/src/chart/range-column/range-column-3d.ts @@ -4,6 +4,7 @@ import { SeriesTypeEnum } from '../../series/interface'; import { Direction } from '../../typings'; import { VChart } from '../../core/vchart'; import { RangeColumn3dSeries } from '../../series'; +import type { IRangeColumn3dChartSpec } from './interface'; VChart.useSeries([RangeColumn3dSeries]); export class RangeColumn3dChart extends CartesianChart { @@ -14,7 +15,8 @@ export class RangeColumn3dChart extends CartesianChart { protected _getDefaultSeriesSpec(spec: any): any { const series: any = { - ...super._getDefaultSeriesSpec(spec) + ...super._getDefaultSeriesSpec(spec), + barGapInGroup: (spec as IRangeColumn3dChartSpec).barGapInGroup }; series.bar3d = spec.bar3d; if (spec.direction === Direction.horizontal) { diff --git a/packages/vchart/src/chart/range-column/range-column.ts b/packages/vchart/src/chart/range-column/range-column.ts index 48622bbf59..59254f790c 100644 --- a/packages/vchart/src/chart/range-column/range-column.ts +++ b/packages/vchart/src/chart/range-column/range-column.ts @@ -5,6 +5,7 @@ import { Direction } from '../../typings'; import { setDefaultCrosshairForCartesianChart } from '../util'; import { VChart } from '../../core/vchart'; import { RangeColumnSeries } from '../../series'; +import type { IRangeColumnChartSpec } from './interface'; VChart.useSeries([RangeColumnSeries]); export class RangeColumnChart extends CartesianChart { @@ -15,7 +16,8 @@ export class RangeColumnChart extends CartesianChart { protected _getDefaultSeriesSpec(spec: any): any { const series: any = { - ...super._getDefaultSeriesSpec(spec) + ...super._getDefaultSeriesSpec(spec), + barGapInGroup: (spec as IRangeColumnChartSpec).barGapInGroup }; series.bar = spec.bar; if (spec.direction === Direction.horizontal) { diff --git a/packages/vchart/src/component/axis/interface.ts b/packages/vchart/src/component/axis/interface.ts index e2daf23567..113f8d29d3 100644 --- a/packages/vchart/src/component/axis/interface.ts +++ b/packages/vchart/src/component/axis/interface.ts @@ -156,11 +156,21 @@ export interface ILinearAxisSpec { export interface IBandAxisSpec { /** - * 轴分组之间间隔,数值在(0,1)之间 - * @default 0.2 + * 同时设置轴的 paddingInner 和 paddingOuter + * **因为有可能存在多层 scale( xField 设置成了数组,即分组场景),所以支持了数组类型,用于多层 scale 的 bandPadding 配置** */ bandPadding?: number | number[]; + /** + * band 轴的内边距 + * ** 因为有可能存在多层 scale( xField 设置成了数组,即分组场景),所以支持了数组类型,用于多层 scale 的 paddingInner 配置** + * @default 0.1 + */ paddingInner?: number | number[]; + /** + * band 轴的外边距 + * ** 因为有可能存在多层 scale( xField 设置成了数组,即分组场景),所以支持了数组类型,用于多层 scale 的 paddingOuter 配置** + * @default 0.3 + */ paddingOuter?: number | number[]; /** * 配置离散轴的数值范围 diff --git a/packages/vchart/src/component/axis/mixin/band-axis-mixin.ts b/packages/vchart/src/component/axis/mixin/band-axis-mixin.ts index eb2abd55dc..ad6a08500f 100644 --- a/packages/vchart/src/component/axis/mixin/band-axis-mixin.ts +++ b/packages/vchart/src/component/axis/mixin/band-axis-mixin.ts @@ -53,6 +53,7 @@ export class BandAxisMixin { const isBandPaddingArray = isArray(bandPadding); const isPaddingInnerArray = isArray(paddingInner); const isPaddingOuterArray = isArray(paddingOuter); + for (let i = 0; i < this._scales.length; i++) { const _padding = isBandPaddingArray ? bandPadding[i] : bandPadding; const _paddingInner = isPaddingInnerArray ? paddingInner[i] : paddingInner; @@ -64,14 +65,6 @@ export class BandAxisMixin { } } computeBandDomain(data: { min: number; max: number; values: any[] }[]): StringOrNumber[] { - // const values = data.map(d => d.values); - // return Array.from(new Set(values.flat())); - - // // 性能优化 old - // const reuslt = {}; - // data.forEach(d => d.values.forEach(v => (reuslt[v] = true))); - // return Object.keys(reuslt); - // 性能优化 9.13 const tempSet = new Set(); for (let i = 0; i < data.length; i++) { diff --git a/packages/vchart/src/series/bar/bar.ts b/packages/vchart/src/series/bar/bar.ts index 47c9896dd9..a88ac232fc 100644 --- a/packages/vchart/src/series/bar/bar.ts +++ b/packages/vchart/src/series/bar/bar.ts @@ -5,7 +5,7 @@ import { CartesianSeries } from '../cartesian/cartesian'; import { MarkTypeEnum } from '../../mark/interface'; import { AttributeLevel } from '../../constant'; import { getActualNumValue } from '../util/utils'; -import type { Maybe, Datum } from '../../typings'; +import type { Maybe, Datum, DirectionType } from '../../typings'; import { merge, valueInScaleRange } from '../../util'; import type { BarAppearPreset, IBarAnimationParams } from './animation'; import { animationConfig, shouldDoMorph, userAnimationConfig } from '../../animation/utils'; @@ -23,6 +23,7 @@ import { BaseSeries } from '../base/base-series'; import { VChart } from '../../core/vchart'; import { RectMark } from '../../mark/rect'; import { TextMark } from '../../mark/text'; +import { array, isValid, last } from '@visactor/vutils'; VChart.useMark([RectMark, TextMark]); @@ -121,16 +122,8 @@ export class BarSeries extends Cartes { x: (datum: Datum) => valueInScaleRange(this.dataToPositionX(datum), xScale), x1: (datum: Datum) => valueInScaleRange(this.dataToPositionX1(datum), xScale), - y: (datum: Datum) => { - const bandWidth = - this.getYAxisHelper().getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? - DefaultBandWidth; - const continuous = isContinuous(yScale.type || 'band'); - const pos = this.dataToPositionY(datum); - const width = this._rectMark.getAttribute('height', datum) as number; - return pos + (bandWidth - width) * 0.5 + (continuous ? -bandWidth / 2 : 0); - }, - height: () => this.getBarWidth(this._yAxisHelper) + y: (datum: Datum) => this._getPosition(this.direction, datum), + height: () => this._getBarWidth(this._yAxisHelper) }, 'normal', AttributeLevel.Series @@ -139,19 +132,11 @@ export class BarSeries extends Cartes this.setMarkStyle( this._rectMark, { - x: (datum: Datum) => { - const bandWidth = - this.getXAxisHelper().getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? - DefaultBandWidth; - const width = this._rectMark.getAttribute('width', datum) as number; - const continuous = isContinuous(this.getXAxisHelper().getScale?.(0).type || 'band'); - const pos = this.dataToPositionX(datum); - return pos + (bandWidth - width) / 2 + (continuous ? -bandWidth / 2 : 0); - }, + x: (datum: Datum) => this._getPosition(this.direction, datum), y: (datum: Datum) => valueInScaleRange(this.dataToPositionY(datum), yScale), y1: (datum: Datum) => valueInScaleRange(this.dataToPositionY1(datum), yScale), width: () => { - return this.getBarWidth(this._xAxisHelper); + return this._getBarWidth(this._xAxisHelper); } }, 'normal', @@ -206,9 +191,10 @@ export class BarSeries extends Cartes ); } - protected getBarWidth(axisHelper: IAxisHelper) { + protected _getBarWidth(axisHelper: IAxisHelper) { const hasBarWidth = this._spec.barWidth !== undefined; const bandWidth = axisHelper.getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? DefaultBandWidth; + if (hasBarWidth) { return getActualNumValue(this._spec.barWidth, bandWidth); } @@ -224,6 +210,53 @@ export class BarSeries extends Cartes return width; } + protected _getPosition(direction: DirectionType, datum: Datum) { + let axisHelper; + let sizeAttribute; + let dataToPosition; + if (direction === Direction.horizontal) { + axisHelper = this.getYAxisHelper(); + sizeAttribute = 'height'; + dataToPosition = this.dataToPositionY.bind(this); + } else { + axisHelper = this.getXAxisHelper(); + sizeAttribute = 'width'; + dataToPosition = this.dataToPositionX.bind(this); + } + const scale = axisHelper.getScale(0); + const size = this._rectMark.getAttribute(sizeAttribute, datum) as number; + const bandWidth = axisHelper.getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? DefaultBandWidth; + if (this._groups?.fields?.length > 1 && isValid(this._spec.barGapInGroup)) { + // 自里向外计算,沿着第一层分组的中心点进行位置调整 + const groupFields = this._groups.fields; + const barInGroup = array(this._spec.barGapInGroup); + let totalWidth: number = 0; + let offSet: number = 0; + + for (let index = groupFields.length - 1; index >= 1; index--) { + const groupField = groupFields[index]; + const groupValues = this.getViewDataStatistics()?.latestData?.[groupField]?.values ?? []; + const groupCount = groupValues.length; + const gap = getActualNumValue(barInGroup[index - 1] ?? last(barInGroup), bandWidth); + const i = groupValues.indexOf(datum[groupField]); + if (index === groupFields.length - 1) { + totalWidth += groupCount * size + (groupCount - 1) * gap; + offSet += i * (size + gap); + } else { + offSet += i * (totalWidth + gap); + totalWidth += totalWidth + (groupCount - 1) * gap; + } + } + + const center = scale.scale(datum[groupFields[0]]) + axisHelper.getBandwidth(0) / 2; + return center - totalWidth / 2 + offSet; + } + + const continuous = isContinuous(scale.type || 'band'); + const pos = dataToPosition(datum); + return pos + (bandWidth - size) * 0.5 + (continuous ? -bandWidth / 2 : 0); + } + /** * spec 更新 * @param spec diff --git a/packages/vchart/src/series/bar/interface.ts b/packages/vchart/src/series/bar/interface.ts index ece65fadba..709e0034de 100644 --- a/packages/vchart/src/series/bar/interface.ts +++ b/packages/vchart/src/series/bar/interface.ts @@ -6,6 +6,7 @@ import type { BarAppearPreset } from './animation'; import type { ILabelSpec } from '../../component/label'; import type { IMarkProgressiveConfig } from '../../mark/interface'; import type { SeriesMarkNameEnum } from '../interface'; +import type { MaybeArray } from '../../typings'; type BarMarks = 'bar'; @@ -37,17 +38,32 @@ export interface IBarSeriesSpec | 'inside-left'; }; /** - * 柱体宽度 + * 柱体宽度,可以设置绝对的像素值,也可以使用百分比(如 '10%') + * 1. number 类型,表示像素值 + * 2. string 类型,百分比用法,如 '10%',该值为对应最后一个分组字段对应的 scale 的 bandWidth 占比(因为柱子是等宽的,所以采用最后一层分组的 scale) */ - barWidth?: number; + barWidth?: number | string; /** - * 柱体最小宽度 + * 柱体最小宽度,可以设置绝对的像素值,也可以使用百分比(如 '10%') + * 1. number 类型,表示像素值 + * 2. string 类型,百分比用法,如 '10%',该值为对应最后一个分组字段对应的 scale 的 bandWidth 占比(因为柱子是等宽的,所以采用最后一层分组的 scale) */ - barMinWidth?: number; + barMinWidth?: number | string; /** - * 柱体最大宽度 + * 柱体最大宽度,可以设置绝对的像素值,也可以使用百分比(如 '10%') + * 1. number 类型,表示像素值 + * 2. string 类型,百分比用法,如 '10%',该值为对应最后一个分组字段对应的 scale 的 bandWidth 占比(因为柱子是等宽的,所以采用最后一层分组的 scale) */ - barMaxWidth?: number; + barMaxWidth?: number | string; + /** + * 分组柱图中各个分组内的柱子间距,可以设置绝对的像素值,也可以使用百分比(如 '10%')。 + * 当存在多层分组时,可以使用数组来设置不同层级的间距,如 [10, '20%'],表示第一层分组的间距为 10px,第二层分组的间距为 '20%'。 + * 如果 barGapInGroup 的数组个数小于分组层数,则后面的分组间距使用最后一个值。 + * 1. number 类型,表示像素值 + * 2. string 类型,百分比用法,如 '10%',该值为对应最后一个分组字段对应的 scale 的 bandWidth 占比(因为柱子是等宽的,所以采用最后一层分组的 scale) + * @since 1.2.0 + */ + barGapInGroup?: MaybeArray; } export interface IBarSeriesTheme extends ICartesianSeriesTheme { diff --git a/packages/vchart/src/series/range-column/range-column.ts b/packages/vchart/src/series/range-column/range-column.ts index 8a5f3373cd..1ba7ffee87 100644 --- a/packages/vchart/src/series/range-column/range-column.ts +++ b/packages/vchart/src/series/range-column/range-column.ts @@ -9,7 +9,6 @@ import type { ITextMark } from '../../mark/text'; import { merge, valueInScaleRange } from '../../util'; import { setRectLabelPos } from '../util/label-mark'; import { AttributeLevel } from '../../constant'; -import { isContinuous } from '@visactor/vscale'; import { animationConfig, shouldDoMorph, userAnimationConfig } from '../../animation/utils'; import { RangeColumnSeriesTooltipHelper } from './tooltip-helper'; import { DEFAULT_MARK_ANIMATION } from '../../animation/config'; @@ -67,20 +66,7 @@ export class RangeColumnSeries extends BarSeries { } initMarkStyle(): void { - const rectMark = this._rectMark; - if (rectMark) { - this.setMarkStyle( - rectMark, - { - fill: this.getColorAttribute() - }, - 'normal', - AttributeLevel.Series - ); - - this._trigger.registerMark(rectMark); - this._tooltipHelper?.activeTriggerSet.mark.add(rectMark); - } + super.initMarkStyle(); const minLabelMark = this._minLabelMark; const minLabelSpec = this._spec.label?.minLabel; @@ -207,16 +193,8 @@ export class RangeColumnSeries extends BarSeries { }), xScale ), - y: (datum: Datum) => { - const bandWidth = - this.getYAxisHelper().getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? - DefaultBandWidth; - const continuous = isContinuous(yScale.type || 'band'); - const pos = this.dataToPositionY(datum); - const width = this._rectMark.getAttribute('height', datum) as number; - return pos + (bandWidth - width) * 0.5 + (continuous ? -bandWidth / 2 : 0); - }, - height: () => this.getBarWidth(this._yAxisHelper) + y: (datum: Datum) => this._getPosition(this.direction, datum), + height: () => this._getBarWidth(this._yAxisHelper) }, 'normal', AttributeLevel.Series @@ -225,15 +203,7 @@ export class RangeColumnSeries extends BarSeries { this.setMarkStyle( this._rectMark, { - x: (datum: Datum) => { - const bandWidth = - this.getXAxisHelper().getBandwidth?.(this._groups ? this._groups.fields.length - 1 : 0) ?? - DefaultBandWidth; - const width = this._rectMark.getAttribute('width', datum) as number; - const continuous = isContinuous(this.getXAxisHelper().getScale?.(0).type || 'band'); - const pos = this.dataToPositionX(datum); - return pos + (bandWidth - width) / 2 + (continuous ? -bandWidth / 2 : 0); - }, + x: (datum: Datum) => this._getPosition(this.direction, datum), y: (datum: Datum) => valueInScaleRange( dataToPosition(this.getDatumPositionValues(datum, this._spec.yField[0]), { @@ -249,7 +219,7 @@ export class RangeColumnSeries extends BarSeries { yScale ), width: () => { - return this.getBarWidth(this._xAxisHelper); + return this._getBarWidth(this._xAxisHelper); } }, 'normal',