From 52cdcf098f309f3c24dc3eec5c09c592a283d21b Mon Sep 17 00:00:00 2001 From: Jeffrey Heer Date: Wed, 8 Jun 2016 08:48:09 -0700 Subject: [PATCH] Add band/point invertExtent. --- README.md | 8 +++++ src/band.js | 24 ++++++++++++- test/band-test.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17a3442..100fc0b 100644 --- a/README.md +++ b/README.md @@ -770,6 +770,10 @@ Constructs a new band scale with the empty [domain](#band_domain), the unit [ran Given a *value* in the input [domain](#band_domain), returns the start of the corresponding band derived from the output [range](#band_range). If the given *value* is not in the scale’s domain, returns undefined. +# band.invertExtent(r0[, r1]) + +Given a range of values from the [range](#band_range), returns an array of corresponding values from the [domain](#band_domain), respecting the scale [padding](#band_padding) and [bandwidth](#band_bandwidth). If two arguments *r0* and *r1* are provided, returns an array of the domain values contained within that range. If only a single argument *r0* is provided, returns an array containing the ordinal value under that point, if found. If no domain values are within the specified range, returns undefined. + # band.domain([domain]) If *domain* is specified, sets the domain to the specified array of values. The first element in *domain* will be mapped to the first band, the second domain value to the second band, and so on. Domain values are stored internally in a map from stringified value to index; the resulting index is then used to determine the band. Thus, a band scale’s values must be coercible to a string, and the stringified version of the domain value uniquely identifies the corresponding band. If *domain* is not specified, this method returns the current domain. @@ -836,6 +840,10 @@ Constructs a new point scale with the empty [domain](#point_domain), the unit [r Given a *value* in the input [domain](#point_domain), returns the corresponding point derived from the output [range](#point_range). If the given *value* is not in the scale’s domain, returns undefined. +# point.invertExtent(r0[, r1]) + +Given a range of values from the [range](#point_range), returns an array of corresponding values from the [domain](#point_domain), respecting the scale [padding](#point_padding) and [bandwidth](#point_bandwidth). If two arguments *r0* and *r1* are provided, returns an array of the domain values contained within that range. If only a single argument *r0* is provided, returns an array containing the ordinal value under that point, if found. If no domain values are within the specified range, returns undefined. + # point.domain([domain]) If *domain* is specified, sets the domain to the specified array of values. The first element in *domain* will be mapped to the first point, the second domain value to the second point, and so on. Domain values are stored internally in a map from stringified value to index; the resulting index is then used to determine the point. Thus, a point scale’s values must be coercible to a string, and the stringified version of the domain value uniquely identifies the corresponding point. If *domain* is not specified, this method returns the current domain. diff --git a/src/band.js b/src/band.js index 05acdde..fcdc1e2 100644 --- a/src/band.js +++ b/src/band.js @@ -1,4 +1,4 @@ -import {range as sequence} from "d3-array"; +import {range as sequence, bisectRight} from "d3-array"; import ordinal from "./ordinal"; export default function band() { @@ -69,6 +69,28 @@ export default function band() { return arguments.length ? (align = Math.max(0, Math.min(1, _)), rescale()) : align; }; + scale.invertExtent = function(r0, r1) { + var lo = +r0, + hi = arguments.length > 1 ? +r1 : lo, + reverse = range[1] < range[0], + values = reverse ? ordinalRange().reverse() : ordinalRange(), + n = values.length - 1, a, b, t; + + // order range inputs, bail if outside of scale range + if (hi < lo) t = lo, lo = hi, hi = t; + if (hi < values[0] || lo > range[1-reverse]) return undefined; + + // binary search to index into scale range + a = Math.max(0, bisectRight(values, lo) - 1); + b = lo===hi ? a : bisectRight(values, hi) - 1; + + // increment index a if lo is within padding gap + if (lo - values[a] > bandwidth + 1e-10) ++a; + + if (reverse) t = a, a = n - b, b = n - t; // map + swap + return (a > b) ? undefined : domain().slice(a, b+1); + }; + scale.copy = function() { return band() .domain(domain()) diff --git a/test/band-test.js b/test/band-test.js index 05d6c9f..1c35df0 100644 --- a/test/band-test.js +++ b/test/band-test.js @@ -252,4 +252,96 @@ tape("band.copy() isolates changes to the range", function(test) { test.end(); }); +tape("band.invertExtent(x) inverts single value", function(test) { + var s = scale.scaleBand().domain(["foo", "bar"]); + + // ascending range + s.range([0,2]); + test.deepEqual(s.invertExtent(-1), undefined); + test.deepEqual(s.invertExtent(0.0), ["foo"]); + test.deepEqual(s.invertExtent(0.5), ["foo"]); + test.deepEqual(s.invertExtent(1.0), ["bar"]); + test.deepEqual(s.invertExtent(1.5), ["bar"]); + test.deepEqual(s.invertExtent(2.0), ["bar"]); + test.deepEqual(s.invertExtent(2.1), undefined); + + // ascending range with padding + s.padding(0.3); + test.deepEqual(s.invertExtent(-1), undefined); + test.deepEqual(s.invertExtent(0.0), undefined); + test.deepEqual(s.invertExtent(0.5), ["foo"]); + test.deepEqual(s.invertExtent(1.0), undefined); + test.deepEqual(s.invertExtent(1.5), ["bar"]); + test.deepEqual(s.invertExtent(2.0), undefined); + test.deepEqual(s.invertExtent(2.1), undefined); + + // descending range + s.padding(0).range([2, 0]); + test.deepEqual(s.invertExtent(-1), undefined); + test.deepEqual(s.invertExtent(0.0), ["bar"]); + test.deepEqual(s.invertExtent(0.5), ["bar"]); + test.deepEqual(s.invertExtent(1.0), ["foo"]); + test.deepEqual(s.invertExtent(1.5), ["foo"]); + test.deepEqual(s.invertExtent(2.0), ["foo"]); + test.deepEqual(s.invertExtent(2.1), undefined); + + // descending range with padding + s.padding(0.3); + test.deepEqual(s.invertExtent(-1), undefined); + test.deepEqual(s.invertExtent(0.0), undefined); + test.deepEqual(s.invertExtent(0.5), ["bar"]); + test.deepEqual(s.invertExtent(1.0), undefined); + test.deepEqual(s.invertExtent(1.5), ["foo"]); + test.deepEqual(s.invertExtent(2.0), undefined); + test.deepEqual(s.invertExtent(2.1), undefined); + + test.end(); +}); + +tape("band.invertExtent(x, y) inverts value range", function(test) { + var s = scale.scaleBand().domain(["foo", "bar"]); + + // ascending range + s.range([0, 2]); + test.deepEqual(s.invertExtent(-2, -1), undefined); + test.deepEqual(s.invertExtent(-1, 0), ["foo"]); + test.deepEqual(s.invertExtent(0, 0.5), ["foo"]); + test.deepEqual(s.invertExtent(0, 1), ["foo", "bar"]); + test.deepEqual(s.invertExtent(0, 2), ["foo", "bar"]); + test.deepEqual(s.invertExtent(2, 3), ["bar"]); + test.deepEqual(s.invertExtent(3, 4), undefined); + + // ascending range with padding + s.padding(0.3); + test.deepEqual(s.invertExtent( -1, 0), undefined); + test.deepEqual(s.invertExtent(0.0, 0.1), undefined); + test.deepEqual(s.invertExtent(0.0, 0.5), ["foo"]); + test.deepEqual(s.invertExtent(0.5, 1.5), ["foo", "bar"]); + test.deepEqual(s.invertExtent(0.9, 1.1), undefined); + test.deepEqual(s.invertExtent(1.0, 1.5), ["bar"]); + test.deepEqual(s.invertExtent(1.9, 2.0), undefined); + + // descending range + s.padding(0).range([2, 0]); + test.deepEqual(s.invertExtent(-2, -1), undefined); + test.deepEqual(s.invertExtent(-1, 0), ["bar"]); + test.deepEqual(s.invertExtent(0, 0.5), ["bar"]); + test.deepEqual(s.invertExtent(0, 1), ["foo", "bar"]); + test.deepEqual(s.invertExtent(0, 2), ["foo", "bar"]); + test.deepEqual(s.invertExtent(2, 3), ["foo"]); + test.deepEqual(s.invertExtent(3, 4), undefined); + + // descending range with padding + s.padding(0.3); + test.deepEqual(s.invertExtent( -1, 0.0), undefined); + test.deepEqual(s.invertExtent(0.0, 0.1), undefined); + test.deepEqual(s.invertExtent(0.0, 0.5), ["bar"]); + test.deepEqual(s.invertExtent(0.5, 1.5), ["foo", "bar"]); + test.deepEqual(s.invertExtent(0.9, 1.1), undefined); + test.deepEqual(s.invertExtent(1.0, 1.5), ["foo"]); + test.deepEqual(s.invertExtent(1.9, 2.0), undefined); + + test.end(); +}); + // TODO align tests for padding & round