Skip to content

Commit

Permalink
feat/log (#138)
Browse files Browse the repository at this point in the history
* feat(log): update nice

* refactor: log nice

* feat(log): update getTicks
  • Loading branch information
pearmini committed May 8, 2021
1 parent 69af96e commit 0fc7053
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 41 deletions.
13 changes: 5 additions & 8 deletions __tests__/unit/scales/log.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,24 @@ describe('log scale test', () => {
expect(x1.getOptions() !== x2.getOptions()).toBeTruthy();
});

test('test nice option', () => {
test('test nice options', () => {
const scale = new Log({
domain: [1.6, 10.4],
nice: true,
domain: [1.1, 10.9],
});

// 调用 map 之后才会触发 nice
// TODO: fix log bug for NaN 下一个 pr 处理
expect(scale.map(1)).toStrictEqual(NaN);

expect(scale.getOptions().domain).toStrictEqual([0, 12]);
expect(scale.getOptions().domain).toEqual([1, 100]);
});

test('test getTicks', () => {
const scale = new Log({
domain: [1, 120],
base: 2,
tickMethod: (min, max, count) => {
tickMethod: (min, max, count, base) => {
expect(min).toBe(1);
expect(max).toBe(120);
expect(count).toBe(5);
expect(base).toBe(2);
return [];
},
});
Expand Down
73 changes: 73 additions & 0 deletions __tests__/unit/tick-methods/d3-log.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { d3Log } from '../../../src/tick-methods/d3-log';

function round(x: number) {
return Math.round(x * 1e12) / 1e12;
}

describe('d3Log', () => {
test('d3Log(a, b, n) generates the expected power-of-ten for ascending ticks', () => {
expect(d3Log(1e-1, 1e1, 10).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(d3Log(1e-1, 1, 10).map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]);
expect(d3Log(-1, -1e-1, 10).map(round)).toEqual([-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1]);
});

test('d3Log(a, b, n) generates the expected power-of-ten ticks for descending domains', () => {
expect(d3Log(-1e-1, -1e1, 10).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(d3Log(-1e-1, -1, 10).map(round)).toEqual(
[-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1].reverse()
);
expect(d3Log(1, 1e-1, 10).map(round)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1].reverse());
});

test('d3Log(a, b, n) generates the expected power-of-ten ticks for small domains', () => {
expect(d3Log(1, 5, 10)).toEqual([1, 2, 3, 4, 5]);
expect(d3Log(5, 1, 10)).toEqual([5, 4, 3, 2, 1]);
expect(d3Log(-1, -5, 10)).toEqual([-1, -2, -3, -4, -5]);
expect(d3Log(-5, -1, 10)).toEqual([-5, -4, -3, -2, -1]);
expect(d3Log(286.9252014, 329.4978332, 1)).toEqual([300]);
expect(d3Log(286.9252014, 329.4978332, 2)).toEqual([300]);
expect(d3Log(286.9252014, 329.4978332, 3)).toEqual([300, 320]);
expect(d3Log(286.9252014, 329.4978332, 4)).toEqual([290, 300, 310, 320]);
expect(d3Log(286.9252014, 329.4978332, 10)).toEqual([290, 295, 300, 305, 310, 315, 320, 325]);
});

test('d3Log(a, b, n) generates linear ticks when the domain extent is small', () => {
expect(d3Log(41, 42, 10)).toEqual([41, 41.1, 41.2, 41.3, 41.4, 41.5, 41.6, 41.7, 41.8, 41.9, 42]);
expect(d3Log(42, 41, 10)).toEqual([42, 41.9, 41.8, 41.7, 41.6, 41.5, 41.4, 41.3, 41.2, 41.1, 41]);
expect(d3Log(1600, 1400, 10)).toEqual([1600, 1580, 1560, 1540, 1520, 1500, 1480, 1460, 1440, 1420, 1400]);
});

test('d3Log(a, b, n, base) generates the expected power-of-base ticks', () => {
expect(d3Log(0.1, 100, 10, Math.E).map(round)).toEqual([
0.135335283237,
0.367879441171,
1,
2.718281828459,
7.389056098931,
20.085536923188,
54.598150033144,
]);
});
});
12 changes: 12 additions & 0 deletions __tests__/unit/utils/d3-log-nice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { d3LogNice } from '../../../src/utils';

describe('d3LogNice', () => {
test('d3LogNice(a, b, base) nices the a, b, extending it to powers of ten', () => {
expect(d3LogNice(10.9, 1.1, 0, 10)).toEqual([100, 1]);
expect(d3LogNice(0.7, 11.001, 0, 10)).toEqual([0.1, 100]);
expect(d3LogNice(123.1, 6.7, 0, 10)).toEqual([1000, 1]);
expect(d3LogNice(0.01, 0.49, 0, 10)).toEqual([0.01, 1]);
expect(d3LogNice(1.5, 50, 0, 10)).toEqual([1, 100]);
expect(d3LogNice(50, 1.5, 0, 10)).toEqual([100, 1]);
});
});
8 changes: 3 additions & 5 deletions src/scales/continuous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,9 @@ export abstract class Continuous<O extends ContinuousOptions> extends Base<O> {
}

protected nice() {
const { nice } = this.options;
if (nice) {
const [min, max, tickCount, ...rest] = this.getTickMethodOptions();
this.options.domain = this.chooseNice()(min, max, tickCount, ...rest);
}
if (!this.options.nice) return;
const [min, max, tickCount, ...rest] = this.getTickMethodOptions();
this.options.domain = this.chooseNice()(min, max, tickCount, ...rest);
}

public getTicks() {
Expand Down
44 changes: 16 additions & 28 deletions src/scales/log.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
import { Continuous } from './continuous';
import { LogOptions } from '../types';
import { createInterpolate } from '../utils';
import { rPretty } from '../tick-methods/r-pretty';

const reflect = (f) => {
return (x) => -f(-x);
};

const transformLog = (base: number, shouldReflect: boolean) => {
let logFn;

if (base === Math.E) {
logFn = Math.log;
} else {
// 只计算一次 Math.log(base)
const baseCache = Math.log(base);
// 使用换底公式
logFn = (x) => Math.log(x) / baseCache;
}

return shouldReflect ? reflect(logFn) : logFn;
};

const transformPow = (base: number, shouldReflect: boolean) => {
const pow = base === Math.E ? Math.exp : (x) => base ** x;
return shouldReflect ? reflect(pow) : pow;
};
import { createInterpolate, logs, pows } from '../utils';
import { d3Log } from '../tick-methods/d3-log';
import { d3LogNice } from '../utils/d3-log-nice';

/**
* Linear 比例尺
Expand All @@ -39,15 +16,26 @@ export class Log extends Continuous<LogOptions> {
range: [0, 1],
base: 10,
interpolate: createInterpolate,
tickMethod: rPretty,
tickMethod: d3Log,
tickCount: 5,
};
}

protected chooseNice() {
return d3LogNice;
}

protected getTickMethodOptions() {
const { domain, tickCount, base } = this.options;
const min = domain[0];
const max = domain[domain.length - 1];
return [min, max, tickCount, base];
}

protected chooseTransforms() {
const { base, domain } = this.options;
const shouldReflect = domain[0] < 0;
return [transformLog(base, shouldReflect), transformPow(base, shouldReflect)];
return [logs(base, shouldReflect), pows(base, shouldReflect)];
}

public clone(): Log {
Expand Down
46 changes: 46 additions & 0 deletions src/tick-methods/d3-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TickMethod } from 'types';
import { d3Linear } from './d3-linear';
import { pows, logs } from '../utils';

export const d3Log: TickMethod = (a, b, n, base = 10) => {
const shouldReflect = a < 0;
const pow = pows(base, shouldReflect);
const log = logs(base, shouldReflect);

const r = b < a;
const min = r ? b : a;
const max = r ? a : b;
let i = log(min);
let j = log(max);
let ticks = [];

// 如果 base 是整数
if (!(base % 1) && j - i < n) {
i = Math.floor(i);
j = Math.ceil(j);
if (shouldReflect) {
for (; i <= j; i += 1) {
const p = pow(i);
for (let k = base - 1; k >= 1; k -= 1) {
const t = p * k;
if (t > max) break;
if (t >= min) ticks.push(t);
}
}
} else {
for (; i <= j; i += 1) {
const p = pow(i);
for (let k = 1; k < base; k += 1) {
const t = p * k;
if (t > max) break;
if (t >= min) ticks.push(t);
}
}
}
if (ticks.length * 2 < n) ticks = d3Linear(min, max, n);
} else {
ticks = d3Linear(i, j, Math.min(j - i, n)).map(pow);
}

return r ? ticks.reverse() : ticks;
};
13 changes: 13 additions & 0 deletions src/utils/d3-log-nice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TickMethod } from '../types';
import { logs, pows } from './log';

export const d3LogNice: TickMethod = (a, b, _, base) => {
const shouldReflect = a < 0;
const log = logs(base, shouldReflect);
const pow = pows(base, shouldReflect);
const r = a > b;
const min = r ? b : a;
const max = r ? a : b;
const niceDomain = [pow(Math.floor(log(min))), pow(Math.ceil(log(max)))];
return r ? niceDomain.reverse() : niceDomain;
};
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { logs, pows } from './log';
export { d3LogNice } from './d3-log-nice';
export { tickIncrement, tickStep } from './ticks';
export { findTickInterval } from './find-tick-interval';

Expand Down
14 changes: 14 additions & 0 deletions src/utils/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const reflect = (f: Function) => {
return (x: number) => -f(-x);
};

export const logs = (base: number, shouldReflect: boolean) => {
const baseCache = Math.log(base);
const log = base === Math.E ? Math.log : (x: number) => Math.log(x) / baseCache;
return shouldReflect ? reflect(log) : log;
};

export const pows = (base: number, shouldReflect: boolean) => {
const pow = base === Math.E ? Math.exp : (x: number) => base ** x;
return shouldReflect ? reflect(pow) : pow;
};

0 comments on commit 0fc7053

Please sign in to comment.