From 2c2ec1f781af2343e69c0d874b9ea6d6e2b1e6d4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 1 Oct 2021 21:42:58 -0700 Subject: [PATCH 1/2] allow rank to take a comparator --- README.md | 5 +++-- src/rank.js | 41 +++++++++++++++++++++-------------------- test/rank-test.js | 11 +++++++++-- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 244699ae..05fcbaf4 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,10 @@ An optional *accessor* function may be specified, which is equivalent to calling 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. -# d3.rank(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/rank.js), [Examples](https://observablehq.com/@d3/rank) +# d3.rank(iterable[, comparator]) · [Source](https://github.com/d3/d3-array/blob/main/src/rank.js), [Examples](https://observablehq.com/@d3/rank) +
# d3.rank(iterable[, accessor]) -Returns an array with the rank of each value in the *iterable*, *i.e.* the index of the value when the iterable is sorted. Nullish values are sorted to the end and ranked NaN. An optional *accessor* function may be specified, which is equivalent to calling *array*.map(*accessor*) before computing the ranks. Ties (equivalent values) all get the same rank, defined as the first time the value is found. +Returns an array with the rank of each value in the *iterable*, *i.e.* the zero-based index of the value when the iterable is sorted. Nullish values are sorted to the end and ranked NaN. An optional *comparator* or *accessor* function may be specified; the latter is equivalent to calling *array*.map(*accessor*) before computing the ranks. If *comparator* is not specified, it defaults to [ascending](#ascending). Ties (equivalent values) all get the same rank, defined as the first time the value is found. ```js d3.rank([{x: 1}, {}, {x: 2}, {x: 0}], d => d.x); // [1, NaN, 2, 0] diff --git a/src/rank.js b/src/rank.js index 488f8203..7c4ebc87 100644 --- a/src/rank.js +++ b/src/rank.js @@ -1,23 +1,24 @@ -import range from "./range.js"; -import sort from "./sort.js"; +import ascending from "./ascending.js"; +import {ascendingDefined, compareDefined} from "./sort.js"; -export default function rank(values, valueof) { +export default function rank(values, valueof = ascending) { if (typeof values[Symbol.iterator] !== "function") throw new TypeError("values is not iterable"); - values = Array.from(values, valueof); - const n = values.length; - const r = new Float64Array(n); - let last, l; - sort(range(n), (i) => values[i]).forEach((j, i) => { - const value = values[j]; - if (value == null || !(value <= value)) { - r[j] = NaN; - return; - } - if (last === undefined || !(value <= last)) { - last = value; - l = i; - } - r[j] = l; - }); - return r; + let V = Array.from(values); + const R = new Float64Array(V.length); + if (valueof.length === 1) 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) => { + const c = compareIndex(j, k === undefined ? j : k); + if (c >= 0) { + if (k === undefined || c > 0) k = j, r = i; + R[j] = r; + } else { + R[j] = NaN; + } + }); + return R; } diff --git a/test/rank-test.js b/test/rank-test.js index 00b59140..5f7faf36 100644 --- a/test/rank-test.js +++ b/test/rank-test.js @@ -1,4 +1,5 @@ import assert from "assert"; +import ascending from "../src/ascending.js"; import rank from "../src/rank.js"; it("rank(numbers) returns the rank of numbers", () => { @@ -28,12 +29,18 @@ it("rank(values, valueof) accepts an accessor", () => { assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], d => d.x), Float64Array.of(2, 0, 1, 3, NaN)); }); -it("rank(values, ties) computes the ties as expected", () => { +it("rank(values, compare) accepts a comparator", () => { + assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], (a, b) => a.x - b.x), Float64Array.of(2, 0, 1, 3, NaN)); + assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], (a, b) => b.x - a.x), Float64Array.of(1, 3, 2, 0, NaN)); + assert.deepStrictEqual(rank(["aa", "ba", "bc", "bb", "ca"], (a, b) => ascending(a[0], b[0]) || ascending(a[1], b[1])), Float64Array.of(0, 1, 3, 2, 4)); +}); + +it("rank(values) computes the ties as expected", () => { assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"]), Float64Array.of(0, 1, 1, 1, 4)); assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"]), Float64Array.of(0, 1, 1, 1, 1, 5)); }); -it("rank(values, ties) handles NaNs as expected", () => { +it("rank(values) handles NaNs as expected", () => { assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null]), Float64Array.of(0, 1, 1, 1, 4, NaN)); assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null]), Float64Array.of(0, 1, 1, 1, 1, 5, NaN)); }); From ced5425122a08da24eaf6fd65177504293a82d68 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 2 Oct 2021 08:13:02 -0700 Subject: [PATCH 2/2] another test, example --- README.md | 1 + test/rank-test.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 05fcbaf4..af354e78 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ Returns an array with the rank of each value in the *iterable*, *i.e.* the zero- ```js d3.rank([{x: 1}, {}, {x: 2}, {x: 0}], d => d.x); // [1, NaN, 2, 0] d3.rank(["b", "c", "b", "a"]); // [1, 3, 1, 0] +d3.rank([1, 2, 3], d3.descending); // [2, 1, 0] ``` # d3.variance(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/variance.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends) diff --git a/test/rank-test.js b/test/rank-test.js index 5f7faf36..9e2d4c46 100644 --- a/test/rank-test.js +++ b/test/rank-test.js @@ -1,5 +1,6 @@ import assert from "assert"; import ascending from "../src/ascending.js"; +import descending from "../src/descending.js"; import rank from "../src/rank.js"; it("rank(numbers) returns the rank of numbers", () => { @@ -33,6 +34,7 @@ it("rank(values, compare) accepts a comparator", () => { assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], (a, b) => a.x - b.x), Float64Array.of(2, 0, 1, 3, NaN)); assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], (a, b) => b.x - a.x), Float64Array.of(1, 3, 2, 0, NaN)); assert.deepStrictEqual(rank(["aa", "ba", "bc", "bb", "ca"], (a, b) => ascending(a[0], b[0]) || ascending(a[1], b[1])), Float64Array.of(0, 1, 3, 2, 4)); + assert.deepStrictEqual(rank(["A", null, "B", "C", "D"], descending), Float64Array.of(3, NaN, 2, 1, 0)); }); it("rank(values) computes the ties as expected", () => {