Fix #2039: improve [un]interpolateNumber precision.

* uninterpolate:

The use of a reciprocal in d3_uninterpolateNumber to avoid division
results in a small loss of precision (the following is a paraphrase):

  function d3_uninterpolateNumber(a, b) {
    var k = b - a ? 1 / (b - a) : 0;
    return function(x) { return (x - a) * k; };
  }

For x = a, there is no problem, since u = (a - a) * k = 0 * k = 0 as
expected.  For x = b, we have u = (b - a) * k, and since k cannot
represent 1 / (b - a) exactly, we don’t get u = 1, but something very
close to 1.

Instead, if we perform the division within the generated function, we
can ensure we always get u = 1 for x = b:

  function d3_uninterpolateNumber(a, b) {
    var k = b - a || Infinity;
    return function(x) { return (x - a) / k; };
  }

Again, for x = a, we simply have u = (a - a) / k = 0.  For x = b, we
have u = (b - a) / k = (b - a) / (b - a) = 1.

* interpolate:

Similarly, for d3_interpolateNumber, we have a small loss of precision,
this time due to subtraction.  Paraphrased:

  function d3_interpolateNumber(a, b) {
    var d = b - a;
    return function(t) { return a + t * d; };
  }

There is no issue for t = 0, because we always get i = a + 0 * d = a.
However, for t = 1, we get i = a + d, which might not be exactly equal
to b as desired.  The following will return precisely b for t = 1:

  function d3_interpolateNumber(a, b) {
    return function(t) { return a * (1 - t) + b * t; };
  }
This commit is contained in:
Jason Davies 2014-10-10 10:09:57 +01:00
Родитель f38ed05731
Коммит a4429fa041
7 изменённых файлов: 26 добавлений и 26 удалений

12
d3.js поставляемый
Просмотреть файл

@ -5605,9 +5605,9 @@
}
d3.interpolateNumber = d3_interpolateNumber;
function d3_interpolateNumber(a, b) {
b -= a = +a;
a = +a, b = +b;
return function(t) {
return a + b * t;
return a * (1 - t) + b * t;
};
}
d3.interpolateString = d3_interpolateString;
@ -5906,15 +5906,15 @@
};
}
function d3_uninterpolateNumber(a, b) {
b = b - (a = +a) ? 1 / (b - a) : 0;
b = b - (a = +a) || Infinity;
return function(x) {
return (x - a) * b;
return (x - a) / b;
};
}
function d3_uninterpolateClamp(a, b) {
b = b - (a = +a) ? 1 / (b - a) : 0;
b = b - (a = +a) || Infinity;
return function(x) {
return Math.max(0, Math.min(1, (x - a) * b));
return Math.max(0, Math.min(1, (x - a) / b));
};
}
d3.layout = {};

4
d3.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,6 +1,6 @@
d3.interpolateNumber = d3_interpolateNumber;
function d3_interpolateNumber(a, b) {
b -= a = +a;
return function(t) { return a + b * t; };
a = +a, b = +b;
return function(t) { return a * (1 - t) + b * t; };
}

Просмотреть файл

@ -1,9 +1,9 @@
function d3_uninterpolateNumber(a, b) {
b = b - (a = +a) ? 1 / (b - a) : 0;
return function(x) { return (x - a) * b; };
b = b - (a = +a) || Infinity;
return function(x) { return (x - a) / b; };
}
function d3_uninterpolateClamp(a, b) {
b = b - (a = +a) ? 1 / (b - a) : 0;
return function(x) { return Math.max(0, Math.min(1, (x - a) * b)); };
b = b - (a = +a) || Infinity;
return function(x) { return Math.max(0, Math.min(1, (x - a) / b)); };
}

Просмотреть файл

@ -10,12 +10,12 @@ suite.addBatch({
"when b is a number": {
"interpolates numbers": function(d3) {
assert.strictEqual(d3.interpolate(2, 12)(.4), 6);
assert.strictEqual(d3.interpolate(2, 12)(.25), 4.5);
},
"coerces a to a number": function(d3) {
assert.strictEqual(d3.interpolate("", 1)(.5), .5);
assert.strictEqual(d3.interpolate("2", 12)(.4), 6);
assert.strictEqual(d3.interpolate([2], 12)(.4), 6);
assert.strictEqual(d3.interpolate("2", 12)(.25), 4.5);
assert.strictEqual(d3.interpolate([2], 12)(.25), 4.5);
}
},
@ -79,11 +79,11 @@ suite.addBatch({
"when b is an array": {
"interpolates each element in b": function(d3) {
assert.strictEqual(JSON.stringify(d3.interpolate([2, 4], [12, 24])(.4)), "[6,12]");
assert.strictEqual(JSON.stringify(d3.interpolate([2, 4], [12, 24])(.25)), "[4.5,9]");
},
"interpolates arrays, even when both a and b are coercible to numbers": function(d3) {
assert.strictEqual(JSON.stringify(d3.interpolate([2], [12])(.4)), "[6]");
assert.strictEqual(JSON.stringify(d3.interpolate([[2]], [[12]])(.4)), "[[6]]");
assert.strictEqual(JSON.stringify(d3.interpolate([2], [12])(.25)), "[4.5]");
assert.strictEqual(JSON.stringify(d3.interpolate([[2]], [[12]])(.25)), "[[4.5]]");
},
"reuses the returned array during interpolation": function(d3) {
var i = d3.interpolate([2], [12]);
@ -93,10 +93,10 @@ suite.addBatch({
"when b is an object": {
"interpolates each property in b": function(d3) {
assert.deepEqual(d3.interpolate({foo: 2, bar: 4}, {foo: 12, bar: 24})(.4), {foo: 6, bar: 12});
assert.deepEqual(d3.interpolate({foo: 2, bar: 4}, {foo: 12, bar: 24})(.25), {foo: 4.5, bar: 9});
},
"interpolates numbers if b is coercible to a number (!isNaN(+b))": function(d3) {
assert.strictEqual(d3.interpolate(new Number(2), new Number(12))(.4), 6);
assert.strictEqual(d3.interpolate(new Number(2), new Number(12))(.25), 4.5);
assert.strictEqual(d3.interpolate(new Date(2012, 0, 1), new Date(2013, 0, 1))(.5), +new Date(2012, 6, 2, 1));
assert.strictEqual(d3.interpolate(1, null)(.4), .6); // +null = 0
assert.isNaN(d3.interpolate("blue", null)(.4));

Просмотреть файл

@ -8,11 +8,11 @@ suite.addBatch({
"interpolateNumber": {
topic: load("interpolate/number").expression("d3.interpolateNumber"),
"interpolates numbers": function(interpolate) {
assert.strictEqual(interpolate(2, 12)(.4), 6);
assert.strictEqual(interpolate(2, 12)(.6), 8);
assert.strictEqual(interpolate(2, 12)(.25), 4.5);
assert.strictEqual(interpolate(2, 12)(.75), 9.5);
},
"coerces strings to numbers": function(interpolate) {
assert.strictEqual(interpolate("2", "12")(.4), 6);
assert.strictEqual(interpolate("2", "12")(.25), 4.5);
}
}
});

Просмотреть файл

@ -12,7 +12,7 @@ suite.addBatch({
assert.strictEqual(interpolate(" 10/20 30", "50/10 100 ")(.4), "26/16 58 ");
},
"coerces objects to strings": function(interpolate) {
assert.strictEqual(interpolate({toString: function() { return "2px"; }}, {toString: function() { return "12px"; }})(.4), "6px");
assert.strictEqual(interpolate({toString: function() { return "2px"; }}, {toString: function() { return "12px"; }})(.25), "4.5px");
},
"preserves non-numbers in string b": function(interpolate) {
assert.strictEqual(interpolate(" 10/20 30", "50/10 foo ")(.2), "18/18 foo ");