Skip to content

Commit

Permalink
Merge branch 'main' into tick-with-base
Browse files Browse the repository at this point in the history
  • Loading branch information
nickofthyme committed Dec 8, 2023
2 parents 79c5d11 + be0ae0d commit 28d1a03
Show file tree
Hide file tree
Showing 30 changed files with 12,890 additions and 1,549 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2010-2021 Mike Bostock
Copyright 2010-2023 Mike Bostock

Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
Expand Down
910 changes: 7 additions & 903 deletions README.md

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-array",
"version": "3.1.1",
"version": "3.2.4",
"description": "Array manipulation, ordering, searching, summarizing, etc.",
"homepage": "https://d3js.org/d3-array/",
"repository": {
Expand Down Expand Up @@ -41,17 +41,18 @@
"internmap": "1 - 2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "13",
"@rollup/plugin-node-resolve": "15",
"d3-dsv": "3",
"d3-random": "2 - 3",
"eslint": "7",
"jsdom": "17",
"mocha": "9",
"rollup": "2",
"eslint": "8",
"jsdom": "21",
"mocha": "10",
"rollup": "3",
"rollup-plugin-terser": "7"
},
"scripts": {
"test": "mocha 'test/**/*-test.js' && eslint src test",
"prepublishOnly": "rm -rf dist && yarn test && rollup -c",
"prepublishOnly": "rm -rf dist && rollup -c",
"postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../${npm_package_name}/dist/${npm_package_name}.js ${npm_package_name}.v${npm_package_version%%.*}.js && cp ../${npm_package_name}/dist/${npm_package_name}.min.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git add ${npm_package_name}.v${npm_package_version%%.*}.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git commit -m \"${npm_package_name} ${npm_package_version}\" && git push && cd -"
},
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {nodeResolve} from "@rollup/plugin-node-resolve";
import {readFileSync} from "fs";
import {terser} from "rollup-plugin-terser";
import * as meta from "./package.json";
import meta from "./package.json" assert {type: "json"};

// Extract copyrights from the LICENSE.
const copyright = readFileSync("./LICENSE", "utf-8")
Expand Down
40 changes: 32 additions & 8 deletions src/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function bin() {
var i,
n = data.length,
x,
step,
values = new Array(n);

for (i = 0; i < n; ++i) {
Expand All @@ -36,6 +37,11 @@ export default function bin() {
if (domain === extent) [x0, x1] = nice(x0, x1, tn);
tz = ticks(x0, x1, tn);

// If the domain is aligned with the first tick (which it will by
// default), then we can use quantization rather than bisection to bin
// values, which is substantially faster.
if (tz[0] <= x0) step = tickIncrement(x0, x1, tn);

// If the last threshold is coincident with the domain’s upper bound, the
// last bin will be zero-width. If the default domain is used, and this
// last threshold is coincident with the maximum input value, we can
Expand All @@ -60,9 +66,11 @@ export default function bin() {
}

// Remove any thresholds outside the domain.
var m = tz.length;
while (tz[0] <= x0) tz.shift(), --m;
while (tz[m - 1] > x1) tz.pop(), --m;
// Be careful not to mutate an array owned by the user!
var m = tz.length, a = 0, b = m;
while (tz[a] <= x0) ++a;
while (tz[b - 1] > x1) --b;
if (a || b < m) tz = tz.slice(a, b), m = b - a;

var bins = new Array(m + 1),
bin;
Expand All @@ -75,10 +83,26 @@ export default function bin() {
}

// Assign data to bins by value, ignoring any outside the domain.
for (i = 0; i < n; ++i) {
x = values[i];
if (x != null && x0 <= x && x <= x1) {
bins[bisect(tz, x, 0, m)].push(data[i]);
if (isFinite(step)) {
if (step > 0) {
for (i = 0; i < n; ++i) {
if ((x = values[i]) != null && x0 <= x && x <= x1) {
bins[Math.min(m, Math.floor((x - x0) / step))].push(data[i]);
}
}
} else if (step < 0) {
for (i = 0; i < n; ++i) {
if ((x = values[i]) != null && x0 <= x && x <= x1) {
const j = Math.floor((x0 - x) * step);
bins[Math.min(m, j + (tz[j] <= x))].push(data[i]); // handle off-by-one due to rounding
}
}
}
} else {
for (i = 0; i < n; ++i) {
if ((x = values[i]) != null && x0 <= x && x <= x1) {
bins[bisect(tz, x, 0, m)].push(data[i]);
}
}
}

Expand All @@ -94,7 +118,7 @@ export default function bin() {
};

histogram.thresholds = function(_) {
return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), histogram) : threshold;
return arguments.length ? (threshold = typeof _ === "function" ? _ : constant(Array.isArray(_) ? slice.call(_) : _), histogram) : threshold;
};

return histogram;
Expand Down
20 changes: 16 additions & 4 deletions src/bisector.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import ascending from "./ascending.js";
import descending from "./descending.js";

export default function bisector(f) {
let delta = f;
let compare1 = f;
let compare2 = f;
let compare1, compare2, delta;

// If an accessor is specified, promote it to a comparator. In this case we
// can test whether the search value is (self-) comparable. We can’t do this
// for a comparator (except for specific, known comparators) because we can’t
// tell if the comparator is symmetric, and an asymmetric comparator can’t be
// used to test whether a single value is comparable.
if (f.length !== 2) {
delta = (d, x) => f(d) - x;
compare1 = ascending;
compare2 = (d, x) => ascending(f(d), x);
delta = (d, x) => f(d) - x;
} else {
compare1 = f === ascending || f === descending ? f : zero;
compare2 = f;
delta = f;
}

function left(a, x, lo = 0, hi = a.length) {
Expand Down Expand Up @@ -42,3 +50,7 @@ export default function bisector(f) {

return {left, center, right};
}

function zero() {
return 0;
}
115 changes: 115 additions & 0 deletions src/blur.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
export function blur(values, r) {
if (!((r = +r) >= 0)) throw new RangeError("invalid r");
let length = values.length;
if (!((length = Math.floor(length)) >= 0)) throw new RangeError("invalid length");
if (!length || !r) return values;
const blur = blurf(r);
const temp = values.slice();
blur(values, temp, 0, length, 1);
blur(temp, values, 0, length, 1);
blur(values, temp, 0, length, 1);
return values;
}

export const blur2 = Blur2(blurf);

export const blurImage = Blur2(blurfImage);

function Blur2(blur) {
return function(data, rx, ry = rx) {
if (!((rx = +rx) >= 0)) throw new RangeError("invalid rx");
if (!((ry = +ry) >= 0)) throw new RangeError("invalid ry");
let {data: values, width, height} = data;
if (!((width = Math.floor(width)) >= 0)) throw new RangeError("invalid width");
if (!((height = Math.floor(height !== undefined ? height : values.length / width)) >= 0)) throw new RangeError("invalid height");
if (!width || !height || (!rx && !ry)) return data;
const blurx = rx && blur(rx);
const blury = ry && blur(ry);
const temp = values.slice();
if (blurx && blury) {
blurh(blurx, temp, values, width, height);
blurh(blurx, values, temp, width, height);
blurh(blurx, temp, values, width, height);
blurv(blury, values, temp, width, height);
blurv(blury, temp, values, width, height);
blurv(blury, values, temp, width, height);
} else if (blurx) {
blurh(blurx, values, temp, width, height);
blurh(blurx, temp, values, width, height);
blurh(blurx, values, temp, width, height);
} else if (blury) {
blurv(blury, values, temp, width, height);
blurv(blury, temp, values, width, height);
blurv(blury, values, temp, width, height);
}
return data;
};
}

function blurh(blur, T, S, w, h) {
for (let y = 0, n = w * h; y < n;) {
blur(T, S, y, y += w, 1);
}
}

function blurv(blur, T, S, w, h) {
for (let x = 0, n = w * h; x < w; ++x) {
blur(T, S, x, x + n, w);
}
}

function blurfImage(radius) {
const blur = blurf(radius);
return (T, S, start, stop, step) => {
start <<= 2, stop <<= 2, step <<= 2;
blur(T, S, start + 0, stop + 0, step);
blur(T, S, start + 1, stop + 1, step);
blur(T, S, start + 2, stop + 2, step);
blur(T, S, start + 3, stop + 3, step);
};
}

// Given a target array T, a source array S, sets each value T[i] to the average
// of {S[i - r], …, S[i], …, S[i + r]}, where r = ⌊radius⌋, start <= i < stop,
// for each i, i + step, i + 2 * step, etc., and where S[j] is clamped between
// S[start] (inclusive) and S[stop] (exclusive). If the given radius is not an
// integer, S[i - r - 1] and S[i + r + 1] are added to the sum, each weighted
// according to r - ⌊radius⌋.
function blurf(radius) {
const radius0 = Math.floor(radius);
if (radius0 === radius) return bluri(radius);
const t = radius - radius0;
const w = 2 * radius + 1;
return (T, S, start, stop, step) => { // stop must be aligned!
if (!((stop -= step) >= start)) return; // inclusive stop
let sum = radius0 * S[start];
const s0 = step * radius0;
const s1 = s0 + step;
for (let i = start, j = start + s0; i < j; i += step) {
sum += S[Math.min(stop, i)];
}
for (let i = start, j = stop; i <= j; i += step) {
sum += S[Math.min(stop, i + s0)];
T[i] = (sum + t * (S[Math.max(start, i - s1)] + S[Math.min(stop, i + s1)])) / w;
sum -= S[Math.max(start, i - s0)];
}
};
}

// Like blurf, but optimized for integer radius.
function bluri(radius) {
const w = 2 * radius + 1;
return (T, S, start, stop, step) => { // stop must be aligned!
if (!((stop -= step) >= start)) return; // inclusive stop
let sum = radius * S[start];
const s = step * radius;
for (let i = start, j = start + s; i < j; i += step) {
sum += S[Math.min(stop, i)];
}
for (let i = start, j = stop; i <= j; i += step) {
sum += S[Math.min(stop, i + s)];
T[i] = sum / w;
sum -= S[Math.max(start, i - s)];
}
};
}
5 changes: 3 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {default as bisect, bisectRight, bisectLeft, bisectCenter} from "./bisect.js";
export {default as ascending} from "./ascending.js";
export {default as bisector} from "./bisector.js";
export {blur, blur2, blurImage} from "./blur.js";
export {default as count} from "./count.js";
export {default as cross} from "./cross.js";
export {default as cumsum} from "./cumsum.js";
Expand All @@ -17,15 +18,15 @@ export {default as thresholdSturges} from "./threshold/sturges.js";
export {default as max} from "./max.js";
export {default as maxIndex} from "./maxIndex.js";
export {default as mean} from "./mean.js";
export {default as median} from "./median.js";
export {default as median, medianIndex} from "./median.js";
export {default as merge} from "./merge.js";
export {default as min} from "./min.js";
export {default as minIndex} from "./minIndex.js";
export {default as mode} from "./mode.js";
export {default as nice} from "./nice.js";
export {default as pairs} from "./pairs.js";
export {default as permute} from "./permute.js";
export {default as quantile, quantileSorted} from "./quantile.js";
export {default as quantile, quantileIndex, quantileSorted} from "./quantile.js";
export {default as quickselect} from "./quickselect.js";
export {default as range} from "./range.js";
export {default as rank} from "./rank.js";
Expand Down
6 changes: 5 additions & 1 deletion src/median.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import quantile from "./quantile.js";
import quantile, {quantileIndex} from "./quantile.js";

export default function median(values, valueof) {
return quantile(values, 0.5, valueof);
}

export function medianIndex(values, valueof) {
return quantileIndex(values, 0.5, valueof);
}
26 changes: 22 additions & 4 deletions src/quantile.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import max from "./max.js";
import maxIndex from "./maxIndex.js";
import min from "./min.js";
import minIndex from "./minIndex.js";
import quickselect from "./quickselect.js";
import number, {numbers} from "./number.js";
import {ascendingDefined} from "./sort.js";
import greatest from "./greatest.js";

export default function quantile(values, p, valueof) {
values = Float64Array.from(numbers(values, valueof));
if (!(n = values.length)) return;
if ((p = +p) <= 0 || n < 2) return min(values);
if (!(n = values.length) || isNaN(p = +p)) return;
if (p <= 0 || n < 2) return min(values);
if (p >= 1) return max(values);
var n,
i = (n - 1) * p,
Expand All @@ -17,8 +21,8 @@ export default function quantile(values, p, valueof) {
}

export function quantileSorted(values, p, valueof = number) {
if (!(n = values.length)) return;
if ((p = +p) <= 0 || n < 2) return +valueof(values[0], 0, values);
if (!(n = values.length) || isNaN(p = +p)) return;
if (p <= 0 || n < 2) return +valueof(values[0], 0, values);
if (p >= 1) return +valueof(values[n - 1], n - 1, values);
var n,
i = (n - 1) * p,
Expand All @@ -27,3 +31,17 @@ export function quantileSorted(values, p, valueof = number) {
value1 = +valueof(values[i0 + 1], i0 + 1, values);
return value0 + (value1 - value0) * (i - i0);
}

export function quantileIndex(values, p, valueof = number) {
if (isNaN(p = +p)) return;
numbers = Float64Array.from(values, (_, i) => number(valueof(values[i], i, values)));
if (p <= 0) return minIndex(numbers);
if (p >= 1) return maxIndex(numbers);
var numbers,
index = Uint32Array.from(values, (_, i) => i),
j = numbers.length - 1,
i = Math.floor(j * p);
quickselect(index, i, 0, j, (i, j) => ascendingDefined(numbers[i], numbers[j]));
i = greatest(index.subarray(0, i + 1), (i) => numbers[i]);
return i >= 0 ? i : -1;
}
9 changes: 8 additions & 1 deletion src/quickselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import {ascendingDefined, compareDefined} from "./sort.js";

// Based on https://github.com/mourner/quickselect
// ISC license, Copyright 2018 Vladimir Agafonkin.
export default function quickselect(array, k, left = 0, right = array.length - 1, compare) {
export default function quickselect(array, k, left = 0, right = Infinity, compare) {
k = Math.floor(k);
left = Math.floor(Math.max(0, left));
right = Math.floor(Math.min(array.length - 1, right));

if (!(left <= k && k <= right)) return array;

compare = compare === undefined ? ascendingDefined : compareDefined(compare);

while (right > left) {
Expand Down Expand Up @@ -36,6 +42,7 @@ export default function quickselect(array, k, left = 0, right = array.length - 1
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}

return array;
}

Expand Down
8 changes: 4 additions & 4 deletions src/rank.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ export default function rank(values, valueof = ascending) {
if (valueof.length !== 2) V = V.map(valueof), valueof = ascending;
const compareIndex = (i, j) => valueof(V[i], V[j]);
let k, r;
Uint32Array
.from(V, (_, i) => i)
.sort(valueof === ascending ? (i, j) => ascendingDefined(V[i], V[j]) : compareDefined(compareIndex))
.forEach((j, i) => {
values = Uint32Array.from(V, (_, i) => i);
// Risky chaining due to Safari 14 https://github.com/d3/d3-array/issues/123
values.sort(valueof === ascending ? (i, j) => ascendingDefined(V[i], V[j]) : compareDefined(compareIndex));
values.forEach((j, i) => {
const c = compareIndex(j, k === undefined ? j : k);
if (c >= 0) {
if (k === undefined || c > 0) k = j, r = i;
Expand Down
Loading

0 comments on commit 28d1a03

Please sign in to comment.