Commit 3c7cc81b authored by Mike Bostock's avatar Mike Bostock
Browse files

Add d3.bisectBy(comparator).

Fixes #1766. Unlike d3.bisector(accessor), this allows you to define a bisector
that works in reverse order.

An awkward aspect of implementing bisection on top of a comparator is that it is
often the case that the sorted array contains objects (e.g., rows from a TSV),
while the search value is a primitive value (e.g., a number). Thus, you want to
apply an accessor to the array elements but not to the search value.

The solution here is to invoke the comparator deterministically: the first
argument is always an element from the array, and the second argument is always
the search value. This lets a comparator apply an accessor to array elements but
not to search values.
parent adeaf201
......@@ -32,9 +32,10 @@
d3_style_setProperty.call(this, name, value + "", priority);
};
}
d3.ascending = function(a, b) {
d3.ascending = d3_ascending;
function d3_ascending(a, b) {
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
};
}
d3.descending = function(a, b) {
return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
};
......@@ -105,16 +106,16 @@
d3.median = function(array, f) {
if (arguments.length > 1) array = array.map(f);
array = array.filter(d3_number);
return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined;
return array.length ? d3.quantile(array.sort(d3_ascending), .5) : undefined;
};
d3.bisector = function(f) {
function d3_bisector(compare) {
return {
left: function(a, x, lo, hi) {
if (arguments.length < 3) lo = 0;
if (arguments.length < 4) hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (f.call(a, a[mid], mid) < x) lo = mid + 1; else hi = mid;
if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid;
}
return lo;
},
......@@ -123,17 +124,20 @@
if (arguments.length < 4) hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (x < f.call(a, a[mid], mid)) hi = mid; else lo = mid + 1;
if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1;
}
return lo;
}
};
}
var d3_bisect = (d3.bisectBy = d3_bisector)(d3_ascending);
d3.bisectLeft = d3_bisect.left;
d3.bisect = d3.bisectRight = d3_bisect.right;
d3.bisector = function(f) {
return d3_bisector(function(d, x) {
return d3_ascending(f(d), x);
});
};
var d3_bisector = d3.bisector(function(d) {
return d;
});
d3.bisectLeft = d3_bisector.left;
d3.bisect = d3.bisectRight = d3_bisector.right;
d3.shuffle = function(array) {
var m = array.length, t, i;
while (m) {
......@@ -872,7 +876,7 @@
return this.order();
};
function d3_selection_sortComparator(comparator) {
if (!arguments.length) comparator = d3.ascending;
if (!arguments.length) comparator = d3_ascending;
return function(a, b) {
return a && b ? comparator(a.__data__, b.__data__) : !a - !b;
};
......@@ -7717,7 +7721,7 @@
if (!arguments.length) return domain;
domain = x.filter(function(d) {
return !isNaN(d);
}).sort(d3.ascending);
}).sort(d3_ascending);
return rescale();
};
scale.range = function(x) {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
d3.ascending = function(a, b) {
d3.ascending = d3_ascending;
function d3_ascending(a, b) {
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
};
}
d3.bisector = function(f) {
import "ascending";
function d3_bisector(compare) {
return {
left: function(a, x, lo, hi) {
if (arguments.length < 3) lo = 0;
if (arguments.length < 4) hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (f.call(a, a[mid], mid) < x) lo = mid + 1;
if (compare(a[mid], x) < 0) lo = mid + 1;
else hi = mid;
}
return lo;
......@@ -15,14 +17,20 @@ d3.bisector = function(f) {
if (arguments.length < 4) hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (x < f.call(a, a[mid], mid)) hi = mid;
if (compare(a[mid], x) > 0) hi = mid;
else lo = mid + 1;
}
return lo;
}
};
};
}
var d3_bisector = d3.bisector(function(d) { return d; });
d3.bisectLeft = d3_bisector.left;
d3.bisect = d3.bisectRight = d3_bisector.right;
var d3_bisect = (d3.bisectBy = d3_bisector)(d3_ascending);
d3.bisectLeft = d3_bisect.left;
d3.bisect = d3.bisectRight = d3_bisect.right;
d3.bisector = function(f) {
return d3_bisector(function(d, x) {
return d3_ascending(f(d), x);
});
};
......@@ -5,5 +5,5 @@ import "quantile";
d3.median = function(array, f) {
if (arguments.length > 1) array = array.map(f);
array = array.filter(d3_number);
return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined;
return array.length ? d3.quantile(array.sort(d3_ascending), .5) : undefined;
};
......@@ -24,7 +24,7 @@ function d3_scale_quantile(domain, range) {
scale.domain = function(x) {
if (!arguments.length) return domain;
domain = x.filter(function(d) { return !isNaN(d); }).sort(d3.ascending);
domain = x.filter(function(d) { return !isNaN(d); }).sort(d3_ascending);
return rescale();
};
......
......@@ -8,7 +8,7 @@ d3_selectionPrototype.sort = function(comparator) {
};
function d3_selection_sortComparator(comparator) {
if (!arguments.length) comparator = d3.ascending;
if (!arguments.length) comparator = d3_ascending;
return function(a, b) {
return a && b ? comparator(a.__data__, b.__data__) : !a - !b;
};
......
var vows = require("vows"),
load = require("../load"),
assert = require("../assert");
assert = require("../assert"),
_ = require("../../");
var suite = vows.describe("d3.bisect");
......@@ -71,6 +72,7 @@ suite.addBatch({
assert.equal(bisect(array, 6, i - 5, i), i - 0);
}
},
"bisectRight": {
topic: load("arrays/bisect").expression("d3.bisectRight"),
"finds the index after an exact match": function(bisect) {
......@@ -129,7 +131,132 @@ suite.addBatch({
assert.equal(bisect(array, 6, i - 5, i), i - 0);
}
},
"bisector(key)": {
"bisectBy(comparator)": {
topic: load("arrays/bisect").expression("d3.bisectBy"),
"left": {
topic: function(bisectBy) {
return bisectBy(function(d, x) { return _.descending(d.key, x); }).left;
},
"finds the index of an exact match": function(bisect) {
var array = [{key: 3}, {key: 2}, {key: 1}];
assert.equal(bisect(array, 3), 0);
assert.equal(bisect(array, 2), 1);
assert.equal(bisect(array, 1), 2);
},
"finds the index of the first match": function(bisect) {
var array = [{key: 3}, {key: 2}, {key: 2}, {key: 1}];
assert.equal(bisect(array, 3), 0);
assert.equal(bisect(array, 2), 1);
assert.equal(bisect(array, 1), 3);
},
"finds the insertion point of a non-exact match": function(bisect) {
var array = [{key: 3}, {key: 2}, {key: 1}];
assert.equal(bisect(array, 3.5), 0);
assert.equal(bisect(array, 2.5), 1);
assert.equal(bisect(array, 1.5), 2);
assert.equal(bisect(array, 0.5), 3);
},
"observes the optional lower bound": function(bisect) {
var array = [{key: 5}, {key: 4}, {key: 3}, {key: 2}, {key: 1}];
assert.equal(bisect(array, 6, 2), 2);
assert.equal(bisect(array, 5, 2), 2);
assert.equal(bisect(array, 4, 2), 2);
assert.equal(bisect(array, 3, 2), 2);
assert.equal(bisect(array, 2, 2), 3);
assert.equal(bisect(array, 1, 2), 4);
assert.equal(bisect(array, 0, 2), 5);
},
"observes the optional bounds": function(bisect) {
var array = [{key: 5}, {key: 4}, {key: 3}, {key: 2}, {key: 1}];
assert.equal(bisect(array, 6, 2, 3), 2);
assert.equal(bisect(array, 5, 2, 3), 2);
assert.equal(bisect(array, 4, 2, 3), 2);
assert.equal(bisect(array, 3, 2, 3), 2);
assert.equal(bisect(array, 2, 2, 3), 3);
assert.equal(bisect(array, 1, 2, 3), 3);
assert.equal(bisect(array, 0, 2, 3), 3);
},
"large arrays": function(bisect) {
var array = [],
i = i30;
array[i++] = {key: 5};
array[i++] = {key: 4};
array[i++] = {key: 3};
array[i++] = {key: 2};
array[i++] = {key: 1};
assert.equal(bisect(array, 6, i - 5, i), i - 5);
assert.equal(bisect(array, 5, i - 5, i), i - 5);
assert.equal(bisect(array, 4, i - 5, i), i - 4);
assert.equal(bisect(array, 3, i - 5, i), i - 3);
assert.equal(bisect(array, 2, i - 5, i), i - 2);
assert.equal(bisect(array, 1, i - 5, i), i - 1);
assert.equal(bisect(array, 0, i - 5, i), i - 0);
}
},
"right": {
topic: function(bisectBy) {
return bisectBy(function(d, x) { return _.ascending(d.key, x); }).right;
},
"finds the index after an exact match": function(bisect) {
var array = [{key: 1}, {key: 2}, {key: 3}];
assert.equal(bisect(array, 1), 1);
assert.equal(bisect(array, 2), 2);
assert.equal(bisect(array, 3), 3);
},
"finds the index after the last match": function(bisect) {
var array = [{key: 1}, {key: 2}, {key: 2}, {key: 3}];
assert.equal(bisect(array, 1), 1);
assert.equal(bisect(array, 2), 3);
assert.equal(bisect(array, 3), 4);
},
"finds the insertion point of a non-exact match": function(bisect) {
var array = [{key: 1}, {key: 2}, {key: 3}];
assert.equal(bisect(array, 0.5), 0);
assert.equal(bisect(array, 1.5), 1);
assert.equal(bisect(array, 2.5), 2);
assert.equal(bisect(array, 3.5), 3);
},
"observes the optional lower bound": function(bisect) {
var array = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}];
assert.equal(bisect(array, 0, 2), 2);
assert.equal(bisect(array, 1, 2), 2);
assert.equal(bisect(array, 2, 2), 2);
assert.equal(bisect(array, 3, 2), 3);
assert.equal(bisect(array, 4, 2), 4);
assert.equal(bisect(array, 5, 2), 5);
assert.equal(bisect(array, 6, 2), 5);
},
"observes the optional bounds": function(bisect) {
var array = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}];
assert.equal(bisect(array, 0, 2, 3), 2);
assert.equal(bisect(array, 1, 2, 3), 2);
assert.equal(bisect(array, 2, 2, 3), 2);
assert.equal(bisect(array, 3, 2, 3), 3);
assert.equal(bisect(array, 4, 2, 3), 3);
assert.equal(bisect(array, 5, 2, 3), 3);
assert.equal(bisect(array, 6, 2, 3), 3);
},
"large arrays": function(bisect) {
var array = [],
i = i30;
array[i++] = {key: 1};
array[i++] = {key: 2};
array[i++] = {key: 3};
array[i++] = {key: 4};
array[i++] = {key: 5};
assert.equal(bisect(array, 0, i - 5, i), i - 5);
assert.equal(bisect(array, 1, i - 5, i), i - 4);
assert.equal(bisect(array, 2, i - 5, i), i - 3);
assert.equal(bisect(array, 3, i - 5, i), i - 2);
assert.equal(bisect(array, 4, i - 5, i), i - 1);
assert.equal(bisect(array, 5, i - 5, i), i - 0);
assert.equal(bisect(array, 6, i - 5, i), i - 0);
}
}
},
"bisector(accessor)": {
topic: load("arrays/bisect").expression("d3.bisector"),
"left": {
topic: function(bisector) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment