From b7adc1e6e87f3d32925ae04677bae59a75a5c3fc Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 8 Jul 2026 18:35:26 +0800 Subject: [PATCH 1/2] feat: support linear x bar auto width --- ...ear-x-auto-bar-width_2026-07-08-10-34.json | 11 ++ .../vchart/__tests__/unit/chart/bar.test.ts | 116 +++++++++++------- packages/vchart/src/series/bar/bar.ts | 112 ++++++++++++++++- 3 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 common/changes/@visactor/vchart/feat-4615-linear-x-auto-bar-width_2026-07-08-10-34.json diff --git a/common/changes/@visactor/vchart/feat-4615-linear-x-auto-bar-width_2026-07-08-10-34.json b/common/changes/@visactor/vchart/feat-4615-linear-x-auto-bar-width_2026-07-08-10-34.json new file mode 100644 index 0000000000..82141e54b6 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-4615-linear-x-auto-bar-width_2026-07-08-10-34.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: support automatic bar width for linear x axis", + "type": "minor", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "lixuef1313@163.com" +} diff --git a/packages/vchart/__tests__/unit/chart/bar.test.ts b/packages/vchart/__tests__/unit/chart/bar.test.ts index 11a6533772..da7f180a22 100644 --- a/packages/vchart/__tests__/unit/chart/bar.test.ts +++ b/packages/vchart/__tests__/unit/chart/bar.test.ts @@ -85,41 +85,82 @@ describe('Bar chart test', () => { }); afterEach(() => { + chart?.release?.(); + chart = null; removeDom(canvasDom); }); - test('Bar chart init', () => { + const createBarChart = (chartSpec: Record) => { const transformer = new BarChart.transformerConstructor({ type: 'bar', seriesType: 'bar', getTheme: getTheme, mode: 'desktop-browser' }); - const info = transformer.initChartSpec(spec as any); - chart = new BarChart( - spec as any, + const info = transformer.initChartSpec(chartSpec as never); + const barChart = new BarChart( + chartSpec as never, { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any), + eventDispatcher: new EventDispatcher({} as never, { addEventListener: () => {} } as never), globalInstance: { isAnimationEnable: () => true, getContainer: () => ({}), getTooltipHandlerByUser: (() => undefined) as () => undefined }, - render: {} as any, + render: {} as never, dataSet, map: new Map(), container: null, mode: 'desktop-browser', getCompiler: getTestCompiler, - globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any), + globalScale: new GlobalScale([], { getAllSeries: (): never[] => [] } as never), getTheme: getTheme, getSpecInfo: () => info - } as any + } as never ); - chart.created(transformer); - chart.init(); + barChart.created(transformer); + barChart.init(); + return barChart; + }; + + const getLinearBarWidth = (extraSpec: Record = {}) => { + const linearSpec = { + type: 'bar', + data: { + values: [ + { x: 0, y: 10 }, + { x: 10, y: 20 }, + { x: 20, y: 12 } + ] + }, + xField: 'x', + yField: 'y', + axes: [ + { orient: 'bottom', type: 'linear' }, + { orient: 'left', type: 'linear' } + ], + ...extraSpec + }; + chart = createBarChart(linearSpec); + const series = chart.getAllSeries()[0] as BarSeries; + const xScale = series.getXAxisHelper().getScale(0) as { + domain: (domain: number[]) => void; + range: (range: number[]) => void; + }; + xScale.domain([0, 20]); + xScale.range([0, 200]); + + return ( + series.getMarkInName('bar') as { + getAttribute: (key: string, datum: unknown) => unknown; + } + ).getAttribute('width', series.getViewData().latestData[0]) as number; + }; + + test('Bar chart init', () => { + chart = createBarChart(spec); // spec const transformSpec = chart.getSpec(); @@ -138,7 +179,8 @@ describe('Bar chart test', () => { }); test('Bar chart updateSpec', () => { - chart.updateSpec(spec as any); + chart = createBarChart(spec); + chart.updateSpec(spec as never); expect(chart.getAllSeries().length).toEqual(1); const series: BarSeries = chart.getAllSeries()[0] as BarSeries; @@ -171,48 +213,34 @@ describe('Bar chart test', () => { yField: 'value', seriesField: 'type' }; - const transformer = new BarChart.transformerConstructor({ - type: 'bar', - seriesType: 'bar', - getTheme: getTheme, - mode: 'desktop-browser' - }); - const info = transformer.initChartSpec(stackSpec as any); - chart = new BarChart( - stackSpec as any, - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any), - globalInstance: { - isAnimationEnable: () => true, - getContainer: () => ({}), - getTooltipHandlerByUser: (() => undefined) as () => undefined - }, - render: {} as any, - dataSet, - map: new Map(), - container: null, - mode: 'desktop-browser', - getCompiler: getTestCompiler, - globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any), - getTheme: getTheme, - getSpecInfo: () => info - } as any - ); - chart.created(transformer); - chart.init(); + chart = createBarChart(stackSpec); const series: BarSeries = chart.getAllSeries()[0] as BarSeries; - const barMark = series.getMarkInName('bar') as any; + const barMark = series.getMarkInName('bar') as unknown as { + _markConfig: { + clipPath: () => Array<{ attribute: { x: number; y: number; y1: number; width: number } }>; + }; + }; const clipPaths = barMark._markConfig.clipPath(); expect(clipPaths.length).toBeGreaterThan(0); - clipPaths.forEach((path: any) => { + clipPaths.forEach(path => { expect(Number.isFinite(path.attribute.x)).toBe(true); expect(Number.isFinite(path.attribute.y)).toBe(true); expect(Number.isFinite(path.attribute.y1)).toBe(true); expect(Number.isFinite(path.attribute.width)).toBe(true); }); }); + + test('linear x axis bar uses adjacent data spacing as automatic width base', () => { + expect(getLinearBarWidth()).toBe(50); + }); + + test('linear x axis bar respects numeric barWidth', () => { + expect(getLinearBarWidth({ barWidth: 18 })).toBe(18); + }); + + test('linear x axis bar resolves percent barWidth from automatic width base', () => { + expect(getLinearBarWidth({ barWidth: '25%' })).toBe(25); + }); }); diff --git a/packages/vchart/src/series/bar/bar.ts b/packages/vchart/src/series/bar/bar.ts index 8df32f83c0..75130c5eb6 100644 --- a/packages/vchart/src/series/bar/bar.ts +++ b/packages/vchart/src/series/bar/bar.ts @@ -66,6 +66,18 @@ const BAR_SERIES_COMPILE_ONLY_KEYS: Record = { stackCornerRadius: true }; +type LinearBarBandWidthCache = { + scale: IBaseScale; + field: string; + data: Datum[]; + dataLength: number; + domainStart: number; + domainEnd: number; + rangeStart: number; + rangeEnd: number; + bandWidth: number; +}; + export class BarSeries extends CartesianSeries { static readonly type: string = SeriesTypeEnum.bar; type: string = SeriesTypeEnum.bar; @@ -80,6 +92,7 @@ export class BarSeries extends Cartes protected _bandPosition = 0; protected _barMark!: IRectMark; protected _barBackgroundMark!: IRectMark; + protected _linearBarBandWidthCache?: LinearBarBandWidthCache; protected _barBackgroundViewData: ICompilableData; @@ -794,12 +807,20 @@ export class BarSeries extends Cartes const depthFromSpec = this._groups ? this._groups.fields.length : 1; const depth = isNil(scaleDepth) ? depthFromSpec : Math.min(depthFromSpec, scaleDepth); - const bandWidth = axisHelper.getBandwidth?.(depth - 1) ?? DefaultBandWidth; const hasBarWidth = isValid(this._spec.barWidth) && depth === depthFromSpec; + const useFixedWidth = + hasBarWidth && + typeof this._spec.barWidth === 'number' && + typeof this._spec.barMinWidth !== 'string' && + typeof this._spec.barMaxWidth !== 'string'; + const axisBandWidth = axisHelper.getBandwidth?.(depth - 1); + const linearBandWidth = + isNil(axisBandWidth) && !useFixedWidth ? this._getLinearBarBandWidth(axisHelper) : undefined; + const bandWidth = axisBandWidth ?? linearBandWidth ?? DefaultBandWidth; const hasBarMinWidth = isValid(this._spec.barMinWidth); const hasBarMaxWidth = isValid(this._spec.barMaxWidth); - let width = bandWidth; + let width = isNil(linearBandWidth) || hasBarWidth ? bandWidth : bandWidth * 0.5; if (hasBarWidth) { width = getActualNumValue(this._spec.barWidth, bandWidth); } @@ -812,6 +833,88 @@ export class BarSeries extends Cartes return width; } + protected _getLinearBarBandWidth(axisHelper: IAxisHelper) { + const scale = axisHelper.getScale?.(0); + const field = this.direction === Direction.horizontal ? this._fieldY[0] : this._fieldX[0]; + const data = this.getViewData()?.latestData as Datum[]; + const domain = scale?.domain?.() as number[]; + const range = scale?.range?.() as number[]; + const domainStart = +domain?.[0]; + const domainEnd = +domain?.[domain.length - 1]; + const rangeStart = +range?.[0]; + const rangeEnd = +range?.[range.length - 1]; + + if ( + !axisHelper.isContinuous || + !field || + !data?.length || + !Number.isFinite(domainStart) || + !Number.isFinite(domainEnd) || + !Number.isFinite(rangeStart) || + !Number.isFinite(rangeEnd) || + domainStart === domainEnd + ) { + return undefined; + } + + const cache = this._linearBarBandWidthCache; + if ( + cache && + cache.scale === scale && + cache.field === field && + cache.data === data && + cache.dataLength === data.length && + cache.domainStart === domainStart && + cache.domainEnd === domainEnd && + cache.rangeStart === rangeStart && + cache.rangeEnd === rangeEnd + ) { + return cache.bandWidth; + } + + const valueSet = new Set(); + data.forEach(datum => { + const value = +datum[field]; + if (Number.isFinite(value)) { + valueSet.add(value); + } + }); + const values = Array.from(valueSet).sort((a, b) => a - b); + if (values.length < 2) { + return undefined; + } + + let minStep = Infinity; + let lastValue = values[0]; + for (let i = 1; i < values.length; i++) { + const value = values[i]; + const step = value - lastValue; + if (step > 0 && step < minStep) { + minStep = step; + } + lastValue = value; + } + + const domainSpan = domainEnd - domainStart; + const rangeSpan = rangeEnd - rangeStart; + const bandWidth = minStep < Infinity ? Math.abs((rangeSpan / domainSpan) * minStep) : undefined; + if (!Number.isFinite(bandWidth)) { + return undefined; + } + this._linearBarBandWidthCache = { + scale, + field, + data, + dataLength: data.length, + domainStart, + domainEnd, + rangeStart, + rangeEnd, + bandWidth + }; + return bandWidth; + } + protected _getPosition(direction: DirectionType, datum: Datum, scaleDepth?: number, mark?: SeriesMarkNameEnum) { let axisHelper; let sizeAttribute; @@ -836,7 +939,8 @@ export class BarSeries extends Cartes const depthFromSpec = this._groups ? this._groups.fields.length : 1; const depth = isNil(scaleDepth) ? depthFromSpec : Math.min(depthFromSpec, scaleDepth); - const bandWidth = axisHelper.getBandwidth?.(depth - 1) ?? DefaultBandWidth; + const bandWidth = + axisHelper.getBandwidth?.(depth - 1) ?? this._getLinearBarBandWidth(axisHelper) ?? DefaultBandWidth; const size = depth === depthFromSpec ? (this._barMark.getAttribute(sizeAttribute, datum) as number) : bandWidth; if (depth > 1 && isValid(this._spec.barGapInGroup)) { @@ -918,6 +1022,7 @@ export class BarSeries extends Cartes onDataUpdate(): void { super.onDataUpdate(); + this._linearBarBandWidthCache = undefined; const region = this.getRegion(); // @ts-ignore @@ -965,6 +1070,7 @@ export class BarSeries extends Cartes viewDataUpdate(d: DataView): void { super.viewDataUpdate(d); + this._linearBarBandWidthCache = undefined; this._barBackgroundViewData?.getDataView()?.reRunAllTransform(); this._barBackgroundViewData?.updateData(); } From 6b715f052f6adff9f8e04e0c83f959f54780423d Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Thu, 9 Jul 2026 17:45:38 +0800 Subject: [PATCH 2/2] fix: share linear bar bandwidth by axis --- .../vchart/__tests__/unit/chart/bar.test.ts | 94 ++++++++++++- .../src/component/axis/cartesian/axis.ts | 128 +++++++++++++++++ .../axis/cartesian/interface/common.ts | 2 +- packages/vchart/src/series/bar/bar.ts | 131 ++++-------------- 4 files changed, 247 insertions(+), 108 deletions(-) diff --git a/packages/vchart/__tests__/unit/chart/bar.test.ts b/packages/vchart/__tests__/unit/chart/bar.test.ts index da7f180a22..697dbb5400 100644 --- a/packages/vchart/__tests__/unit/chart/bar.test.ts +++ b/packages/vchart/__tests__/unit/chart/bar.test.ts @@ -4,7 +4,7 @@ import { GlobalScale } from '../../../src/scale/global-scale'; import { EventDispatcher } from '../../../src/event/event-dispatcher'; import type { BarSeries } from '../../../src'; // eslint-disable-next-line no-duplicate-imports -import { BarChart } from '../../../src'; +import { BarChart, CommonChart } from '../../../src'; import { DataSet } from '@visactor/vdataset'; import { createCanvas, removeDom } from '../../util/dom'; import { getTheme, initChartDataSet } from '../../util/context'; @@ -74,7 +74,7 @@ const spec = { describe('Bar chart test', () => { let canvasDom: HTMLCanvasElement; - let chart: BarChart; + let chart: BarChart | CommonChart; beforeEach(() => { canvasDom = createCanvas(); canvasDom.style.position = 'relative'; @@ -125,6 +125,40 @@ describe('Bar chart test', () => { return barChart; }; + const createCommonChart = (chartSpec: Record) => { + const transformer = new CommonChart.transformerConstructor({ + type: 'common', + getTheme: getTheme, + mode: 'desktop-browser' + }); + const info = transformer.initChartSpec(chartSpec as never); + const commonChart = new CommonChart( + chartSpec as never, + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + eventDispatcher: new EventDispatcher({} as never, { addEventListener: () => {} } as never), + globalInstance: { + isAnimationEnable: () => true, + getContainer: () => ({}), + getTooltipHandlerByUser: (() => undefined) as () => undefined + }, + render: {} as never, + dataSet, + map: new Map(), + container: null, + mode: 'desktop-browser', + getCompiler: getTestCompiler, + globalScale: new GlobalScale([], { getAllSeries: (): never[] => [] } as never), + getTheme: getTheme, + getSpecInfo: () => info + } as never + ); + commonChart.created(transformer); + commonChart.init(); + return commonChart; + }; + const getLinearBarWidth = (extraSpec: Record = {}) => { const linearSpec = { type: 'bar', @@ -243,4 +277,60 @@ describe('Bar chart test', () => { test('linear x axis bar resolves percent barWidth from automatic width base', () => { expect(getLinearBarWidth({ barWidth: '25%' })).toBe(25); }); + + test('linear x axis common chart bar series share automatic width base', () => { + chart = createCommonChart({ + type: 'common', + data: [ + { + id: 'barA', + values: [ + { x: 0, y: 10 }, + { x: 10, y: 20 } + ] + }, + { + id: 'barB', + values: [ + { x: 11, y: 12 }, + { x: 20, y: 18 } + ] + }, + { + id: 'line', + values: [ + { x: 10.2, y: 30 }, + { x: 10.3, y: 36 } + ] + } + ], + series: [ + { type: 'bar', dataId: 'barA', xField: 'x', yField: 'y' }, + { type: 'bar', dataId: 'barB', xField: 'x', yField: 'y' }, + { type: 'line', dataId: 'line', xField: 'x', yField: 'y' } + ], + axes: [ + { orient: 'bottom', type: 'linear' }, + { orient: 'left', type: 'linear' } + ] + }); + + const barSeriesList = chart.getAllSeries().filter(series => series.type === 'bar') as BarSeries[]; + const xScale = barSeriesList[0].getXAxisHelper().getScale(0) as { + domain: (domain: number[]) => void; + range: (range: number[]) => void; + }; + xScale.domain([0, 20]); + xScale.range([0, 200]); + + const getWidth = (series: BarSeries) => + ( + series.getMarkInName('bar') as { + getAttribute: (key: string, datum: unknown) => unknown; + } + ).getAttribute('width', series.getViewData().latestData[0]) as number; + + expect(getWidth(barSeriesList[0])).toBe(5); + expect(getWidth(barSeriesList[1])).toBe(5); + }); }); diff --git a/packages/vchart/src/component/axis/cartesian/axis.ts b/packages/vchart/src/component/axis/cartesian/axis.ts index 9d866d10d5..f67b4b92f1 100644 --- a/packages/vchart/src/component/axis/cartesian/axis.ts +++ b/packages/vchart/src/component/axis/cartesian/axis.ts @@ -35,6 +35,7 @@ import type { ILayoutRect, ILayoutType } from '../../../typings/layout'; import type { IComponentOption } from '../../interface'; // eslint-disable-next-line no-duplicate-imports import { ComponentTypeEnum } from '../../interface/type'; +import { SeriesTypeEnum } from '../../../series/interface/type'; import type { AxisItem, LineAxisAttributes } from '@visactor/vrender-components'; // eslint-disable-next-line no-duplicate-imports import { getAxisItem, isValidCartesianAxis, shouldUpdateAxis } from '../util'; @@ -53,6 +54,15 @@ import { getCombinedSizeOfRegions } from '../../../util/region'; const CartesianAxisPlugin = [AxisSyncPlugin]; +type ContinuousBarBandwidthCache = { + scale: IBaseScale; + domainStart: number; + domainEnd: number; + rangeStart: number; + rangeEnd: number; + bandWidth: number; +}; + export abstract class CartesianAxis extends AxisComponent implements IAxis @@ -75,6 +85,7 @@ export abstract class CartesianAxis { return this._scales[depth]; }; + const getBandwidth = (depth: number = 0) => { + return this._getContinuousBarBandwidth(depth); + }; return { isContinuous: isContinuous(this._scale.type), dataToPosition: this.dataToPosition.bind(this), getScale, + getBandwidth, getAxisType: () => this.type, getAxisId: () => this.id, isInverse: () => this._inverse === true, @@ -387,6 +402,109 @@ export abstract class CartesianAxis { + this._continuousBarBandwidthCache = undefined; + }; + + protected _getContinuousBarBandwidth(depth: number = 0) { + if (depth !== 0 || !isContinuous(this._scale.type)) { + return undefined; + } + + const scale = this._scales[0]; + const domain = scale.domain() as number[]; + const range = scale.range() as number[]; + const domainStart = +domain?.[0]; + const domainEnd = +domain?.[domain.length - 1]; + const rangeStart = +range?.[0]; + const rangeEnd = +range?.[range.length - 1]; + + if ( + !Number.isFinite(domainStart) || + !Number.isFinite(domainEnd) || + !Number.isFinite(rangeStart) || + !Number.isFinite(rangeEnd) || + domainStart === domainEnd + ) { + return undefined; + } + + const cache = this._continuousBarBandwidthCache; + if ( + cache && + cache.scale === scale && + cache.domainStart === domainStart && + cache.domainEnd === domainEnd && + cache.rangeStart === rangeStart && + cache.rangeEnd === rangeEnd + ) { + return cache.bandWidth; + } + + const valueSet = new Set(); + const orient = this.getOrient(); + eachSeries( + this._regions, + s => { + if (s.type !== SeriesTypeEnum.bar) { + return; + } + const barSeries = s as ICartesianSeries; + const field = + isXAxis(orient) && barSeries.direction === Direction.vertical + ? barSeries.fieldX[0] + : isYAxis(orient) && barSeries.direction === Direction.horizontal + ? barSeries.fieldY[0] + : undefined; + const values = (field ? s.getViewDataStatistics?.()?.latestData?.[field]?.values : undefined) as + | unknown[] + | undefined; + values?.forEach(value => { + const numericValue = +value; + if (Number.isFinite(numericValue)) { + valueSet.add(numericValue); + } + }); + }, + { + userId: this._seriesUserId, + specIndex: this._seriesIndex + } + ); + + const values = Array.from(valueSet).sort((a, b) => a - b); + if (values.length < 2) { + return undefined; + } + + let minStep = Infinity; + let lastValue = values[0]; + for (let i = 1; i < values.length; i++) { + const value = values[i]; + const step = value - lastValue; + if (step > 0 && step < minStep) { + minStep = step; + } + lastValue = value; + } + + const bandWidth = + minStep < Infinity ? Math.abs(((rangeEnd - rangeStart) / (domainEnd - domainStart)) * minStep) : undefined; + if (!Number.isFinite(bandWidth)) { + return undefined; + } + + this._continuousBarBandwidthCache = { + scale, + domainStart, + domainEnd, + rangeStart, + rangeEnd, + bandWidth + }; + return bandWidth; + } + /** LifeCycle API**/ afterCompile() { const product = this._axisMark?.getProduct(); @@ -789,6 +907,16 @@ export abstract class CartesianAxis { + s.getViewDataStatistics?.()?.target.addListener('change', this._clearContinuousBarBandwidthCache); + }, + { + userId: this._seriesUserId, + specIndex: this._seriesIndex + } + ); if (this._specVisible) { // 过程: dolayout -> getBoundsInRect: update tick attr -> forceLayout -> updateLayoutAttr: update tick attr -> chart layout -> scale update -> mark encode diff --git a/packages/vchart/src/component/axis/cartesian/interface/common.ts b/packages/vchart/src/component/axis/cartesian/interface/common.ts index 02dcc4e779..dfb785b360 100644 --- a/packages/vchart/src/component/axis/cartesian/interface/common.ts +++ b/packages/vchart/src/component/axis/cartesian/interface/common.ts @@ -93,7 +93,7 @@ export interface IAxisHelper { dataToPosition: (values: any[], cfg?: IAxisLocationCfg) => number; valueToPosition?: (value: any, cfg?: IAxisLocationCfg) => number; getScale?: (depth: number) => IBaseScale; - getBandwidth?: (depth: number) => number; // band轴特有 + getBandwidth?: (depth: number) => number | undefined; // band轴特有;连续轴可按关联柱系列返回虚拟 bandwidth // 用户其他模块扩充轴scale的区间 setExtendDomain?: (key: string, value: number | undefined) => void; diff --git a/packages/vchart/src/series/bar/bar.ts b/packages/vchart/src/series/bar/bar.ts index 75130c5eb6..5ef1938b62 100644 --- a/packages/vchart/src/series/bar/bar.ts +++ b/packages/vchart/src/series/bar/bar.ts @@ -66,18 +66,6 @@ const BAR_SERIES_COMPILE_ONLY_KEYS: Record = { stackCornerRadius: true }; -type LinearBarBandWidthCache = { - scale: IBaseScale; - field: string; - data: Datum[]; - dataLength: number; - domainStart: number; - domainEnd: number; - rangeStart: number; - rangeEnd: number; - bandWidth: number; -}; - export class BarSeries extends CartesianSeries { static readonly type: string = SeriesTypeEnum.bar; type: string = SeriesTypeEnum.bar; @@ -92,7 +80,6 @@ export class BarSeries extends Cartes protected _bandPosition = 0; protected _barMark!: IRectMark; protected _barBackgroundMark!: IRectMark; - protected _linearBarBandWidthCache?: LinearBarBandWidthCache; protected _barBackgroundViewData: ICompilableData; @@ -107,6 +94,27 @@ export class BarSeries extends Cartes }; } + getStatisticFields() { + const fields = super.getStatisticFields(); + const positionAxisHelper = this.direction === Direction.horizontal ? this.getYAxisHelper() : this.getXAxisHelper(); + const positionFields = this.direction === Direction.horizontal ? this._fieldY : this._fieldX; + const positionScale = positionAxisHelper?.getScale?.(0); + + if (positionScale && isContinuous(positionScale.type)) { + positionFields.forEach(field => { + const fieldStatistics = fields.find(entry => entry.key === field); + if (fieldStatistics) { + if (!fieldStatistics.operations.includes('values')) { + fieldStatistics.operations.push('values'); + } + } else { + fields.push({ key: field, operations: ['values'] }); + } + }); + } + return fields; + } + initMark(): void { this._initBarBackgroundMark(); @@ -813,14 +821,12 @@ export class BarSeries extends Cartes typeof this._spec.barWidth === 'number' && typeof this._spec.barMinWidth !== 'string' && typeof this._spec.barMaxWidth !== 'string'; - const axisBandWidth = axisHelper.getBandwidth?.(depth - 1); - const linearBandWidth = - isNil(axisBandWidth) && !useFixedWidth ? this._getLinearBarBandWidth(axisHelper) : undefined; - const bandWidth = axisBandWidth ?? linearBandWidth ?? DefaultBandWidth; + const axisBandWidth = useFixedWidth ? undefined : axisHelper.getBandwidth?.(depth - 1); + const bandWidth = axisBandWidth ?? DefaultBandWidth; const hasBarMinWidth = isValid(this._spec.barMinWidth); const hasBarMaxWidth = isValid(this._spec.barMaxWidth); - let width = isNil(linearBandWidth) || hasBarWidth ? bandWidth : bandWidth * 0.5; + let width = axisHelper.isContinuous && !isNil(axisBandWidth) && !hasBarWidth ? bandWidth * 0.5 : bandWidth; if (hasBarWidth) { width = getActualNumValue(this._spec.barWidth, bandWidth); } @@ -833,88 +839,6 @@ export class BarSeries extends Cartes return width; } - protected _getLinearBarBandWidth(axisHelper: IAxisHelper) { - const scale = axisHelper.getScale?.(0); - const field = this.direction === Direction.horizontal ? this._fieldY[0] : this._fieldX[0]; - const data = this.getViewData()?.latestData as Datum[]; - const domain = scale?.domain?.() as number[]; - const range = scale?.range?.() as number[]; - const domainStart = +domain?.[0]; - const domainEnd = +domain?.[domain.length - 1]; - const rangeStart = +range?.[0]; - const rangeEnd = +range?.[range.length - 1]; - - if ( - !axisHelper.isContinuous || - !field || - !data?.length || - !Number.isFinite(domainStart) || - !Number.isFinite(domainEnd) || - !Number.isFinite(rangeStart) || - !Number.isFinite(rangeEnd) || - domainStart === domainEnd - ) { - return undefined; - } - - const cache = this._linearBarBandWidthCache; - if ( - cache && - cache.scale === scale && - cache.field === field && - cache.data === data && - cache.dataLength === data.length && - cache.domainStart === domainStart && - cache.domainEnd === domainEnd && - cache.rangeStart === rangeStart && - cache.rangeEnd === rangeEnd - ) { - return cache.bandWidth; - } - - const valueSet = new Set(); - data.forEach(datum => { - const value = +datum[field]; - if (Number.isFinite(value)) { - valueSet.add(value); - } - }); - const values = Array.from(valueSet).sort((a, b) => a - b); - if (values.length < 2) { - return undefined; - } - - let minStep = Infinity; - let lastValue = values[0]; - for (let i = 1; i < values.length; i++) { - const value = values[i]; - const step = value - lastValue; - if (step > 0 && step < minStep) { - minStep = step; - } - lastValue = value; - } - - const domainSpan = domainEnd - domainStart; - const rangeSpan = rangeEnd - rangeStart; - const bandWidth = minStep < Infinity ? Math.abs((rangeSpan / domainSpan) * minStep) : undefined; - if (!Number.isFinite(bandWidth)) { - return undefined; - } - this._linearBarBandWidthCache = { - scale, - field, - data, - dataLength: data.length, - domainStart, - domainEnd, - rangeStart, - rangeEnd, - bandWidth - }; - return bandWidth; - } - protected _getPosition(direction: DirectionType, datum: Datum, scaleDepth?: number, mark?: SeriesMarkNameEnum) { let axisHelper; let sizeAttribute; @@ -939,8 +863,7 @@ export class BarSeries extends Cartes const depthFromSpec = this._groups ? this._groups.fields.length : 1; const depth = isNil(scaleDepth) ? depthFromSpec : Math.min(depthFromSpec, scaleDepth); - const bandWidth = - axisHelper.getBandwidth?.(depth - 1) ?? this._getLinearBarBandWidth(axisHelper) ?? DefaultBandWidth; + const bandWidth = axisHelper.getBandwidth?.(depth - 1) ?? DefaultBandWidth; const size = depth === depthFromSpec ? (this._barMark.getAttribute(sizeAttribute, datum) as number) : bandWidth; if (depth > 1 && isValid(this._spec.barGapInGroup)) { @@ -966,7 +889,7 @@ export class BarSeries extends Cartes } } - const center = scale.scale(datum[groupFields[0]]) + axisHelper.getBandwidth(0) / 2; + const center = scale.scale(datum[groupFields[0]]) + (axisHelper.getBandwidth(0) ?? bandWidth) / 2; return center - totalWidth / 2 + offSet; } @@ -1022,7 +945,6 @@ export class BarSeries extends Cartes onDataUpdate(): void { super.onDataUpdate(); - this._linearBarBandWidthCache = undefined; const region = this.getRegion(); // @ts-ignore @@ -1070,7 +992,6 @@ export class BarSeries extends Cartes viewDataUpdate(d: DataView): void { super.viewDataUpdate(d); - this._linearBarBandWidthCache = undefined; this._barBackgroundViewData?.getDataView()?.reRunAllTransform(); this._barBackgroundViewData?.updateData(); }