Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,22 +141,14 @@ 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>ties</i>][, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/rank.js)<!-- , [Examples](https://observablehq.com/@d3/rank) -->
<a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/rank.js), [Examp
les](https://observablehq.com/@d3/rank)

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 by default as the first time the value is found.

The *ties* option sets a different strategy for ties:

* `low` (default) - the first time the value was found
* `mean` - the average time the value was found
* `round` - the rounded average time the value was found
* `high` - the last time the value was found
* `order` - each tie has its own rank
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.

```js
d3.rank([{x: 1}, {x: 2}, {x: 0}, {}], d => d.x); // returns [1, 2, 0, NaN]
d3.rank(["b", "c", "b", "a"]); // returns [1, 3, 1, 0]
d3.rank(["b", "c", "b", "a"], "order"); // returns [1, 3, 2, 0]
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]
```

<a name="variance" href="#variance">#</a> d3.<b>variance</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/variance.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends)
Expand Down
30 changes: 3 additions & 27 deletions src/rank.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import range from "./range.js";
import sort from "./sort.js";

export default function rank(values, ties = "low", valueof) {
if (typeof ties === "function") {
valueof = ties; ties = "low";
}
export default function rank(values, valueof) {
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;
const order = sort(range(n), (i) => values[i]);
order.forEach((j, i) => {
sort(range(n), (i) => values[i]).forEach((j, i) => {
const value = values[j];
if (value == null || !(value <= value)) {
r[j] = NaN;
Expand All @@ -23,25 +19,5 @@ export default function rank(values, ties = "low", valueof) {
}
r[j] = l;
});

// backtrack to handle ties: low, mean, round, high, order
if (ties === "order") {
order.forEach((i,j) => r[i] = isNaN(r[i]) ? NaN : j);
} else if (ties !== "low") {
let find, replace;
for (let i = n - 1; i >= 0; i--) {
const j = r[order[i]];
if (i !== j && find !== j) {
find = j;
switch (ties) {
case "mean": replace = (i + j) / 2; break;
case "round": replace = (i + j) >> 1; break;
case "high": replace = i; break;
}
}
if (j === find) r[order[i]] = replace;
}
}

return r;
}
43 changes: 13 additions & 30 deletions test/rank-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,38 @@ import assert from "assert";
import rank from "../src/rank.js";

it("rank(numbers) returns the rank of numbers", () => {
assert.deepStrictEqual(rank([1000, 10, 0]), new Float64Array([2, 1, 0]));
assert.deepStrictEqual(rank([1.2, 1.1, 1.2, 1.0, 1.5, 1.2]), new Float64Array([2, 1, 2, 0, 5, 2]));
assert.deepStrictEqual(rank([1000, 10, 0]), Float64Array.of(2, 1, 0));
assert.deepStrictEqual(rank([1.2, 1.1, 1.2, 1.0, 1.5, 1.2]), Float64Array.of(2, 1, 2, 0, 5, 2));
});

it("rank(strings) returns the rank of letters", () => {
assert.deepStrictEqual(rank([..."EDGFCBA"]), new Float64Array([4, 3, 6, 5, 2, 1, 0]));
assert.deepStrictEqual(rank([..."EDGFCBA"]), Float64Array.of(4, 3, 6, 5, 2, 1, 0));
});

it("rank(dates) returns the rank of Dates", () => {
assert.deepStrictEqual(rank([new Date(2000, 0, 1), new Date(2000, 0, 1), new Date(1999, 0, 1), new Date(2001, 0, 1)]), new Float64Array([1, 1, 0, 3]));
assert.deepStrictEqual(rank([new Date(2000, 0, 1), new Date(2000, 0, 1), new Date(1999, 0, 1), new Date(2001, 0, 1)]), Float64Array.of(1, 1, 0, 3));
});

it("rank(iterator) accepts an iterator", () => {
assert.deepStrictEqual(rank(new Set(["B", "C", "A"])), new Float64Array([1, 2, 0]));
assert.deepStrictEqual(rank(new Set(["B", "C", "A"]), "high"), new Float64Array([1, 2, 0]));
assert.deepStrictEqual(rank({length: 3}, (_, i) => i), new Float64Array([0, 1, 2]));
assert.deepStrictEqual(rank(new Set(["B", "C", "A"])), Float64Array.of(1, 2, 0));
assert.deepStrictEqual(rank({length: 3}, (_, i) => i), Float64Array.of(0, 1, 2));
});

it("rank(undefineds) ranks undefined as NaN", () => {
assert.deepStrictEqual(rank([1.2, 1.1, undefined, 1.0, undefined, 1.5]), new Float64Array([2, 1, NaN, 0, NaN, 3]));
assert.deepStrictEqual(rank([, null, , 1.2, 1.1, undefined, 1.0, NaN, 1.5]), new Float64Array([NaN, NaN, NaN, 2, 1, NaN, 0, NaN, 3]));
assert.deepStrictEqual(rank([1.2, 1.1, undefined, 1.0, undefined, 1.5]), Float64Array.of(2, 1, NaN, 0, NaN, 3));
assert.deepStrictEqual(rank([, null, , 1.2, 1.1, undefined, 1.0, NaN, 1.5]), Float64Array.of(NaN, NaN, NaN, 2, 1, NaN, 0, NaN, 3));
});

it("rank(values, valueof) accepts an accessor", () => {
assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], d => d.x), new Float64Array([2, 0, 1, 3, NaN]));
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", () => {
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "low"), new Float64Array([0, 1, 1, 1, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "mean"), new Float64Array([0, 2, 2, 2, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "round"), new Float64Array([0, 2, 2, 2, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "high"), new Float64Array([0, 3, 3, 3, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "order"), new Float64Array([0, 1, 2, 3, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "low"), new Float64Array([0, 1, 1, 1, 1, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "mean"), new Float64Array([0, 2.5, 2.5, 2.5, 2.5, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "round"), new Float64Array([0, 2, 2, 2, 2, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "high"), new Float64Array([0, 4, 4, 4, 4, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "order"), new Float64Array([0, 1, 2, 3, 4, 5]));
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", () => {
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "low"), new Float64Array([0, 1, 1, 1, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "mean"), new Float64Array([0, 2, 2, 2, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "round"), new Float64Array([0, 2, 2, 2, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "high"), new Float64Array([0, 3, 3, 3, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "order"), new Float64Array([0, 1, 2, 3, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "low"), new Float64Array([0, 1, 1, 1, 1, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "mean"), new Float64Array([0, 2.5, 2.5, 2.5, 2.5, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "round"), new Float64Array([0, 2, 2, 2, 2, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "high"), new Float64Array([0, 4, 4, 4, 4, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "order"), new Float64Array([0, 1, 2, 3, 4, 5, NaN]));
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));
});