Skip to content

Commit

Permalink
feat(scale): add linear, identity, band, ordinal
Browse files Browse the repository at this point in the history
pearmini committed Nov 28, 2021
1 parent 5f3aa47 commit c583bb1
Showing 10 changed files with 252 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -12,5 +12,7 @@ module.exports = {
},
rules: {
'import/prefer-default-export': 0,
'no-use-before-define': 0,
'no-shadow': 0,
},
};
24 changes: 24 additions & 0 deletions __tests__/scale/band.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createBand, bandWidth, bandStep } from '../../src/scale';

describe('test Band', () => {
const options = {
domain: ['a', 'b', 'c'],
range: [0, 32],
padding: 0.2,
};

test('createBand(options) return mapper maps discrete domain to continuous range.', () => {
const s = createBand(options);
expect(s('a')).toBe(2);
expect(s('b')).toBe(12);
expect(s('c')).toBe(22);
});

test('bandWidth(options) returns band width.', () => {
expect(bandWidth(options)).toBe(8);
});

test('bandStep(options) returns step width.', () => {
expect(bandStep(options)).toBe(10);
});
});
12 changes: 12 additions & 0 deletions __tests__/scale/identity.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createIdentity } from '../../src/scale';

describe('tes Identity', () => {
test('createIdentity() returns a identity function.', () => {
const s = createIdentity();

expect(s(1)).toBe(1);
expect(s(true)).toBe(true);
expect(s('hello world')).toBe('hello world');
expect(s({ a: 1 })).toEqual({ a: 1 });
});
});
74 changes: 74 additions & 0 deletions __tests__/scale/linear.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
createLinear,
linearNice,
linearTicks,
interpolateNumber,
} from '../../src/scale';

describe('test Linear', () => {
test('createLinear(options) returns a a linear function.', () => {
const s = createLinear({
domain: [0, 1],
range: [0, 100],
});

expect(s(0)).toBe(0);
expect(s(0.3)).toBe(30);
expect(s(0.5)).toBe(50);
expect(s(0.7)).toBe(70);
expect(s(1)).toBe(100);
});

test('createLinear(options) uses custom interpolate.', () => {
const s = createLinear({
domain: [0, 1],
range: ['a', 'z'],
interpolate: (t, start, end) => {
const charCode = interpolateNumber(
t,
start.charCodeAt(),
end.charCodeAt(),
);
return String.fromCharCode(charCode);
},
});

expect(s(0)).toBe('a');
expect(s(1)).toBe('z');
expect(s(0.5)).toBe('m');
});

test('linearNice(domain, tickCount) extends domain for better ticks.', () => {
expect(linearNice([1.1, 10.9], 10)).toEqual([1, 11]);
expect(linearNice([0.7, 11.001], 10)).toEqual([0, 12]);
expect(linearNice([0, 0.49], 10)).toEqual([0, 0.5]);
expect(linearNice([12, 87], 5)).toEqual([0, 100]);
expect(linearNice([12, 87], 10)).toEqual([10, 90]);
expect(linearNice([12, 87], 100)).toEqual([12, 87]);
});

test('linearTicks(domain, tickCount) return ticks in 1, 2, 5 * 10 ^ n format', () => {
const ticks10 = linearTicks([0, 1], 10);
const ticks9 = linearTicks([0, 1], 9);
const ticks8 = linearTicks([0, 1], 9);
const ticks7 = linearTicks([0, 1], 7);
const ticks6 = linearTicks([0, 1], 6);
const ticks5 = linearTicks([0, 1], 5);
const ticks4 = linearTicks([0, 1], 4);
const ticks3 = linearTicks([0, 1], 3);
const ticks2 = linearTicks([0, 1], 2);
const ticks1 = linearTicks([0, 1], 1);
expect(ticks10).toEqual([
0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1,
]);
expect(ticks9).toEqual([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]);
expect(ticks8).toEqual([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]);
expect(ticks7).toEqual([0, 0.2, 0.4, 0.6, 0.8, 1]);
expect(ticks6).toEqual([0, 0.2, 0.4, 0.6, 0.8, 1]);
expect(ticks5).toEqual([0, 0.2, 0.4, 0.6, 0.8, 1]);
expect(ticks4).toEqual([0, 0.2, 0.4, 0.6, 0.8, 1]);
expect(ticks3).toEqual([0, 0.5, 1]);
expect(ticks2).toEqual([0, 0.5, 1]);
expect(ticks1).toEqual([0, 1]);
});
});
22 changes: 22 additions & 0 deletions __tests__/scale/ordinal.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createOrdinal } from '../../src/scale';

describe('test Ordinal', () => {
test('createOrdinal(options) returns a one-to-one mapper.', () => {
const s = createOrdinal({
domain: ['a', 'b', 'c'],
range: ['red', 'yellow', 'blue'],
});
expect(s('a')).toBe('red');
expect(s('b')).toBe('yellow');
expect(s('c')).toBe('blue');
});

test('createOrdinal(options) will mod map.', () => {
const s = createOrdinal({
domain: ['a', 'b', 'c', 'd', 'e'],
range: ['red', 'yellow', 'blue'],
});
expect(s('d')).toBe('red');
expect(s('e')).toBe('yellow');
});
});
27 changes: 27 additions & 0 deletions src/scale/band.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createOrdinal } from './ordinal';

export function createBand(options) {
const { bandRange } = band(options);
return createOrdinal({ ...options, range: bandRange });
}

export function bandWidth(options) {
return band(options).bandWidth;
}

export function bandStep(options) {
return band(options).step;
}

function band({ domain, range, padding }) {
const [r0, r1] = range;
const n = domain.length;
const step = (r1 - r0) / (n + padding);
const bandWidth = step * (1 - padding);
const interval = step - bandWidth;
return {
step,
bandWidth,
bandRange: new Array(n).fill(0).map((_, i) => r0 + interval + step * i),
};
}
3 changes: 3 additions & 0 deletions src/scale/identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function createIdentity() {
return (x) => x;
}
6 changes: 6 additions & 0 deletions src/scale/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
createLinear, linearNice, linearTicks, interpolateNumber,
} from './linear';
export { createIdentity } from './identity';
export { createOrdinal } from './ordinal';
export { createBand, bandStep, bandWidth } from './band';
72 changes: 72 additions & 0 deletions src/scale/linear.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export function createLinear({
domain: [d0, d1],
range: [r0, r1],
interpolate = interpolateNumber,
}) {
return (x) => {
const t = normalize(x, d0, d1);
return interpolate(t, r0, r1);
};
}

export function linearNice(domain, tickCount) {
const step = tickStep(...domain, tickCount);
return nice(domain, {
floor: (x) => floor(x, step),
ceil: (x) => ceil(x, step),
});
}

export function linearTicks(domain, tickCount) {
return ticks(...domain, tickCount);
}

export function normalize(value, start, stop) {
return (value - start) / (stop - start);
}

export function interpolateNumber(t, start, stop) {
return start * (1 - t) + stop * t;
}

export function ceil(n, base) {
return base * Math.ceil(n / base);
}

export function floor(n, base) {
return base * Math.floor(n / base);
}

export function round(n) {
return Math.round(n * 1e12) / 1e12;
}

export function tickStep(min, max, count) {
const e10 = Math.sqrt(50);
const e5 = Math.sqrt(10);
const e2 = Math.sqrt(2);
const step0 = Math.abs(max - min) / 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 step1;
}

export function nice(domain, interval) {
const [min, max] = domain;
return [interval.floor(min), interval.ceil(max)];
}

export function ticks(min, max, count) {
const step = tickStep(min, max, count);
const start = Math.ceil(min / step);
const stop = Math.floor(max / step);
const n = Math.ceil(stop - start + 1);
const values = new Array(n);
for (let i = 0; i < n; i += 1) {
values[i] = round((start + i) * step);
}
return values;
}
10 changes: 10 additions & 0 deletions src/scale/ordinal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function createOrdinal({ domain, range }) {
return (x) => {
const index = domain.findIndex((d) => equal(d, x));
return range[index % range.length];
};
}

export function equal(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}

0 comments on commit c583bb1

Please sign in to comment.