From dcb8df6dccec305a6597e53593bef913a396412e Mon Sep 17 00:00:00 2001 From: lxfu1 <954055752@qq.com> Date: Wed, 3 Sep 2025 20:40:34 +0800 Subject: [PATCH 1/2] feat: breaks supports the nice configuration --- __tests__/unit/scales/linear-breaks.spec.ts | 41 +++++++++++++++++++++ src/scales/continuous.ts | 4 +- src/scales/linear.ts | 13 ++++++- src/types.ts | 2 + 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/__tests__/unit/scales/linear-breaks.spec.ts b/__tests__/unit/scales/linear-breaks.spec.ts index 121a502..42f5e27 100644 --- a/__tests__/unit/scales/linear-breaks.spec.ts +++ b/__tests__/unit/scales/linear-breaks.spec.ts @@ -109,4 +109,45 @@ describe('Linear Scale with Breaks', () => { expect(scale2).toBeInstanceOf(Linear); expect(scale2.getOptions()).toEqual(scale.getOptions()); }); + test('breaks with nice', () => { + const scale = new Linear({ + domain: [0, 3106679], + breaks: [{ start: 5000, end: 50000, gap: 0.03 }], + }); + + const { domain, range } = scale.getOptions(); + expect(domain).toStrictEqual([0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3106679]); + expect(range).toStrictEqual([ + 1, 0.8, 0.7700000000000001, 0.6781128658609403, 0.5171692987914104, 0.35622573172188055, 0.1952821646523506, + 0.03433859758282076, 0, + ]); + scale.update({ + domain: [0, 3106679], + nice: true, + breaks: [{ start: 5000, end: 50000, gap: 0.03 }], + }); + const scaleOptions = scale.getOptions(); + expect(scaleOptions.domain).toStrictEqual([0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3500000]); + expect(scaleOptions.range).toStrictEqual([ + 1, 0.8, 0.7700000000000001, 0.7142857142857143, 0.5714285714285714, 0.4285714285714286, 0.2857142857142857, + 0.1428571428571429, 0, + ]); + scale.update({ + domain: [0, 3106679], + nice: true, + breaks: [ + { start: 5000, end: 50000, gap: 0.03 }, + { + start: 105000, + end: 3100000, + gap: 0.03, + }, + ], + }); + const scaleOptions2 = scale.getOptions(); + expect(scaleOptions2.domain).toStrictEqual([0, 5000, 50000, 105000, 3100000, 3106679]); + expect(scaleOptions2.range).toStrictEqual([ + 1, 0.8, 0.7700000000000001, 0.49917586754215676, 0.46917586754215673, 0, + ]); + }); }); diff --git a/src/scales/continuous.ts b/src/scales/continuous.ts index 6507cbb..1077d08 100644 --- a/src/scales/continuous.ts +++ b/src/scales/continuous.ts @@ -1,4 +1,4 @@ -import { identity } from '@antv/util'; +import { identity, isArray } from '@antv/util'; import { Base } from './base'; import { ContinuousOptions, Domain, Range, NiceMethod, TickMethodOptions, CreateTransform, Transform } from '../types'; import { @@ -110,7 +110,7 @@ export abstract class Continuous extends Base { } protected nice() { - if (!this.options.nice) return; + if (!this.options.nice || isArray(this.options.breaks)) return; const [min, max, tickCount, ...rest] = this.getTickMethodOptions(); this.options.domain = this.chooseNice()(min, max, tickCount, ...rest); } diff --git a/src/scales/linear.ts b/src/scales/linear.ts index fda97b6..736e3cd 100644 --- a/src/scales/linear.ts +++ b/src/scales/linear.ts @@ -58,8 +58,17 @@ export class Linear extends Continuous { protected transformDomain(options: LinearOptions): { breaksDomain: number[]; breaksRange: number[] } { const RANGE_LIMIT = [0.2, 0.8]; const DEFAULT_GAP = 0.03; - const { domain = [], range = [1, 0], breaks = [], tickCount = 5 } = options; - const [domainMin, domainMax] = [Math.min(...domain), Math.max(...domain)]; + const { domain = [], range = [1, 0], breaks = [], tickCount = 5, nice } = options; + const [min, max] = [Math.min(...domain), Math.max(...domain)]; + let niceDomainMin = min; + let niceDomainMax = max; + if (nice && breaks.length < 2) { + const niceDomain = this.chooseNice()(min, max, tickCount) as number[]; + niceDomainMin = niceDomain[0]; + niceDomainMax = niceDomain[niceDomain.length - 1]; + } + const domainMin = Math.min(niceDomainMin, min); + const domainMax = Math.max(niceDomainMax, max); const sortedBreaks = breaks.filter(({ end }) => end < domainMax).sort((a, b) => a.start - b.start); const breaksDomain = d3Ticks(domainMin, domainMax, tickCount, sortedBreaks); if (last(breaksDomain) < domainMax) { diff --git a/src/types.ts b/src/types.ts index 0f44ab3..d213627 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,6 +102,8 @@ export type ContinuousOptions = { round?: boolean; /** 插值器的工厂函数,返回一个对归一化后的输入在值域指定范围内插值的函数 */ interpolate?: Interpolates; + /** 断轴选项 */ + breaks?: BreakOptions[]; }; /** Linear 比例尺的选项 */ From 1a411efbf0df129024aa13c592fb56731bb4e87f Mon Sep 17 00:00:00 2001 From: lxfu1 <954055752@qq.com> Date: Thu, 4 Sep 2025 15:45:24 +0800 Subject: [PATCH 2/2] feat: optimize the maximum value logic and update the documentation --- __tests__/unit/scales/linear-breaks.spec.ts | 22 +++++----- docs/api/scales/linear.md | 48 +++++++++++++++++++++ src/scales/linear.ts | 7 ++- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/__tests__/unit/scales/linear-breaks.spec.ts b/__tests__/unit/scales/linear-breaks.spec.ts index 42f5e27..1fe5cd3 100644 --- a/__tests__/unit/scales/linear-breaks.spec.ts +++ b/__tests__/unit/scales/linear-breaks.spec.ts @@ -60,10 +60,8 @@ describe('Linear Scale with Breaks', () => { }); const { domain, range } = scale.getOptions(); - expect(domain).toStrictEqual([0, 200, 300, 500, 600, 800, 980]); - expect(range).toStrictEqual([ - 0, 0.20408163265306123, 0.35816326530612247, 0.45816326530612245, 0.6892857142857143, 0.7392857142857143, 1, - ]); + expect(domain).toStrictEqual([0, 200, 300, 500, 600, 800, 1000]); + expect(range).toStrictEqual([0, 0.2, 0.35000000000000003, 0.45, 0.6749999999999999, 0.725, 1]); }); test('single break: update', () => { @@ -109,17 +107,18 @@ describe('Linear Scale with Breaks', () => { expect(scale2).toBeInstanceOf(Linear); expect(scale2.getOptions()).toEqual(scale.getOptions()); }); - test('breaks with nice', () => { + test.only('breaks with nice', () => { const scale = new Linear({ domain: [0, 3106679], breaks: [{ start: 5000, end: 50000, gap: 0.03 }], }); const { domain, range } = scale.getOptions(); - expect(domain).toStrictEqual([0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3106679]); + expect(domain).toStrictEqual([0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3150000]); + expect(range).toStrictEqual([ - 1, 0.8, 0.7700000000000001, 0.6781128658609403, 0.5171692987914104, 0.35622573172188055, 0.1952821646523506, - 0.03433859758282076, 0, + 1, 0.8, 0.7700000000000001, 0.6825396825396826, 0.5238095238095238, 0.3650793650793651, 0.2063492063492064, + 0.04761904761904767, 0, ]); scale.update({ domain: [0, 3106679], @@ -145,9 +144,8 @@ describe('Linear Scale with Breaks', () => { ], }); const scaleOptions2 = scale.getOptions(); - expect(scaleOptions2.domain).toStrictEqual([0, 5000, 50000, 105000, 3100000, 3106679]); - expect(scaleOptions2.range).toStrictEqual([ - 1, 0.8, 0.7700000000000001, 0.49917586754215676, 0.46917586754215673, 0, - ]); + expect(scaleOptions2.domain).toStrictEqual([0, 5000, 50000, 105000, 3100000, 3108000]); + + expect(scaleOptions2.range).toStrictEqual([1, 0.8, 0.7700000000000001, 0.4993951093951094, 0.46939510939510937, 0]); }); }); diff --git a/docs/api/scales/linear.md b/docs/api/scales/linear.md index 124f0f5..9f32714 100644 --- a/docs/api/scales/linear.md +++ b/docs/api/scales/linear.md @@ -162,6 +162,45 @@ x2.getTicks(); // [0, 2.5, 5, 7.5, 10, 12.5, 15, 17.5] x3.getTicks(); // [2, 4.5, 7, 9.5, 12, 14.5] ``` +- Breaks + +```ts +import { Linear, LinearOptions } from '@antv/scale'; + +const options1: LinearOptions = { + domain: [0, 3106679], + breaks: [{ start: 5000, end: 50000, gap: 0.03 }], +}; + +const x1 = new Linear(options1); + +x1.getTicks(); // [0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3150000] + +// with nice +const options2: LinearOptions = { + domain: [0, 3106679], + nice: true, + breaks: [{ start: 5000, end: 50000, gap: 0.03 }], +}; + +const x2 = new Linear(options2); + +x2.getTicks(); // [0, 5000, 50000, 1000000, 1500000, 2000000, 2500000, 3000000, 3500000] + +// multi breaks +const options3:LinearOptions = { + domain: [0, 200], + breaks: [ + { start: 40, end: 100, gap: 0.1 }, + { start: 120, end: 160, gap: 0.1 }, + ] +} + +const x3 = new Linear(options3); + +x3.getTicks(); // [0, 40, 100, 120, 160, 200] +``` + ## Options | Key | Description | Type | Default| @@ -175,6 +214,15 @@ x3.getTicks(); // [2, 4.5, 7, 9.5, 12, 14.5] | clamp | Constrains the return value of map within the scale’s range if it is true. | `boolean` | `false` | | nice | Extends the domain so that it starts and ends on nice round values if it is true. | `boolean` | `false` | | interpolate | Sets the scale’s range interpolator factory if it is specified. | `(a: number, b: number) => (t: number) => T` | `(a, b) => (t) => a * (1 - t) + b * t` | +| breaks | Set linear breaks display and style. | [breaks](#breaks) | - | + +### breaks + +| Key | Description | Type | Default Value | +| ------- | ------- | ------- | ------- | +| start | start value. | `number` | - | +| end |end value. | `number` | - | +| gap | Proportion of the broken (0 ~ 1). | `number` | 0.03 | ## Methods diff --git a/src/scales/linear.ts b/src/scales/linear.ts index 736e3cd..33fbace 100644 --- a/src/scales/linear.ts +++ b/src/scales/linear.ts @@ -4,6 +4,7 @@ import { LinearOptions, Transform } from '../types'; import { Base } from './base'; import { createInterpolateValue } from '../utils'; import { d3Ticks } from '../tick-methods/d3-ticks'; +import { d3LinearNice } from '../utils/d3-linear-nice'; /** * Linear 比例尺 @@ -68,11 +69,13 @@ export class Linear extends Continuous { niceDomainMax = niceDomain[niceDomain.length - 1]; } const domainMin = Math.min(niceDomainMin, min); - const domainMax = Math.max(niceDomainMax, max); + let domainMax = Math.max(niceDomainMax, max); const sortedBreaks = breaks.filter(({ end }) => end < domainMax).sort((a, b) => a.start - b.start); const breaksDomain = d3Ticks(domainMin, domainMax, tickCount, sortedBreaks); if (last(breaksDomain) < domainMax) { - breaksDomain.push(domainMax); + const nicest = d3LinearNice(0, domainMax - last(breaksDomain), 3); + breaksDomain.push(last(breaksDomain) + last(nicest)); + domainMax = last(breaksDomain); } const [r0, r1] = [range[0], last(range)] as number[]; const diffDomain = domainMax - domainMin;