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..7b21f28 --- /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": "patch" + } + ], + "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..776055f --- /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": "patch" + } + ], + "packageName": "@visactor/vscale" +} \ No newline at end of file diff --git a/packages/vdataset/src/data-view.ts b/packages/vdataset/src/data-view.ts index ee1575c..ee2b802 100644 --- a/packages/vdataset/src/data-view.ts +++ b/packages/vdataset/src/data-view.ts @@ -94,7 +94,10 @@ export class DataView { del: any; }; - constructor(public dataSet: DataSet, public options?: IDataViewOptions) { + constructor( + public dataSet: DataSet, + public options?: IDataViewOptions + ) { let name; if (options?.name) { name = options.name; 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/__tests__/log.test.ts b/packages/vscale/__tests__/log.test.ts index 8743df3..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([-100, -90, -80, -70, -60, -50, -40, -30, -20, -10, -9, -8, -7, -6, -5, -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.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.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', () => { @@ -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,22 +239,16 @@ 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, 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([-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', () => { @@ -263,25 +257,27 @@ it('log.ticks() generates the expected power-of-ten ticks for small domains', () 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([300]); - expect(s.domain([286.9252014, 329.4978332]).ticks(2)).toEqual([300]); - expect(s.domain([286.9252014, 329.4978332]).ticks(3)).toEqual([280, 300, 320, 340]); - 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([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([ + 287, 288, 292, 295, 299, 302, 305, 309, 313, 316, 320, 324, 327, 329 + ]); }); 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([1600, 1580, 1560, 1540, 1520, 1500, 1480, 1460, 1440, 1420, 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([ + 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.018315638889, 0.135335283237, 1, 7.389056098931, 54.598150033144, 403.428793492735 - ]); + 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', () => { @@ -291,7 +287,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 +321,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..9fcac8a 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([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([-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, 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([-10, -8, -6, -5, -4, -3, -2, -1, -0].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()); +}); + +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, 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([ + 287, 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([ + 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, 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]); +}); + +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/src/log-scale.ts b/packages/vscale/src/log-scale.ts index a700071..e0644e1 100644 --- a/packages/vscale/src/log-scale.ts +++ b/packages/vscale/src/log-scale.ts @@ -1,4 +1,4 @@ -import { ticks, forceTicks, stepTicks, parseNiceOptions } 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'; @@ -47,6 +47,7 @@ export class LogScale extends ContinuousScale { const logs = logp(this._base); const pows = powp(this._base); + const domain = this._niceDomain ?? this._domain; if (domain[0] < 0) { @@ -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[d.length - 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[d.length - 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[d.length - 1], step, this.transformer, this.untransformer); } nice(count: number = 10, option?: NiceOptions): this { @@ -211,7 +214,8 @@ export class LogScale extends ContinuousScale { if (this._domain) { niceDomain[niceDomain.length - 1] = maxD; - this.domain(niceDomain); + this._niceDomain = niceDomain; + this.rescale(); } return this; @@ -228,7 +232,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 e67bce7..75eb2bc 100644 --- a/packages/vscale/src/symlog-scale.ts +++ b/packages/vscale/src/symlog-scale.ts @@ -1,7 +1,8 @@ -import type { ContinuousScaleType } from './interface'; +import type { ContinuousScaleType, NiceOptions, NiceType } from './interface'; import { LinearScale } from './linear-scale'; import { ScaleEnum } from './type'; -import { symlog, symexp } from './utils/utils'; +import { forceTicksBaseTransform, parseNiceOptions, 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 +37,105 @@ 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[d.length - 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[d.length - 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[d.length - 1], step, this.transformer, this.untransformer); + } + + 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; + } + + /** + * 只对min区间进行nice + * 如果保持某一边界的值,就很难有好的nice效果,所以这里实现就是nice之后还原固定的边界值 + */ + niceMin(): this { + const maxD = this._domain[this._domain.length - 1]; + this.nice(); + const niceDomain = this._domain.slice(); + + if (this._domain) { + niceDomain[niceDomain.length - 1] = maxD; + this._niceDomain = niceDomain; + this.rescale(); + } + + return this; + } + + /** + * 只对max区间进行nice + * 如果保持某一边界的值,就很难有好的nice效果,所以这里实现就是nice之后还原固定的边界值 + */ + niceMax(): this { + const minD = this._domain[0]; + this.nice(); + const niceDomain = this._domain.slice(); + + if (this._domain) { + niceDomain[0] = minD; + 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 7d4f344..cbcc3f8 100644 --- a/packages/vscale/src/utils/tick-sample.ts +++ b/packages/vscale/src/utils/tick-sample.ts @@ -1,11 +1,32 @@ import { range, memoize, isNumber } from '@visactor/vutils'; -import type { ContinuousTicksFunc, NiceOptions, NiceType } from '../interface'; +import { LinearScale } from '../linear-scale'; +import type { TransformType, ContinuousTicksFunc, NiceOptions, NiceType } from '../interface'; +import { niceNumber, restrictNumber } from './utils'; const e10 = Math.sqrt(50); const e5 = Math.sqrt(10); 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, noDecimals?: boolean) => { let step = 1; let start = value; @@ -450,3 +471,67 @@ 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, + 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 = ticks(startExp, stopExp, 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) + ? fixPrecision(start, stop, power) + : fixPrecision(start, stop, niceNumber(power)); + // scope + const scopePower = fixPrecision(start, stop, restrictNumber(nicePower, [start, stop])); + // dedupe + if (!ticksMap[scopePower] && !isNaN(scopePower) && ticksExp.length > 1) { + ticksMap[scopePower] = 1; + ticksResult.push(scopePower); + } + }); + 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); +}; diff --git a/tools/bundler/src/tasks/modules.ts b/tools/bundler/src/tasks/modules.ts index ecb35c8..b3a9d5d 100644 --- a/tools/bundler/src/tasks/modules.ts +++ b/tools/bundler/src/tasks/modules.ts @@ -76,12 +76,15 @@ export function compile( let sourcesStream = gulp.src(sources); for (const [key, value] of Object.entries(envs)) { sourcesStream = sourcesStream.pipe( - gulpIF(file => { - if (gulpMatch(file, '**/*.d.ts')) { - return false; - } - return true; - }, gulpReplace(key, value)) + gulpIF( + file => { + if (gulpMatch(file, '**/*.d.ts')) { + return false; + } + return true; + }, + gulpReplace(key, value) + ) ); }