Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: opt computeBandState & use internMap instead of Map #194

Merged
merged 1 commit into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}