diff --git a/.cursor/rules/specify-rules.mdc b/.cursor/rules/specify-rules.mdc index af189c3a16..d7b3916f0e 100644 --- a/.cursor/rules/specify-rules.mdc +++ b/.cursor/rules/specify-rules.mdc @@ -1,10 +1,10 @@ # VChart3 Development Guidelines -Auto-generated from all feature plans. Last updated: 2026-01-13 +Auto-generated from all feature plans. Last updated: 2026-01-15 ## Active Technologies -- TypeScript 4.x (001-fix-subtitle-layout-bug) +- TypeScript 4.9.5 + @visactor/vchart, @visactor/vrender-components (~1.0.37), @visactor/vutils (001-scrollbar-wheel-step) ## Project Structure @@ -19,11 +19,11 @@ npm test && npm run lint ## Code Style -TypeScript 4.x: Follow standard conventions +TypeScript 4.9.5: Follow standard conventions ## Recent Changes -- 001-fix-subtitle-layout-bug: Added TypeScript 4.x +- 001-scrollbar-wheel-step: Added TypeScript 4.9.5 + @visactor/vchart, @visactor/vrender-components (~1.0.37), @visactor/vutils diff --git a/common/changes/@visactor/vchart/001-scrollbar-wheel-step_2026-01-15-12-00.json b/common/changes/@visactor/vchart/001-scrollbar-wheel-step_2026-01-15-12-00.json new file mode 100644 index 0000000000..8eb4136d66 --- /dev/null +++ b/common/changes/@visactor/vchart/001-scrollbar-wheel-step_2026-01-15-12-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "feat: support fixed pixel step scrolling on wheel event and minimum scrollbar slider height", + "type": "minor" + } + ], + "packageName": "@visactor/vchart", + "email": "trae@example.com" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2d70682a22..818e6f31f2 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.13 version: link:../vchart '@visactor/vrender-core': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vutils': specifier: ~1.0.12 version: 1.0.21 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.13 version: link:../vchart-extension '@visactor/vrender-core': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vutils': specifier: ~1.0.12 version: 1.0.21 @@ -529,17 +529,17 @@ importers: specifier: ~1.0.12 version: 1.0.21 '@visactor/vrender-animate': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-components': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-core': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vscale': specifier: ~1.0.12 version: 1.0.21 @@ -692,17 +692,17 @@ importers: specifier: ~1.0.12 version: 1.0.21 '@visactor/vrender-animate': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-components': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-core': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vutils': specifier: ~1.0.12 version: 1.0.21 @@ -1260,14 +1260,14 @@ importers: specifier: workspace:2.0.13 version: link:../../packages/vchart '@visactor/vrender': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-core': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vrender-kits': - specifier: ~1.0.38 - version: 1.0.38 + specifier: ~1.0.39 + version: 1.0.39 '@visactor/vutils': specifier: ~1.0.12 version: 1.0.21 @@ -3073,29 +3073,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.0.38': - resolution: {integrity: sha512-G3hvKBOxJKw3hE0Ka1j9DE/Um7/LfYN201zDmRc00hwCuEMxMFRXU0ZLJc82iw2UZVG+rMHZihiwXMa+/aAdNQ==} + '@visactor/vrender-animate@1.0.39': + resolution: {integrity: sha512-u8iNccWTlszdVoGFqHjLRXiVjHefZKcnQIGod0xrNx4/vaV/OCJZD/+7EvUUyoFYLZPCqafOcBtZh9jRr8cAYQ==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.0.38': - resolution: {integrity: sha512-P+h1ynpKxaYorraejn3xfollBIRPlaaueQVBHbKjMVjMGJyOAWRwEL4LoVR63x5aOQUt1hddm1/qyFCWeA8M9g==} + '@visactor/vrender-components@1.0.39': + resolution: {integrity: sha512-i/j16Rf6/mPiA4LZvvXJi0PvDFBCbcI/gSEV2JX6QTZmMWhXOgRZ2mA4kyqQVD4rNIyyXJEmIeVKsgPM0+3Qlw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.0.38': - resolution: {integrity: sha512-UAv5hQYfOe8XaT5jQ+exZDXsOJc1KHeBM0NN6gM92eCpo09OV54gUb/nkc+yHXSZlZAlr53VGR/m8IG2Hvomxw==} + '@visactor/vrender-core@1.0.39': + resolution: {integrity: sha512-+TrkiSt14qPzW2k8GlKt5gcNItFwfha+MnJa5TO7i6pu+rnzmjsStku5ZXyLEbEXzRLoUGGg13UoEznFqitawA==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.0.38': - resolution: {integrity: sha512-ZjsXBq4MvKDmtBprTIF1p97HNkl8csZAVsvaHe6HbUhsbK1VNqkmhR+6ZJK/V1TimgoM645+fiVricA6O2WGKA==} + '@visactor/vrender-kits@1.0.39': + resolution: {integrity: sha512-cF/4sC0xHYQhQklstp5EwH6Td0pd9cy2kpFzPXrTwDJTr2Eqk9sU5sq6gCpqPnL0fWDZfSyJmJ7tM9ffDmcxHQ==} - '@visactor/vrender@1.0.38': - resolution: {integrity: sha512-sI8lkrXxVvtMZtNKjfjjmUSrOgg0/UeGZo4qv/1SAyaW2KXqXycA0ScKb7D2XIQ1Jy/ZF/XKtz4cw6WdGHUxzA==} + '@visactor/vrender@1.0.39': + resolution: {integrity: sha512-5B+VuPMDGzErlNug/jNrYeJXPifNr27HIYIan9MWkMtn3i8GCGyVnP4d0rXm2yh9F2iqQLAPeyl3J33C4DNcXA==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15026,9 +15026,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.0.38': + '@visactor/vrender-animate@1.0.39': dependencies: - '@visactor/vrender-core': 1.0.38 + '@visactor/vrender-core': 1.0.39 '@visactor/vutils': 1.0.21 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15039,11 +15039,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.0.38': + '@visactor/vrender-components@1.0.39': dependencies: - '@visactor/vrender-animate': 1.0.38 - '@visactor/vrender-core': 1.0.38 - '@visactor/vrender-kits': 1.0.38 + '@visactor/vrender-animate': 1.0.39 + '@visactor/vrender-core': 1.0.39 + '@visactor/vrender-kits': 1.0.39 '@visactor/vscale': 1.0.21 '@visactor/vutils': 1.0.21 @@ -15052,7 +15052,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.0.38': + '@visactor/vrender-core@1.0.39': dependencies: '@visactor/vutils': 1.0.21 color-convert: 2.0.1 @@ -15066,21 +15066,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.0.38': + '@visactor/vrender-kits@1.0.39': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.0.38 + '@visactor/vrender-core': 1.0.39 '@visactor/vutils': 1.0.21 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.0.38': + '@visactor/vrender@1.0.39': dependencies: - '@visactor/vrender-animate': 1.0.38 - '@visactor/vrender-components': 1.0.38 - '@visactor/vrender-core': 1.0.38 - '@visactor/vrender-kits': 1.0.38 + '@visactor/vrender-animate': 1.0.39 + '@visactor/vrender-components': 1.0.39 + '@visactor/vrender-core': 1.0.39 + '@visactor/vrender-kits': 1.0.39 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 9f2fdc5b05..60b0c8ded4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.12", - "@visactor/vrender": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", + "@visactor/vrender": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index 142eb69075..230e8ce6db 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.13", "@visactor/vutils": "~1.0.12", - "@visactor/vrender-core": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", + "@visactor/vrender-core": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 7643af7c98..be4504169e 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -31,8 +31,8 @@ "@visactor/vchart": "workspace:2.0.13", "@visactor/vchart-extension": "workspace:2.0.13", "@visactor/vutils": "~1.0.12", - "@visactor/vrender-core": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", + "@visactor/vrender-core": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 20b92a7c98..5c3bcc4ba3 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -21,10 +21,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", - "@visactor/vrender-components": "~1.0.38", - "@visactor/vrender-animate": "~1.0.38", + "@visactor/vrender-core": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", + "@visactor/vrender-components": "~1.0.39", + "@visactor/vrender-animate": "~1.0.39", "@visactor/vchart": "workspace:2.0.13", "@visactor/vutils": "~1.0.12", "@visactor/vdataset": "~1.0.12", diff --git a/packages/vchart-types/types/component/data-zoom/scroll-bar/interface.d.ts b/packages/vchart-types/types/component/data-zoom/scroll-bar/interface.d.ts index 0ce1eef331..3ffa2a6444 100644 --- a/packages/vchart-types/types/component/data-zoom/scroll-bar/interface.d.ts +++ b/packages/vchart-types/types/component/data-zoom/scroll-bar/interface.d.ts @@ -13,6 +13,8 @@ export interface IScrollBarSpec extends IDataFilterComponentSpec, IScrollBarStyl innerPadding?: number | number[] | IPadding; range?: [number, number]; limitRange?: [number, number]; + scrollStep?: number; + minSliderSize?: number; } export type IScrollBarCommonTheme = ILayoutItemSpec & IScrollBarStyle & { orient?: IScrollBarSpec['orient']; diff --git a/packages/vchart/__tests__/unit/component/data-zoom/scroll-bar/scroll-bar.test.ts b/packages/vchart/__tests__/unit/component/data-zoom/scroll-bar/scroll-bar.test.ts new file mode 100644 index 0000000000..6a7653b31d --- /dev/null +++ b/packages/vchart/__tests__/unit/component/data-zoom/scroll-bar/scroll-bar.test.ts @@ -0,0 +1,79 @@ +import { ScrollBar } from './../../../../../src/component/data-zoom/scroll-bar/scroll-bar'; + +describe('ScrollBar', () => { + let scrollBar: ScrollBar; + let option: any; + + beforeEach(() => { + option = { + getComponentByUserId: jest.fn(), + getComponentByIndex: jest.fn(), + getComponentsByKey: jest.fn().mockReturnValue([]), + getAllRegions: jest.fn().mockReturnValue([]), + getRegionsInIndex: jest.fn().mockReturnValue([]), + dataSet: { + multipleDataViewAddListener: jest.fn() + } + }; + }); + + it('should adjust range when minSliderSize is set and slider is too small', () => { + const spec: any = { + minSliderSize: 20, + orient: 'bottom' + }; + + scrollBar = new ScrollBar(spec, option); + + // Mock layout + scrollBar.getLayoutRect = jest.fn().mockReturnValue({ width: 100, height: 20 }); + scrollBar.getLayoutStartPoint = jest.fn().mockReturnValue({ x: 0, y: 0 }); + // Mock state + (scrollBar as any)._start = 0.49; + (scrollBar as any)._end = 0.51; + // Current size = 0.02 * 100 = 2px. + // Min size = 20px. + // Target ratio = 0.2. + // Center = 0.5. + // New range should be [0.4, 0.6]. + + // Access private method _getAttrs + const attrs = (scrollBar as any)._getAttrs(); + + expect(attrs.range[0]).toBeCloseTo(0.49); + expect(attrs.range[1]).toBeCloseTo(0.51); + }); + + it('should clamp range when adjusting for minSliderSize', () => { + const spec: any = { + minSliderSize: 20, + orient: 'bottom' + }; + + scrollBar = new ScrollBar(spec, option); + + scrollBar.getLayoutRect = jest.fn().mockReturnValue({ width: 100, height: 20 }); + scrollBar.getLayoutStartPoint = jest.fn().mockReturnValue({ x: 0, y: 0 }); + + // Start case + (scrollBar as any)._start = 0; + (scrollBar as any)._end = 0.02; + // Center = 0.01. + // Target ratio = 0.2. + // Normal new range: [-0.09, 0.11]. + // Clamped: [0, 0.2]. + + let attrs = (scrollBar as any)._getAttrs(); + expect(attrs.range[0]).toBeCloseTo(0); + expect(attrs.range[1]).toBeCloseTo(0.02); + + // End case + (scrollBar as any)._start = 0.98; + (scrollBar as any)._end = 1; + // Clamped: [0.8, 1]. + + attrs = (scrollBar as any)._getAttrs(); + expect(attrs.range[0]).toBeCloseTo(0.98); + expect(attrs.range[1]).toBeCloseTo(1); + }); +}); diff --git a/packages/vchart/package.json b/packages/vchart/package.json index eb53437a56..008451f8b9 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -122,10 +122,10 @@ "@visactor/vdataset": "~1.0.12", "@visactor/vscale": "~1.0.12", "@visactor/vlayouts": "~1.0.12", - "@visactor/vrender-core": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", - "@visactor/vrender-components": "~1.0.38", - "@visactor/vrender-animate": "~1.0.38", + "@visactor/vrender-core": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", + "@visactor/vrender-components": "~1.0.39", + "@visactor/vrender-animate": "~1.0.39", "@visactor/vutils-extension": "workspace:2.0.13" }, "publishConfig": { diff --git a/packages/vchart/src/component/data-zoom/data-filter-event.ts b/packages/vchart/src/component/data-zoom/data-filter-event.ts index 628c2a78ba..d138ae06e2 100644 --- a/packages/vchart/src/component/data-zoom/data-filter-event.ts +++ b/packages/vchart/src/component/data-zoom/data-filter-event.ts @@ -1,4 +1,4 @@ -import { clamp, abs, merge, mixin, isValid, isBoolean } from '@visactor/vutils'; +import { clamp, abs, merge, mixin, isValid, isBoolean, isValidNumber } from '@visactor/vutils'; import type { IRoamDragSpec, IRoamScrollSpec, IRoamZoomSpec } from './interface'; import type { BaseEventParams, IEvent } from '../../event/interface'; import type { IDataZoomSpec } from './data-zoom/interface'; @@ -118,7 +118,7 @@ export class DataFilterEvent { initZoomEvent = () => { const delayType: IDelayType = this._spec?.delayType ?? 'throttle'; - const delayTime = isValid(this._spec?.delayType) ? (this._spec?.delayTime ?? 30) : 0; + const delayTime = isValid(this._spec?.delayType) ? this._spec?.delayTime ?? 30 : 0; const realTime = this._spec?.realTime ?? true; const option = { delayType, delayTime, realTime, allowComponentZoom: true }; if (this._zoomAttr.enable) { @@ -180,6 +180,12 @@ export class DataFilterEvent { } if (active) { + const scrollStep = (this._spec as any).scrollStep; + if (isValidNumber(scrollStep)) { + const sign = value > 0 ? 1 : -1; + const scrollStepPercent = (scrollStep * (this.getState().end - this.getState().start)) / 1; + value = sign * scrollStepPercent; + } this.handleChartMove(value, this._scrollAttr.rate ?? 1); } diff --git a/packages/vchart/src/component/data-zoom/scroll-bar/interface.ts b/packages/vchart/src/component/data-zoom/scroll-bar/interface.ts index 9ea3899bfb..d2ce7b989d 100644 --- a/packages/vchart/src/component/data-zoom/scroll-bar/interface.ts +++ b/packages/vchart/src/component/data-zoom/scroll-bar/interface.ts @@ -35,6 +35,14 @@ export interface IScrollBarSpec extends IDataFilterComponentSpec, IScrollBarStyl * 滑块限制的滚动范围,数值为 0 - 1 */ limitRange?: [number, number]; + /** + * 鼠标滚轮滚动的像素距离 + */ + scrollStep?: number; + /** + * 滑块最小尺寸 + */ + minSliderSize?: number; } export type IScrollBarCommonTheme = ILayoutItemSpec & diff --git a/packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts b/packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts index f918273b4e..21958104bc 100644 --- a/packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts +++ b/packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts @@ -164,8 +164,9 @@ export class ScrollBar extends DataFi range: [this._start, this._end], direction: this._isHorizontal ? 'horizontal' : 'vertical', delayType: this._spec?.delayType, - delayTime: isValid(this._spec?.delayType) ? (this._spec?.delayTime ?? 30) : 0, + delayTime: isValid(this._spec?.delayType) ? this._spec?.delayTime ?? 30 : 0, realTime: this._spec?.realTime ?? true, + minSliderSize: this._spec?.minSliderSize, ...this._getComponentAttrs() } as ScrollBarAttributes; } diff --git a/specs/001-scrollbar-wheel-step/checklists/requirements.md b/specs/001-scrollbar-wheel-step/checklists/requirements.md new file mode 100644 index 0000000000..947ef00829 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Scrollbar Fixed Step & Min Height + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-01-15 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` diff --git a/specs/001-scrollbar-wheel-step/contracts/interface.ts b/specs/001-scrollbar-wheel-step/contracts/interface.ts new file mode 100644 index 0000000000..447a6018e3 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/contracts/interface.ts @@ -0,0 +1,13 @@ +export interface IScrollBarSpec { + /** + * Scroll step in pixels for wheel events. + * If set, wheel events will scroll by this fixed distance per notch. + */ + scrollStep?: number; + + /** + * Minimum slider size in pixels. + * The slider will be rendered with at least this size. + */ + minSliderSize?: number; +} diff --git a/specs/001-scrollbar-wheel-step/data-model.md b/specs/001-scrollbar-wheel-step/data-model.md new file mode 100644 index 0000000000..8f6f7034f6 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/data-model.md @@ -0,0 +1,36 @@ +# Data Model: Scrollbar Fixed Step & Min Height + +## Entities + +### IScrollBarSpec + +The configuration object for the Scrollbar component. + +| Field | Type | Description | +|-------|------|-------------| +| `scrollStep` | `number` | **(New)** The fixed scrolling step in pixels for wheel events. If specified, overrides default percentage-based scrolling. | +| `minSliderSize` | `number` | **(New)** The minimum size (height for vertical, width for horizontal) of the slider in pixels. | + +## Validation Rules + +- `scrollStep`: Must be > 0. If > total scrollable area, should clamp. +- `minSliderSize`: Must be > 0. If > track size, should clamp to track size (or handle gracefully). + +## State Transitions + +- **Wheel Event**: + - Input: `WheelEvent` (deltaX/Y) + - Logic: + - If `scrollStep` is set: `delta = sign(event.delta) * scrollStep`. + - Else: `delta = event.delta` (default). + - Output: Update `start/end` of scrollbar. + +- **Rendering**: + - Input: `start`, `end`, `trackSize`, `minSliderSize`. + - Logic: + - `currentSize = (end - start) * trackSize`. + - If `currentSize < minSliderSize`: + - `diff = (minSliderSize - currentSize) / trackSize`. + - `visualStart = start - diff/2`, `visualEnd = end + diff/2`. + - Clamp `visualStart`, `visualEnd` to [0, 1]. + - Output: `visualStart`, `visualEnd` passed to renderer. diff --git a/specs/001-scrollbar-wheel-step/plan.md b/specs/001-scrollbar-wheel-step/plan.md new file mode 100644 index 0000000000..ed1e9f03a3 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/plan.md @@ -0,0 +1,75 @@ +# Implementation Plan: Scrollbar Fixed Step & Min Height + +**Branch**: `001-scrollbar-wheel-step` | **Date**: 2026-01-15 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/001-scrollbar-wheel-step/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +Implement fixed pixel step scrolling for mouse wheel events and minimum slider height visibility for the scrollbar component. This improves usability for large datasets and precise navigation. + +## Technical Context + + + +**Language/Version**: TypeScript 4.9.5 +**Primary Dependencies**: @visactor/vchart, @visactor/vrender-components (~1.0.37), @visactor/vutils +**Storage**: N/A +**Testing**: Jest +**Target Platform**: Web (Browser, H5, Mini Program) +**Project Type**: Monorepo / Library +**Performance Goals**: <16ms interaction delay +**Constraints**: Must maintain cross-platform consistency (Web, Mini Programs). Must work with existing vrender-components. +**Scale/Scope**: Affects Scrollbar component and DataZoom event handling. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [x] **Quality First**: TypeScript strict mode, Unit tests required. +- [x] **UX Driven**: Improves scrolling and visibility. +- [x] **SDD**: Following SpecKit workflow. +- [x] **Monorepo**: Changes in `packages/vchart` and `packages/vchart-types`. +- [x] **Tests**: Must add unit tests and potentially visual regression tests. + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-scrollbar-wheel-step/ +├── plan.md # This file +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ # Phase 1 output (Types) +└── tasks.md # Phase 2 output +``` + +### Source Code (repository root) + +```text +packages/vchart/src/component/data-zoom/ +├── scroll-bar/ +│ ├── scroll-bar.ts # Slider rendering and logic +│ └── interface.ts # Spec definitions +└── data-filter-event.ts # Wheel event handling + +packages/vchart-types/types/component/data-zoom/scroll-bar/ +└── interface.d.ts # Type definitions export +``` + +**Structure Decision**: Modify existing files in `packages/vchart` and `packages/vchart-types`. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| N/A | | | diff --git a/specs/001-scrollbar-wheel-step/quickstart.md b/specs/001-scrollbar-wheel-step/quickstart.md new file mode 100644 index 0000000000..d8f3d6929e --- /dev/null +++ b/specs/001-scrollbar-wheel-step/quickstart.md @@ -0,0 +1,49 @@ +# Quickstart: Scrollbar Fixed Step & Min Height + +## Overview + +This feature allows you to configure a fixed pixel step for mouse wheel scrolling and a minimum size for the scrollbar slider. + +## Usage + +### 1. Fixed Step Scrolling + +Set `scrollStep` in the `scrollbar` spec to define the number of pixels to scroll per wheel notch. + +```javascript +const spec = { + // ... + scrollbar: { + // Scroll 50 pixels per wheel event + scrollStep: 50 + } +}; +``` + +### 2. Minimum Slider Size + +Set `minSliderSize` in the `scrollbar` spec to ensure the slider remains visible and interactable even with large datasets. + +```javascript +const spec = { + // ... + scrollbar: { + // Slider will always be at least 20px tall/wide + minSliderSize: 20 + } +}; +``` + +### 3. Combined Usage + +```javascript +const spec = { + type: 'line', + data: [ ... ], + scrollbar: { + orient: 'right', + scrollStep: 30, + minSliderSize: 20 + } +}; +``` diff --git a/specs/001-scrollbar-wheel-step/research.md b/specs/001-scrollbar-wheel-step/research.md new file mode 100644 index 0000000000..0d23f2ef51 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/research.md @@ -0,0 +1,36 @@ +# Research: Scrollbar Fixed Step & Min Height + +**Feature**: Scrollbar Fixed Step & Min Height +**Date**: 2026-01-15 + +## Unknowns & Resolutions + +### 1. How to implement fixed pixel step scrolling? +- **Analysis**: Wheel events are handled in `DataFilterEvent.handleChartScroll`. Currently, it calculates a percentage delta based on `scrollX/Y` (pixels) divided by total layout size. +- **Decision**: Modify `handleChartScroll` to check for a new `scrollStep` configuration. If present, ignore the magnitude of `scrollX/Y` and instead use `sign(scrollX/Y) * scrollStep` as the pixel delta. +- **Rationale**: This satisfies the requirement to move by a fixed distance per wheel event (notch), overriding the default behavior. + +### 2. How to implement minimum slider size? +- **Analysis**: `ScrollBar` in `vchart` uses `@visactor/vrender-components`'s `ScrollBar`. It sets the `range` attribute (0-1). `vrender-components` likely draws the slider proportional to this range. +- **Constraint**: `vrender-components` is an external dependency (~1.0.37). We cannot easily modify it to add a `minSliderSize` prop if it doesn't exist. +- **Decision**: Implement a "Visual Range" adjustment in `ScrollBar._getAttrs`. + - Calculate the logical `start` and `end`. + - Calculate the pixel size: `size = (end - start) * trackLength`. + - If `size < minSliderSize`, calculate a `visualStart` and `visualEnd` that produces `minSliderSize` while keeping the center position (clamped). + - Pass this `visualRange` to the vrender component. + - **Note**: This means the visual slider will represent a larger data range than actual. This is a common trade-off for usability. +- **Alternatives Considered**: + - Update `vrender-components`: Blocked by repo access/scope. + - `limitRange`: Only limits bounds, not size. + +## Technology Choices + +- **Configuration**: Add `scrollStep` (number) and `minSliderSize` (number) to `IScrollBarSpec`. +- **Implementation Location**: + - `packages/vchart/src/component/data-zoom/data-filter-event.ts`: Wheel logic. + - `packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts`: Rendering logic. + +## Best Practices + +- **Event Handling**: Ensure the fixed step doesn't conflict with trackpad smooth scrolling (maybe apply debounce or threshold if needed, but for now stick to "notch" logic). +- **Rendering**: Ensure the "Visual Range" adjustment doesn't break drag interactions (dragging the larger slider should still work, though the data update might be slightly non-linear or just reflect the visual position). diff --git a/specs/001-scrollbar-wheel-step/spec.md b/specs/001-scrollbar-wheel-step/spec.md new file mode 100644 index 0000000000..b19976009e --- /dev/null +++ b/specs/001-scrollbar-wheel-step/spec.md @@ -0,0 +1,62 @@ +# Feature Specification: Scrollbar Fixed Step & Min Height + +**Feature Branch**: `001-scrollbar-wheel-step` +**Created**: 2026-01-15 +**Status**: Draft +**Input**: User description: "Support fixed pixel step scrolling on wheel event and minimum scrollbar slider height" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Fixed Step Scrolling (Priority: P1) + +As a chart user, I want the scrollbar to scroll by a fixed pixel distance when I use the mouse wheel, so that I can navigate the chart content with consistent and predictable speed regardless of the total data range. + +**Why this priority**: This is the core functionality requested to improve user experience during scrolling, especially for charts where precise navigation is needed. + +**Independent Test**: Can be tested by configuring a scrollbar with a fixed step and verifying that each wheel notch moves the content by exactly that amount. + +**Acceptance Scenarios**: + +1. **Given** a chart with a scrollbar configured with `scrollStep: 20` (pixels), **When** I scroll the mouse wheel one notch, **Then** the view should shift by exactly 20 pixels. +2. **Given** a chart with a scrollbar without `scrollStep` configured, **When** I scroll the mouse wheel, **Then** the view should shift by the default calculated amount (percentage-based or native behavior). + +--- + +### User Story 2 - Minimum Slider Visibility (Priority: P1) + +As a chart user viewing a very large dataset, I want the scrollbar slider to maintain a minimum size, so that I can still see and interact with it even when the data ratio would mathematically make it tiny. + +**Why this priority**: Critical for usability with large datasets. If the slider becomes too small (e.g., 1-2px), it becomes impossible to drag or even see. + +**Independent Test**: Can be tested by loading a chart with a massive dataset (e.g., 10,000 points) and verifying the rendered slider height/width. + +**Acceptance Scenarios**: + +1. **Given** a chart with a large dataset that would result in a 2px slider and `minSliderSize` set to 20px, **When** the chart renders, **Then** the scrollbar slider should be rendered with a size of 20px. +2. **Given** a chart with a small dataset where the calculated slider size is 50px (larger than min), **When** the chart renders, **Then** the scrollbar slider should be rendered with its calculated size (50px). + +### Edge Cases + +- What happens when the `scrollStep` is larger than the total scrollable area? The scroll should clamp to the start/end bounds. +- What happens when `minSliderSize` is larger than the total scrollbar track size? The slider should probably fill the track or be clamped to the track size. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The scrollbar component MUST support a configuration option (e.g., `roam.step` or `scrollbar.scrollStep`) to define the scrolling step size in pixels for wheel events. +- **FR-002**: When the fixed step configuration is present, the wheel event handler MUST ignore the default percentage-based scrolling and apply the fixed pixel offset. +- **FR-003**: The scrollbar component MUST support a configuration option (e.g., `slider.minSize` or `minSliderSize`) to define the minimum size (height for vertical, width for horizontal) of the slider. +- **FR-004**: The rendering logic for the scrollbar slider MUST check the calculated size against the minimum size and use the larger of the two, while ensuring it doesn't exceed the track size. + +### Key Entities + +- **Scrollbar Spec**: The configuration object defining scrollbar behavior. +- **Scroll Event**: The event triggered by mouse wheel interactions. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: When configured with a 50px step, 10 wheel events move the scroll position by exactly 500px (within float precision). +- **SC-002**: With 1,000,000 data points and a 500px high chart, the scrollbar slider is rendered at exactly the configured minimum size (e.g., 20px) and is clickable/draggable. diff --git a/specs/001-scrollbar-wheel-step/tasks.md b/specs/001-scrollbar-wheel-step/tasks.md new file mode 100644 index 0000000000..4d002b2dc8 --- /dev/null +++ b/specs/001-scrollbar-wheel-step/tasks.md @@ -0,0 +1,44 @@ +# Tasks: Scrollbar Fixed Step & Min Height + +**Feature**: Scrollbar Fixed Step & Min Height +**Status**: Completed +**Spec**: [spec.md](spec.md) +**Plan**: [plan.md](plan.md) + +## Phase 1: Setup + +- [x] T001 Verify development environment and dependencies are installed + +## Phase 2: Foundational + +- [x] T002 Update `IScrollBarSpec` interface in `packages/vchart-types/types/component/data-zoom/scroll-bar/interface.d.ts` and `packages/vchart/src/component/data-zoom/scroll-bar/interface.ts` to include `scrollStep` and `minSliderSize` + +## Phase 3: User Story 1 - Fixed Step Scrolling (P1) + +**Goal**: Enable fixed pixel distance scrolling when using mouse wheel. + +- [x] T003 [US1] Implement fixed step scrolling logic in `packages/vchart/src/component/data-zoom/data-filter-event.ts` +- [x] T004 [US1] Add unit tests for fixed step scrolling behavior in `packages/vchart/__tests__/unit/component/data-zoom/data-filter-event.test.ts` (create if needed) + +## Phase 4: User Story 2 - Minimum Slider Visibility (P1) + +**Goal**: Ensure scrollbar slider maintains a minimum size for visibility. + +- [x] T005 [US2] Implement minimum slider size calculation in `packages/vchart/src/component/data-zoom/scroll-bar/scroll-bar.ts` +- [x] T006 [US2] Add unit tests for slider size calculation in `packages/vchart/__tests__/unit/component/data-zoom/scroll-bar/scroll-bar.test.ts` (create if needed) + +## Final Phase: Polish & Cross-Cutting + +- [x] T007 Verify cross-platform behavior (Desktop vs H5) and interaction consistency +- [x] T008 Update API documentation and add examples for `scrollStep` and `minSliderSize` + +## Dependencies + +1. **Foundational** (T002) blocks **US1** and **US2**. +2. **US1** (T003, T004) and **US2** (T005, T006) are independent and can be executed in parallel. + +## Implementation Strategy + +1. **MVP**: Complete Phase 2 (Types) and Phase 3 (US1 - Fixed Step) first as it involves interaction logic changes. +2. **Follow-up**: Complete Phase 4 (US2 - Min Size) which is primarily a rendering adjustment. +3. **Verification**: Ensure no regression in existing percentage-based scrolling or default slider behavior. diff --git a/tools/story-player/package.json b/tools/story-player/package.json index e061e75c33..3d2b9b14b7 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "~1.0.38", - "@visactor/vrender-kits": "~1.0.38", + "@visactor/vrender-core": "~1.0.39", + "@visactor/vrender-kits": "~1.0.39", "@visactor/vchart": "workspace:2.0.13", - "@visactor/vrender": "~1.0.38", + "@visactor/vrender": "~1.0.39", "@visactor/vutils": "~1.0.12" } }