diff --git a/.eslintrc.js b/.eslintrc.js index 281dd7b..3b6ddb2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { 'no-param-reassign': 'off', 'func-names': ['error', 'never'], 'no-else-return': 'off', + 'no-restricted-syntax': 'off', }, settings: { 'import/parsers': { diff --git a/__tests__/unit/scales/band.spec.ts b/__tests__/unit/scales/band.spec.ts index ae65731..10d9ad0 100644 --- a/__tests__/unit/scales/band.spec.ts +++ b/__tests__/unit/scales/band.spec.ts @@ -220,4 +220,35 @@ describe('band scale', () => { expect(bandScale.map('B')).toBeCloseTo(166.67); expect(bandScale.map('C')).toBeCloseTo(416.67); }); + + test('test flex is all 1', () => { + const bandScale = new Band({ + domain: ['A', 'B', 'C'], + flex: [1, 1, 1], + range: [0, 300], + }); + expect(bandScale.getBandWidth()).toBe(100); + expect(bandScale.getStep()).toBe(100); + }); + + test('test domain length is null', () => { + const bandScale = new Band({ + domain: [], + range: [0, 500], + }); + expect(bandScale.getBandWidth()).toBe(1); + expect(bandScale.getStep()).toBe(1); + }); + + test('test flex options with object type domain', () => { + const time = [new Date(Date.UTC(2022, 9, 5)), new Date(Date.UTC(2022, 9, 6)), new Date(Date.UTC(2022, 9, 7))]; + const bandScale = new Band({ + domain: time, + flex: [2, 3], + range: [0, 500], + }); + expect(bandScale.map(new Date(Date.UTC(2022, 9, 5)))).toBe(0); + expect(bandScale.map(new Date(Date.UTC(2022, 9, 6)))).toBeCloseTo(166.67); + expect(bandScale.map(new Date(Date.UTC(2022, 9, 7)))).toBeCloseTo(416.67); + }); }); diff --git a/__tests__/unit/utils/interMap.spec.ts b/__tests__/unit/utils/interMap.spec.ts new file mode 100644 index 0000000..eac5ca3 --- /dev/null +++ b/__tests__/unit/utils/interMap.spec.ts @@ -0,0 +1,32 @@ +import { InternMap } from '../../../src/utils'; + +describe('create InternMap ', () => { + test('create InternMap with key of string', () => { + const internMap = new InternMap([ + [1, 'dog'], + [2, 'cat'], + ]); + internMap.set(3, 'cow'); + expect(internMap.get(1)).toBe('dog'); + expect(internMap.get(3)).toBe('cow'); + + internMap.set(1, 'mouse'); + expect(internMap.get(1)).toBe('mouse'); + + internMap.delete(2); + expect(internMap.has(2)).toBeFalsy(); + }); + + test('create InternMap with key of object', () => { + const time1 = new Date(Date.UTC(2022, 9, 5)); + const time2 = new Date(Date.UTC(2022, 9, 5)); + expect(time1 === time2).toBeFalsy(); + + const internMap = new InternMap([ + [time1, 'time1'], + [time2, 'time2'], + ]); + + expect(internMap.get(time1)).toBe('time2'); + }); +}); diff --git a/src/scales/band.ts b/src/scales/band.ts index 7fe11d6..af9e577 100644 --- a/src/scales/band.ts +++ b/src/scales/band.ts @@ -1,3 +1,4 @@ +import { InternMap } from '../utils'; import { BandOptions, Domain } from '../types'; import { Ordinal } from './ordinal'; @@ -34,22 +35,13 @@ interface BandStateOptions { } /** - * 基于 band 基础配置获取 band 的状态 + * 基于 band 基础配置获取存在 flex band 的状态 */ -function computeBandState(options: BandStateOptions) { - const { domain } = options; - const n = domain.length; - if (n === 0) { - return { - valueBandWidth: undefined, - valueStep: undefined, - adjustedRange: [], - }; - } - +function computeFlexBandState(options: BandStateOptions) { // 如果 flex 比 domain 少,那么就补全 // 如果 flex 比 domain 多,就截取 - const { range, paddingOuter, paddingInner, flex: F = [], round, align } = options; + const { domain, range, paddingOuter, paddingInner, flex: F = [], round, align } = options; + const n = domain.length; const flex = splice(F, domain.length); // 根据下面的等式可以计算出所有 step 的总和 @@ -74,13 +66,13 @@ function computeBandState(options: BandStateOptions) { const minBandWidth = bandWidthSum / flexSum; // 计算每个 bandWidth 和 step,并且用定义域内的值索引 - const valueBandWidth = new Map( + const valueBandWidth: InternMap = new InternMap( domain.map((d, i) => { const bandWidth = normalizedFlex[i] * minBandWidth; return [d, round ? Math.floor(bandWidth) : bandWidth]; }) ); - const valueStep = new Map( + const valueStep: InternMap = new InternMap( domain.map((d, i) => { const bandWidth = normalizedFlex[i] * minBandWidth; const step = bandWidth + PI; @@ -112,6 +104,71 @@ function computeBandState(options: BandStateOptions) { }; } +/** + * 基于 band 基础配置获取 band 的状态 + */ +function computeBandState(options: BandStateOptions) { + const { domain, flex } = options; + const n = domain.length; + if (n === 0) { + return { + valueBandWidth: undefined, + valueStep: undefined, + adjustedRange: [], + }; + } + + const hasFlex = !!flex?.length; + if (hasFlex) { + return computeFlexBandState(options); + } + + const { range, paddingOuter, paddingInner, round, align } = options; + + let step: number; + let bandWidth: number; + + let rangeStart = range[0]; + const rangeEnd = range[1]; + + // range 的计算方式如下: + // = stop - start + // = (n * step(n 个 step) ) + // + (2 * step * paddingOuter(两边的 padding)) + // - (1 * step * paddingInner(多出的一个 inner)) + const deltaRange = rangeEnd - rangeStart; + const outerTotal = paddingOuter * 2; + const innerTotal = n - paddingInner; + step = deltaRange / Math.max(1, outerTotal + innerTotal); + + // 优化成整数 + if (round) { + step = Math.floor(step); + } + + // 基于 align 实现偏移 + rangeStart += (deltaRange - step * (n - paddingInner)) * align; + + // 一个 step 的组成如下: + // step = bandWidth + step * paddingInner, + // 则 bandWidth = step - step * (paddingInner) + bandWidth = step * (1 - paddingInner); + + if (round) { + rangeStart = Math.round(rangeStart); + bandWidth = Math.round(bandWidth); + } + + // 转化后的 range + const adjustedRange = new Array(n).fill(0).map((_, i) => rangeStart + i * step); + + return { + valueStep: step, + valueBandWidth: bandWidth, + adjustedRange, + }; +} + /** * Band 比例尺 * @@ -145,10 +202,10 @@ export class Band extends Ordinal { private adjustedRange: O['range']; // domain 中每一个 value 对应的条的宽度(不包含 padding) - private valueBandWidth: Map; + private valueBandWidth: InternMap | number; // domain 中每一个 value 对应的条的步长(包含 padding) - private valueStep: Map; + private valueStep: InternMap | number; // 覆盖默认配置 protected getDefaultOptions() { @@ -177,6 +234,11 @@ export class Band extends Ordinal { public getStep(x?: Domain) { if (this.valueStep === undefined) return 1; + // 没有 flex 的情况时, valueStep 是 number 类型 + if (typeof this.valueStep === 'number') { + return this.valueStep; + } + // 对于 flex 都为 1 的情况,x 不是必须要传入的 // 这种情况所有的条的 step 都相等,所以返回第一个就好 if (x === undefined) return Array.from(this.valueStep.values())[0]; @@ -186,6 +248,11 @@ export class Band extends Ordinal { public getBandWidth(x?: Domain) { if (this.valueBandWidth === undefined) return 1; + // 没有 flex, valueBandWidth 是 number 类型 + if (typeof this.valueBandWidth === 'number') { + return this.valueBandWidth; + } + // 对于 flex 都为 1 的情况,x 不是必须要传入的 // 这种情况所有的条的 bandWidth 都相等,所以返回第一个 if (x === undefined) return Array.from(this.valueBandWidth.values())[0]; diff --git a/src/utils/index.ts b/src/utils/index.ts index 5a29bc7..3c2e4af 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -51,3 +51,4 @@ export { } from './utc-interval'; export { chooseNiceTimeMask } from './choose-mask'; +export { InternMap } from './internMap'; diff --git a/src/utils/internMap.ts b/src/utils/internMap.ts new file mode 100644 index 0000000..dcd2fd5 --- /dev/null +++ b/src/utils/internMap.ts @@ -0,0 +1,58 @@ +function internGet({ map, initKey }, value) { + const key = initKey(value); + return map.has(key) ? map.get(key) : value; +} + +function internSet({ map, initKey }, value) { + const key = initKey(value); + if (map.has(key)) return map.get(key); + map.set(key, value); + return value; +} + +function internDelete({ map, initKey }, value) { + const key = initKey(value); + if (map.has(key)) { + value = map.get(key); + map.delete(key); + } + return value; +} + +function keyof(value) { + return typeof value === 'object' ? value.valueOf() : value; +} + +/** + * @see 参考 https://github.com/mbostock/internmap/blob/main/src/index.js + */ +export class InternMap extends Map { + private map = new Map(); + + private initKey = keyof; + + constructor(entries) { + super(); + if (entries !== null) { + for (const [key, value] of entries) { + this.set(key, value); + } + } + } + + get(key: K) { + return super.get(internGet({ map: this.map, initKey: this.initKey }, key)); + } + + has(key: K) { + return super.has(internGet({ map: this.map, initKey: this.initKey }, key)); + } + + set(key: K, value: V) { + return super.set(internSet({ map: this.map, initKey: this.initKey }, key), value); + } + + delete(key: K) { + return super.delete(internDelete({ map: this.map, initKey: this.initKey }, key)); + } +}