From dabd616ae9cecda7b8dc01e3077607127cadb66a Mon Sep 17 00:00:00 2001 From: xuefei1313 Date: Fri, 20 Mar 2026 06:33:02 +0000 Subject: [PATCH 1/3] feat(vchart,vchart-types): Add hideWhenEmpty axis option for cartesian axes --- .cursor/rules/specify-rules.mdc | 3 +- .../010-hide-empty-axes_2026-03-20-06-29.json | 11 ++ .../axis/cartesian/interface/spec.d.ts | 1 + .../cartesian/axis/hide-when-empty.test.ts | 122 ++++++++++++++++++ .../vchart/src/component/axis/base-axis.ts | 53 +++++++- .../src/component/axis/cartesian/axis.ts | 11 +- .../axis/cartesian/interface/spec.ts | 7 + .../component/axis/mixin/band-axis-mixin.ts | 2 + .../component/axis/mixin/linear-axis-mixin.ts | 2 + .../vchart/src/component/axis/polar/axis.ts | 2 +- .../ICartesianAxisSpec-Type-Definition.md | 17 +++ .../checklists/requirements.md | 34 +++++ .../contracts/interface.ts | 7 + specs/010-hide-empty-axes/data-model.md | 56 ++++++++ specs/010-hide-empty-axes/plan.md | 85 ++++++++++++ specs/010-hide-empty-axes/quickstart.md | 54 ++++++++ specs/010-hide-empty-axes/research.md | 40 ++++++ specs/010-hide-empty-axes/spec.md | 89 +++++++++++++ specs/010-hide-empty-axes/tasks.md | 43 ++++++ 19 files changed, 632 insertions(+), 7 deletions(-) create mode 100644 common/changes/@visactor/vchart/010-hide-empty-axes_2026-03-20-06-29.json create mode 100644 packages/vchart/__tests__/unit/component/cartesian/axis/hide-when-empty.test.ts create mode 100644 specs/010-hide-empty-axes/checklists/requirements.md create mode 100644 specs/010-hide-empty-axes/contracts/interface.ts create mode 100644 specs/010-hide-empty-axes/data-model.md create mode 100644 specs/010-hide-empty-axes/plan.md create mode 100644 specs/010-hide-empty-axes/quickstart.md create mode 100644 specs/010-hide-empty-axes/research.md create mode 100644 specs/010-hide-empty-axes/spec.md create mode 100644 specs/010-hide-empty-axes/tasks.md diff --git a/.cursor/rules/specify-rules.mdc b/.cursor/rules/specify-rules.mdc index f4448e2042..56092a6035 100644 --- a/.cursor/rules/specify-rules.mdc +++ b/.cursor/rules/specify-rules.mdc @@ -6,6 +6,7 @@ Auto-generated from all feature plans. Last updated: 2026-01-15 - TypeScript 4.x+ (Project uses TS) + `@visactor/vchart` (Core logic), `@visactor/react-vchart` (React wrapper) (007-fix-datazoom-react) - N/A (In-memory chart state) (007-fix-datazoom-react) - Markdown + JSON(文档内容与菜单配置) + `@internal/docs` 文档构建体系、`docs/assets/guide/menu.json` 导航配置、现有教程目录结构 (001-vchart-skill-tutorial) +- TypeScript 4.9.5 + `@visactor/vchart`, `@visactor/vutils`, `@visactor/vrender-components` (010-hide-empty-axes) - TypeScript/React 18 + @visactor/react-vchart, @visactor/vchar (001-react-vchart-demo) @@ -27,9 +28,9 @@ npm test && npm run lint TypeScript 4.9.5: Follow standard conventions ## Recent Changes +- 010-hide-empty-axes: Added TypeScript 4.9.5 + `@visactor/vchart`, `@visactor/vutils`, `@visactor/vrender-components` - 001-vchart-skill-tutorial: Added Markdown + JSON(文档内容与菜单配置) + `@internal/docs` 文档构建体系、`docs/assets/guide/menu.json` 导航配置、现有教程目录结构 - 007-fix-datazoom-react: Added TypeScript 4.x+ (Project uses TS) + `@visactor/vchart` (Core logic), `@visactor/react-vchart` (React wrapper) -- 007-fix-datazoom-react: Added [if applicable, e.g., PostgreSQL, CoreData, files or N/A] diff --git a/common/changes/@visactor/vchart/010-hide-empty-axes_2026-03-20-06-29.json b/common/changes/@visactor/vchart/010-hide-empty-axes_2026-03-20-06-29.json new file mode 100644 index 0000000000..f540ce9ec1 --- /dev/null +++ b/common/changes/@visactor/vchart/010-hide-empty-axes_2026-03-20-06-29.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "update changes for 010-hide-empty-axes: Add an opt-in hideWhenEmpty axis option for cartesian axes.", + "type": "none", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/packages/vchart-types/types/component/axis/cartesian/interface/spec.d.ts b/packages/vchart-types/types/component/axis/cartesian/interface/spec.d.ts index 29e8f4b791..cae05123be 100644 --- a/packages/vchart-types/types/component/axis/cartesian/interface/spec.d.ts +++ b/packages/vchart-types/types/component/axis/cartesian/interface/spec.d.ts @@ -21,6 +21,7 @@ export type ICartesianZ = { orient: 'z'; }; export type ICartesianAxisCommonSpec = ICommonAxisSpec & { + hideWhenEmpty?: boolean; grid?: IGrid; subGrid?: IGrid; domainLine?: ICartesianDomainLine; diff --git a/packages/vchart/__tests__/unit/component/cartesian/axis/hide-when-empty.test.ts b/packages/vchart/__tests__/unit/component/cartesian/axis/hide-when-empty.test.ts new file mode 100644 index 0000000000..eb2b7080f5 --- /dev/null +++ b/packages/vchart/__tests__/unit/component/cartesian/axis/hide-when-empty.test.ts @@ -0,0 +1,122 @@ +import { default as VChartConstructor } from '../../../../../src'; +import { createCanvas, removeDom } from '../../../../util/dom'; + +describe('cartesian axis hideWhenEmpty', () => { + let canvasDom: HTMLCanvasElement; + let vchart: any; + + beforeEach(() => { + canvasDom = createCanvas(); + canvasDom.style.position = 'relative'; + canvasDom.style.width = '500px'; + canvasDom.style.height = '500px'; + canvasDom.width = 500; + canvasDom.height = 500; + }); + + afterEach(() => { + removeDom(canvasDom); + vchart?.release(); + }); + + const getAxis = (orient: string) => vchart.getComponents().find(com => com.layout?.layoutOrient === orient) as any; + + test('should hide axis on initial render when bound series has no collected data', () => { + vchart = new VChartConstructor( + { + type: 'line', + width: 400, + height: 300, + data: [{ id: 'lineData', values: [] }], + axes: [ + { id: 'axis-left', orient: 'left', hideWhenEmpty: true }, + { id: 'axis-bottom', orient: 'bottom', type: 'band' } + ], + series: [{ type: 'line', dataId: 'lineData', xField: 'x', yField: 'y' }] + } as any, + { + renderCanvas: canvasDom, + animation: false + } + ); + + vchart.renderSync(); + + const leftAxis = getAxis('left'); + expect(leftAxis.getVisible()).toBe(false); + expect(leftAxis.getLayoutRect().width).toBe(0); + }); + + test('should keep default behavior when hideWhenEmpty is not enabled', () => { + vchart = new VChartConstructor( + { + type: 'line', + width: 400, + height: 300, + data: [{ id: 'lineData', values: [] }], + axes: [ + { id: 'axis-left', orient: 'left' }, + { id: 'axis-bottom', orient: 'bottom', type: 'band' } + ], + series: [{ type: 'line', dataId: 'lineData', xField: 'x', yField: 'y' }] + } as any, + { + renderCanvas: canvasDom, + animation: false + } + ); + + vchart.renderSync(); + + const leftAxis = getAxis('left'); + expect(leftAxis.getVisible()).toBe(true); + }); + + test('should only hide empty bound axes and show them again after data updates', async () => { + vchart = new VChartConstructor( + { + type: 'common', + width: 400, + height: 300, + data: [ + { id: 'emptyLine', values: [] }, + { + id: 'activeLine', + values: [ + { x: 'Mon', y: 10 }, + { x: 'Tue', y: 20 } + ] + } + ], + axes: [ + { id: 'axis-left', orient: 'left', seriesIndex: [0], hideWhenEmpty: true }, + { id: 'axis-right', orient: 'right', seriesIndex: [1], hideWhenEmpty: true }, + { id: 'axis-bottom', orient: 'bottom', type: 'band' } + ], + series: [ + { type: 'line', dataId: 'emptyLine', xField: 'x', yField: 'y' }, + { type: 'line', dataId: 'activeLine', xField: 'x', yField: 'y' } + ] + } as any, + { + renderCanvas: canvasDom, + animation: false + } + ); + + vchart.renderSync(); + + const leftAxis = getAxis('left'); + const rightAxis = getAxis('right'); + expect(leftAxis.getVisible()).toBe(false); + expect(rightAxis.getVisible()).toBe(true); + + await vchart.updateData('emptyLine', [ + { x: 'Mon', y: 5 }, + { x: 'Tue', y: 15 } + ]); + + expect(leftAxis.getVisible()).toBe(true); + expect(leftAxis.getLayoutRect().width).toBeGreaterThan(0); + }); +}); diff --git a/packages/vchart/src/component/axis/base-axis.ts b/packages/vchart/src/component/axis/base-axis.ts index bf7e7fad78..5afe6dd768 100644 --- a/packages/vchart/src/component/axis/base-axis.ts +++ b/packages/vchart/src/component/axis/base-axis.ts @@ -96,10 +96,16 @@ export abstract class AxisComponent { @@ -378,6 +400,31 @@ export abstract class AxisComponent 0; + } + + protected _refreshVisibilityByData() { + const nextVisible = this._specVisible && (!this._hideWhenEmpty || this._hasCollectedSeriesData()); + const changed = this._visible !== nextVisible; + this._visible = nextVisible; + + if (this._axisMark || this._gridMark) { + this._syncComponentVisibility(); + } + + if (changed) { + this._forceLayout(); + } + + return changed; + } + + protected _syncComponentVisibility() { + this._axisMark?.setSimpleStyle({ visible: this._visible }); + this._gridMark?.setSimpleStyle({ visible: this._visible && this._spec.grid?.visible !== false }); + } + protected _clearRawDomain() { // 留给各个类型的 axis 来 override } diff --git a/packages/vchart/src/component/axis/cartesian/axis.ts b/packages/vchart/src/component/axis/cartesian/axis.ts index d8976623f2..a058a91b11 100644 --- a/packages/vchart/src/component/axis/cartesian/axis.ts +++ b/packages/vchart/src/component/axis/cartesian/axis.ts @@ -302,7 +302,7 @@ export abstract class CartesianAxis getBoundsInRect: update tick attr -> forceLayout -> updateLayoutAttr: update tick attr -> chart layout -> scale update -> mark encode // 问题: chart layout之后, scale发生变化, 导致tick 和 mark position 不同步 // 解决方案: chart layout 之后重新计算tick位置 @@ -805,6 +805,11 @@ export abstract class CartesianAxis { + if (!this._visible) { + this._syncComponentVisibility(); + return; + } + const startPoint = this.getLayoutStartPoint(); const { grid: updateGridAttrs, ...updateAxisAttrs } = this._getUpdateAttribute(false); const axisAttrs = mergeSpec({ x: startPoint.x, y: startPoint.y }, this._axisStyle, updateAxisAttrs); @@ -822,6 +827,8 @@ export abstract class CartesianAxis number; _onTickDataChange: (compilableData: CompilableData) => void; registerTicksTransform: () => string; + _refreshVisibilityByData: () => boolean; } export class BandAxisMixin { @@ -199,6 +200,7 @@ export class BandAxisMixin { } } this.transformScaleDomain(); + this._refreshVisibilityByData(); this.event.emit(ChartEvent.scaleDomainUpdate, { model: this as unknown as IModel }); this.event.emit(ChartEvent.scaleUpdate, { model: this as unknown as IModel, value: 'domain' }); } diff --git a/packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts b/packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts index a73a417db3..29b3143cb7 100644 --- a/packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts +++ b/packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts @@ -52,6 +52,7 @@ export interface LinearAxisMixin { _orient: IOrientType; _option: IComponentOption; niceLabelFormatter: (value: StringOrNumber) => StringOrNumber; + _refreshVisibilityByData: () => boolean; } export class LinearAxisMixin { @@ -403,6 +404,7 @@ export class LinearAxisMixin { this._updateNiceLabelFormatter(domain); this._domainAfterSpec = this._scale.domain(); + this._refreshVisibilityByData(); this.event.emit(ChartEvent.scaleDomainUpdate, { model: this as any }); this.event.emit(ChartEvent.scaleUpdate, { model: this as any, value: 'domain' }); } diff --git a/packages/vchart/src/component/axis/polar/axis.ts b/packages/vchart/src/component/axis/polar/axis.ts index 9bb5755bf1..6eee0e97ab 100644 --- a/packages/vchart/src/component/axis/polar/axis.ts +++ b/packages/vchart/src/component/axis/polar/axis.ts @@ -201,7 +201,7 @@ export abstract class PolarAxis