diff --git a/README.md b/README.md
index f6c9adf..68034d5 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,10 @@ Returns the mean of the given *iterable* of numbers. If the iterable contains no
Returns the median of the given *iterable* of numbers using the [R-7 method](https://en.wikipedia.org/wiki/Quantile#Estimating_quantiles_from_a_sample). If the iterable contains no numbers, returns undefined. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the median. This method ignores undefined and NaN values; this is useful for ignoring missing data.
+# d3.medianIndex(array, p[, accessor]) [Source](https://github.com/d3/d3-array/blob/main/src/median.js "Source")
+
+Similar to *median*, but returns the index of the element to the left of the median.
+
# d3.cumsum(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/cumsum.js), [Examples](https://observablehq.com/@d3/d3-cumsum)
Returns the cumulative sum of the given *iterable* of numbers, as a Float64Array of the same length. If the iterable contains no numbers, returns zeros. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the cumulative sum. This method ignores undefined and NaN values; this is useful for ignoring missing data.
@@ -137,6 +141,10 @@ d3.quantile(a, 0.1); // 2
An optional *accessor* function may be specified, which is equivalent to calling *array*.map(*accessor*) before computing the quantile.
+# d3.quantileIndex(array, p[, accessor]) [Source](https://github.com/d3/d3-array/blob/main/src/quantile.js "Source")
+
+Similar to *quantile*, but returns the index to the left of *p*.
+
# d3.quantileSorted(array, p[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/quantile.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends)
Similar to *quantile*, but expects the input to be a **sorted** *array* of values. In contrast with *quantile*, the accessor is only called on the elements needed to compute the quantile.
diff --git a/src/index.js b/src/index.js
index 29fc334..7759f6d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -18,7 +18,7 @@ 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";
@@ -26,7 +26,7 @@ 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";
diff --git a/src/median.js b/src/median.js
index fca6a82..2604df8 100644
--- a/src/median.js
+++ b/src/median.js
@@ -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);
+}
diff --git a/src/quantile.js b/src/quantile.js
index 09ddac7..163343a 100644
--- a/src/quantile.js
+++ b/src/quantile.js
@@ -1,7 +1,11 @@
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));
@@ -27,3 +31,15 @@ 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) {
+ values = Float64Array.from(numbers(values, valueof));
+ if (!(n = values.length)) return;
+ if ((p = +p) <= 0 || n < 2) return minIndex(values);
+ if (p >= 1) return maxIndex(values);
+ var n,
+ i = Math.floor((n - 1) * p),
+ order = (i, j) => ascendingDefined(values[i], values[j]),
+ index = quickselect(Uint32Array.from(values, (_, i) => i), i, 0, n - 1, order);
+ return greatest(index.subarray(0, i + 1), i => values[i]);
+}
diff --git a/src/quickselect.js b/src/quickselect.js
index b4ed884..e4b6c84 100644
--- a/src/quickselect.js
+++ b/src/quickselect.js
@@ -36,6 +36,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;
}
diff --git a/test/median-test.js b/test/median-test.js
index b13c234..6537c57 100644
--- a/test/median-test.js
+++ b/test/median-test.js
@@ -1,5 +1,5 @@
import assert from "assert";
-import {median} from "../src/index.js";
+import {median, medianIndex} from "../src/index.js";
import {OneTimeNumber} from "./OneTimeNumber.js";
it("median(array) returns the median value for numbers", () => {
@@ -98,6 +98,16 @@ it("median(array, f) uses the undefined context", () => {
assert.deepStrictEqual(results, [undefined, undefined]);
});
+it("medianIndex(array) returns the index", () => {
+ assert.deepStrictEqual(medianIndex([1, 2]), 0);
+ assert.deepStrictEqual(medianIndex([1, 2, 3]), 1);
+ assert.deepStrictEqual(medianIndex([1, 3, 2]), 2);
+ assert.deepStrictEqual(medianIndex([2, 3, 1]), 0);
+ assert.deepStrictEqual(medianIndex([1]), 0);
+ assert.deepStrictEqual(medianIndex([]), undefined);
+});
+
+
function box(value) {
return {value: value};
}
diff --git a/test/quantile-test.js b/test/quantile-test.js
index a00a07b..adebec1 100644
--- a/test/quantile-test.js
+++ b/test/quantile-test.js
@@ -1,5 +1,5 @@
import assert from "assert";
-import {quantile, quantileSorted} from "../src/index.js";
+import {quantile, quantileIndex, quantileSorted} from "../src/index.js";
it("quantileSorted(array, p) requires sorted numeric input, quantile doesn't", () => {
assert.strictEqual(quantileSorted([1, 2, 3, 4], 0), 1);
@@ -83,6 +83,33 @@ it("quantile(array, p, f) observes the specified accessor", () => {
assert.strictEqual(quantile([], 1, unbox), undefined);
});
+it("quantileIndex(array, p) returns the index", () => {
+ assert.deepStrictEqual(quantileIndex([1, 2], 0.2), 0);
+ assert.deepStrictEqual(quantileIndex([1, 2, 3], 0.2), 0);
+ assert.deepStrictEqual(quantileIndex([1, 3, 2], 0.2), 0);
+ assert.deepStrictEqual(quantileIndex([2, 3, 1], 0.2), 2);
+ assert.deepStrictEqual(quantileIndex([1], 0.2), 0);
+ assert.deepStrictEqual(quantileIndex([], 0.2), undefined);
+});
+
+it("quantileIndex(array, 0) returns the minimum index", () => {
+ assert.deepStrictEqual(quantileIndex([1, 2], 0), 0);
+ assert.deepStrictEqual(quantileIndex([1, 2, 3], 0), 0);
+ assert.deepStrictEqual(quantileIndex([1, 3, 2], 0), 0);
+ assert.deepStrictEqual(quantileIndex([2, 3, 1], 0), 2);
+ assert.deepStrictEqual(quantileIndex([1], 0), 0);
+ assert.deepStrictEqual(quantileIndex([], 0), undefined);
+});
+
+it("quantileIndex(array, 1) returns the maxium index", () => {
+ assert.deepStrictEqual(quantileIndex([1, 2], 1), 1);
+ assert.deepStrictEqual(quantileIndex([1, 2, 3], 1), 2);
+ assert.deepStrictEqual(quantileIndex([1, 3, 2], 1), 1);
+ assert.deepStrictEqual(quantileIndex([2, 3, 1], 1), 1);
+ assert.deepStrictEqual(quantileIndex([1], 1), 0);
+ assert.deepStrictEqual(quantileIndex([], 1), undefined);
+});
+
function box(value) {
return {value: value};
}