Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow rank to take a comparator #237

Merged
merged 2 commits into from Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -141,13 +141,15 @@ 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.

<a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/main/src/rank.js), [Examples](https://observablehq.com/@d3/rank)
<a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>comparator</i>]) · [Source](https://github.com/d3/d3-array/blob/main/src/rank.js), [Examples](https://observablehq.com/@d3/rank)
<br><a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>accessor</i>])

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]
d3.rank(["b", "c", "b", "a"]); // [1, 3, 1, 0]
d3.rank([1, 2, 3], d3.descending); // [2, 1, 0]
```

<a name="variance" href="#variance">#</a> d3.<b>variance</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/main/src/variance.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends)
Expand Down
41 changes: 21 additions & 20 deletions 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;
}
13 changes: 11 additions & 2 deletions test/rank-test.js
@@ -1,4 +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", () => {
Expand Down Expand Up @@ -28,12 +30,19 @@ 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));
assert.deepStrictEqual(rank(["A", null, "B", "C", "D"], descending), Float64Array.of(3, NaN, 2, 1, 0));
});

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));
});