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

Add band/point invertExtent. #64

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -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.

<a name="band_invertExtent" href="#band_invertExtent">#</a> <i>band</i>.<b>invertExtent</b>(<i>r0</i>[, <i>r1</i>])

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.

<a name="band_domain" href="#band_domain">#</a> <i>band</i>.<b>domain</b>([<i>domain</i>])

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.
Expand Down Expand Up @@ -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.

<a name="point_invertExtent" href="#point_invertExtent">#</a> <i>point</i>.<b>invertExtent</b>(<i>r0</i>[, <i>r1</i>])

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.

<a name="point_domain" href="#point_domain">#</a> <i>point</i>.<b>domain</b>([<i>domain</i>])

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.
Expand Down
24 changes: 23 additions & 1 deletion 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() {
Expand Down Expand Up @@ -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())
Expand Down
92 changes: 92 additions & 0 deletions test/band-test.js
Expand Up @@ -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