From e1e4976c0b559e1e1a50b07484d13afa43f27eaa Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 30 Sep 2021 21:03:30 -0700 Subject: [PATCH 1/5] =?UTF-8?q?only=20implement=20=E2=80=9Clow=E2=80=9D=20?= =?UTF-8?q?ties=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rank.js | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/rank.js b/src/rank.js index 5e785a17..6cc20561 100644 --- a/src/rank.js +++ b/src/rank.js @@ -1,17 +1,12 @@ 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) { 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; @@ -23,25 +18,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; } From f202edb74cf4f34562f915594ab0b024a17adb1e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 30 Sep 2021 21:05:35 -0700 Subject: [PATCH 2/5] =?UTF-8?q?only=20test=20=E2=80=9Clow=E2=80=9D=20ties?= =?UTF-8?q?=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/rank-test.js | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/test/rank-test.js b/test/rank-test.js index 2ebbfa23..bbd8bb81 100644 --- a/test/rank-test.js +++ b/test/rank-test.js @@ -16,7 +16,6 @@ it("rank(dates) returns the rank of Dates", () => { 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])); }); @@ -30,27 +29,11 @@ it("rank(values, valueof) accepts an accessor", () => { }); 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"]), new Float64Array([0, 1, 1, 1, 4])); + assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"]), new Float64Array([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]), new Float64Array([0, 1, 1, 1, 4, NaN])); + assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null]), new Float64Array([0, 1, 1, 1, 1, 5, NaN])); }); From 84d23f052cd1cd4ba6ffe91b30ef2b8f4a9adf74 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 30 Sep 2021 21:10:57 -0700 Subject: [PATCH 3/5] only allow iterables --- src/rank.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rank.js b/src/rank.js index 6cc20561..488f8203 100644 --- a/src/rank.js +++ b/src/rank.js @@ -2,6 +2,7 @@ import range from "./range.js"; import sort from "./sort.js"; 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); From a22814dbf84f94ac35eedb637babc4fbfcd9121e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 30 Sep 2021 21:14:01 -0700 Subject: [PATCH 4/5] adopt Float64Array.of --- test/rank-test.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/rank-test.js b/test/rank-test.js index bbd8bb81..4d23bedd 100644 --- a/test/rank-test.js +++ b/test/rank-test.js @@ -2,38 +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({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"]), new Float64Array([0, 1, 1, 1, 4])); - assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"]), new Float64Array([0, 1, 1, 1, 1, 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]), new Float64Array([0, 1, 1, 1, 4, NaN])); - assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null]), new Float64Array([0, 1, 1, 1, 1, 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)); }); From 4f3deaf5746853068b9bb871bc5a7491c877f36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 1 Oct 2021 14:39:34 +0200 Subject: [PATCH 5/5] documentation: remove ties; observablehq/@d3/rank --- README.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8c0a3793..194f41d5 100644 --- a/README.md +++ b/README.md @@ -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. -# d3.rank(iterable[, ties][, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/rank.js) +# d3.rank(iterable[, accessor]) · [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] ``` # d3.variance(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/variance.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends)