Skip to content

Commit

Permalink
feat: opt computeBandState & use internMap instead of Map (close: #193)…
Browse files Browse the repository at this point in the history
… (#194)
  • Loading branch information
pepper-nice committed Sep 6, 2022
1 parent 384ab2d commit 8908324
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 18 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down
51 changes: 51 additions & 0 deletions __tests__/unit/scales/band.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,56 @@ describe('band scale', () => {
expect(bandScale.map('A')).toBe(0);
expect(bandScale.map('B')).toBeCloseTo(166.67);
expect(bandScale.map('C')).toBeCloseTo(416.67);

bandScale.update({
range: [0, 300],
paddingOuter: 0.2,
paddingInner: 0.2,
round: true,
align: 0.5,
});
expect(bandScale.map('A')).toBe(19);
expect(bandScale.map('B')).toBeCloseTo(112);
expect(bandScale.map('C')).toBeCloseTo(243);
});

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: [],
flex: [1, 2, 3],
range: [0, 500],
});
expect(bandScale.getBandWidth()).toBe(1);
expect(bandScale.getStep()).toBe(1);
expect(bandScale.getRange()).toStrictEqual([]);
});

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);

const ba = bandScale.getBandWidth(new Date(Date.UTC(2022, 9, 5)));
const bb = bandScale.getBandWidth(new Date(Date.UTC(2022, 9, 6)));
const bc = bandScale.getBandWidth(new Date(Date.UTC(2022, 9, 7)));
expect([ba, bb, bc].map((d) => d / bc)).toEqual([2, 3, 1]);

expect(bandScale.getStep(new Date(Date.UTC(2022, 9, 5)))).toBeCloseTo(166.67, 2);
});
});
32 changes: 32 additions & 0 deletions __tests__/unit/utils/interMap.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
102 changes: 84 additions & 18 deletions src/scales/band.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { InternMap } from '../utils';
import { BandOptions, Domain } from '../types';
import { Ordinal } from './ordinal';

Expand Down Expand Up @@ -34,23 +35,14 @@ 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 flex = splice(F, domain.length);
const { domain, range, paddingOuter, paddingInner, flex: F, round, align } = options;
const n = domain.length;
const flex = splice(F, n);

// 根据下面的等式可以计算出所有 step 的总和
// stepSum = step1 + step2 ... + stepN;
Expand All @@ -74,13 +66,13 @@ function computeBandState(options: BandStateOptions) {
const minBandWidth = bandWidthSum / flexSum;

// 计算每个 bandWidth 和 step,并且用定义域内的值索引
const valueBandWidth = new Map(
const valueBandWidth: InternMap<string, any> = 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<string, any> = new InternMap(
domain.map((d, i) => {
const bandWidth = normalizedFlex[i] * minBandWidth;
const step = bandWidth + PI;
Expand Down Expand Up @@ -112,6 +104,70 @@ function computeBandState(options: BandStateOptions) {
};
}

/**
* 基于 band 基础配置获取 band 的状态
*/
function computeBandState(options: BandStateOptions) {
const { domain } = options;
const n = domain.length;
if (n === 0) {
return {
valueBandWidth: undefined,
valueStep: undefined,
adjustedRange: [],
};
}
const hasFlex = !!options.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 比例尺
*
Expand Down Expand Up @@ -145,10 +201,10 @@ export class Band<O extends BandOptions = BandOptions> extends Ordinal<O> {
private adjustedRange: O['range'];

// domain 中每一个 value 对应的条的宽度(不包含 padding)
private valueBandWidth: Map<any, number>;
private valueBandWidth: InternMap<any, number> | number;

// domain 中每一个 value 对应的条的步长(包含 padding)
private valueStep: Map<any, number>;
private valueStep: InternMap<any, number> | number;

// 覆盖默认配置
protected getDefaultOptions() {
Expand Down Expand Up @@ -177,6 +233,11 @@ export class Band<O extends BandOptions = BandOptions> extends Ordinal<O> {
public getStep(x?: Domain<BandOptions>) {
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];
Expand All @@ -186,6 +247,11 @@ export class Band<O extends BandOptions = BandOptions> extends Ordinal<O> {
public getBandWidth(x?: Domain<BandOptions>) {
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];
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ export {
} from './utc-interval';

export { chooseNiceTimeMask } from './choose-mask';
export { InternMap } from './internMap';
58 changes: 58 additions & 0 deletions src/utils/internMap.ts
Original file line number Diff line number Diff line change
@@ -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<K, V> extends Map {
private map = new Map<K, V>();

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));
}
}

0 comments on commit 8908324

Please sign in to comment.