Skip to content

Commit

Permalink
Merge pull request #45 from jtrim-ons/new-api-for-calcExtents
Browse files Browse the repository at this point in the history
New API for calcExtents (Issue #44)
  • Loading branch information
mhkeller authored Jul 21, 2021
2 parents e945609 + e495ada commit fc12cd5
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 63 deletions.
22 changes: 15 additions & 7 deletions src/LayerCake.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,20 @@
* Suffix these with `_d`
*/
const activeGetters_d = derived([_x, _y, _z, _r], ([$x, $y, $z, $r]) => {
return [
{ field: 'x', accessor: $x },
{ field: 'y', accessor: $y },
{ field: 'z', accessor: $z },
{ field: 'r', accessor: $r }
].filter(d => d.accessor);
const obj = {};
if ($x) {
obj.x = $x;
}
if ($y) {
obj.y = $y;
}
if ($z) {
obj.z = $z;
}
if ($r) {
obj.r = $r;
}
return obj;
});
const padding_d = derived([_padding, _containerWidth, _containerHeight], ([$padding]) => {
Expand Down Expand Up @@ -212,7 +220,7 @@
* and filling that in with anything set by the user
*/
const extents_d = derived([_flatData, activeGetters_d, _extents], ([$flatData, $activeGetters, $extents]) => {
return { ...calcExtents($flatData, $activeGetters.filter(d => !$extents[d.field])), ...$extents };
return { ...calcExtents($flatData, filterObject($activeGetters, $extents)), ...$extents };
});
const xDomain_d = derived([extents_d, _xDomain], calcDomain('x'));
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getPadFunctions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import findScaleType from './findScaleType.js';
import identity from './identity.js';
import identity from '../utils/identity.js';

function log(sign) {
return x => Math.log(sign * x);
Expand Down
92 changes: 52 additions & 40 deletions src/lib/calcExtents.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,73 @@
/* --------------------------------------------
*
* Calculate the extents of desired fields
* For example, a fields object like this:
* `{'x': d => d.x, 'y': d => d.y}`
* For data like this:
* [{ x: 0, y: -10 }, { x: 10, y: 0 }, { x: 5, y: 10 }]
* Returns an object like:
* `{x: [0, 10], y: [-10, 10]}` if `fields` is
* `[{field:'x', accessor: d => d.x}, {field:'y', accessor: d => d.y}]`
* `{ x: [0, 10], y: [-10, 10] }`
*
* --------------------------------------------
*/
export default function calcExtents (data, fields) {
if (!Array.isArray(data) || data.length === 0) return null;
if (!Array.isArray(data)) {
throw new TypeError('The first argument of calcExtents() must be an array.');
}

if (
Array.isArray(fields)
|| fields === undefined
|| fields === null
) {
throw new TypeError('The second argument of calcExtents() must be an '
+ 'object with field names as keys as accessor functions as values.');
}

const extents = {};
const fl = fields.length;

const keys = Object.keys(fields);
const kl = keys.length;
let i;
let j;
let f;
let val;
let k;
let s;
let min;
let max;
let acc;
let val;

if (fl) {
for (i = 0; i < fl; i += 1) {
const firstRow = fields[i].accessor(data[0]);
if (firstRow === undefined || firstRow === null || Number.isNaN(firstRow) === true) {
extents[fields[i].field] = [Infinity, -Infinity];
} else {
extents[fields[i].field] = Array.isArray(firstRow) ? firstRow : [firstRow, firstRow];
}
}
const dl = data.length;
for (i = 0; i < dl; i += 1) {
for (j = 0; j < fl; j += 1) {
f = fields[j];
val = f.accessor(data[i]);
s = f.field;
if (Array.isArray(val)) {
const vl = val.length;
for (let k = 0; k < vl; k += 1) {
if (val[k] !== undefined && val[k] !== null && Number.isNaN(val[k]) === false) {
if (val[k] < extents[s][0]) {
extents[s][0] = val[k];
}
if (val[k] > extents[s][1]) {
extents[s][1] = val[k];
}
const dl = data.length;
for (i = 0; i < kl; i += 1) {
s = keys[i];
acc = fields[s];
min = null;
max = null;
for (j = 0; j < dl; j += 1) {
val = acc(data[j]);
if (Array.isArray(val)) {
const vl = val.length;
for (k = 0; k < vl; k += 1) {
if (val[k] !== undefined && val[k] !== null && Number.isNaN(val[k]) === false) {
if (min === null || val[k] < min) {
min = val[k];
}
if (max === null || val[k] > max) {
max = val[k];
}
}
} else if (val !== undefined && val !== null && Number.isNaN(val) === false) {
if (val < extents[s][0]) {
extents[s][0] = val;
}
if (val > extents[s][1]) {
extents[s][1] = val;
}
}
} else if (val !== undefined && val !== null && Number.isNaN(val) === false) {
if (min === null || val < min) {
min = val;
}
if (max === null || val > max) {
max = val;
}
}
}
} else {
return null;
extents[s] = [min, max];
}

return extents;
}
5 changes: 3 additions & 2 deletions src/utils/filterObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ function fromEntries(iter) {
return obj;
}

export default function filterObject (obj) {
export default function filterObject (obj, comparisonObj = {}) {
return fromEntries(Object.entries(obj).filter(([key, value]) => {
return value !== undefined;
return value !== undefined
&& comparisonObj[key] === undefined;
}));
}
File renamed without changes.
57 changes: 44 additions & 13 deletions test/calcExtents.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,109 @@ import calcExtents from '../src/lib/calcExtents.js';
const name = 'calcExtents';

const tests = [
{ args: [[]], expected: null },
{ args: [{}], expected: null },
{ args: [[0, 1, 2], []], expected: null },
{ args: [[0, 1, 2], {}], expected: {} },
{ args: [[undefined, null, NaN], { x: d => d }], expected: { x: [null, null] } },
{
args: [[
{ x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{}, { x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{ x: null }, { x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{ x: 'd' / 1 }, { x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{ x: NaN }, { x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{ x: Number.NaN }, { x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: [0, 4] }
},
{
args: [[
{ x: '2010-01-04' }, { x: '2010-01-02' }, { x: '2010-01-04' }, { x: '2010-01-05' }, { x: '2010-01-06' }
], [{ field: 'x', accessor: d => d.x }]],
], { x: d => d.x }],
expected: { x: ['2010-01-02', '2010-01-06'] }
},
{
args: [[
{ x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }, { field: 'y', accessor: d => d.y }]],
], { x: d => d.x, y: d => d.y }],
expected: { x: [0, 4], y: [1, 5] }
},
{
args: [[
{ x: [-4, 0], y: [1, 6] }, { x: [-5, 1], y: [2, 7]}, { x: [-3, 2], y: [3, 8] }, { x: [-2, 3], y: [4, 9] }, { x: [-1, 4], y: [5, 10] }
], [{ field: 'x', accessor: d => d.x }, { field: 'y', accessor: d => d.y }]],
], { x: d => d.x, y: d => d.y }],
expected: { x: [-5, 4], y: [1, 10] }
},
{
args: [[
{ start: 0, end: 1 }, { start: -10000, end: 0 }
], [{ field: 'y', accessor: d => [d.start, d.end] }]],
], { y: d => [d.start, d.end] }],
expected: { y: [-10000, 1] }
}
];

const errorTests = [
{
args: [[]],
expected: /^TypeError: The second argument of calcExtents\(\) must be an object with field names as keys as accessor functions as values.$/
},
{
// Old-style API with array of objects as second argument
args: [[
{ x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }
], [{ field: 'x', accessor: d => d.x }]],
expected: /^TypeError: The second argument of calcExtents\(\) must be an object with field names as keys as accessor functions as values.$/
},
{ args: [{}], expected: /^TypeError: The first argument of calcExtents\(\) must be an array.$/ }
];

describe(name, () => {
tests.forEach(test => {
describe(JSON.stringify(test.args), () => {
it('should not modify data passed to calcExtent()', () => {
const dataBeforeCall = test.args[0];
calcExtents(...test.args);
const dataAfterCall = test.args[0];
assert.deepStrictEqual(dataBeforeCall, dataAfterCall);
});
});
describe(JSON.stringify(test.args), () => {
it(`should equal ${JSON.stringify(test.expected)}`, () => {
const actual = calcExtents(...test.args);
assert.deepStrictEqual(actual, test.expected);
});
});
});

errorTests.forEach(test => {
describe(JSON.stringify(test.args), () => {
it(`should throw error ${test.expected}`, () => {
const actual = () => calcExtents(...test.args);
assert.throws(actual, test.expected);
});
});
});
});
3 changes: 3 additions & 0 deletions test/filterObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const tests = [
{ args: [{ x: null, y: undefined }], expected: { x: null } },
{ args: [{ x: 'a', y: undefined, z: undefined }], expected: { x: 'a' } },
{ args: [{ x: 'a', y: 'b', z: undefined }], expected: { x: 'a', y: 'b' } },
{ args: [{ x: 'a', y: 'b', z: undefined }, { x: 10 }], expected: { y: 'b' } },
{ args: [{ x: 'a', y: 'b', z: undefined }, { x: false }], expected: { y: 'b' } },
{ args: [{ x: 'a', y: 'b', z: undefined }, { x: undefined }], expected: { x: 'a', y: 'b' } },
];

describe(name, () => {
Expand Down

0 comments on commit fc12cd5

Please sign in to comment.