diff --git a/__tests__/unit/scales/base.spec.ts b/__tests__/unit/scales/base.spec.ts index 7abe5c56..fcb6a586 100644 --- a/__tests__/unit/scales/base.spec.ts +++ b/__tests__/unit/scales/base.spec.ts @@ -1,5 +1,5 @@ -import { Base } from '../../../src/scales/base'; -import { BaseOptions, Domain, Range } from '../../../src/types'; +import { Base, BaseOptions } from '../../../src'; +import { Domain, Range } from '../../../src/types'; class Scale extends Base { protected getDefaultOptions() { diff --git a/__tests__/unit/scales/time.spec.ts b/__tests__/unit/scales/time.spec.ts index f2a8f095..f9b54103 100644 --- a/__tests__/unit/scales/time.spec.ts +++ b/__tests__/unit/scales/time.spec.ts @@ -84,11 +84,12 @@ describe('Time', () => { test('getTicks() calls options.tickMethod and return its return value', () => { const scale = new Time({ tickInterval: 100, - tickMethod: (min, max, count, interval) => { + tickMethod: (min, max, count, interval, utc) => { expect(min).toEqual(new Date(2000, 0, 1)); expect(max).toEqual(new Date(2000, 0, 2)); expect(count).toBe(5); expect(interval).toBe(100); + expect(utc).toBe(false); return []; }, }); diff --git a/__tests__/unit/tick-methods/d3-time.spec.ts b/__tests__/unit/tick-methods/d3-time.spec.ts index 6079990d..c02207a1 100644 --- a/__tests__/unit/tick-methods/d3-time.spec.ts +++ b/__tests__/unit/tick-methods/d3-time.spec.ts @@ -1,10 +1,421 @@ import { d3Time } from '../../../src/tick-methods/d3-time'; +import { DURATION_SECOND } from '../../../src'; + +function UTC( + year: number, + month: number, + day: number = 1, + hours: number = 0, + minutes: number = 0, + seconds: number = 0, + ms: number = 0 +) { + return new Date(Date.UTC(year, month, day, hours, minutes, seconds, ms)); +} describe('d3Time', () => { - test('d3Time uses d3Linear right now', () => { - const [start, end] = [0, 1000]; - const ticks = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; - const date = (d: number) => new Date(d * 1000); - expect(d3Time(date(start), date(end), 10)).toEqual(ticks.map(date)); + test('d3Time(hi, lo, count) can exchange hi and lo', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 1), new Date(2011, 0, 1, 12, 0, 0), 4)).toEqual( + [ + new Date(2011, 0, 1, 12, 0, 0, 0), + new Date(2011, 0, 1, 12, 0, 0, 200), + new Date(2011, 0, 1, 12, 0, 0, 400), + new Date(2011, 0, 1, 12, 0, 0, 600), + new Date(2011, 0, 1, 12, 0, 0, 800), + new Date(2011, 0, 1, 12, 0, 1, 0), + ].reverse() + ); + }); + + test('d3Time(start, stop) uses 5 as default count', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 4))).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 1), + new Date(2011, 0, 1, 12, 0, 2), + new Date(2011, 0, 1, 12, 0, 3), + new Date(2011, 0, 1, 12, 0, 4), + ]); + }); + + test('d3Time(start, stop, count, interval) can generate ticks with interval', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 20), 4, 5 * DURATION_SECOND)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 5), + new Date(2011, 0, 1, 12, 0, 10), + new Date(2011, 0, 1, 12, 0, 15), + new Date(2011, 0, 1, 12, 0, 20), + ]); + }); + + test('d3Time(start, stop, count) can generate sub-second ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 1), 4)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0, 0), + new Date(2011, 0, 1, 12, 0, 0, 200), + new Date(2011, 0, 1, 12, 0, 0, 400), + new Date(2011, 0, 1, 12, 0, 0, 600), + new Date(2011, 0, 1, 12, 0, 0, 800), + new Date(2011, 0, 1, 12, 0, 1, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-second ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 4), 4)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 1), + new Date(2011, 0, 1, 12, 0, 2), + new Date(2011, 0, 1, 12, 0, 3), + new Date(2011, 0, 1, 12, 0, 4), + ]); + }); + + test('d3Time(start, stop, count) can generate 5-second ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 20), 4)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 5), + new Date(2011, 0, 1, 12, 0, 10), + new Date(2011, 0, 1, 12, 0, 15), + new Date(2011, 0, 1, 12, 0, 20), + ]); + }); + + test('d3Time(start, stop, count) can generate 15-second ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 0, 50), 4)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 15), + new Date(2011, 0, 1, 12, 0, 30), + new Date(2011, 0, 1, 12, 0, 45), + ]); + }); + + test('d3Time(start, stop, count) can generate 30-second ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 0), new Date(2011, 0, 1, 12, 1, 50), 4)).toEqual([ + new Date(2011, 0, 1, 12, 0, 0), + new Date(2011, 0, 1, 12, 0, 30), + new Date(2011, 0, 1, 12, 1, 0), + new Date(2011, 0, 1, 12, 1, 30), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-minute ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 0, 27), new Date(2011, 0, 1, 12, 4, 12), 4)).toEqual([ + new Date(2011, 0, 1, 12, 1), + new Date(2011, 0, 1, 12, 2), + new Date(2011, 0, 1, 12, 3), + new Date(2011, 0, 1, 12, 4), + ]); + }); + + test('d3Time(start, stop, count) can generate 5-minute ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 3, 27), new Date(2011, 0, 1, 12, 21, 12), 4)).toEqual([ + new Date(2011, 0, 1, 12, 5), + new Date(2011, 0, 1, 12, 10), + new Date(2011, 0, 1, 12, 15), + new Date(2011, 0, 1, 12, 20), + ]); + }); + + test('d3Time(start, stop, count) can generate 15-minute ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 8, 27), new Date(2011, 0, 1, 13, 4, 12), 4)).toEqual([ + new Date(2011, 0, 1, 12, 15), + new Date(2011, 0, 1, 12, 30), + new Date(2011, 0, 1, 12, 45), + new Date(2011, 0, 1, 13, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 30-minute ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 28, 27), new Date(2011, 0, 1, 14, 4, 12), 4)).toEqual([ + new Date(2011, 0, 1, 12, 30), + new Date(2011, 0, 1, 13, 0), + new Date(2011, 0, 1, 13, 30), + new Date(2011, 0, 1, 14, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-hour ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 12, 28, 27), new Date(2011, 0, 1, 16, 34, 12), 4)).toEqual([ + new Date(2011, 0, 1, 13, 0), + new Date(2011, 0, 1, 14, 0), + new Date(2011, 0, 1, 15, 0), + new Date(2011, 0, 1, 16, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 3-hour ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 14, 28, 27), new Date(2011, 0, 2, 1, 34, 12), 4)).toEqual([ + new Date(2011, 0, 1, 15, 0), + new Date(2011, 0, 1, 18, 0), + new Date(2011, 0, 1, 21, 0), + new Date(2011, 0, 2, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 6-hour ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 16, 28, 27), new Date(2011, 0, 2, 14, 34, 12), 4)).toEqual([ + new Date(2011, 0, 1, 18, 0), + new Date(2011, 0, 2, 0, 0), + new Date(2011, 0, 2, 6, 0), + new Date(2011, 0, 2, 12, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 12-hour ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 16, 28, 27), new Date(2011, 0, 3, 21, 34, 12), 4)).toEqual([ + new Date(2011, 0, 2, 0, 0), + new Date(2011, 0, 2, 12, 0), + new Date(2011, 0, 3, 0, 0), + new Date(2011, 0, 3, 12, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-day ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 16, 28, 27), new Date(2011, 0, 5, 21, 34, 12), 4)).toEqual([ + new Date(2011, 0, 2, 0, 0), + new Date(2011, 0, 3, 0, 0), + new Date(2011, 0, 4, 0, 0), + new Date(2011, 0, 5, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 2-day ticks', () => { + expect(d3Time(new Date(2011, 0, 2, 16, 28, 27), new Date(2011, 0, 9, 21, 34, 12), 4)).toEqual([ + new Date(2011, 0, 3, 0, 0), + new Date(2011, 0, 5, 0, 0), + new Date(2011, 0, 7, 0, 0), + new Date(2011, 0, 9, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-week ticks', () => { + expect(d3Time(new Date(2011, 0, 1, 16, 28, 27), new Date(2011, 0, 23, 21, 34, 12), 4)).toEqual([ + new Date(2011, 0, 2, 0, 0), + new Date(2011, 0, 9, 0, 0), + new Date(2011, 0, 16, 0, 0), + new Date(2011, 0, 23, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-month ticks', () => { + expect(d3Time(new Date(2011, 0, 18), new Date(2011, 4, 2), 4)).toEqual([ + new Date(2011, 1, 1, 0, 0), + new Date(2011, 2, 1, 0, 0), + new Date(2011, 3, 1, 0, 0), + new Date(2011, 4, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 3-month ticks', () => { + expect(d3Time(new Date(2010, 11, 18), new Date(2011, 10, 2), 4)).toEqual([ + new Date(2011, 0, 1, 0, 0), + new Date(2011, 3, 1, 0, 0), + new Date(2011, 6, 1, 0, 0), + new Date(2011, 9, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate 1-year ticks', () => { + expect(d3Time(new Date(2010, 11, 18), new Date(2014, 2, 2), 4)).toEqual([ + new Date(2011, 0, 1, 0, 0), + new Date(2012, 0, 1, 0, 0), + new Date(2013, 0, 1, 0, 0), + new Date(2014, 0, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count) can generate multi-year ticks', () => { + const start = new Date(-1, 11, 18); + start.setFullYear(0); + expect(d3Time(start, new Date(2014, 2, 2), 6)).toEqual([ + new Date(500, 0, 1, 0, 0), + new Date(1000, 0, 1, 0, 0), + new Date(1500, 0, 1, 0, 0), + new Date(2000, 0, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate sub-second ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 0), UTC(2011, 0, 1, 12, 0, 1), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 0, 0, 0), + UTC(2011, 0, 1, 12, 0, 0, 200), + UTC(2011, 0, 1, 12, 0, 0, 400), + UTC(2011, 0, 1, 12, 0, 0, 600), + UTC(2011, 0, 1, 12, 0, 0, 800), + UTC(2011, 0, 1, 12, 0, 1, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-second ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 0), UTC(2011, 0, 1, 12, 0, 4), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 0, 0), + UTC(2011, 0, 1, 12, 0, 1), + UTC(2011, 0, 1, 12, 0, 2), + UTC(2011, 0, 1, 12, 0, 3), + UTC(2011, 0, 1, 12, 0, 4), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 5-second ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 0), UTC(2011, 0, 1, 12, 0, 20), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 0, 0), + UTC(2011, 0, 1, 12, 0, 5), + UTC(2011, 0, 1, 12, 0, 10), + UTC(2011, 0, 1, 12, 0, 15), + UTC(2011, 0, 1, 12, 0, 20), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 15-second ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 0), UTC(2011, 0, 1, 12, 0, 50), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 0, 0), + UTC(2011, 0, 1, 12, 0, 15), + UTC(2011, 0, 1, 12, 0, 30), + UTC(2011, 0, 1, 12, 0, 45), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 30-second ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 0), UTC(2011, 0, 1, 12, 1, 50), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 0, 0), + UTC(2011, 0, 1, 12, 0, 30), + UTC(2011, 0, 1, 12, 1, 0), + UTC(2011, 0, 1, 12, 1, 30), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-minute ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 0, 27), UTC(2011, 0, 1, 12, 4, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 1), + UTC(2011, 0, 1, 12, 2), + UTC(2011, 0, 1, 12, 3), + UTC(2011, 0, 1, 12, 4), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 5-minute ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 3, 27), UTC(2011, 0, 1, 12, 21, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 5), + UTC(2011, 0, 1, 12, 10), + UTC(2011, 0, 1, 12, 15), + UTC(2011, 0, 1, 12, 20), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 15-minute ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 8, 27), UTC(2011, 0, 1, 13, 4, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 15), + UTC(2011, 0, 1, 12, 30), + UTC(2011, 0, 1, 12, 45), + UTC(2011, 0, 1, 13, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 30-minute ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 28, 27), UTC(2011, 0, 1, 14, 4, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 12, 30), + UTC(2011, 0, 1, 13, 0), + UTC(2011, 0, 1, 13, 30), + UTC(2011, 0, 1, 14, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-hour ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 12, 28, 27), UTC(2011, 0, 1, 16, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 13, 0), + UTC(2011, 0, 1, 14, 0), + UTC(2011, 0, 1, 15, 0), + UTC(2011, 0, 1, 16, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 3-hour ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 14, 28, 27), UTC(2011, 0, 2, 1, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 15, 0), + UTC(2011, 0, 1, 18, 0), + UTC(2011, 0, 1, 21, 0), + UTC(2011, 0, 2, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 6-hour ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 16, 28, 27), UTC(2011, 0, 2, 14, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 18, 0), + UTC(2011, 0, 2, 0, 0), + UTC(2011, 0, 2, 6, 0), + UTC(2011, 0, 2, 12, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 12-hour ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 16, 28, 27), UTC(2011, 0, 3, 21, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 2, 0, 0), + UTC(2011, 0, 2, 12, 0), + UTC(2011, 0, 3, 0, 0), + UTC(2011, 0, 3, 12, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-day ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 16, 28, 27), UTC(2011, 0, 5, 21, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 2, 0, 0), + UTC(2011, 0, 3, 0, 0), + UTC(2011, 0, 4, 0, 0), + UTC(2011, 0, 5, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 2-day ticks', () => { + expect(d3Time(UTC(2011, 0, 2, 16, 28, 27), UTC(2011, 0, 9, 21, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 3, 0, 0), + UTC(2011, 0, 5, 0, 0), + UTC(2011, 0, 7, 0, 0), + UTC(2011, 0, 9, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-week ticks', () => { + expect(d3Time(UTC(2011, 0, 1, 16, 28, 27), UTC(2011, 0, 23, 21, 34, 12), 4, undefined, true)).toEqual([ + UTC(2011, 0, 2, 0, 0), + UTC(2011, 0, 9, 0, 0), + UTC(2011, 0, 16, 0, 0), + UTC(2011, 0, 23, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-month ticks', () => { + expect(d3Time(UTC(2011, 0, 18), UTC(2011, 4, 2), 4, undefined, true)).toEqual([ + UTC(2011, 1, 1, 0, 0), + UTC(2011, 2, 1, 0, 0), + UTC(2011, 3, 1, 0, 0), + UTC(2011, 4, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 3-month ticks', () => { + expect(d3Time(UTC(2010, 11, 18), UTC(2011, 10, 2), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 0, 0), + UTC(2011, 3, 1, 0, 0), + UTC(2011, 6, 1, 0, 0), + UTC(2011, 9, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate 1-year ticks', () => { + expect(d3Time(UTC(2010, 11, 18), UTC(2014, 2, 2), 4, undefined, true)).toEqual([ + UTC(2011, 0, 1, 0, 0), + UTC(2012, 0, 1, 0, 0), + UTC(2013, 0, 1, 0, 0), + UTC(2014, 0, 1, 0, 0), + ]); + }); + + test('d3Time(start, stop, count, undefined, true) can generate multi-year ticks', () => { + const start = UTC(-1, 11, 18); + start.setUTCFullYear(0); + expect(d3Time(start, UTC(2014, 2, 2), 6, undefined, true)).toEqual([ + UTC(500, 0, 1, 0, 0), + UTC(1000, 0, 1, 0, 0), + UTC(1500, 0, 1, 0, 0), + UTC(2000, 0, 1, 0, 0), + ]); }); }); diff --git a/__tests__/unit/utils/bisect.spec.ts b/__tests__/unit/utils/bisect.spec.ts index 63f6d724..66f4ad02 100644 --- a/__tests__/unit/utils/bisect.spec.ts +++ b/__tests__/unit/utils/bisect.spec.ts @@ -47,4 +47,12 @@ describe('bisect', () => { expect(bisect(array, 5, 2, 3)).toBe(3); expect(bisect(array, 6, 2, 3)).toBe(3); }); + + test('bisect(array, x, lo, hi, getter) uses custom getter', () => { + const array = [{ v: 1 }, { v: 2 }, { v: 3 }, { v: 4 }]; + expect(bisect(array, 1, 0, array.length, (d) => d.v)).toBe(1); + expect(bisect(array, 2, 0, array.length, (d) => d.v)).toBe(2); + expect(bisect(array, 3, 0, array.length, (d) => d.v)).toBe(3); + expect(bisect(array, 4, 0, array.length, (d) => d.v)).toBe(4); + }); }); diff --git a/__tests__/unit/utils/d3-time-nice.spec.ts b/__tests__/unit/utils/d3-time-nice.spec.ts index 91d3c489..11ec9b9b 100644 --- a/__tests__/unit/utils/d3-time-nice.spec.ts +++ b/__tests__/unit/utils/d3-time-nice.spec.ts @@ -1,10 +1,100 @@ -import { d3TimeNice } from '../../../src/utils'; +import { d3TimeNice, DURATION_DAY, DURATION_WEEK, DURATION_MONTH, DURATION_YEAR } from '../../../src/utils'; + +function UTC( + year: number, + month: number, + day: number = 1, + hours: number = 0, + minutes: number = 0, + seconds: number = 0, + ms: number = 0 +) { + return new Date(Date.UTC(year, month, day, hours, minutes, seconds, ms)); +} describe('d3TimeNice', () => { - test('d3TimeNice returns original min and max right now', () => { - expect(d3TimeNice(new Date(2020, 3, 5), new Date(2020, 3, 10))).toEqual([ - new Date(2020, 3, 5), - new Date(2020, 3, 10), + test('d3TimeNice(hi, lo, count) can exchange hi, lo', () => { + expect(d3TimeNice(new Date(2013, 0, 1, 12, 0, 0, 128), new Date(2013, 0, 1, 12, 0, 0, 0), 10)).toEqual([ + new Date(2013, 0, 1, 12, 0, 0, 130), + new Date(2013, 0, 1, 12, 0, 0, 0), + ]); + }); + + test('d3TimeNice(lo, hi, count) can nice sub-second domains', () => { + expect(d3TimeNice(new Date(2013, 0, 1, 12, 0, 0, 0), new Date(2013, 0, 1, 12, 0, 0, 128), 10)).toEqual([ + new Date(2013, 0, 1, 12, 0, 0, 0), + new Date(2013, 0, 1, 12, 0, 0, 130), + ]); + }); + + test('d3TimeNice(lo, hi, count) can nice multi-year domains', () => { + expect(d3TimeNice(new Date(2001, 0, 1), new Date(2138, 0, 1), 10)).toEqual([ + new Date(2000, 0, 1), + new Date(2140, 0, 1), ]); }); + + test('d3TimeNice(lo, hi, count) nices using the specified tick count', () => { + const lo = new Date(2009, 0, 1, 0, 17); + const hi = new Date(2009, 0, 1, 23, 42); + expect(d3TimeNice(lo, hi, 100)).toEqual([new Date(2009, 0, 1, 0, 15), new Date(2009, 0, 1, 23, 45)]); + expect(d3TimeNice(lo, hi, 10)).toEqual([new Date(2009, 0, 1), new Date(2009, 0, 2)]); + }); + + test('d3TimeNice(lo, hi, count, interval) nices using the specified time interval', () => { + const lo = new Date(2009, 0, 1, 0, 12); + const hi = new Date(2009, 0, 1, 23, 48); + expect(d3TimeNice(lo, hi, 10, DURATION_DAY)).toEqual([new Date(2009, 0, 1), new Date(2009, 0, 2)]); + expect(d3TimeNice(lo, hi, 10, DURATION_WEEK)).toEqual([new Date(2008, 11, 28), new Date(2009, 0, 4)]); + expect(d3TimeNice(lo, hi, 10, DURATION_MONTH)).toEqual([new Date(2009, 0, 1), new Date(2009, 1, 1)]); + expect(d3TimeNice(lo, hi, 10, DURATION_YEAR)).toEqual([new Date(2009, 0, 1), new Date(2010, 0, 1)]); + }); + + test('d3TimeNice(lo, hi, count, interval) nices using the specified time interval and step', () => { + const lo = new Date(2009, 0, 1, 0, 12); + const hi = new Date(2009, 0, 1, 23, 48); + expect(d3TimeNice(lo, hi, 10, DURATION_DAY * 3)).toEqual([new Date(2009, 0, 1), new Date(2009, 0, 4)]); + expect(d3TimeNice(lo, hi, 10, DURATION_WEEK * 2)).toEqual([new Date(2008, 11, 28), new Date(2009, 0, 11)]); + expect(d3TimeNice(lo, hi, 10, DURATION_MONTH * 3)).toEqual([new Date(2009, 0, 1), new Date(2009, 3, 1)]); + expect(d3TimeNice(lo, hi, 10, DURATION_YEAR * 10)).toEqual([new Date(2000, 0, 1), new Date(2010, 0, 1)]); + }); + + test('d3TimeNice(lo, hi, count, undefined, true) can nice sub-second domains', () => { + expect(d3TimeNice(UTC(2013, 0, 1, 12, 0, 0, 0), UTC(2013, 0, 1, 12, 0, 0, 128), 10, undefined, true)).toEqual([ + UTC(2013, 0, 1, 12, 0, 0, 0), + UTC(2013, 0, 1, 12, 0, 0, 130), + ]); + }); + + test('d3TimeNice(lo, hi, count, undefined, true) can nice multi-year domains', () => { + expect(d3TimeNice(UTC(2001, 0, 1), UTC(2138, 0, 1), 10, undefined, true)).toEqual([ + UTC(2000, 0, 1), + UTC(2140, 0, 1), + ]); + }); + + test('d3TimeNice(lo, hi, count, undefined, true) nices using the specified tick count', () => { + const lo = UTC(2009, 0, 1, 0, 17); + const hi = UTC(2009, 0, 1, 23, 42); + expect(d3TimeNice(lo, hi, 100, undefined, true)).toEqual([UTC(2009, 0, 1, 0, 15), UTC(2009, 0, 1, 23, 45)]); + expect(d3TimeNice(lo, hi, 10, undefined, true)).toEqual([UTC(2009, 0, 1), UTC(2009, 0, 2)]); + }); + + test('d3TimeNice(lo, hi, count, interval, true) nices using the specified time interval', () => { + const lo = UTC(2009, 0, 1, 0, 12); + const hi = UTC(2009, 0, 1, 23, 48); + expect(d3TimeNice(lo, hi, 10, DURATION_DAY, true)).toEqual([UTC(2009, 0, 1), UTC(2009, 0, 2)]); + expect(d3TimeNice(lo, hi, 10, DURATION_WEEK, true)).toEqual([UTC(2008, 11, 28), UTC(2009, 0, 4)]); + expect(d3TimeNice(lo, hi, 10, DURATION_MONTH, true)).toEqual([UTC(2009, 0, 1), UTC(2009, 1, 1)]); + expect(d3TimeNice(lo, hi, 10, DURATION_YEAR, true)).toEqual([UTC(2009, 0, 1), UTC(2010, 0, 1)]); + }); + + test('d3TimeNice(lo, hi, count, interval, true) nices using the specified time interval and step', () => { + const lo = UTC(2009, 0, 1, 0, 12); + const hi = UTC(2009, 0, 1, 23, 48); + expect(d3TimeNice(lo, hi, 10, DURATION_DAY * 3, true)).toEqual([UTC(2009, 0, 1), UTC(2009, 0, 4)]); + expect(d3TimeNice(lo, hi, 10, DURATION_WEEK * 2, true)).toEqual([UTC(2008, 11, 28), UTC(2009, 0, 11)]); + expect(d3TimeNice(lo, hi, 10, DURATION_MONTH * 3, true)).toEqual([UTC(2009, 0, 1), UTC(2009, 3, 1)]); + expect(d3TimeNice(lo, hi, 10, DURATION_YEAR * 10, true)).toEqual([UTC(2000, 0, 1), UTC(2010, 0, 1)]); + }); }); diff --git a/__tests__/unit/utils/ticks.spec.ts b/__tests__/unit/utils/ticks.spec.ts new file mode 100644 index 00000000..0897b944 --- /dev/null +++ b/__tests__/unit/utils/ticks.spec.ts @@ -0,0 +1,70 @@ +import { tickStep, tickIncrement } from '../../../src/utils'; + +describe('ticks', () => { + test('tickStep', () => { + expect(tickStep(0, 1, 10)).toBe(0.1); + expect(tickStep(0, 1, 9)).toBe(0.1); + expect(tickStep(0, 1, 8)).toBe(0.1); + expect(tickStep(0, 1, 7)).toBe(0.2); + expect(tickStep(0, 1, 6)).toBe(0.2); + expect(tickStep(0, 1, 5)).toBe(0.2); + expect(tickStep(0, 1, 4)).toBe(0.2); + expect(tickStep(0, 1, 3)).toBe(0.5); + expect(tickStep(0, 1, 2)).toBe(0.5); + expect(tickStep(0, 1, 1)).toBe(1.0); + expect(tickStep(0, 10, 10)).toBe(1); + expect(tickStep(0, 10, 9)).toBe(1); + expect(tickStep(0, 10, 8)).toBe(1); + expect(tickStep(0, 10, 7)).toBe(2); + expect(tickStep(0, 10, 6)).toBe(2); + expect(tickStep(0, 10, 5)).toBe(2); + expect(tickStep(0, 10, 4)).toBe(2); + expect(tickStep(0, 10, 3)).toBe(5); + expect(tickStep(0, 10, 2)).toBe(5); + expect(tickStep(0, 10, 1)).toBe(10); + expect(tickStep(10, 0, 1)).toBe(-10); + expect(tickStep(-10, 10, 10)).toBe(2); + expect(tickStep(-10, 10, 9)).toBe(2); + expect(tickStep(-10, 10, 8)).toBe(2); + expect(tickStep(-10, 10, 7)).toBe(2); + expect(tickStep(-10, 10, 6)).toBe(5); + expect(tickStep(-10, 10, 5)).toBe(5); + expect(tickStep(-10, 10, 4)).toBe(5); + expect(tickStep(-10, 10, 3)).toBe(5); + expect(tickStep(-10, 10, 2)).toBe(10); + expect(tickStep(-10, 10, 1)).toBe(20); + }); + + test('tickIncrement', () => { + expect(tickIncrement(0, 1, 10)).toBe(-10); + expect(tickIncrement(0, 1, 9)).toBe(-10); + expect(tickIncrement(0, 1, 8)).toBe(-10); + expect(tickIncrement(0, 1, 7)).toBe(-5); + expect(tickIncrement(0, 1, 6)).toBe(-5); + expect(tickIncrement(0, 1, 5)).toBe(-5); + expect(tickIncrement(0, 1, 4)).toBe(-5); + expect(tickIncrement(0, 1, 3)).toBe(-2); + expect(tickIncrement(0, 1, 2)).toBe(-2); + expect(tickIncrement(0, 1, 1)).toBe(1); + expect(tickIncrement(0, 10, 10)).toBe(1); + expect(tickIncrement(0, 10, 9)).toBe(1); + expect(tickIncrement(0, 10, 8)).toBe(1); + expect(tickIncrement(0, 10, 7)).toBe(2); + expect(tickIncrement(0, 10, 6)).toBe(2); + expect(tickIncrement(0, 10, 5)).toBe(2); + expect(tickIncrement(0, 10, 4)).toBe(2); + expect(tickIncrement(0, 10, 3)).toBe(5); + expect(tickIncrement(0, 10, 2)).toBe(5); + expect(tickIncrement(0, 10, 1)).toBe(10); + expect(tickIncrement(-10, 10, 10)).toBe(2); + expect(tickIncrement(-10, 10, 9)).toBe(2); + expect(tickIncrement(-10, 10, 8)).toBe(2); + expect(tickIncrement(-10, 10, 7)).toBe(2); + expect(tickIncrement(-10, 10, 6)).toBe(5); + expect(tickIncrement(-10, 10, 5)).toBe(5); + expect(tickIncrement(-10, 10, 4)).toBe(5); + expect(tickIncrement(-10, 10, 3)).toBe(5); + expect(tickIncrement(-10, 10, 2)).toBe(10); + expect(tickIncrement(-10, 10, 1)).toBe(20); + }); +}); diff --git a/__tests__/unit/utils/time-interval.spec.ts b/__tests__/unit/utils/time-interval.spec.ts index c5072936..e87454cd 100644 --- a/__tests__/unit/utils/time-interval.spec.ts +++ b/__tests__/unit/utils/time-interval.spec.ts @@ -1,13 +1,6 @@ +import { millisecond, second, minute, hour, day, week, month, year, localIntervalMap } from '../../../src/utils'; + import { - millisecond, - second, - minute, - hour, - day, - week, - month, - year, - localIntervalMap, DURATION_SECOND, DURATION_MINUTE, DURATION_HOUR, @@ -15,7 +8,7 @@ import { DURATION_WEEK, DURATION_MONTH, DURATION_YEAR, -} from '../../../src/utils'; +} from '../../../src'; describe('time floor', () => { test('defaults durations', () => { @@ -39,68 +32,134 @@ describe('time floor', () => { expect(localIntervalMap.year).toEqual(year); }); - test('millisecond(date) returns milliseconds', () => { + test('millisecond.fn(date) returns milliseconds', () => { expect(millisecond.duration).toBe(1); - expect(millisecond.floor(new Date(2021, 11, 31, 23, 59, 59))).toEqual(new Date(2021, 11, 31, 23, 59, 59)); - // expect(millisecond.ceil(new Date(2021, 11, 31, 23, 59, 59))).toEqual(new Date(2022, 0, 1, 0, 0, 0)); + expect(millisecond.ceil(new Date(2021, 11, 31, 23, 59, 59, 999))).toEqual(new Date(2021, 11, 31, 23, 59, 59, 999)); + + expect(millisecond.range(new Date(2021, 5, 1, 1, 1, 1, 1), new Date(2021, 5, 1, 1, 1, 1, 11), 3)).toEqual([ + new Date(2021, 5, 1, 1, 1, 1, 1), + new Date(2021, 5, 1, 1, 1, 1, 4), + new Date(2021, 5, 1, 1, 1, 1, 7), + new Date(2021, 5, 1, 1, 1, 1, 10), + ]); }); - test('second(date) return seconds', () => { + test('second.fn(date) return seconds', () => { expect(second.duration).toBe(DURATION_SECOND); - expect(second.floor(new Date(2021, 11, 31, 23, 59, 59, 999))).toEqual(new Date(2021, 11, 31, 23, 59, 59)); expect(second.floor(new Date(2021, 0, 1, 0, 0, 0, 0))).toEqual(new Date(2021, 0, 1, 0, 0, 0, 0)); expect(second.floor(new Date(2021, 0, 1, 0, 0, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 0, 0, 0)); - // expect(second.ceil(new Date(2021, 0, 1, 0, 0, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 0, 1)); + expect(second.ceil(new Date(2021, 0, 1, 0, 0, 0, 999))).toEqual(new Date(2021, 0, 1, 0, 0, 1)); + expect(second.ceil(new Date(2021, 0, 1, 0, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 0, 1)); + + expect(second.range(new Date(2021, 5, 1, 1, 1, 1), new Date(2021, 5, 1, 1, 1, 11), 3)).toEqual([ + new Date(2021, 5, 1, 1, 1, 1), + new Date(2021, 5, 1, 1, 1, 4), + new Date(2021, 5, 1, 1, 1, 7), + new Date(2021, 5, 1, 1, 1, 10), + ]); }); - test('minute(date) return minutes', () => { + test('minute.fn(date) return minutes', () => { expect(minute.duration).toBe(DURATION_MINUTE); expect(minute.floor(new Date(2021, 11, 31, 23, 59, 59))).toEqual(new Date(2021, 11, 31, 23, 59)); expect(minute.floor(new Date(2021, 0, 1, 0, 0, 0))).toEqual(new Date(2021, 0, 1, 0, 0, 0)); expect(minute.floor(new Date(2021, 0, 1, 0, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 0, 0)); + + expect(minute.ceil(new Date(2021, 0, 1, 0, 0, 59))).toEqual(new Date(2021, 0, 1, 0, 1, 0)); + expect(minute.ceil(new Date(2021, 0, 1, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 1)); + + expect(minute.range(new Date(2021, 5, 1, 1, 1), new Date(2021, 5, 1, 1, 11), 3)).toEqual([ + new Date(2021, 5, 1, 1, 1), + new Date(2021, 5, 1, 1, 4), + new Date(2021, 5, 1, 1, 7), + new Date(2021, 5, 1, 1, 10), + ]); }); - test('hour(date) return hours', () => { + test('hour.fn(date) return hours', () => { expect(hour.duration).toBe(DURATION_HOUR); expect(hour.floor(new Date(2021, 11, 31, 23, 59))).toEqual(new Date(2021, 11, 31, 23)); expect(hour.floor(new Date(2021, 0, 1, 0, 0))).toEqual(new Date(2021, 0, 1, 0, 0)); expect(hour.floor(new Date(2021, 0, 1, 0, 1))).toEqual(new Date(2021, 0, 1, 0, 0)); + + expect(hour.ceil(new Date(2021, 0, 1, 0, 59))).toEqual(new Date(2021, 0, 1, 1)); + expect(hour.ceil(new Date(2021, 0, 1, 1))).toEqual(new Date(2021, 0, 1, 1)); + + expect(hour.range(new Date(2021, 5, 1, 1), new Date(2021, 5, 1, 11), 3)).toEqual([ + new Date(2021, 5, 1, 1), + new Date(2021, 5, 1, 4), + new Date(2021, 5, 1, 7), + new Date(2021, 5, 1, 10), + ]); }); - test('day(date) return days', () => { + test('day.fn(date) return days', () => { expect(day.duration).toBe(DURATION_DAY); expect(day.floor(new Date(2021, 3, 25, 23))).toEqual(new Date(2021, 3, 25)); expect(day.floor(new Date(2021, 3, 25))).toEqual(new Date(2021, 3, 25)); expect(day.floor(new Date(2021, 3, 25, 1))).toEqual(new Date(2021, 3, 25)); + + expect(day.ceil(new Date(2021, 0, 1, 23))).toEqual(new Date(2021, 0, 2)); + expect(day.ceil(new Date(2021, 0, 1))).toEqual(new Date(2021, 0, 1)); + + expect(day.range(new Date(2021, 5, 1), new Date(2021, 5, 11), 3)).toEqual([ + new Date(2021, 5, 1), + new Date(2021, 5, 4), + new Date(2021, 5, 7), + new Date(2021, 5, 10), + ]); }); - test('week(date) return weeks', () => { + test('week.fn(date) return weeks', () => { expect(week.duration).toBe(DURATION_WEEK); expect(week.floor(new Date(2021, 3, 24))).toEqual(new Date(2021, 3, 18)); expect(week.floor(new Date(2021, 3, 25))).toEqual(new Date(2021, 3, 25)); expect(week.floor(new Date(2021, 3, 26))).toEqual(new Date(2021, 3, 25)); + + expect(week.ceil(new Date(2021, 3, 18))).toEqual(new Date(2021, 3, 18)); + expect(week.ceil(new Date(2021, 3, 24))).toEqual(new Date(2021, 3, 25)); }); - test('month(date) return month', () => { + test('month.fn(date) return month', () => { expect(month.duration).toBe(DURATION_MONTH); expect(month.floor(new Date(2021, 0, 31))).toEqual(new Date(2021, 0)); expect(month.floor(new Date(2021, 1, 1))).toEqual(new Date(2021, 1)); expect(month.floor(new Date(2021, 1, 2))).toEqual(new Date(2021, 1)); + + expect(month.ceil(new Date(2021, 0, 31))).toEqual(new Date(2021, 1)); + expect(month.ceil(new Date(2021, 0))).toEqual(new Date(2021, 0)); + + expect(month.range(new Date(2021, 1), new Date(2021, 11), 3)).toEqual([ + new Date(2021, 1), + new Date(2021, 4), + new Date(2021, 7), + new Date(2021, 10), + ]); }); - test('year(date) return weeks', () => { + test('year.fn(date) return years', () => { expect(year.duration).toBe(DURATION_YEAR); expect(year.floor(new Date(2021, 0))).toEqual(new Date(2021, 0)); expect(year.floor(new Date(2021, 3))).toEqual(new Date(2021, 0)); expect(year.floor(new Date(2021, 11))).toEqual(new Date(2021, 0)); + + expect(year.ceil(new Date(2021, 11))).toEqual(new Date(2022, 0)); + expect(year.ceil(new Date(2021, 0))).toEqual(new Date(2021, 0)); + + expect(year.range(new Date(2001, 0), new Date(2011, 0), 3)).toEqual([ + new Date(2001, 0), + new Date(2004, 0), + new Date(2007, 0), + new Date(2010, 0), + ]); }); }); diff --git a/__tests__/unit/utils/utc-interval.spec.ts b/__tests__/unit/utils/utc-interval.spec.ts index d5b38414..d12dc0c0 100644 --- a/__tests__/unit/utils/utc-interval.spec.ts +++ b/__tests__/unit/utils/utc-interval.spec.ts @@ -45,6 +45,14 @@ describe('time floor', () => { expect(utcMillisecond.duration).toBe(1); expect(utcMillisecond.floor(UTC(2021, 11, 31, 23, 59, 59))).toEqual(UTC(2021, 11, 31, 23, 59, 59)); + expect(utcMillisecond.ceil(UTC(2021, 11, 31, 23, 59, 59, 999))).toEqual(UTC(2021, 11, 31, 23, 59, 59, 999)); + + expect(utcMillisecond.range(UTC(2021, 5, 1, 1, 1, 1, 1), UTC(2021, 5, 1, 1, 1, 1, 11), 3)).toEqual([ + UTC(2021, 5, 1, 1, 1, 1, 1), + UTC(2021, 5, 1, 1, 1, 1, 4), + UTC(2021, 5, 1, 1, 1, 1, 7), + UTC(2021, 5, 1, 1, 1, 1, 10), + ]); }); test('utcSecond(date) return seconds', () => { @@ -53,6 +61,16 @@ describe('time floor', () => { expect(utcSecond.floor(UTC(2021, 11, 31, 23, 59, 59, 999))).toEqual(UTC(2021, 11, 31, 23, 59, 59)); expect(utcSecond.floor(UTC(2021, 0, 1, 0, 0, 0, 0))).toEqual(UTC(2021, 0, 1, 0, 0, 0, 0)); expect(utcSecond.floor(UTC(2021, 0, 1, 0, 0, 0, 1))).toEqual(UTC(2021, 0, 1, 0, 0, 0, 0)); + + expect(utcSecond.ceil(UTC(2021, 0, 1, 0, 0, 0, 999))).toEqual(UTC(2021, 0, 1, 0, 0, 1)); + expect(utcSecond.ceil(UTC(2021, 0, 1, 0, 0, 1))).toEqual(UTC(2021, 0, 1, 0, 0, 1)); + + expect(utcSecond.range(UTC(2021, 5, 1, 1, 1, 1), UTC(2021, 5, 1, 1, 1, 11), 3)).toEqual([ + UTC(2021, 5, 1, 1, 1, 1), + UTC(2021, 5, 1, 1, 1, 4), + UTC(2021, 5, 1, 1, 1, 7), + UTC(2021, 5, 1, 1, 1, 10), + ]); }); test('utcMinute(date) return minutes', () => { @@ -61,6 +79,16 @@ describe('time floor', () => { expect(utcMinute.floor(UTC(2021, 11, 31, 23, 59, 59))).toEqual(UTC(2021, 11, 31, 23, 59)); expect(utcMinute.floor(UTC(2021, 0, 1, 0, 0, 0))).toEqual(UTC(2021, 0, 1, 0, 0, 0)); expect(utcMinute.floor(UTC(2021, 0, 1, 0, 0, 1))).toEqual(UTC(2021, 0, 1, 0, 0, 0)); + + expect(utcMinute.ceil(UTC(2021, 0, 1, 0, 0, 59))).toEqual(UTC(2021, 0, 1, 0, 1, 0)); + expect(utcMinute.ceil(UTC(2021, 0, 1, 0, 1))).toEqual(UTC(2021, 0, 1, 0, 1)); + + expect(utcMinute.range(UTC(2021, 5, 1, 1, 1), UTC(2021, 5, 1, 1, 11), 3)).toEqual([ + UTC(2021, 5, 1, 1, 1), + UTC(2021, 5, 1, 1, 4), + UTC(2021, 5, 1, 1, 7), + UTC(2021, 5, 1, 1, 10), + ]); }); test('utcHour(date) return hours', () => { @@ -69,6 +97,16 @@ describe('time floor', () => { expect(utcHour.floor(UTC(2021, 11, 31, 23, 59))).toEqual(UTC(2021, 11, 31, 23)); expect(utcHour.floor(UTC(2021, 0, 1, 0, 0))).toEqual(UTC(2021, 0, 1, 0, 0)); expect(utcHour.floor(UTC(2021, 0, 1, 0, 1))).toEqual(UTC(2021, 0, 1, 0, 0)); + + expect(utcHour.ceil(UTC(2021, 0, 1, 0, 59))).toEqual(UTC(2021, 0, 1, 1)); + expect(utcHour.ceil(UTC(2021, 0, 1, 1))).toEqual(UTC(2021, 0, 1, 1)); + + expect(utcHour.range(UTC(2021, 5, 1, 1), UTC(2021, 5, 1, 11), 3)).toEqual([ + UTC(2021, 5, 1, 1), + UTC(2021, 5, 1, 4), + UTC(2021, 5, 1, 7), + UTC(2021, 5, 1, 10), + ]); }); test('utcDay(date) return days', () => { @@ -77,6 +115,16 @@ describe('time floor', () => { expect(utcDay.floor(UTC(2021, 3, 25, 23))).toEqual(UTC(2021, 3, 25)); expect(utcDay.floor(UTC(2021, 3, 25))).toEqual(UTC(2021, 3, 25)); expect(utcDay.floor(UTC(2021, 3, 25, 1))).toEqual(UTC(2021, 3, 25)); + + expect(utcDay.ceil(UTC(2021, 0, 1, 23))).toEqual(UTC(2021, 0, 2)); + expect(utcDay.ceil(UTC(2021, 0, 1))).toEqual(UTC(2021, 0, 1)); + + expect(utcDay.range(UTC(2021, 5, 1), UTC(2021, 5, 11), 3)).toEqual([ + UTC(2021, 5, 1), + UTC(2021, 5, 4), + UTC(2021, 5, 7), + UTC(2021, 5, 10), + ]); }); test('utcWeek(date) return weeks', () => { @@ -85,6 +133,9 @@ describe('time floor', () => { expect(utcWeek.floor(UTC(2021, 3, 24))).toEqual(UTC(2021, 3, 18)); expect(utcWeek.floor(UTC(2021, 3, 25))).toEqual(UTC(2021, 3, 25)); expect(utcWeek.floor(UTC(2021, 3, 26))).toEqual(UTC(2021, 3, 25)); + + expect(utcWeek.ceil(UTC(2021, 3, 18))).toEqual(UTC(2021, 3, 18)); + expect(utcWeek.ceil(UTC(2021, 3, 24))).toEqual(UTC(2021, 3, 25)); }); test('utcMonth(date) return month', () => { @@ -93,6 +144,16 @@ describe('time floor', () => { expect(utcMonth.floor(UTC(2021, 0, 31))).toEqual(UTC(2021, 0)); expect(utcMonth.floor(UTC(2021, 1, 1))).toEqual(UTC(2021, 1)); expect(utcMonth.floor(UTC(2021, 1, 2))).toEqual(UTC(2021, 1)); + + expect(utcMonth.ceil(UTC(2021, 0, 31))).toEqual(UTC(2021, 1)); + expect(utcMonth.ceil(UTC(2021, 0))).toEqual(UTC(2021, 0)); + + expect(utcMonth.range(UTC(2021, 1), UTC(2021, 11), 3)).toEqual([ + UTC(2021, 1), + UTC(2021, 4), + UTC(2021, 7), + UTC(2021, 10), + ]); }); test('utcYear(date) return weeks', () => { @@ -101,5 +162,14 @@ describe('time floor', () => { expect(utcYear.floor(UTC(2021, 0))).toEqual(UTC(2021, 0)); expect(utcYear.floor(UTC(2021, 3))).toEqual(UTC(2021, 0)); expect(utcYear.floor(UTC(2021, 11))).toEqual(UTC(2021, 0)); + expect(utcYear.ceil(UTC(2021, 11))).toEqual(UTC(2022, 0)); + expect(utcYear.ceil(UTC(2021, 0))).toEqual(UTC(2021, 0)); + + expect(utcYear.range(UTC(2001, 0), UTC(2011, 0), 3)).toEqual([ + UTC(2001, 0), + UTC(2004, 0), + UTC(2007, 0), + UTC(2010, 0), + ]); }); }); diff --git a/src/index.ts b/src/index.ts index dce8fbe2..25cb172b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ export { Band } from './scales/band'; export { Ordinal } from './scales/ordinal'; export { Constant } from './scales/constant'; -export { Continuous } from './scales/continuous'; export { Identity } from './scales/identity'; export { Linear } from './scales/linear'; export { Point } from './scales/point'; @@ -36,8 +35,18 @@ export type { SqrtOptions, QuantileOptions, LogOptions, - ContinuousOptions, } from './types'; // others export type { TickMethod, Interpolate, Comparator } from './types'; + +// constants +export { + DURATION_SECOND, + DURATION_MINUTE, + DURATION_HOUR, + DURATION_DAY, + DURATION_WEEK, + DURATION_YEAR, + DURATION_MONTH, +} from './utils'; diff --git a/src/scales/time.ts b/src/scales/time.ts index 3606e7c2..eb898681 100644 --- a/src/scales/time.ts +++ b/src/scales/time.ts @@ -40,10 +40,10 @@ export class Time extends Continuous { } protected getTickMethodOptions() { - const { domain, tickCount, tickInterval } = this.options; + const { domain, tickCount, tickInterval, utc } = this.options; const min = domain[0]; const max = domain[domain.length - 1]; - return [min, max, tickCount, tickInterval]; + return [min, max, tickCount, tickInterval, utc]; } public getFormatter() { diff --git a/src/tick-methods/d3-linear.ts b/src/tick-methods/d3-linear.ts index 94f98ab8..1ac6a945 100644 --- a/src/tick-methods/d3-linear.ts +++ b/src/tick-methods/d3-linear.ts @@ -1,5 +1,5 @@ import { TickMethod } from '../types'; -import { tickIncrement } from '../utils/tick-utils'; +import { tickIncrement } from '../utils'; export const d3Linear: TickMethod = (begin: number, end: number, count: number) => { let n; diff --git a/src/tick-methods/d3-time.ts b/src/tick-methods/d3-time.ts index 1467f52d..0b0d44ec 100644 --- a/src/tick-methods/d3-time.ts +++ b/src/tick-methods/d3-time.ts @@ -1,8 +1,11 @@ import { TickMethod } from '../types'; -import { d3Linear } from './d3-linear'; +import { findTickInterval } from '../utils'; -// 暂时用 d3Linear 代替 -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const d3Time: TickMethod = (min, max, count, interval) => { - return d3Linear(+min, +max, count).map((d) => new Date(d)); +export const d3Time: TickMethod = (min, max, count, interval, utc: boolean) => { + const r = min > max; + const lo = r ? max : min; + const hi = r ? min : max; + const [tickInterval, step] = findTickInterval(lo, hi, count, interval, utc); + const ticks = tickInterval.range(lo, new Date(+hi + 1), step, true); + return r ? ticks.reverse() : ticks; }; diff --git a/src/utils/bisect.ts b/src/utils/bisect.ts index d32d2a44..558a626c 100644 --- a/src/utils/bisect.ts +++ b/src/utils/bisect.ts @@ -7,12 +7,13 @@ * @param hi 结束的索引 * @returns 最右边一个匹配的值后一个的索引 */ -export function bisect(array: number[], x: number, lo?: number, hi?: number): number { +export function bisect(array: any[], x: number, lo?: number, hi?: number, getter?: (any) => any): number { let i = lo || 0; let j = hi || array.length; + const get = getter || ((x) => x); while (i < j) { const mid = Math.floor((i + j) / 2); - if (array[mid] > x) { + if (get(array[mid]) > x) { j = mid; } else { i = mid + 1; diff --git a/src/utils/d3-linear-nice.ts b/src/utils/d3-linear-nice.ts index 480ccba9..a490f2ab 100644 --- a/src/utils/d3-linear-nice.ts +++ b/src/utils/d3-linear-nice.ts @@ -1,7 +1,7 @@ // 参考 d3-linear nice 的实现 // https://github.com/d3/d3-scale -import { tickIncrement } from './tick-utils'; +import { tickIncrement } from './ticks'; import { NiceMethod } from '../types'; export const d3LinearNice: NiceMethod = (min: number, max: number, count: number = 5) => { diff --git a/src/utils/d3-time-nice.ts b/src/utils/d3-time-nice.ts index 4c22aa58..6df7405f 100644 --- a/src/utils/d3-time-nice.ts +++ b/src/utils/d3-time-nice.ts @@ -1,7 +1,11 @@ import { NiceMethod } from '../types'; +import { findTickInterval } from './find-tick-interval'; -// 暂时用 d3LinearNice 代替 -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const d3TimeNice: NiceMethod = (min, max, count, interval) => { - return [min, max]; +export const d3TimeNice: NiceMethod = (min, max, count, interval, utc) => { + const r = min > max; + const lo = r ? max : min; + const hi = r ? min : max; + const [tickInterval, step] = findTickInterval(lo, hi, count, interval, utc); + const domain = [tickInterval.floor(lo, step), tickInterval.ceil(hi, step)]; + return r ? domain.reverse() : domain; }; diff --git a/src/utils/find-tick-interval.ts b/src/utils/find-tick-interval.ts new file mode 100644 index 00000000..184c2833 --- /dev/null +++ b/src/utils/find-tick-interval.ts @@ -0,0 +1,62 @@ +import { Interval, localIntervalMap } from './time-interval'; +import { utcIntervalMap } from './utc-interval'; +import { bisect } from './bisect'; +import { tickStep } from './ticks'; + +type TickInterval = [Interval, number]; + +function chooseTickIntervals(utc: boolean): { year: Interval; millisecond: Interval; tickIntervals: TickInterval[] } { + const intervalMap = utc ? utcIntervalMap : localIntervalMap; + const { year, month, week, day, hour, minute, second, millisecond } = intervalMap; + const tickIntervals: TickInterval[] = [ + [second, 1], + [second, 5], + [second, 15], + [second, 30], + [minute, 1], + [minute, 5], + [minute, 15], + [minute, 30], + [hour, 1], + [hour, 3], + [hour, 6], + [hour, 12], + [day, 1], + [day, 2], + [week, 1], + [month, 1], + [month, 3], + [year, 1], + ]; + return { + tickIntervals, + year, + millisecond, + }; +} + +export function findTickInterval(start: Date, stop: Date, count: number, interval: number, utc: boolean) { + const lo = +start; + const hi = +stop; + const { tickIntervals, year, millisecond } = chooseTickIntervals(utc); + const getter = ([interval, count]) => interval.duration * count; + const targetCount = interval ? (hi - lo) / interval : count || 5; + const targetInterval = interval || (hi - lo) / targetCount; + + const len = tickIntervals.length; + const i = bisect(tickIntervals, targetInterval, 0, len, getter); + let matchInterval: TickInterval; + if (i === len) { + const step = tickStep(lo / year.duration, hi / year.duration, targetCount); + matchInterval = [year, step]; + } else if (i) { + const closeToLow = targetInterval / getter(tickIntervals[i - 1]) < getter(tickIntervals[i]) / targetInterval; + const [timeInterval, targetStep] = closeToLow ? tickIntervals[i - 1] : tickIntervals[i]; + const step = interval ? Math.ceil(interval / timeInterval.duration) : targetStep; + matchInterval = [timeInterval, step]; + } else { + const step = Math.max(tickStep(lo, hi, targetCount), 1); + matchInterval = [millisecond, step]; + } + return matchInterval; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 894da59b..b115ec91 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,6 +6,8 @@ export { bisect } from './bisect'; export { d3LinearNice } from './d3-linear-nice'; export { d3TimeNice } from './d3-time-nice'; export { isValid } from './is-valid'; +export { tickIncrement, tickStep } from './ticks'; +export { findTickInterval } from './find-tick-interval'; export { DURATION_SECOND, diff --git a/src/utils/tick-utils.ts b/src/utils/ticks.ts similarity index 61% rename from src/utils/tick-utils.ts rename to src/utils/ticks.ts index 53ec7950..4060b4a3 100644 --- a/src/utils/tick-utils.ts +++ b/src/utils/ticks.ts @@ -13,3 +13,13 @@ export function tickIncrement(start: number, stop: number, count: number): numbe // eslint-disable-next-line no-nested-ternary return -(10 ** -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1); } + +export function tickStep(start: number, stop: number, count: number) { + const step0 = Math.abs(stop - start) / Math.max(0, count); + let step1 = 10 ** Math.floor(Math.log(step0) / Math.LN10); + const error = step0 / step1; + if (error >= e10) step1 *= 10; + else if (error >= e5) step1 *= 5; + else if (error >= e2) step1 *= 2; + return stop < start ? -step1 : step1; +} diff --git a/src/utils/time-interval.ts b/src/utils/time-interval.ts index 83336c68..1ccc6a48 100644 --- a/src/utils/time-interval.ts +++ b/src/utils/time-interval.ts @@ -1,4 +1,7 @@ -export type TimeTransform = (d: Date) => Date; +export type TimeTransform = (d: Date, ...rest: any[]) => Date; +type TimeRange = (start: Date, stop: Date, step: number, shouldAdjust?: boolean) => Date[]; +type TimeProcess = (d: Date, ...rest: any[]) => void; +type TimeField = (d: Date) => number; export const DURATION_SECOND = 1000; export const DURATION_MINUTE = DURATION_SECOND * 60; @@ -10,7 +13,8 @@ export const DURATION_YEAR = DURATION_DAY * 365; export type Interval = { floor: TimeTransform; - // ceil: TimeTransform; + ceil: TimeTransform; + range: TimeRange; duration: number; }; @@ -25,61 +29,148 @@ export type IntervalMap = { year: Interval; }; -export function createInterval(floor: TimeTransform, duration: number) { - // const ceil: TimeTransform = (date) => new Date(+floor(date) + duration); +export function createInterval(duration: number, floorish: TimeProcess, offseti: TimeProcess, field?: TimeField) { + const adjust: TimeTransform = (date, step) => { + const test = (date: Date) => field(date) % step === 0; + let i = step; + while (i && !test(date)) { + offseti(date, -1); + i -= 1; + } + return date; + }; + + const floori: TimeProcess = (date, step?: number) => { + if (step) adjust(date, step); + floorish(date); + }; + + const floor: TimeTransform = (date, step?: number) => { + const d = new Date(+date); + floori(d, step); + return d; + }; + + const ceil: TimeTransform = (date, step?: number) => { + const d = new Date(+date - 1); + floori(d, step); + offseti(d, step); + floori(d); + return d; + }; + + const range = (start: Date, stop: Date, step: number, shouldAdjust?: boolean) => { + const ticks = []; + const roundStep = Math.floor(step); + const t = shouldAdjust ? ceil(start, step) : ceil(start); + for (let i = t; +i < +stop; offseti(i, roundStep), floori(i)) { + ticks.push(new Date(+i)); + } + return ticks; + }; + return { + ceil, floor, - // ceil, + range, duration, }; } -export const millisecond: Interval = createInterval((date) => new Date(date), 1); - -export const second: Interval = createInterval((date) => { - const d = new Date(date); - d.setMilliseconds(0); - return d; -}, DURATION_SECOND); - -export const minute: Interval = createInterval((date) => { - const d = new Date(date); - d.setSeconds(0, 0); - return d; -}, DURATION_MINUTE); - -export const hour: Interval = createInterval((date) => { - const d = new Date(date); - d.setMinutes(0, 0, 0); - return d; -}, DURATION_HOUR); - -export const day: Interval = createInterval((date) => { - const d = new Date(date); - d.setHours(0, 0, 0, 0); - return d; -}, DURATION_DAY); - -export const week: Interval = createInterval((date) => { - const d = new Date(date); - d.setDate(d.getDate() - (d.getDay() % 7)); - d.setHours(0, 0, 0, 0); - return d; -}, DURATION_WEEK); - -export const month: Interval = createInterval((date) => { - const d = new Date(date); - d.setDate(1); - d.setHours(0, 0, 0, 0); - return d; -}, DURATION_MONTH); - -export const year: Interval = createInterval((date) => { - const d = new Date(date); - d.setMonth(0, 1); - d.setHours(0, 0, 0, 0); - return d; -}, DURATION_YEAR); +export const millisecond: Interval = createInterval( + 1, + (date) => date, + (date, step = 1) => { + date.setTime(+date + step); + }, + (date) => date.getTime() +); + +export const second: Interval = createInterval( + DURATION_SECOND, + (date) => { + date.setMilliseconds(0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_SECOND * step); + }, + (date) => date.getSeconds() +); + +export const minute: Interval = createInterval( + DURATION_MINUTE, + (date) => { + date.setSeconds(0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_MINUTE * step); + }, + (date) => date.getMinutes() +); + +export const hour: Interval = createInterval( + DURATION_HOUR, + (date) => { + date.setMinutes(0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_HOUR * step); + }, + (date) => date.getHours() +); + +export const day: Interval = createInterval( + DURATION_DAY, + (date) => { + date.setHours(0, 0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_DAY * step); + }, + (date) => date.getDate() - 1 +); + +export const month: Interval = createInterval( + DURATION_MONTH, + (date) => { + date.setDate(1); + date.setHours(0, 0, 0, 0); + }, + (date, step = 1) => { + const month = date.getMonth(); + date.setMonth(month + step); + }, + (date) => date.getMonth() +); + +export const week: Interval = createInterval( + DURATION_WEEK, + (date) => { + date.setDate(date.getDate() - (date.getDay() % 7)); + date.setHours(0, 0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_WEEK * step); + }, + (date) => { + const start = month.floor(date); + const end = new Date(+date); + return Math.floor((+end - +start) / DURATION_WEEK); + } +); + +export const year: Interval = createInterval( + DURATION_YEAR, + (date) => { + date.setMonth(0, 1); + date.setHours(0, 0, 0, 0); + }, + (date, step = 1) => { + const year = date.getFullYear(); + date.setFullYear(year + step); + }, + (date) => date.getFullYear() +); export const localIntervalMap: IntervalMap = { millisecond, diff --git a/src/utils/utc-interval.ts b/src/utils/utc-interval.ts index 0472e713..14f1bfbf 100644 --- a/src/utils/utc-interval.ts +++ b/src/utils/utc-interval.ts @@ -11,52 +11,100 @@ import { DURATION_YEAR, } from './time-interval'; -export const utcMillisecond: Interval = createInterval((date) => new Date(date), 1); +export const utcMillisecond: Interval = createInterval( + 1, + (date) => date, + (date, step = 1) => { + date.setTime(+date + step); + }, + (date) => date.getTime() +); -export const utcSecond: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCMilliseconds(0); - return d; -}, DURATION_SECOND); +export const utcSecond: Interval = createInterval( + DURATION_SECOND, + (date) => { + date.setUTCMilliseconds(0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_SECOND * step); + }, + (date) => date.getUTCSeconds() +); -export const utcMinute: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCSeconds(0, 0); - return d; -}, DURATION_MINUTE); +export const utcMinute: Interval = createInterval( + DURATION_MINUTE, + (date) => { + date.setUTCSeconds(0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_MINUTE * step); + }, + (date) => date.getUTCMinutes() +); -export const utcHour: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCMinutes(0, 0, 0); - return d; -}, DURATION_HOUR); +export const utcHour: Interval = createInterval( + DURATION_HOUR, + (date) => { + date.setUTCMinutes(0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_HOUR * step); + }, + (date) => date.getUTCHours() +); -export const utcDay: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCHours(0, 0, 0, 0); - return d; -}, DURATION_DAY); +export const utcDay: Interval = createInterval( + DURATION_DAY, + (date) => { + date.setUTCHours(0, 0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_DAY * step); + }, + (date) => date.getUTCDate() - 1 +); -export const utcWeek: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 7) % 7)); - d.setUTCHours(0, 0, 0, 0); - return d; -}, DURATION_WEEK); +export const utcMonth: Interval = createInterval( + DURATION_MONTH, + (date) => { + date.setUTCDate(1); + date.setUTCHours(0, 0, 0, 0); + }, + (date, step = 1) => { + const month = date.getUTCMonth(); + date.setUTCMonth(month + step); + }, + (date) => date.getUTCMonth() +); -export const utcMonth: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCDate(1); - d.setUTCHours(0, 0, 0, 0); - return d; -}, DURATION_MONTH); +export const utcWeek: Interval = createInterval( + DURATION_WEEK, + (date) => { + date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 7) % 7)); + date.setUTCHours(0, 0, 0, 0); + }, + (date, step = 1) => { + date.setTime(+date + DURATION_WEEK * step); + }, + (date) => { + const start = utcMonth.floor(date); + const end = new Date(+date); + return Math.floor((+end - +start) / DURATION_WEEK); + } +); -export const utcYear: Interval = createInterval((date) => { - const d = new Date(date); - d.setUTCMonth(0, 1); - d.setUTCHours(0, 0, 0, 0); - return d; -}, DURATION_YEAR); +export const utcYear: Interval = createInterval( + DURATION_YEAR, + (date) => { + date.setUTCMonth(0, 1); + date.setUTCHours(0, 0, 0, 0); + }, + (date, step = 1) => { + const year = date.getUTCFullYear(); + date.setUTCFullYear(year + step); + }, + (date) => date.getUTCFullYear() +); export const utcIntervalMap: IntervalMap = { millisecond: utcMillisecond,