-
Notifications
You must be signed in to change notification settings - Fork 204
feat: support area enlargement (linear axis custom distribution) #4441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
231fe75
401ced3
b8361fc
3c7c04d
27dd0ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@visactor/vchart", | ||
| "comment": "feat: support area enlargement (linear axis custom distribution)", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "@visactor/vchart", | ||
| "email": "lixuef1313@163.com" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { IChartInfo } from './interface'; | ||
|
|
||
| const spec = { | ||
| type: 'line', | ||
| data: [ | ||
| { | ||
| id: 'line', | ||
| values: [ | ||
| { x: '1', y: 1 }, | ||
| { x: '2', y: 5 }, | ||
| { x: '3', y: 7 }, | ||
| { x: '4', y: 8 }, | ||
| { x: '5', y: 8.5 }, | ||
| { x: '6', y: 9 }, | ||
| { x: '7', y: 9.5 }, | ||
| { x: '8', y: 10 } | ||
| ] | ||
| } | ||
| ], | ||
| xField: 'x', | ||
| yField: 'y', | ||
| axes: [ | ||
| { | ||
| orient: 'left', | ||
| type: 'linear', | ||
| customDistribution: { | ||
| domain: [0, 7, 9, 10], | ||
| ratio: [0.2, 0.6, 0.2] | ||
| } | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| const areaEnlargement: IChartInfo = { | ||
| name: 'Area Enlargement', | ||
| spec | ||
| }; | ||
|
|
||
| export default areaEnlargement; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { GlobalScale } from '../../../../../src/scale/global-scale'; | ||
| import { DataSet, csvParser } from '@visactor/vdataset'; | ||
| import { dimensionStatistics } from '../../../../../src/data/transforms/dimension-statistics'; | ||
| import type { CartesianLinearAxis } from '../../../../../src/index'; | ||
| // eslint-disable-next-line no-duplicate-imports | ||
| import { CartesianAxis } from '../../../../../src/index'; | ||
| import { ComponentTypeEnum, type IComponent, type IComponentOption } from '../../../../../src/component/interface'; | ||
| import { EventDispatcher } from '../../../../../src/event/event-dispatcher'; | ||
| import { getTestCompiler } from '../../../../util/factory/compiler'; | ||
| import { getTheme, initChartDataSet } from '../../../../util/context'; | ||
| import { getCartesianAxisInfo } from '../../../../../src/component/axis/cartesian/util'; | ||
|
|
||
| const dataSet = new DataSet(); | ||
| initChartDataSet(dataSet); | ||
| dataSet.registerParser('csv', csvParser); | ||
| dataSet.registerTransform('dimensionStatistics', dimensionStatistics); | ||
|
|
||
| const ctx: IComponentOption = { | ||
| type: ComponentTypeEnum.cartesianLinearAxis, | ||
| eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any) as any, | ||
| dataSet, | ||
| map: new Map(), | ||
| mode: 'desktop-browser', | ||
| globalInstance: { | ||
| isAnimationEnable: () => true, | ||
| getContainer: () => ({}), | ||
| getTooltipHandlerByUser: (() => undefined) as () => undefined | ||
| } as any, | ||
| getCompiler: getTestCompiler, | ||
| getAllRegions: () => [], | ||
| getRegionsInIndex: () => [], | ||
| getChart: () => ({ getSpec: () => ({}) } as any), | ||
| getRegionsInIds: () => [], | ||
| getRegionsInUserIdOrIndex: () => [], | ||
| getAllSeries: () => [], | ||
| getSeriesInIndex: () => [], | ||
| getSeriesInIds: () => [], | ||
| getSeriesInUserIdOrIndex: () => [], | ||
| getAllComponents: () => [], | ||
| getComponentByIndex: () => undefined, | ||
| getComponentsByKey: () => [], | ||
| getComponentsByType: () => [], | ||
| getChartLayoutRect: () => ({ width: 0, height: 0, x: 0, y: 0 }), | ||
| getChartViewRect: () => ({ width: 500, height: 500 } as any), | ||
| globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any), | ||
| getTheme: getTheme, | ||
| getComponentByUserId: () => undefined, | ||
| animation: false, | ||
| onError: () => {}, | ||
| getSeriesData: () => undefined | ||
| }; | ||
|
|
||
| const getAxisSpec = (spec: any) => ({ | ||
| sampling: 'simple', | ||
| ...spec | ||
| }); | ||
|
|
||
| describe('LinearAxis customDistribution', () => { | ||
| beforeAll(() => { | ||
| // @ts-ignore | ||
| jest.spyOn(CartesianAxis.prototype, 'collectData').mockImplementation(() => { | ||
| return [{ min: 0, max: 10 }]; | ||
| }); | ||
| }); | ||
|
|
||
| test('should create piecewise domain and range from customDistribution', () => { | ||
| // Mock getNewScaleRange to return [0, 100] | ||
| // @ts-ignore | ||
| jest.spyOn(CartesianAxis.prototype, 'getNewScaleRange').mockReturnValue([0, 100]); | ||
|
|
||
| let spec = getAxisSpec({ | ||
| orient: 'left', | ||
| customDistribution: { | ||
| domain: [0, 5, 10], | ||
| ratio: [0.8, 0.2] | ||
| } | ||
| }); | ||
|
|
||
| const transformer = new CartesianAxis.transformerConstructor({ | ||
| type: 'cartesianAxis-linear', | ||
| getTheme: getTheme, | ||
| mode: 'desktop-browser' | ||
| }); | ||
| spec = transformer.transformSpec(spec, {}).spec; | ||
| const linearAxis = CartesianAxis.createComponent( | ||
| { | ||
| type: getCartesianAxisInfo(spec).componentName, | ||
| spec | ||
| }, | ||
| ctx | ||
| ) as CartesianLinearAxis; | ||
|
|
||
| linearAxis.created(); | ||
| linearAxis.init({}); | ||
|
|
||
| // Test Domain | ||
| // @ts-ignore | ||
| linearAxis.updateScaleDomain(); | ||
| const scale = linearAxis.getScale(); | ||
| expect(scale.domain()).toEqual([0, 5, 10]); | ||
|
|
||
| // Test Range | ||
| // @ts-ignore | ||
| const newRange = linearAxis.getNewScaleRange(); | ||
| // 0 -> 0 | ||
| // 5 -> 0 + 0.8 * 100 = 80 | ||
| // 10 -> 80 + 0.2 * 100 = 100 | ||
| expect(newRange).toEqual([0, 80, 100]); | ||
| }); | ||
|
|
||
| test('should handle gaps in customDistribution', () => { | ||
| let spec = getAxisSpec({ | ||
| orient: 'left', | ||
| customDistribution: { | ||
| domain: [0, 5, 8, 10], | ||
| ratio: [0.4, 0.4] | ||
| } | ||
| }); | ||
|
|
||
| const transformer = new CartesianAxis.transformerConstructor({ | ||
| type: 'cartesianAxis-linear', | ||
| getTheme: getTheme, | ||
| mode: 'desktop-browser' | ||
| }); | ||
| spec = transformer.transformSpec(spec, {}).spec; | ||
| const linearAxis = CartesianAxis.createComponent( | ||
| { | ||
| type: getCartesianAxisInfo(spec).componentName, | ||
| spec | ||
| }, | ||
| ctx | ||
| ) as CartesianLinearAxis; | ||
|
|
||
| linearAxis.created(); | ||
| linearAxis.init({}); | ||
|
|
||
| // @ts-ignore | ||
| linearAxis.updateScaleDomain(); | ||
| const scale = linearAxis.getScale(); | ||
| expect(scale.domain()).toEqual([0, 5, 8, 10]); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -131,6 +131,37 @@ export class CartesianLinearAxis< | |||||||||||||||||||||||||||
| newRange = combineDomains(this._break.scope).map(val => newRange[0] + (last(newRange) - newRange[0]) * val); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if ((this._spec as any).customDistribution?.domain?.length && this._scale) { | ||||||||||||||||||||||||||||
| const customDistribution = (this._spec as any).customDistribution as { | ||||||||||||||||||||||||||||
| domain: number[]; | ||||||||||||||||||||||||||||
| ratio: number[]; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| const domain = this._scale.domain(); | ||||||||||||||||||||||||||||
| if (domain.length > 2) { | ||||||||||||||||||||||||||||
| const start = newRange[0]; | ||||||||||||||||||||||||||||
| const end = last(newRange); | ||||||||||||||||||||||||||||
| const totalRange = end - start; | ||||||||||||||||||||||||||||
| const resultRange = [start]; | ||||||||||||||||||||||||||||
| let currentPos = start; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const segmentWeights: number[] = customDistribution.ratio; | ||||||||||||||||||||||||||||
| const totalWeight = segmentWeights.reduce((acc, cur) => acc + cur, 0); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (totalWeight > 0) { | ||||||||||||||||||||||||||||
| for (let i = 0; i < segmentWeights.length; i++) { | ||||||||||||||||||||||||||||
| const weight = segmentWeights[i]; | ||||||||||||||||||||||||||||
| const segmentLen = totalRange * (weight / totalWeight); | ||||||||||||||||||||||||||||
| currentPos += segmentLen; | ||||||||||||||||||||||||||||
| resultRange.push(currentPos); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| } | |
| } | |
| // If there are no gaps but the defined ratios sum to less than 1, | |
| // proportionally stretch all segments so that the final position reaches `end`. | |
| if (totalGapDomain === 0 && totalDefinedRatio > 0 && totalDefinedRatio < 1) { | |
| const occupiedRange = currentPos - start; | |
| if (occupiedRange !== 0) { | |
| const scale = totalRange / occupiedRange; | |
| for (let i = 1; i < resultRange.length; i++) { | |
| resultRange[i] = start + (resultRange[i] - start) * scale; | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
"should handle gaps in customDistribution"test verifies that the computed domain includes the gap boundary ([0, 5, 8, 10]), but it does not assert anything about the resulting range, so the gap allocation logic ingetNewScaleRange(which is relatively complex) is effectively untested here. To strengthen coverage of the new behavior, consider extending this test (or adding a new one) to stub the basegetNewScaleRangeand assert the expected piecewise range values for a configuration with a gap, similar to how the first test asserts the range for the no-gap case.