From 450d912f759c11a28860cad13cd377a8cd1f63d6 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 18 Aug 2023 17:07:06 +0800 Subject: [PATCH 1/9] feat(symLog): ticks caculation --- packages/vscale/__tests__/log.test.ts | 87 +++++++--------- packages/vscale/__tests__/symlog.test.ts | 120 ++++++++++++++++++++++- packages/vscale/package.json | 2 +- packages/vscale/src/log-scale.ts | 11 ++- packages/vscale/src/symlog-scale.ts | 71 +++++++++++++- packages/vscale/src/utils/tick-sample.ts | 84 +++++++++++++++- packages/vscale/src/utils/utils.ts | 44 +++++++++ 7 files changed, 360 insertions(+), 59 deletions(-) diff --git a/packages/vscale/__tests__/log.test.ts b/packages/vscale/__tests__/log.test.ts index ab69846..c3c300f 100644 --- a/packages/vscale/__tests__/log.test.ts +++ b/packages/vscale/__tests__/log.test.ts @@ -31,7 +31,7 @@ test('LogScale().domain(…) coerces values to numbers', () => { it('log.domain(…) can take negative values', () => { const x = new LogScale().domain([-100, -1]); - expect(x.ticks()).toEqual([-100, -90, -80, -70, -60, -50, -40, -30, -20, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1]); + expect(x.ticks()).toEqual([-90, -55, -33, -20, -12, -7, -4, -3, -2, -1]); expect(x.scale(-50)).toBeCloseTo(0.150515, 5); }); @@ -50,8 +50,8 @@ it('log.domain(…) preserves specified domain exactly, with no floating point e it('log.ticks(…) returns exact ticks, with no floating point error', () => { expect(new LogScale().domain([0.15, 0.68]).ticks()).toEqual([0.2, 0.3, 0.4, 0.5, 0.6]); expect(new LogScale().domain([0.68, 0.15]).ticks()).toEqual([0.6, 0.5, 0.4, 0.3, 0.2]); - expect(new LogScale().domain([-0.15, -0.68]).ticks()).toEqual([-0.2, -0.3, -0.4, -0.5, -0.6]); - expect(new LogScale().domain([-0.68, -0.15]).ticks()).toEqual([-0.6, -0.5, -0.4, -0.3, -0.2]); + expect(new LogScale().domain([-0.15, -0.68]).ticks()).toEqual([-0.2, -0.3, -0.4, -0.5, -0.68]); + expect(new LogScale().domain([-0.68, -0.15]).ticks()).toEqual([-0.68, -0.5, -0.4, -0.3, -0.2]); }); it('log.range(…) does not coerce values to numbers', () => { @@ -133,7 +133,7 @@ it('log.invert(y) coerces y to number', () => { it('log.base(b) sets the log base, changing the ticks', () => { const x = new LogScale().domain([1, 32]); - expect(x.base(2).ticks().map(x.tickFormat())).toEqual([1, 2, 4, 8, 16, 32]); + expect(x.base(2).ticks().map(x.tickFormat())).toEqual([1, 2, 3, 4, 6, 8, 11, 16, 23, 32]); }); it('log.nice() nices the domain, extending it to powers of ten', () => { @@ -239,49 +239,43 @@ it('log.clone() isolates changes to clamping', () => { it('log.ticks() generates the expected power-of-ten for ascending ticks', () => { const s = new LogScale(); - expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([ - 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 - ]); - expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]); - expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1]); + expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0.1, 1, 2, 3, 4, 6, 10]); + expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1]); + expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-1, -0.8, -0.7, -0.5, -0.4, -0.3, -0.2, -0.1]); }); it('log.ticks() generates the expected power-of-ten ticks for descending domains', () => { const s = new LogScale(); - expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual( - [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse() - ); - expect(s.domain([-1e-1, -1]).ticks().map(round)).toEqual( - [-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse() - ); - expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1].reverse()); + expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-7, -4, -3, -2, -1, -0.1].reverse()); + expect(s.domain([-1e-1, -1]).ticks().map(round)).toEqual([-1, -0.8, -0.7, -0.5, -0.4, -0.3, -0.2, -0.1].reverse()); + expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1].reverse()); }); it('log.ticks() generates the expected power-of-ten ticks for small domains', () => { const s = new LogScale(); - expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4, 5]); - expect(s.domain([5, 1]).ticks()).toEqual([5, 4, 3, 2, 1]); + expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4]); + expect(s.domain([5, 1]).ticks()).toEqual([4, 3, 2, 1]); expect(s.domain([-1, -5]).ticks()).toEqual([-1, -2, -3, -4, -5]); expect(s.domain([-5, -1]).ticks()).toEqual([-5, -4, -3, -2, -1]); - expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([300]); - expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([300]); - expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([280, 320, 360]); - expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([280, 300, 320, 340]); - expect(s.domain([286.9252014, 329.4978332]).ticks()).toEqual([285, 290, 295, 300, 305, 310, 315, 320, 325, 330]); + expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([]); + expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([288, 302, 316]); + expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([288, 302, 316]); + expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([288, 302, 316]); + expect(s.domain([286.9252014, 329.4978332]).ticks()).toEqual([ + 288, 292, 295, 299, 302, 305, 309, 313, 316, 320, 324, 327 + ]); }); it('log.ticks() generates linear ticks when the domain extent is small', () => { const s = new LogScale(); - expect(s.domain([41, 42]).ticks()).toEqual([41, 41.1, 41.2, 41.3, 41.4, 41.5, 41.6, 41.7, 41.8, 41.9, 42]); - expect(s.domain([42, 41]).ticks()).toEqual([42, 41.9, 41.8, 41.7, 41.6, 41.5, 41.4, 41.3, 41.2, 41.1, 41]); - expect(s.domain([1600, 1400]).ticks()).toEqual([1760, 1720, 1680, 1640, 1600, 1560, 1520, 1480, 1440, 1400]); + expect(s.domain([41, 42]).ticks()).toEqual([41, 42]); + expect(s.domain([42, 41]).ticks()).toEqual([42, 41]); + expect(s.domain([1600, 1400]).ticks()).toEqual([1585, 1567, 1549, 1531, 1514, 1496, 1479, 1462, 1445, 1429, 1413]); }); it('log.base(base).ticks() generates the expected power-of-base ticks', () => { const s = new LogScale().base(Math.E); - expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([ - 0.018315638889, 0.135335283237, 1, 7.389056098931, 54.598150033144, 403.428793492735 - ]); + expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0.2, 0.5, 1, 5, 10, 50, 100]); }); it('log.ticks() returns the empty array when the domain is degenerate', () => { @@ -291,7 +285,7 @@ it('log.ticks() returns the empty array when the domain is degenerate', () => { expect(x.domain([0, -1]).ticks()).toEqual([]); expect(x.domain([-1, 0]).ticks()).toEqual([]); expect(x.domain([-1, 1]).ticks()).toEqual([]); - expect(x.domain([0, 0]).ticks()).toEqual([]); + expect(x.domain([0, 0]).ticks()).toEqual([0]); }); it('log.forceTicks() generates the expected power-of-ten for ascending ticks', () => { @@ -325,30 +319,23 @@ it('log.forceTicks() return right tick count when the domain extent is small', ( it('log.forceTicks() return right tick count when the domain is degenerate', () => { const x = new LogScale(); - expect(x.domain([0, 1]).forceTicks()).toHaveLength(10); - expect(x.domain([1, 0]).forceTicks()).toHaveLength(10); - expect(x.domain([0, -1]).forceTicks()).toHaveLength(10); - expect(x.domain([-1, 0]).forceTicks()).toHaveLength(10); - expect(x.domain([-1, 1]).forceTicks()).toHaveLength(10); + expect(x.domain([0, 1]).forceTicks()).toHaveLength(0); + expect(x.domain([1, 0]).forceTicks()).toHaveLength(0); + expect(x.domain([0, -1]).forceTicks()).toHaveLength(0); + expect(x.domain([-1, 0]).forceTicks()).toHaveLength(0); + expect(x.domain([-1, 1]).forceTicks()).toHaveLength(0); expect(x.domain([0, 0]).forceTicks()).toHaveLength(1); }); -it('log.forceTicks() return right tick count as input params', () => { - const s = new LogScale(); - expect(s.domain([286.9252014, 329.4978332]).forceTicks(1)).toHaveLength(1); - expect(s.domain([286.9252014, 329.4978332]).forceTicks(2)).toHaveLength(2); - expect(s.domain([286.9252014, 329.4978332]).forceTicks(3)).toHaveLength(3); - expect(s.domain([286.9252014, 329.4978332]).forceTicks(4)).toHaveLength(4); - expect(s.domain([286.9252014, 329.4978332]).forceTicks()).toHaveLength(10); -}); +// function round(x: number) { +// return Math.round(x * 1e12) / 1e12; +// } -it('log.stepTicks() return right tick as input params', () => { - const s = new LogScale(); - expect(s.domain([286.9252014, 287.4978332]).stepTicks(1)).toEqual([286.9252014]); - expect(s.domain([286.9252014, 288.4978332]).stepTicks(1)).toEqual([286.9252014, 287.9252014]); - expect(s.domain([286.9252014, 289.4978332]).stepTicks(2)).toEqual([286.9252014, 288.9252014]); - expect(s.domain([286.9252014, 290.4978332]).stepTicks(2)).toEqual([286.9252014, 288.9252014]); - expect(s.domain([286.9252014, 291.4978332]).stepTicks(2)).toEqual([286.9252014, 288.9252014, 290.9252014]); +it('log.ticks(…) ', () => { + // expect(new LogScale().domain([1, 1000]).ticks(4)).toEqual([1, 10, 100, 1000]); + expect(new LogScale().domain([1, 16000]).base(Math.E).ticks(6)).toEqual([1, 10, 50, 500, 5000, 16000]); + expect(new LogScale().domain([2, 2048]).base(2).ticks()).toEqual([2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048]); + expect(new LogScale().domain([0, 1]).ticks()).toEqual([]); }); function round(x: number) { diff --git a/packages/vscale/__tests__/symlog.test.ts b/packages/vscale/__tests__/symlog.test.ts index e61cbda..11da46d 100644 --- a/packages/vscale/__tests__/symlog.test.ts +++ b/packages/vscale/__tests__/symlog.test.ts @@ -130,8 +130,8 @@ it('symlog.clone() returns a copy with changes to the domain are isolated', () = expect(y.domain()).toEqual([2, 3]); const y2 = x.domain([1, 1.9]).clone(); - x.nice(5); - expect(x.domain()).toEqual([1, 2]); + x.nice(); + expect(x.domain()).toEqual([0, 6.38905609893065]); expect(y2.domain()).toEqual([1, 1.9]); }); @@ -167,3 +167,119 @@ it('symlog().clamp(true).invert(x) cannot return a value outside the domain', () expect(x.invert(0)).toBe(1); expect(x.invert(1)).toBe(20); }); + +it('symlog.ticks() with positive domain', () => { + const x = new SymlogScale().domain([10, 100]).constant(10); + expect(x.ticks(3)).toEqual([17, 35, 64]); + expect(x.ticks(4)).toEqual([17, 35, 64]); + expect(x.ticks(5)).toEqual([17, 35, 64]); + expect(x.ticks(6)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); + expect(x.ticks(7)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); + expect(x.ticks(8)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); + expect(x.ticks(9)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); + expect(x.ticks(10)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); +}); + +it('symlog.ticks() can take negative values', () => { + const x = new SymlogScale().domain([-100, -1]); + expect(x.ticks()).toEqual([-89, -54, -32, -19, -11, -6, -3, -2]); + expect(x.scale(-50)).toBeCloseTo(0.1742222155862007, 5); +}); + +it('log.ticks() generates the expected power-of-ten for ascending ticks', () => { + const s = new SymlogScale(); + expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0.1, 1, 2, 3, 4, 5, 6, 8]); + expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]); + expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1]); +}); + +it('symLog.ticks() generates the expected power-of-ten ticks for descending domains', () => { + const s = new SymlogScale(); + expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-8, -6, -5, -4, -3, -2, -1, -0.1].reverse()); + expect(s.domain([-1e-1, -1]).ticks().map(round)).toEqual( + [-0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse() + ); + expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].reverse()); +}); + +it('symLog.ticks() generates the expected power-of-ten ticks for small domains', () => { + const s = new SymlogScale(); + expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4]); + expect(s.domain([5, 1]).ticks()).toEqual([4, 3, 2, 1]); + expect(s.domain([-1, -5]).ticks()).toEqual([-1, -2, -3, -4]); + expect(s.domain([-5, -1]).ticks()).toEqual([-4, -3, -2, -1]); + expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([298, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([298, 313, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([298, 313, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([298, 313, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks()).toEqual([ + 289, 292, 295, 298, 301, 304, 307, 310, 313, 316, 320, 323, 326, 329 + ]); +}); + +it('symLog.ticks() generates linear ticks when the domain extent is small', () => { + const s = new SymlogScale(); + expect(s.domain([41, 42]).ticks()).toEqual([41, 42]); + expect(s.domain([42, 41]).ticks()).toEqual([42, 41]); + expect(s.domain([1600, 1400]).ticks()).toEqual([ + 1587, 1571, 1555, 1540, 1524, 1509, 1494, 1479, 1465, 1450, 1436, 1421, 1407 + ]); +}); + +it('symLog.base(base).ticks() generates the expected power-of-base ticks', () => { + const s = new SymlogScale().constant(Math.E); + expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0.1, 2, 5, 10, 20, 50, 100]); +}); + +it('symLog.ticks() returns the empty array when the domain is degenerate', () => { + const x = new SymlogScale(); + expect(x.domain([0, 1]).ticks()).toEqual([0, 1]); + expect(x.domain([1, 0]).ticks()).toEqual([1, 0]); + expect(x.domain([0, -1]).ticks()).toEqual([0, -1]); + expect(x.domain([-1, 0]).ticks()).toEqual([-1, -0]); + expect(x.domain([-1, 1]).ticks()).toEqual([-1, -0, 1]); + expect(x.domain([0, 0]).ticks()).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +}); + +it('symLog.forceTicks() generates the expected power-of-ten for ascending ticks', () => { + const s = new SymlogScale(); + expect(s.domain([1e-1, 1e1]).forceTicks().map(round)).toHaveLength(10); + expect(s.domain([1e-1, 1]).forceTicks().map(round)).toHaveLength(10); + expect(s.domain([-1, -1e-1]).forceTicks().map(round)).toHaveLength(10); +}); + +it('symLog.forceTicks() return right tick count for descending domains', () => { + const s = new SymlogScale(); + expect(s.domain([-1e-1, -1e1]).forceTicks().map(round)).toHaveLength(10); + expect(s.domain([-1e-1, -1]).forceTicks().map(round)).toHaveLength(10); + expect(s.domain([1, 1e-1]).forceTicks().map(round)).toHaveLength(10); +}); + +it('symLog.forceTicks() return right tick count for small domains', () => { + const s = new SymlogScale(); + expect(s.domain([1, 5]).forceTicks()).toHaveLength(10); + expect(s.domain([5, 1]).forceTicks()).toHaveLength(10); + expect(s.domain([-1, -5]).forceTicks()).toHaveLength(10); + expect(s.domain([-5, -1]).forceTicks()).toHaveLength(10); +}); + +it('symLog.forceTicks() return right tick count when the domain extent is small', () => { + const s = new SymlogScale(); + expect(s.domain([41, 42]).forceTicks()).toHaveLength(10); + expect(s.domain([42, 41]).forceTicks()).toHaveLength(10); + expect(s.domain([1600, 1400]).forceTicks()).toHaveLength(10); +}); + +it('symLog.forceTicks() return right tick count when the domain is degenerate', () => { + const x = new SymlogScale(); + expect(x.domain([0, 1]).forceTicks()).toHaveLength(10); + expect(x.domain([1, 0]).forceTicks()).toHaveLength(10); + expect(x.domain([0, -1]).forceTicks()).toHaveLength(10); + expect(x.domain([-1, 0]).forceTicks()).toHaveLength(10); + expect(x.domain([-1, 1]).forceTicks()).toHaveLength(10); + expect(x.domain([0, 0]).forceTicks()).toHaveLength(1); +}); + +function round(x: number) { + return Math.round(x * 1e12) / 1e12; +} diff --git a/packages/vscale/package.json b/packages/vscale/package.json index 40c1b4e..bb99ec6 100644 --- a/packages/vscale/package.json +++ b/packages/vscale/package.json @@ -29,7 +29,7 @@ "build": "bundle --clean", "dev": "bundle --clean -f es -w", "start": "vite ./vite", - "test": "jest", + "test": "jest __tests__/symLog.test.ts", "test-check": "DEBUG=jest jest --forceExit --detectOpenHandles --silent false --verbose false --runInBand", "test-cov": "jest -w 16 --coverage" }, diff --git a/packages/vscale/src/log-scale.ts b/packages/vscale/src/log-scale.ts index 40eee67..59ce501 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -1,9 +1,10 @@ -import { ticks, forceTicks, stepTicks } from './utils/tick-sample'; +import { ticks, forceTicks, stepTicks, ticksBaseTransform, forceTicksBaseTransform } from './utils/tick-sample'; import { ContinuousScale } from './continuous-scale'; import { ScaleEnum } from './type'; import { logp, nice, powp, logNegative, expNegative, identity } from './utils/utils'; import type { ContinuousScaleType } from './interface'; import { cloneDeep } from '@visactor/vutils'; +import { LinearScale } from './linear-scale'; /** * 逆反函数 @@ -138,7 +139,9 @@ export class LogScale extends ContinuousScale { } ticks(count: number = 10) { - return this.d3Ticks(count); + // return this.d3Ticks(count); + const d = this.calculateVisibleDomain(this._range); + return ticksBaseTransform(d[0], d[1], count, this._base, this.transformer, this.untransformer); } /** @@ -147,7 +150,7 @@ export class LogScale extends ContinuousScale { */ forceTicks(count: number = 10): any[] { const d = this.calculateVisibleDomain(this._range); - return forceTicks(d[0], d[d.length - 1], count); + return forceTicksBaseTransform(d[0], d[1], count, this.transformer, this.untransformer); } /** @@ -156,7 +159,7 @@ export class LogScale extends ContinuousScale { */ stepTicks(step: number): any[] { const d = this.calculateVisibleDomain(this._range); - return stepTicks(d[0], d[d.length - 1], step); + return forceTicksBaseTransform(d[0], d[1], step, this.transformer, this.untransformer); } nice(): this { diff --git a/packages/vscale/src/symlog-scale.ts b/packages/vscale/src/symlog-scale.ts index e67bce7..399dde4 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -1,7 +1,9 @@ +import { cloneDeep } from '@visactor/vutils'; import type { ContinuousScaleType } from './interface'; import { LinearScale } from './linear-scale'; import { ScaleEnum } from './type'; -import { symlog, symexp } from './utils/utils'; +import { forceTicksBaseTransform, stepTicks, ticksBaseTransform } from './utils/tick-sample'; +import { symlog, symexp, nice } from './utils/utils'; export class SymlogScale extends LinearScale { readonly type: ContinuousScaleType = ScaleEnum.Symlog; @@ -36,4 +38,71 @@ export class SymlogScale extends LinearScale { return this.rescale(slience); } + + ticks(count: number = 10) { + // return this.d3Ticks(count); + const d = this.calculateVisibleDomain(this._range); + return ticksBaseTransform(d[0], d[1], count, this._const, this.transformer, this.untransformer); + } + + /** + * 生成tick数组,这个tick数组的长度就是count的长度 + * @param count + */ + forceTicks(count: number = 10): any[] { + const d = this.calculateVisibleDomain(this._range); + return forceTicksBaseTransform(d[0], d[1], count, this.transformer, this.untransformer); + } + + /** + * 基于给定step的ticks数组生成 + * @param step + */ + stepTicks(step: number): any[] { + const d = this.calculateVisibleDomain(this._range); + return forceTicksBaseTransform(d[0], d[1], step, this.transformer, this.untransformer); + } + + nice(): this { + return this.domain( + nice(this.domain(), { + floor: (x: number) => this.untransformer(Math.floor(this.transformer(x))), + ceil: (x: number) => this.untransformer(Math.ceil(this.transformer(x))) + }) + ); + } + + /** + * 只对min区间进行nice + * 如果保持某一边界的值,就很难有好的nice效果,所以这里实现就是nice之后还原固定的边界值 + */ + niceMin(): this { + const maxD = this._domain[this._domain.length - 1]; + this.nice(); + const niceDomain = cloneDeep(this._domain); + + if (this._domain) { + niceDomain[niceDomain.length - 1] = maxD; + this.domain(niceDomain); + } + + return this; + } + + /** + * 只对max区间进行nice + * 如果保持某一边界的值,就很难有好的nice效果,所以这里实现就是nice之后还原固定的边界值 + */ + niceMax(): this { + const minD = this._domain[0]; + this.nice(); + const niceDomain = cloneDeep(this._domain); + + if (this._domain) { + niceDomain[0] = minD; + this.domain(niceDomain); + } + + return this; + } } diff --git a/packages/vscale/src/utils/tick-sample.ts b/packages/vscale/src/utils/tick-sample.ts index a7aa5c3..a68ad90 100644 --- a/packages/vscale/src/utils/tick-sample.ts +++ b/packages/vscale/src/utils/tick-sample.ts @@ -1,4 +1,7 @@ -import { range, memoize } from '@visactor/vutils'; +import { range, memoize, isValid } from '@visactor/vutils'; +import { LinearScale } from '../linear-scale'; +import type { TransformType } from '../interface'; +import { niceNumber, restrictNumber } from './utils'; const e10 = Math.sqrt(50); const e5 = Math.sqrt(10); @@ -6,6 +9,23 @@ const e2 = Math.sqrt(2); const niceNumbers = [1, 2, 5, 10]; type TicksFunc = (start: number, stop: number, count: number) => number[]; +// eslint-disable-next-line max-len +type TicksBaseTransformFunc = ( + start: number, + stop: number, + count: number, + base: number, + transformer: TransformType, + untransformer: TransformType +) => number[]; +// eslint-disable-next-line max-len +type ForceTicksBaseTransformFunc = ( + start: number, + stop: number, + count: number, + transformer: TransformType, + untransformer: TransformType +) => number[]; export const calculateTicksOfSingleValue = (value: number, tickCount: number, allowDecimals?: boolean) => { let step = 1; @@ -381,3 +401,65 @@ export function niceLinear(d: number[], count: number = 10) { return; } + +export const ticksBaseTransform = memoize( + ( + start: number, + stop: number, + count: number, + base: number, + transformer: TransformType, + untransformer: TransformType + ) => { + const ticksResult: number[] = []; + const ticksMap = {}; + const startExp = transformer(start); + const stopExp = transformer(stop); + let ticksExp = []; + // get ticks exp + if (Number.isInteger(base)) { + ticksExp = new LinearScale().domain([startExp, stopExp]).ticks(count); + } else { + const stepExp = (stopExp - startExp) / (count - 1); + for (let i = 0; i < count; i++) { + ticksExp.push(startExp + i * stepExp); + } + } + ticksExp.forEach((tl: number) => { + // get pow + const power = untransformer(tl); + // nice + const nicePower = Number.isInteger(base) + ? Math.abs(stop - start) < 1 + ? +power.toFixed(1) + : Math.round(+power) + : niceNumber(power); + // scope + const scopeExp = restrictNumber(nicePower, [start, stop]); + // dedupe + if (!ticksMap[nicePower] && !isNaN(nicePower) && ticksExp.length > 1) { + ticksMap[nicePower] = 1; + ticksResult.push(scopeExp); + } + }); + return ticksResult; + } +); + +export const forceTicksBaseTransform = memoize( + (start: number, stop: number, count: number, transformer: TransformType, untransformer: TransformType) => { + const startExp = transformer(start); + const stopExp = transformer(stop); + const ticksExp = forceTicks(startExp, stopExp, count); + return ticksExp.map((te: number) => niceNumber(untransformer(te))); + } +); + +export const forceStepTicksBaseTransform = memoize( + (start: number, stop: number, step: number, transformer: TransformType, untransformer: TransformType) => { + const startExp = transformer(start); + const stopExp = transformer(stop); + const ticksExp = stepTicks(startExp, stopExp, step); + return ticksExp.map((te: number) => niceNumber(untransformer(te))); + } +); diff --git a/packages/vscale/src/utils/utils.ts b/packages/vscale/src/utils/utils.ts index 4cf28b1..fc6bad8 100644 --- a/packages/vscale/src/utils/utils.ts +++ b/packages/vscale/src/utils/utils.ts @@ -160,3 +160,47 @@ export const nice = (domain: number[] | Date[], options: FloorCeilType) => return newDomain; }; + +export const niceNumber = (value: number, round: boolean = false) => { + const exponent = Math.floor(Math.log10(value)); + const fraction = value / Math.pow(10, exponent); + + let niceFraction: number; + + if (round) { + if (fraction < 1.5) { + niceFraction = 1; + } else if (fraction < 3) { + niceFraction = 2; + } else if (fraction < 7) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } else { + if (fraction <= 1) { + niceFraction = 1; + } else if (fraction <= 2) { + niceFraction = 2; + } else if (fraction <= 5) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } + + return niceFraction * Math.pow(10, exponent); +}; + +export const restrictNumber = (value: number, domain: [number, number]) => { + let min; + let max; + if (domain[0] < domain[1]) { + min = domain[0]; + max = domain[1]; + } else { + min = domain[1]; + max = domain[0]; + } + return Math.min(Math.max(value, min), max); +}; From 46e27ed94c1af6791d6de565b7f7dd67001037ba Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 18 Aug 2023 17:24:12 +0800 Subject: [PATCH 2/9] chore: update rush change log --- .../vdataset/feat-symLog-ticks_2023-08-18-09-23.json | 10 ++++++++++ .../vscale/feat-symLog-ticks_2023-08-18-09-23.json | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json create mode 100644 common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json diff --git a/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json b/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json new file mode 100644 index 0000000..a3988e2 --- /dev/null +++ b/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vdataset", + "comment": "feat(symLog): ticks caculation. feat visactor/vchart#508", + "type": "none" + } + ], + "packageName": "@visactor/vdataset" +} \ No newline at end of file diff --git a/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json b/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json new file mode 100644 index 0000000..f3db4ab --- /dev/null +++ b/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vscale", + "comment": "feat(symLog): ticks caculation. feat visactor/vchart#508", + "type": "none" + } + ], + "packageName": "@visactor/vscale" +} \ No newline at end of file From bd52c727351d06de56fb9681e4695f513081fb31 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 18 Aug 2023 17:25:42 +0800 Subject: [PATCH 3/9] chore: update rush change log --- .../@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json | 2 +- .../@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json b/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json index a3988e2..7b21f28 100644 --- a/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json +++ b/common/changes/@visactor/vdataset/feat-symLog-ticks_2023-08-18-09-23.json @@ -3,7 +3,7 @@ { "packageName": "@visactor/vdataset", "comment": "feat(symLog): ticks caculation. feat visactor/vchart#508", - "type": "none" + "type": "patch" } ], "packageName": "@visactor/vdataset" diff --git a/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json b/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json index f3db4ab..776055f 100644 --- a/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json +++ b/common/changes/@visactor/vscale/feat-symLog-ticks_2023-08-18-09-23.json @@ -3,7 +3,7 @@ { "packageName": "@visactor/vscale", "comment": "feat(symLog): ticks caculation. feat visactor/vchart#508", - "type": "none" + "type": "patch" } ], "packageName": "@visactor/vscale" From f6e2093a90b9722d302222d774e54e8cb26206e2 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 18 Aug 2023 18:19:45 +0800 Subject: [PATCH 4/9] fix(symLog): fix some merge error --- packages/vscale/src/utils/tick-sample.ts | 54 ++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/vscale/src/utils/tick-sample.ts b/packages/vscale/src/utils/tick-sample.ts index e025326..e09136d 100644 --- a/packages/vscale/src/utils/tick-sample.ts +++ b/packages/vscale/src/utils/tick-sample.ts @@ -1,8 +1,7 @@ -import { range, memoize } from '@visactor/vutils'; +import { range, memoize, isNumber } from '@visactor/vutils'; import { LinearScale } from '../linear-scale'; -import type { TransformType } from '../interface'; +import type { TransformType, ContinuousTicksFunc, NiceOptions, NiceType } from '../interface'; import { niceNumber, restrictNumber } from './utils'; -import type { ContinuousTicksFunc } from '../interface'; const e10 = Math.sqrt(50); const e5 = Math.sqrt(10); @@ -424,6 +423,55 @@ export function niceLinear(d: number[], count: number = 10) { return; } +export function parseNiceOptions(originalDomain: number[], option: NiceOptions) { + const hasForceMin = isNumber(option.forceMin); + const hasForceMax = isNumber(option.forceMax); + let niceType: NiceType = null; + const niceMinMax = []; + let niceDomain: number[] = null; + + const domainValidator = + hasForceMin && hasForceMax + ? (x: number) => x >= option.forceMin && x <= option.forceMax + : hasForceMin + ? (x: number) => x >= option.forceMin + : hasForceMax + ? (x: number) => x <= option.forceMax + : null; + + if (hasForceMin) { + niceMinMax[0] = option.forceMin; + } else if ( + isNumber(option.min) && + option.min <= Math.min(originalDomain[0], originalDomain[originalDomain.length - 1]) + ) { + niceMinMax[0] = option.min; + } + + if (hasForceMax) { + niceMinMax[1] = option.forceMax; + } else if ( + isNumber(option.max) && + option.max >= Math.max(originalDomain[0], originalDomain[originalDomain.length - 1]) + ) { + niceMinMax[1] = option.max; + } + + if (isNumber(niceMinMax[0]) && isNumber(niceMinMax[1])) { + niceDomain = originalDomain.slice(); + niceDomain[0] = niceMinMax[0]; + niceDomain[niceDomain.length - 1] = niceMinMax[1]; + } else if (!isNumber(niceMinMax[0]) && !isNumber(niceMinMax[1])) { + niceType = 'all'; + } else if (!isNumber(niceMinMax[0])) { + niceType = 'min'; + } else { + niceType = 'max'; + } + + return { niceType, niceDomain, niceMinMax, domainValidator }; +} + export const ticksBaseTransform = memoize( ( start: number, From e1f7b59d4dfb0422dfaeb2b2121a062ccc3fc17c Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 21 Aug 2023 21:18:29 +0800 Subject: [PATCH 5/9] chore: ticks reconfig --- packages/vscale/__tests__/log.test.ts | 34 ++++++++------- packages/vscale/__tests__/symlog.test.ts | 54 ++++++++++++------------ packages/vscale/src/log-scale.ts | 27 +++++++----- packages/vscale/src/symlog-scale.ts | 23 +++++----- packages/vscale/src/utils/tick-sample.ts | 20 +++++---- 5 files changed, 85 insertions(+), 73 deletions(-) diff --git a/packages/vscale/__tests__/log.test.ts b/packages/vscale/__tests__/log.test.ts index c3c300f..e69800d 100644 --- a/packages/vscale/__tests__/log.test.ts +++ b/packages/vscale/__tests__/log.test.ts @@ -31,7 +31,7 @@ test('LogScale().domain(…) coerces values to numbers', () => { it('log.domain(…) can take negative values', () => { const x = new LogScale().domain([-100, -1]); - expect(x.ticks()).toEqual([-90, -55, -33, -20, -12, -7, -4, -3, -2, -1]); + expect(x.ticks()).toEqual([-100, -90, -55, -33, -20, -12, -7, -4, -3, -2, -1]); expect(x.scale(-50)).toBeCloseTo(0.150515, 5); }); @@ -48,10 +48,10 @@ it('log.domain(…) preserves specified domain exactly, with no floating point e }); it('log.ticks(…) returns exact ticks, with no floating point error', () => { - expect(new LogScale().domain([0.15, 0.68]).ticks()).toEqual([0.2, 0.3, 0.4, 0.5, 0.6]); - expect(new LogScale().domain([0.68, 0.15]).ticks()).toEqual([0.6, 0.5, 0.4, 0.3, 0.2]); - expect(new LogScale().domain([-0.15, -0.68]).ticks()).toEqual([-0.2, -0.3, -0.4, -0.5, -0.68]); - expect(new LogScale().domain([-0.68, -0.15]).ticks()).toEqual([-0.68, -0.5, -0.4, -0.3, -0.2]); + expect(new LogScale().domain([0.15, 0.68]).ticks()).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]); + expect(new LogScale().domain([0.68, 0.15]).ticks()).toEqual([0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1]); + expect(new LogScale().domain([-0.15, -0.68]).ticks()).toEqual([-0.1, -0.2, -0.3, -0.4, -0.5, -0.7]); + expect(new LogScale().domain([-0.68, -0.15]).ticks()).toEqual([-0.7, -0.5, -0.4, -0.3, -0.2, -0.1]); }); it('log.range(…) does not coerce values to numbers', () => { @@ -239,30 +239,30 @@ it('log.clone() isolates changes to clamping', () => { it('log.ticks() generates the expected power-of-ten for ascending ticks', () => { const s = new LogScale(); - expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0.1, 1, 2, 3, 4, 6, 10]); + expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0, 1, 2, 3, 4, 6, 10]); expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1]); expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-1, -0.8, -0.7, -0.5, -0.4, -0.3, -0.2, -0.1]); }); it('log.ticks() generates the expected power-of-ten ticks for descending domains', () => { const s = new LogScale(); - expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-7, -4, -3, -2, -1, -0.1].reverse()); + expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-10, -7, -4, -3, -2, -1, -0].reverse()); expect(s.domain([-1e-1, -1]).ticks().map(round)).toEqual([-1, -0.8, -0.7, -0.5, -0.4, -0.3, -0.2, -0.1].reverse()); expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1].reverse()); }); it('log.ticks() generates the expected power-of-ten ticks for small domains', () => { const s = new LogScale(); - expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4]); - expect(s.domain([5, 1]).ticks()).toEqual([4, 3, 2, 1]); + expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4, 5]); + expect(s.domain([5, 1]).ticks()).toEqual([5, 4, 3, 2, 1]); expect(s.domain([-1, -5]).ticks()).toEqual([-1, -2, -3, -4, -5]); expect(s.domain([-5, -1]).ticks()).toEqual([-5, -4, -3, -2, -1]); - expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([]); - expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([288, 302, 316]); - expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([288, 302, 316]); - expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([288, 302, 316]); + expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([287, 316, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([287, 288, 302, 316, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([287, 288, 302, 316, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([287, 288, 302, 316, 329]); expect(s.domain([286.9252014, 329.4978332]).ticks()).toEqual([ - 288, 292, 295, 299, 302, 305, 309, 313, 316, 320, 324, 327 + 287, 288, 292, 295, 299, 302, 305, 309, 313, 316, 320, 324, 327, 329 ]); }); @@ -270,12 +270,14 @@ it('log.ticks() generates linear ticks when the domain extent is small', () => { const s = new LogScale(); expect(s.domain([41, 42]).ticks()).toEqual([41, 42]); expect(s.domain([42, 41]).ticks()).toEqual([42, 41]); - expect(s.domain([1600, 1400]).ticks()).toEqual([1585, 1567, 1549, 1531, 1514, 1496, 1479, 1462, 1445, 1429, 1413]); + expect(s.domain([1600, 1400]).ticks()).toEqual([ + 1600, 1585, 1567, 1549, 1531, 1514, 1496, 1479, 1462, 1445, 1429, 1413, 1400 + ]); }); it('log.base(base).ticks() generates the expected power-of-base ticks', () => { const s = new LogScale().base(Math.E); - expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0.2, 0.5, 1, 5, 10, 50, 100]); + expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0, 1, 5, 10, 50, 100]); }); it('log.ticks() returns the empty array when the domain is degenerate', () => { diff --git a/packages/vscale/__tests__/symlog.test.ts b/packages/vscale/__tests__/symlog.test.ts index 11da46d..9fcac8a 100644 --- a/packages/vscale/__tests__/symlog.test.ts +++ b/packages/vscale/__tests__/symlog.test.ts @@ -170,50 +170,50 @@ it('symlog().clamp(true).invert(x) cannot return a value outside the domain', () it('symlog.ticks() with positive domain', () => { const x = new SymlogScale().domain([10, 100]).constant(10); - expect(x.ticks(3)).toEqual([17, 35, 64]); - expect(x.ticks(4)).toEqual([17, 35, 64]); - expect(x.ticks(5)).toEqual([17, 35, 64]); - expect(x.ticks(6)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); - expect(x.ticks(7)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); - expect(x.ticks(8)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); - expect(x.ticks(9)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); - expect(x.ticks(10)).toEqual([12, 17, 23, 31, 40, 50, 64, 80]); + expect(x.ticks(3)).toEqual([10, 17, 35, 64, 100]); + expect(x.ticks(4)).toEqual([10, 17, 35, 64, 100]); + expect(x.ticks(5)).toEqual([10, 17, 35, 64, 100]); + expect(x.ticks(6)).toEqual([10, 12, 17, 23, 31, 40, 50, 64, 80, 100]); + expect(x.ticks(7)).toEqual([10, 12, 17, 23, 31, 40, 50, 64, 80, 100]); + expect(x.ticks(8)).toEqual([10, 12, 17, 23, 31, 40, 50, 64, 80, 100]); + expect(x.ticks(9)).toEqual([10, 12, 17, 23, 31, 40, 50, 64, 80, 100]); + expect(x.ticks(10)).toEqual([10, 12, 17, 23, 31, 40, 50, 64, 80, 100]); }); it('symlog.ticks() can take negative values', () => { const x = new SymlogScale().domain([-100, -1]); - expect(x.ticks()).toEqual([-89, -54, -32, -19, -11, -6, -3, -2]); + expect(x.ticks()).toEqual([-100, -89, -54, -32, -19, -11, -6, -3, -2, -1]); expect(x.scale(-50)).toBeCloseTo(0.1742222155862007, 5); }); it('log.ticks() generates the expected power-of-ten for ascending ticks', () => { const s = new SymlogScale(); - expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0.1, 1, 2, 3, 4, 5, 6, 8]); - expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]); - expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1]); + expect(s.domain([1e-1, 1e1]).ticks().map(round)).toEqual([0, 1, 2, 3, 4, 5, 6, 8, 10]); + expect(s.domain([1e-1, 1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]); + expect(s.domain([-1, -1e-1]).ticks().map(round)).toEqual([-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1]); }); it('symLog.ticks() generates the expected power-of-ten ticks for descending domains', () => { const s = new SymlogScale(); - expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-8, -6, -5, -4, -3, -2, -1, -0.1].reverse()); + expect(s.domain([-1e-1, -1e1]).ticks().map(round)).toEqual([-10, -8, -6, -5, -4, -3, -2, -1, -0].reverse()); expect(s.domain([-1e-1, -1]).ticks().map(round)).toEqual( - [-0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse() + [-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse() ); - expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].reverse()); + expect(s.domain([1, 1e-1]).ticks().map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1].reverse()); }); it('symLog.ticks() generates the expected power-of-ten ticks for small domains', () => { const s = new SymlogScale(); - expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4]); - expect(s.domain([5, 1]).ticks()).toEqual([4, 3, 2, 1]); - expect(s.domain([-1, -5]).ticks()).toEqual([-1, -2, -3, -4]); - expect(s.domain([-5, -1]).ticks()).toEqual([-4, -3, -2, -1]); - expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([298, 329]); - expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([298, 313, 329]); - expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([298, 313, 329]); - expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([298, 313, 329]); + expect(s.domain([1, 5]).ticks()).toEqual([1, 2, 3, 4, 5]); + expect(s.domain([5, 1]).ticks()).toEqual([5, 4, 3, 2, 1]); + expect(s.domain([-1, -5]).ticks()).toEqual([-1, -2, -3, -4, -5]); + expect(s.domain([-5, -1]).ticks()).toEqual([-5, -4, -3, -2, -1]); + expect(s.domain([286.9252014, 329.4978332]).ticks(1)).toEqual([287, 298, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([287, 298, 313, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([287, 298, 313, 329]); + expect(s.domain([286.9252014, 329.4978332]).ticks(4)).toEqual([287, 298, 313, 329]); expect(s.domain([286.9252014, 329.4978332]).ticks()).toEqual([ - 289, 292, 295, 298, 301, 304, 307, 310, 313, 316, 320, 323, 326, 329 + 287, 289, 292, 295, 298, 301, 304, 307, 310, 313, 316, 320, 323, 326, 329 ]); }); @@ -222,13 +222,13 @@ it('symLog.ticks() generates linear ticks when the domain extent is small', () = expect(s.domain([41, 42]).ticks()).toEqual([41, 42]); expect(s.domain([42, 41]).ticks()).toEqual([42, 41]); expect(s.domain([1600, 1400]).ticks()).toEqual([ - 1587, 1571, 1555, 1540, 1524, 1509, 1494, 1479, 1465, 1450, 1436, 1421, 1407 + 1600, 1587, 1571, 1555, 1540, 1524, 1509, 1494, 1479, 1465, 1450, 1436, 1421, 1407, 1400 ]); }); it('symLog.base(base).ticks() generates the expected power-of-base ticks', () => { const s = new SymlogScale().constant(Math.E); - expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0.1, 2, 5, 10, 20, 50, 100]); + expect(s.domain([0.1, 100]).ticks().map(round)).toEqual([0, 2, 5, 10, 20, 50, 100]); }); it('symLog.ticks() returns the empty array when the domain is degenerate', () => { @@ -238,7 +238,7 @@ it('symLog.ticks() returns the empty array when the domain is degenerate', () => expect(x.domain([0, -1]).ticks()).toEqual([0, -1]); expect(x.domain([-1, 0]).ticks()).toEqual([-1, -0]); expect(x.domain([-1, 1]).ticks()).toEqual([-1, -0, 1]); - expect(x.domain([0, 0]).ticks()).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + expect(x.domain([0, 0]).ticks()).toEqual([0]); }); it('symLog.forceTicks() generates the expected power-of-ten for ascending ticks', () => { diff --git a/packages/vscale/src/log-scale.ts b/packages/vscale/src/log-scale.ts index 59ce501..bcfe972 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -50,7 +50,9 @@ export class LogScale extends ContinuousScale { const logs = logp(this._base); const pows = powp(this._base); - if (this._domain[0] < 0) { + const domain = this._niceDomain ?? this._domain; + + if (domain[0] < 0) { this._logs = reflect(logs); this._pows = reflect(pows); @@ -150,7 +152,7 @@ export class LogScale extends ContinuousScale { */ forceTicks(count: number = 10): any[] { const d = this.calculateVisibleDomain(this._range); - return forceTicksBaseTransform(d[0], d[1], count, this.transformer, this.untransformer); + return forceTicksBaseTransform(d[0], d[d.length - 1], count, this.transformer, this.untransformer); } /** @@ -159,16 +161,17 @@ export class LogScale extends ContinuousScale { */ stepTicks(step: number): any[] { const d = this.calculateVisibleDomain(this._range); - return forceTicksBaseTransform(d[0], d[1], step, this.transformer, this.untransformer); + return forceTicksBaseTransform(d[0], d[d.length - 1], step, this.transformer, this.untransformer); } nice(): this { - return this.domain( - nice(this.domain(), { - floor: (x: number) => this._pows(Math.floor(this._logs(x))), - ceil: (x: number) => this._pows(Math.ceil(this._logs(x))) - }) - ); + const niceDomain = cloneDeep(this._domain); + this._niceDomain = nice(niceDomain, { + floor: (x: number) => this._pows(Math.floor(this._logs(x))), + ceil: (x: number) => this._pows(Math.ceil(this._logs(x))) + }) as number[]; + this.rescale(); + return this; } /** @@ -182,7 +185,8 @@ export class LogScale extends ContinuousScale { if (this._domain) { niceDomain[niceDomain.length - 1] = maxD; - this.domain(niceDomain); + this._niceDomain = niceDomain; + this.rescale(); } return this; @@ -199,7 +203,8 @@ export class LogScale extends ContinuousScale { if (this._domain) { niceDomain[0] = minD; - this.domain(niceDomain); + this._niceDomain = niceDomain; + this.rescale(); } return this; diff --git a/packages/vscale/src/symlog-scale.ts b/packages/vscale/src/symlog-scale.ts index 399dde4..126b485 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -42,7 +42,7 @@ export class SymlogScale extends LinearScale { ticks(count: number = 10) { // return this.d3Ticks(count); const d = this.calculateVisibleDomain(this._range); - return ticksBaseTransform(d[0], d[1], count, this._const, this.transformer, this.untransformer); + return ticksBaseTransform(d[0], d[d.length - 1], count, this._const, this.transformer, this.untransformer); } /** @@ -51,7 +51,7 @@ export class SymlogScale extends LinearScale { */ forceTicks(count: number = 10): any[] { const d = this.calculateVisibleDomain(this._range); - return forceTicksBaseTransform(d[0], d[1], count, this.transformer, this.untransformer); + return forceTicksBaseTransform(d[0], d[d.length - 1], count, this.transformer, this.untransformer); } /** @@ -64,12 +64,13 @@ export class SymlogScale extends LinearScale { } nice(): this { - return this.domain( - nice(this.domain(), { - floor: (x: number) => this.untransformer(Math.floor(this.transformer(x))), - ceil: (x: number) => this.untransformer(Math.ceil(this.transformer(x))) - }) - ); + const niceDomain = cloneDeep(this._domain); + this._niceDomain = nice(niceDomain, { + floor: (x: number) => this.untransformer(Math.floor(this.transformer(x))), + ceil: (x: number) => this.untransformer(Math.ceil(this.transformer(x))) + }) as number[]; + this.rescale(); + return this; } /** @@ -83,7 +84,8 @@ export class SymlogScale extends LinearScale { if (this._domain) { niceDomain[niceDomain.length - 1] = maxD; - this.domain(niceDomain); + this._niceDomain = niceDomain; + this.rescale(); } return this; @@ -100,7 +102,8 @@ export class SymlogScale extends LinearScale { if (this._domain) { niceDomain[0] = minD; - this.domain(niceDomain); + this._niceDomain = niceDomain; + this.rescale(); } return this; diff --git a/packages/vscale/src/utils/tick-sample.ts b/packages/vscale/src/utils/tick-sample.ts index e09136d..cbcc3f8 100644 --- a/packages/vscale/src/utils/tick-sample.ts +++ b/packages/vscale/src/utils/tick-sample.ts @@ -472,6 +472,10 @@ export function parseNiceOptions(originalDomain: number[], option: NiceOptions) return { niceType, niceDomain, niceMinMax, domainValidator }; } +export const fixPrecision = (start: number, stop: number, value: number) => { + return Math.abs(stop - start) < 1 ? +value.toFixed(1) : Math.round(+value); +}; + export const ticksBaseTransform = memoize( ( start: number, @@ -488,7 +492,7 @@ export const ticksBaseTransform = memoize( let ticksExp = []; // get ticks exp if (Number.isInteger(base)) { - ticksExp = new LinearScale().domain([startExp, stopExp]).ticks(count); + ticksExp = ticks(startExp, stopExp, count); } else { const stepExp = (stopExp - startExp) / (count - 1); for (let i = 0; i < count; i++) { @@ -500,16 +504,14 @@ export const ticksBaseTransform = memoize( const power = untransformer(tl); // nice const nicePower = Number.isInteger(base) - ? Math.abs(stop - start) < 1 - ? +power.toFixed(1) - : Math.round(+power) - : niceNumber(power); + ? fixPrecision(start, stop, power) + : fixPrecision(start, stop, niceNumber(power)); // scope - const scopeExp = restrictNumber(nicePower, [start, stop]); + const scopePower = fixPrecision(start, stop, restrictNumber(nicePower, [start, stop])); // dedupe - if (!ticksMap[nicePower] && !isNaN(nicePower) && ticksExp.length > 1) { - ticksMap[nicePower] = 1; - ticksResult.push(scopeExp); + if (!ticksMap[scopePower] && !isNaN(scopePower) && ticksExp.length > 1) { + ticksMap[scopePower] = 1; + ticksResult.push(scopePower); } }); return ticksResult; From 9c657ce038d103028fc304f18946cf4994b001e3 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 22 Aug 2023 10:50:53 +0800 Subject: [PATCH 6/9] chore: domain length judge problem --- packages/vscale/package.json | 2 +- packages/vscale/src/log-scale.ts | 2 +- packages/vscale/src/symlog-scale.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vscale/package.json b/packages/vscale/package.json index f83f956..72d1bcc 100644 --- a/packages/vscale/package.json +++ b/packages/vscale/package.json @@ -29,7 +29,7 @@ "build": "bundle --clean", "dev": "bundle --clean -f es -w", "start": "vite ./vite", - "test": "jest __tests__/symLog.test.ts", + "test": "jest", "test-check": "DEBUG=jest jest --forceExit --detectOpenHandles --silent false --verbose false --runInBand", "test-cov": "jest -w 16 --coverage" }, diff --git a/packages/vscale/src/log-scale.ts b/packages/vscale/src/log-scale.ts index bcfe972..99b2ccc 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -143,7 +143,7 @@ export class LogScale extends ContinuousScale { ticks(count: number = 10) { // return this.d3Ticks(count); const d = this.calculateVisibleDomain(this._range); - return ticksBaseTransform(d[0], d[1], count, this._base, this.transformer, this.untransformer); + return ticksBaseTransform(d[0], d[d.length - 1], count, this._base, this.transformer, this.untransformer); } /** diff --git a/packages/vscale/src/symlog-scale.ts b/packages/vscale/src/symlog-scale.ts index 126b485..f2da345 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -60,7 +60,7 @@ export class SymlogScale extends LinearScale { */ stepTicks(step: number): any[] { const d = this.calculateVisibleDomain(this._range); - return forceTicksBaseTransform(d[0], d[1], step, this.transformer, this.untransformer); + return forceTicksBaseTransform(d[0], d[d.length - 1], step, this.transformer, this.untransformer); } nice(): this { From 543f590b7c9f136ecd6f3b1bf85bb9bcb129931f Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 22 Aug 2023 11:13:28 +0800 Subject: [PATCH 7/9] chore: unit test about log scale nice function --- .../vscale/__tests__/log-nice-option.test.ts | 8 +-- packages/vscale/src/log-scale.ts | 50 +++++++++++++++---- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/vscale/__tests__/log-nice-option.test.ts b/packages/vscale/__tests__/log-nice-option.test.ts index f521ba4..ede9ea7 100644 --- a/packages/vscale/__tests__/log-nice-option.test.ts +++ b/packages/vscale/__tests__/log-nice-option.test.ts @@ -18,7 +18,7 @@ test('log.nice() width option forceMin', function () { scale.nice(10, { forceMin: 0.5 }); expect(scale.domain()).toEqual([0.5, 10]); - expect(scale.ticks(5)).toEqual([0.5, 0.6, 0.7, 0.8, 0.9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(scale.ticks(5)).toEqual([1, 2, 3, 4, 6, 10]); expect(scale.domain()).toEqual([0.5, 10]); expect(scale.scale(0.1)).toBeUndefined(); expect(scale.scale(-10)).toBeUndefined(); @@ -28,7 +28,7 @@ test('log.nice() width option forceMin', function () { scale.nice(10, { forceMin: 2 }); expect(scale.domain()).toEqual([2, 10]); - expect(scale.ticks(5)).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(scale.ticks(5)).toEqual([2, 3, 4, 5, 6, 8, 10]); expect(scale.domain()).toEqual([2, 10]); expect(scale.scale(-20)).toBeUndefined(); expect(scale.scale(-10)).toBeUndefined(); @@ -42,7 +42,7 @@ test('log.nice() width option forceMax', function () { scale.nice(10, { forceMax: 700 }); expect(scale.domain()).toEqual([100, 700]); - expect(scale.ticks(5)).toEqual([100, 200, 300, 400, 500, 600, 700]); + expect(scale.ticks(5)).toEqual([100, 158, 251, 398, 631, 700]); expect(scale.domain()).toEqual([100, 700]); expect(scale.scale(10)).toBeCloseTo(-1.1832946624549383); expect(scale.scale(200)).toBeCloseTo(0.3562071871080222); @@ -51,7 +51,7 @@ test('log.nice() width option forceMax', function () { scale.nice(10, { forceMax: 1123 }); expect(scale.domain()).toEqual([100, 1123]); - expect(scale.ticks(5)).toEqual([100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]); + expect(scale.ticks(5)).toEqual([100, 158, 251, 398, 631, 1000, 1123]); expect(scale.domain()).toEqual([100, 1123]); expect(scale.scale(10)).toBeCloseTo(-0.952036626790323); expect(scale.scale(200)).toBeCloseTo(0.28659158163464227); diff --git a/packages/vscale/src/log-scale.ts b/packages/vscale/src/log-scale.ts index 99b2ccc..9bcded8 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -1,10 +1,9 @@ -import { ticks, forceTicks, stepTicks, ticksBaseTransform, forceTicksBaseTransform } from './utils/tick-sample'; +import { ticks, ticksBaseTransform, forceTicksBaseTransform, parseNiceOptions } from './utils/tick-sample'; import { ContinuousScale } from './continuous-scale'; import { ScaleEnum } from './type'; import { logp, nice, powp, logNegative, expNegative, identity } from './utils/utils'; -import type { ContinuousScaleType } from './interface'; +import type { ContinuousScaleType, NiceOptions, NiceType } from './interface'; import { cloneDeep } from '@visactor/vutils'; -import { LinearScale } from './linear-scale'; /** * 逆反函数 @@ -164,13 +163,44 @@ export class LogScale extends ContinuousScale { return forceTicksBaseTransform(d[0], d[d.length - 1], step, this.transformer, this.untransformer); } - nice(): this { - const niceDomain = cloneDeep(this._domain); - this._niceDomain = nice(niceDomain, { - floor: (x: number) => this._pows(Math.floor(this._logs(x))), - ceil: (x: number) => this._pows(Math.ceil(this._logs(x))) - }) as number[]; - this.rescale(); + nice(count: number = 10, option?: NiceOptions): this { + const originalDomain = this._domain; + let niceMinMax: number[] = []; + let niceType: NiceType = null; + + if (option) { + const res = parseNiceOptions(originalDomain, option); + niceMinMax = res.niceMinMax; + this._domainValidator = res.domainValidator; + + niceType = res.niceType; + + if (res.niceDomain) { + this._niceDomain = res.niceDomain; + this.rescale(); + return this; + } + } else { + niceType = 'all'; + } + + if (niceType) { + const niceDomain = nice(originalDomain.slice(), { + floor: (x: number) => this._pows(Math.floor(this._logs(x))), + ceil: (x: number) => this._pows(Math.ceil(this._logs(x))) + }); + + if (niceType === 'min') { + niceDomain[niceDomain.length - 1] = niceMinMax[1] ?? niceDomain[niceDomain.length - 1]; + } else if (niceType === 'max') { + niceDomain[0] = niceMinMax[0] ?? niceDomain[0]; + } + + this._niceDomain = niceDomain as number[]; + this.rescale(); + return this; + } + return this; } From 76bbf31dd8f17cbb990e5ceb92225ee3d297a673 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 22 Aug 2023 11:20:33 +0800 Subject: [PATCH 8/9] chore: nice function about symLog scale --- packages/vscale/src/symlog-scale.ts | 49 +++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/vscale/src/symlog-scale.ts b/packages/vscale/src/symlog-scale.ts index f2da345..c24e737 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -1,8 +1,8 @@ import { cloneDeep } from '@visactor/vutils'; -import type { ContinuousScaleType } from './interface'; +import type { ContinuousScaleType, NiceOptions, NiceType } from './interface'; import { LinearScale } from './linear-scale'; import { ScaleEnum } from './type'; -import { forceTicksBaseTransform, stepTicks, ticksBaseTransform } from './utils/tick-sample'; +import { forceTicksBaseTransform, parseNiceOptions, stepTicks, ticksBaseTransform } from './utils/tick-sample'; import { symlog, symexp, nice } from './utils/utils'; export class SymlogScale extends LinearScale { @@ -63,13 +63,44 @@ export class SymlogScale extends LinearScale { return forceTicksBaseTransform(d[0], d[d.length - 1], step, this.transformer, this.untransformer); } - nice(): this { - const niceDomain = cloneDeep(this._domain); - this._niceDomain = nice(niceDomain, { - floor: (x: number) => this.untransformer(Math.floor(this.transformer(x))), - ceil: (x: number) => this.untransformer(Math.ceil(this.transformer(x))) - }) as number[]; - this.rescale(); + nice(count: number = 10, option?: NiceOptions): this { + const originalDomain = this._domain; + let niceMinMax: number[] = []; + let niceType: NiceType = null; + + if (option) { + const res = parseNiceOptions(originalDomain, option); + niceMinMax = res.niceMinMax; + this._domainValidator = res.domainValidator; + + niceType = res.niceType; + + if (res.niceDomain) { + this._niceDomain = res.niceDomain; + this.rescale(); + return this; + } + } else { + niceType = 'all'; + } + + if (niceType) { + const niceDomain = nice(originalDomain.slice(), { + floor: (x: number) => this.untransformer(Math.floor(this.transformer(x))), + ceil: (x: number) => this.untransformer(Math.ceil(this.transformer(x))) + }); + + if (niceType === 'min') { + niceDomain[niceDomain.length - 1] = niceMinMax[1] ?? niceDomain[niceDomain.length - 1]; + } else if (niceType === 'max') { + niceDomain[0] = niceMinMax[0] ?? niceDomain[0]; + } + + this._niceDomain = niceDomain as number[]; + this.rescale(); + return this; + } + return this; } From 25cbde40f4612375d556ace015f04e5d7ba295ba Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 24 Aug 2023 14:49:58 +0800 Subject: [PATCH 9/9] fix: slice replace cloneDeep consider about performance --- packages/vscale/src/log-scale.ts | 5 ++--- packages/vscale/src/symlog-scale.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/vscale/src/log-scale.ts b/packages/vscale/src/log-scale.ts index 9bcded8..e0644e1 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -3,7 +3,6 @@ import { ContinuousScale } from './continuous-scale'; import { ScaleEnum } from './type'; import { logp, nice, powp, logNegative, expNegative, identity } from './utils/utils'; import type { ContinuousScaleType, NiceOptions, NiceType } from './interface'; -import { cloneDeep } from '@visactor/vutils'; /** * 逆反函数 @@ -211,7 +210,7 @@ export class LogScale extends ContinuousScale { niceMin(): this { const maxD = this._domain[this._domain.length - 1]; this.nice(); - const niceDomain = cloneDeep(this._domain); + const niceDomain = this._domain.slice(); if (this._domain) { niceDomain[niceDomain.length - 1] = maxD; @@ -229,7 +228,7 @@ export class LogScale extends ContinuousScale { niceMax(): this { const minD = this._domain[0]; this.nice(); - const niceDomain = cloneDeep(this._domain); + const niceDomain = this._domain.slice(); if (this._domain) { niceDomain[0] = minD; diff --git a/packages/vscale/src/symlog-scale.ts b/packages/vscale/src/symlog-scale.ts index c24e737..75eb2bc 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -1,4 +1,3 @@ -import { cloneDeep } from '@visactor/vutils'; import type { ContinuousScaleType, NiceOptions, NiceType } from './interface'; import { LinearScale } from './linear-scale'; import { ScaleEnum } from './type'; @@ -111,7 +110,7 @@ export class SymlogScale extends LinearScale { niceMin(): this { const maxD = this._domain[this._domain.length - 1]; this.nice(); - const niceDomain = cloneDeep(this._domain); + const niceDomain = this._domain.slice(); if (this._domain) { niceDomain[niceDomain.length - 1] = maxD; @@ -129,7 +128,7 @@ export class SymlogScale extends LinearScale { niceMax(): this { const minD = this._domain[0]; this.nice(); - const niceDomain = cloneDeep(this._domain); + const niceDomain = this._domain.slice(); if (this._domain) { niceDomain[0] = minD;